Skip to content

fix(runtime): drop interactive writes to an exited peer instead of failing with EPIPE - #102

Open
TakalaWang wants to merge 1 commit into
wasm-oj:mainfrom
TakalaWang:fix/interactive-peer-exit
Open

TakalaWang wants to merge 1 commit into
wasm-oj:mainfrom
TakalaWang:fix/interactive-peer-exit

Conversation

@TakalaWang

Copy link
Copy Markdown
Contributor

Closes #101.

Why

When one side of an interact session exited or closed its stdin, the other side's next write failed with EPIPE, on the server (virtual_fs::Pipe) and in the browser (shared-memory pipes from #99). Judges that run both programs natively keep draining each program's stdout until it exits, so the writer never sees EPIPE: its next read gets EOF and it exits with its own verdict.

The common case: the contestant prints its last guess and exits without reading the reply. On 0.2.4 a CPython interactor that then prints correct fails with BrokenPipeError, exits 120, and the case becomes a system error. Both hosts' stdout wrappers record into the transcript before writing to the pipe, and CPython retries the failed flush, so the transcript holds correct\n five times. A C interactor's puts loses its newline, because wasi-libc writes the two iovecs separately and the second write fails.

Change

  • Server: InteractiveOutput::poll_write (crates/runtime-core/src/interactive.rs) treats BrokenPipe from the pipe as a successful write. The bytes are already in the transcript and are dropped.
  • Browser: StreamOutput::poll_write (crates/runtime-core/src/run/web_interactive.rs) does the same when the shared-memory pipe reports that its reader closed.
  • Reads are unchanged: a side that has exited, or closed its stdout, still delivers its buffered bytes and then EOF.
  • Transcript: contestantToInteractor and interactorToContestant record every byte each side wrote to stdout exactly once, up to its output limit, including bytes written after the peer exited. The judge sees the interactor's final correct/wrong line, and the transcript depends only on what the writer wrote, not on when the reader exited. Those bytes still count toward the writer's output limit, so a writer that keeps writing to a dead peer still ends with output-limit.
  • docs/library-contract.md, docs/architecture.md and CHANGELOG.md describe the semantics.
  • runtime-core_bg.wasm is rebuilt with pnpm run runtime:build (wasm-bindgen 0.2.127, Rust 1.97.1) and the runtime identity pins are refreshed, which changes cost profiles: wasm 9fe6417d…, source root 8d33f355…, identity 9e4f094c….

Verification

Tests:

  • runtime-core unit tests (WAT): the interactor drains to EOF and then writes after the contestant exited, exits 42, transcript correct\n once; the contestant reads the interactor's buffered bytes and then EOF; the contestant writes three times after the interactor exited, every write succeeds, exits 42. On main both write tests fail with errno 64 (EPIPE).
  • Server integration (WASM_OJ_RUN_JUDGE_INTEGRATION=1), real C and CPython interactors: each replies correct (exit 42) or wrong (exit 43) to a contestant that already exited, transcript once. A contestant gets EOF after the interactor exits, and its writes succeed. Both tests fail on main.
  • Strict-CSP suite: new interactive-interactor-exits-eof, the rewritten interactive-interactor-exits (writes after the interactor exited must succeed; it used to expect exit 32 for EPIPE), and interactive-reply-{after-exit,race}-{c,python}. The race variants reply right after reading, which is NOJV's report.

Before and after, CPython interactor replying to an exited contestant:

Host main (0.2.4) This PR
Server, 3 of 3 runs exit 120, BrokenPipeError, correct\n × 5 exit 42, correct\n
Chromium exit 120, × 5 exit 42, once
Firefox exit 120, × 5 exit 42, once

Commands, all passing:

  • cargo test, cargo fmt --check, cargo clippy --all-targets -- -D warnings (native and wasm32-unknown-unknown --features web), pnpm run runtime:check-web
  • pnpm run ci:verify (977 tests)
  • src/server/judge.integration.test.ts with WASM_OJ_RUN_JUDGE_INTEGRATION=1: 7 of 7
  • scripts/verify-browser-csp.mjs, full suite: Chromium 110 of 110, Firefox 110 of 110, WebKit 109 of 110. The WebKit failure is python-16mb-recursion_error, which also fails on main on this machine: WebKit 26.5 runs out of native stack first. The interactive fixtures pass in all three browsers. One Firefox run hit the known compile-stage timeout (Compiler stage did not produce a complete output within 55000 ms) on two fixtures, which passed when rerun.

Risks

  • This changes behaviour. A program that relied on EPIPE to notice that its peer was gone now has to read and see EOF instead. That matches native judging, and a peer's exit is still visible through EOF on reads.
  • A writer that loops forever writing to an exited peer now runs until its output, instruction or wall limit instead of dying on the first EPIPE.
  • The new runtime identity invalidates cached cost profiles, as each runtime-core change does.

🤖 Generated with Claude Code

…iling with EPIPE

Closes wasm-oj#101. When one side of an interact session exited or closed its stdin, its peer's next write failed with EPIPE on both hosts. A CPython interactor that replied to a contestant that had already exited died with BrokenPipeError and exit 120, and because the stdout wrapper records into the transcript before writing to the pipe, each retried flush added the reply again (five copies). Judges that run both programs natively keep draining each program's output until it exits, so the interactor reads EOF and exits with its own verdict.

InteractiveOutput (server) and StreamOutput (browser side Workers) now treat a broken pipe as a successful write: the bytes are already in the transcript and are dropped. Reads from a side that has exited still return the remaining buffered bytes and then EOF. The transcript records each byte a side writes exactly once, up to its output limit, including bytes written after the peer exited, so it depends only on what the writer wrote.

Tests cover both hosts: runtime-core unit tests for writes after either side exits and EOF after buffered bytes, server integration tests with C and CPython interactors exiting 42/43 after replying to an exited contestant, and strict-CSP browser fixtures for the same cases plus the reply-before-EOF race. runtime-core_bg.wasm is rebuilt and the runtime identity pins refreshed, which changes cost profiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TakalaWang

Copy link
Copy Markdown
Contributor Author

#103 (WebKit empty-loop fix) is stacked on this PR, because both rebuild runtime-core_bg.wasm and refresh the runtime identity pins. #104 (silently killed Workers) is independent.

This branch has not been deployed

No deployments
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.

Interactive writes fail with EPIPE after the other side exits, unlike a judge that keeps draining

1 participant