You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Race in PseudoTerminal handle close vs MonitorExitAsync wait #39
In PseudoTerminal, MonitorExitAsync blocks on WaitForSingleObject(_hProcess, INFINITE) while Dispose() calls CloseHandle(_hProcess) and zeros the field. Closing a handle while another thread waits on it is Win32 undefined behaviour — WaitForSingleObject can return prematurely, after which Exited?.Invoke() fires even though the child process may not have actually exited.
Where it bites
MainWindow.DisposeAndWaitForExitAsync (introduced in PR #35) subscribes to pty.Exited and then calls vm.Dispose(), expecting the Exited event to mean "the child has exited so its ~/.claude.json write is done." With the race, the event can fire on handle-close instead, and the next claude teardown can begin before the previous one has flushed.
Practical impact today is bounded by:
Dispose() also calls ClosePseudoConsole(_hPC), which signals the child to shut down, so it usually exits promptly anyway.
The caller has a Task.Delay(Math.Min(postExitMs, 1000)) post-exit pause as belt-and-braces.
So the worst-case gap between teardowns is ~1s instead of "real exit + 1s," not zero. But the abstraction is leaky and the rare close-race could still corrupt ~/.claude.json.
Proposed fix
Pick one:
DuplicateHandle for the wait. Duplicate _hProcess into a wait handle before disposing the PTY; MonitorExitAsync waits on the duplicate and the original can be closed independently in Dispose().
PseudoTerminal.WaitForExitAsync(timeoutMs) API. New method that waits on the process handle (kept alive until the wait returns) and only then proceeds with handle cleanup. Dispose() can call this internally if the user didn't.
Either is ~15 lines.
Discovered by
Copilot review on PR #35, validated. Filed as follow-up rather than fixed inline because the practical impact is limited and the fix touches PseudoTerminal's public surface.
Activity
AThraen commentedon May 28, 2026
Shipped in e386863 ("low-hanging fruit bundle").