Open
Description
Where did this happen?
Desktop app (macOS)
Version
Source build, main at 19db630
What happened?
After a terminal is closed, the session stays in the store until the process exits. In that window handleResize still calls pty.resize() on a PTY whose master fd is already closed. A newly opened PTY can reuse that fd number and get resized instead. handleInput already refuses an exited session.
Expected: resize returns EEXITED, the same as input.
Steps to reproduce
- Close a terminal whose process takes a moment to exit.
- Resize the pane before the process exits.
- The resize goes to the closed fd.
Logs, screenshots, or recordings
Found by model checking every order of events, then reproduced as a failing test against the real code. A fix with that regression test is ready.
Implementation notes
Files
packages/pty-daemon/src/handlers/handlers.tshandleResize: nosession.exitedcheck.
Approach
Add the same EEXITED guard that handleInput has.
Metadata
Metadata
Assignees
Labels
No labels
Activity
github-actions commentedon Nov 7, 2026
Triage summary
The code supports this report. When the pty-daemon gets a
closemessage, it disposes the PTY and marks the sessionexited = true. The session stays in theSessionStoreuntil the process's exit callback removes it. During that window,handleInputrejects the session withEEXITED, buthandleResizehas no such check. It still callssession.pty.resize(), which issues the window-size change against a master fd thatdispose()has already closed. If a new PTY opened in that window got the same fd number, the resize lands on the wrong terminal. Shells that trap or ignore SIGHUP for a while make the window wider. Both PTY types are affected.Affected code
packages/pty-daemon/src/handlers/handlers.ts:119-131:handleResizechecks only that the session exists (:124), then callssession.pty.resize(...)(:126). It has nosession.exitedcheck.packages/pty-daemon/src/handlers/handlers.ts:104-105: the guardhandleInputalready has, which returnsEEXITED.packages/pty-daemon/src/handlers/handlers.ts:143-158:handleClosecallskill()and thendispose(), and setssession.exited = true, but leaves the session in the store.packages/pty-daemon/src/Server/Server.ts:651: the session is onlystore.deleted from the exit callback, so it can stay in the store for some time afterclose.packages/pty-daemon/src/Pty/Pty.ts:283-296and:319-323:NodePtyAdapter.dispose()callsterm.destroy(), which closes the master fd.resize()has nodisposed/exitedcheck (unlikepause/resumeat:343-351) and callsterm.resize()straight away. I couldn't check whether node-pty itself refuses a resize afterdestroy(), becausenode_modulesisn't installed in this checkout. The adapter doesn't rely on that.packages/pty-daemon/src/Pty/Pty.ts:574-591:AdoptedPty.resize()runssttywithstdio: [this.fd, ...]and also has nodisposed/exitFiredcheck. Afterdispose()→reader.destroy()(:545),this.fdis closed and can be reused, so a resize could hand some other descriptor tostty. This is the same fd-reuse problem, in a second place.Suspected root cause
The
exitedcheck fromhandleInputwas never added tohandleResize. Neither PTY adapter protects its ownresize()after disposal, so nothing below the handler catches it either.Reproduction (bun:test sketch, in
handlers.test.ts)On current
main, I expect this test to fail:handleResizereturnsundefinedand the fake PTY records the new size. I haven't run it. Manual check: open a terminal running a shell that ignores SIGHUP for a moment (for example,trap '' HUP; sleep 5), close the pane, and resize before the process exits.Suggested fix
handleResize, add the same check ashandleInput, right after theENOENTcheck:resize()do nothing (or throw) oncedisposed/exitedis set.NodePtyAdapteralready does this inpause/resume. That closes the fd-reuse risk for any caller, not just this handler. It matters most forAdoptedPty, wherethis.fdgoes directly to a child process.The regression test above covers the handler. A small adapter-level test that calls
dispose()and thenresize()would cover the second change.