Repository navigation
fix(runtime): drop interactive writes to an exited peer instead of failing with EPIPE - #102
Open
TakalaWang wants to merge 1 commit into
Open
TakalaWang wants to merge 1 commit into
TakalaWang wants to merge 1 commit into
Conversation
…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>
This was referenced Oct 9, 2026
Contributor
Author
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #101.
Why
When one side of an
interactsession exited or closed its stdin, the other side's next write failed withEPIPE, 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 seesEPIPE: 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
correctfails withBrokenPipeError, 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 holdscorrect\nfive times. A C interactor'sputsloses its newline, because wasi-libc writes the two iovecs separately and the second write fails.Change
InteractiveOutput::poll_write(crates/runtime-core/src/interactive.rs) treatsBrokenPipefrom the pipe as a successful write. The bytes are already in the transcript and are dropped.StreamOutput::poll_write(crates/runtime-core/src/run/web_interactive.rs) does the same when the shared-memory pipe reports that its reader closed.contestantToInteractorandinteractorToContestantrecord 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 finalcorrect/wrongline, 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 withoutput-limit.docs/library-contract.md,docs/architecture.mdandCHANGELOG.mddescribe the semantics.runtime-core_bg.wasmis rebuilt withpnpm run runtime:build(wasm-bindgen 0.2.127, Rust 1.97.1) and the runtime identity pins are refreshed, which changes cost profiles: wasm9fe6417d…, source root8d33f355…, identity9e4f094c….Verification
Tests:
correct\nonce; 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. Onmainboth write tests fail with errno 64 (EPIPE).WASM_OJ_RUN_JUDGE_INTEGRATION=1), real C and CPython interactors: each repliescorrect(exit 42) orwrong(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 onmain.interactive-interactor-exits-eof, the rewritteninteractive-interactor-exits(writes after the interactor exited must succeed; it used to expect exit 32 forEPIPE), andinteractive-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:
main(0.2.4)BrokenPipeError,correct\n× 5correct\nCommands, all passing:
cargo test,cargo fmt --check,cargo clippy --all-targets -- -D warnings(native andwasm32-unknown-unknown --features web),pnpm run runtime:check-webpnpm run ci:verify(977 tests)src/server/judge.integration.test.tswithWASM_OJ_RUN_JUDGE_INTEGRATION=1: 7 of 7scripts/verify-browser-csp.mjs, full suite: Chromium 110 of 110, Firefox 110 of 110, WebKit 109 of 110. The WebKit failure ispython-16mb-recursion_error, which also fails onmainon 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
EPIPEto 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.EPIPE.🤖 Generated with Claude Code