Skip to content

Don't double-close the child pty fd (fixes parallel Bad file descriptor) (#3975)#3976

Merged
gaborbernat merged 2 commits into
tox-dev:mainfrom
apoorvdarshan:fix-3975-pty-fd-double-close
Jul 8, 2026
Merged

Don't double-close the child pty fd (fixes parallel Bad file descriptor) (#3975)#3976
gaborbernat merged 2 commits into
tox-dev:mainfrom
apoorvdarshan:fix-3975-pty-fd-double-close

Conversation

@apoorvdarshan

Copy link
Copy Markdown
Contributor

Summary

Fixes #3975 — a regression in 4.56.2 (from #3974) where parallel runs intermittently fail with Bad file descriptor / Input/output error.

Root cause

In get_stream_file_no (the pty path used when running under a tty), the child fd is closed once the child has inherited it, and then closed again on generator teardown:

main_fd, child_fd = allocated_pty
try:
    yield child_fd
    os.close(child_fd)          # (1) close once inherited
    yield main_fd
finally:
    for fd in (child_fd, main_fd):   # (2) closes child_fd a *second* time
        with suppress(OSError):
            os.close(fd)

In a serial run the second os.close(child_fd) is a harmless suppressed EBADF. But under parallel execution, between (1) and (2) the freed fd number can be reused by a sibling run — so (2) closes that run's descriptor instead, causing the intermittent Bad file descriptor / Input/output error.

Fix

Track whether the child fd was already closed and skip it in the teardown loop, so each fd is closed exactly once. The master fd is still released (no leak), and the early-teardown path (before the child fd is inherited) still closes both.

Tests

  • Added test_get_stream_file_no_closes_each_pty_fd_once: it fails on main (the child fd is closed twice: [22, 22, 11]) and passes here.
  • The existing pty fd tests (test_local_subprocess_tty_closes_master_fd, test_pty_closes_fds_when_termios_fails) still pass; full tests/execute/local_subprocess/test_local_subprocess.py is green (50 passed, 1 skipped). ruff check clean. Changelog fragment added.

Disclosure: prepared with AI assistance; reviewed and verified locally.

get_stream_file_no closed the child pty fd once the child inherited it, then
closed it again on generator teardown. The second close is a harmless
suppressed EBADF in a serial run, but under parallel execution the freed fd
number can be reused by a sibling run in between, so the second close
corrupts the sibling's descriptor -> intermittent 'Bad file descriptor' /
'Input/output error'.

Track whether the child fd was already closed and skip it in the teardown
loop, so each fd is closed exactly once (the master fd is still released).

Fixes tox-dev#3975.
@gaborbernat gaborbernat merged commit 6f8dabc into tox-dev:main Jul 8, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parallel runs expose usage of closed fds

2 participants