Skip to content

fix(ext/node): close libuv handle on HandleWrap.close() for new-style handles#32958

Merged
bartlomieju merged 4 commits intodenoland:mainfrom
bartlomieju:fix/tty-fd-leak
Mar 24, 2026
Merged

fix(ext/node): close libuv handle on HandleWrap.close() for new-style handles#32958
bartlomieju merged 4 commits intodenoland:mainfrom
bartlomieju:fix/tty-fd-leak

Conversation

@bartlomieju
Copy link
Copy Markdown
Member

@bartlomieju bartlomieju commented Mar 24, 2026

Summary

  • Fix file descriptor leak when closing TTY handles (e.g. from node-pty)
  • HandleWrap.close() was only calling the JS-side _onClose() callback for new-style uv_compat handles, but never calling uv_compat::uv_close() — so stop_tty() never ran and the underlying FD was never closed
  • Each PTY spawn via node-pty leaked its /dev/ptmx master FD
  • Fix: call uv_compat::uv_close() for Handle::New handles in HandleWrap.close()

Towards #32846

… handles

When a TTY (or other new-style uv_compat handle) was closed via
HandleWrap.close(), only the JS-side _onClose() callback was called.
The actual uv_compat::uv_close() was never invoked, so stop_tty()
never ran and the underlying file descriptor was never closed.

This caused /dev/ptmx FD leaks when using node-pty: each PTY spawn
leaked its master FD because tty.ReadStream.destroy() never closed it.

Fix: call uv_compat::uv_close() for Handle::New handles before
running the JS close path. This matches Node.js behavior where
uv_close() is always called to shut down the libuv handle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartlomieju
Copy link
Copy Markdown
Member Author

bartlomieju commented Mar 24, 2026

This needs a test Test added

bartlomieju and others added 3 commits March 24, 2026 19:44
Verify that destroying a tty.WriteStream properly closes the
underlying file descriptor. Creates 10 TTY streams under script(1)
(for a real PTY) and checks the fd count doesn't grow.

Without the HandleWrap.close() fix, this leaks 10 fds
(before=22 after=32).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Linux, script(1) requires -c to pass the command as a string,
while on macOS it takes the command as trailing arguments. Write the
helper to a temp file to avoid shell quoting issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartlomieju bartlomieju merged commit d1e7f67 into denoland:main Mar 24, 2026
112 checks passed
@kajukitli
Copy link
Copy Markdown
Contributor

nice fix overall. one thing i think is still missing in the regression test: it assumes script(1) exists. if someone runs tests/unit_node/tty_test.ts on a linux/mac box without script installed, new Deno.Command("script", ...) will fail the test for an environment issue rather than skip.

can we make that path skip gracefully when script is unavailable? that keeps the test focused on the fd leak instead of depending on a host utility being present.

@bartlomieju bartlomieju deleted the fix/tty-fd-leak branch March 24, 2026 21:21
bartlomieju added a commit to nathanwhit/deno that referenced this pull request Mar 25, 2026
Conflicts resolved:
- handle_wrap.rs: kept PR's close_handle() refactor which already
  includes the uv_compat::uv_close FD leak fix from main (denoland#32958)
- _tls_wrap.js: kept PR's TLS rewrite (main's old _wrapHandle code
  is superseded by the new implementation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants