Skip to content

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

Open
Jay (jay23606) wants to merge 3 commits into
microsoft:devfrom
jay23606:fix/executor-close-surviving-stdout
Open

Jay (jay23606) wants to merge 3 commits into
microsoft:devfrom
jay23606:fix/executor-close-surviving-stdout

Conversation

@jay23606

Copy link
Copy Markdown
Contributor

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

Copilot AI balanced review requested due to automatic review settings October 16, 2026 18:39

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.

🟡 Changes recommended

Deferred cleanup still permits unbounded response buffering and leaves stdout open after the reader exits.

2 open findings
What changed in this PR

Addresses #503 by bounding executor cleanup when a surviving child holds stdout open.

Changes:

  • Joins the protocol reader before closing stdout and reports a cleanup error if it remains blocked.
  • Adds a real-process regression test for inherited stdout.
File Description
scripts/​test_adversarial.py Tests bounded closure with a surviving child.
scripts/​nvx_tools/​adversarial.py Defers stdout closure until the reader finishes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/nvx_tools/adversarial.py Outdated
Comment thread scripts/nvx_tools/adversarial.py Outdated
Copilot AI balanced review requested due to automatic review settings October 16, 2026 20:20

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.

🔵 Needs a closer look

The regression test rejects a valid BrokenPipeError, causing scheduling-dependent failures on POSIX.

0 open findings

2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Accept BrokenPipeError when wrapper exits before request flushes

scripts/​test_adversarial.py:2638

On POSIX, the wrapper can exit before request() flushes its write. The shell redirects the background child's stdin to /dev/null, so the flush fails and leaves buffered data; _stdin.close() then raises BrokenPipeError. close() preserves that error rather than replacing it with ScriptError, even though cleanup finishes within two seconds. Accept BrokenPipeError here too, while retaining the elapsed-time assertion, so this valid scheduling order does not fail the regression test.

🧠 Review effort: Balanced

Jay (jay23606) and others added 2 commits October 18, 2026 22:14
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 microsoft#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>
@jay23606
Jay (jay23606) force-pushed the fix/executor-close-surviving-stdout branch from 01186fa to cc08768 Compare October 19, 2026 02:14
Copilot AI balanced review requested due to automatic review settings October 19, 2026 02:14

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.

🟡 Changes recommended

The new regression test can fail on a valid BrokenPipeError cleanup path depending on process scheduling.

1 open issue

Review effort: Balanced

Comment thread scripts/test_adversarial.py Outdated
…ed request

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 19, 2026 22:21

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

2 participants