Skip to content

adversarial: keep executor close bounded when a child holds stdout - #657

Merged
Pedro Henrique Penna (ppenna) merged 3 commits into
devfrom
ppenna/executor-close-surviving-stdout-20261020
Oct 20, 2026
Merged

Pedro Henrique Penna (ppenna) merged 3 commits into
devfrom
ppenna/executor-close-surviving-stdout-20261020

Conversation

@ppenna

Copy link
Copy Markdown
Contributor

Replaces #586 by Jay (@jay23606), whose commits are carried here unchanged (head 756f215b2). It is opened from a microsoft/nvx branch so that CI runs on a stable head: automated rebases of the original kept superseding its runs.


Fixes #503 (executor close blocks on inherited stdout).

This replaces #532, which was closed. This branch is a single commit on current dev, with no merge commits.

Summary

  • ExecutorClient.close() closed the buffered stdout stream after the wrapper exited. If a child it started still held stdout, the protocol reader was blocked in readline and held that stream's lock, so close() waited until the child exited (about 20 seconds in the reproduction).
  • close() now joins the reader with its existing timeout first. If the reader is still blocked, stdout is not closed, and close() raises executor output is still held open by a surviving process. Otherwise stdout closes as before. The reader is a daemon thread and ends when the surviving process exits.
  • Known limit: the surviving child is not terminated. Containing it (for example with a Windows job object) is a separate change.

Test

  • ExecutorCloseTests.test_close_is_bounded_when_a_surviving_child_holds_executor_stdout runs a real wrapper that exits after starting a child that inherits stdout. Before the change, close() blocked about 20 seconds and returned without error. After the change it raises within the bound. The test kills the surviving child in a finally block.
  • python -B -m unittest test_adversarial: 71 tests, OK (6 skipped, as before).
  • ruff check and ruff format --check are clean for both files.
  • Not run: the full nvx.py verify, which needs the OpenVMM submodule, and hardware-backed campaigns.

🤖 Generated with Claude Code

Jay (jay23606) and others added 3 commits October 20, 2026 16:12
ExecutorClient.close() closed the buffered stdout stream after the wrapper had
exited. If a child process it started still held stdout, the protocol reader was
blocked in readline and holds the buffered stream's lock, so close waited until
that child exited. The reader join with its timeout ran only after the close.

Join the reader first. If it is still blocked, do not close stdout; report that
a surviving process holds the executor output. The reader is a daemon thread and
ends when that process exits.

Adds a real-process regression test in which the wrapper exits and leaves a
child holding stdout. Before the change, close blocked for the child's full
lifetime and returned without error.

Fixes #503 (executor close blocks on inherited stdout)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
When close gives up on a surviving process, the reader kept queueing its output
for a client that no longer reads, and stdout was closed only on the synchronous
path, so a deferred close never released the pipe.

Once close abandons the reader, the reader drains and discards what the surviving
process writes, and the reader closes stdout when its output ends.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
…ed request

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused changes address the blocking cleanup path with regression coverage and no identified blocking issues.

No open issues 🎉

What changed in this PR

Fixes #503 by keeping executor cleanup bounded when a surviving child holds stdout open.

Changes:

  • Moves stdout closure to the reader thread and reports when bounded cleanup leaves it running.
  • Discards output received after the reader is abandoned.
  • Adds regression coverage for bounded close, stream closure, and discarded output.
File Description
scripts/​test_adversarial.py Adds executor cleanup and reader regression tests.
scripts/​nvx_tools/​adversarial.py Implements reader-owned stdout closure and bounded cleanup.

Review effort: Balanced

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.

Track adversarial executor bugs: timeout cleanup blocks on inherited stdout

3 participants