You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[WSLC] Stop state-aware exec failures from corrupting the output they report on - #1307
Two ways a state-aware exec could report something untrue about a run it had already finished.
A failed exec corrupted its own output.exec streams the script's stdout as it arrives. When the run then failed — a timeout, a daemon fault, output truncation — the executor appended its JSON error envelope to that same stdout. The result was neither valid JSON nor clean program output: PRE_FAILURE_MARKER{"error":{"code":"backend_error",...}}. The envelope now goes to stderr for a live exec, leaving stdout owned solely by the script. Non-exec phases and exec dry-run are unchanged, because they stream nothing and their envelope is the only thing on stdout.
A truncated exec overwrote the reason it ended. When a caller does not drain output fast enough, the daemon drops bytes and latches a truncation flag. That flag then replaced the terminal outcome: a clean run lost the exit code it had produced, and a run that timed out or was cancelled lost the truncation notice entirely — the caller was told the output was complete when it was not. Truncation now travels alongside the outcome as its own frame, so a timeout still reports as a timeout, and a truncated clean run still fails the call but carries its exit code in details.exitCode.
Also corrects docs/wsl/wslc-state-aware.md, which documented the old overwriting behaviour as intentional.
🔗 References
Addresses defect 3 of #843. The truncation defects were surfaced while analysing it; the second one became reachable on every terminal variant when #1252 added TimedOut and Cancelled.
Of #843's four defects: 1 is closed (#1187, #954), 2's output half is closed (#1252) and its stdin half is blocked on the WSLc SDK surfacing WSLCProcessFlagsStdin (#1258), 4 is closed (#1167 → #1240). This is 3.
🔍 Validation
cargo fmt --all -- --check and cargo clippy --all-targets --features wslc -- -D warnings clean across wslc_common, wxc_wslc_daemon, wxc, and mxc_engine.
cargo test --workspace, plus new unit tests at each seam: the stream-selection branch in the executor, the daemon's frame sequence for every terminal variant, and the exit code surviving a truncation failure.
On a real WSL2 host:run_wslc_state_aware_tests.ps1, 82/82.
The stderr-framing fix has an E2E regression guard: a command that emits output and then fails asserts the marker is on stdout, no envelope fragment is on stdout, and the typed error is on stderr. Against the pre-fix binary that suite reports 3 failures and captures the corruption verbatim.
Truncation has no E2E coverage — provoking it means out-racing the daemon's bounded queue, which is inherently racy from PowerShell. It is covered by unit tests against in-memory duplex pipes only, so that behaviour has not been exercised against a real container.
PROTOCOL_VERSION goes 5 → 6 for the new frame. The persistent per-user daemon is refused by a newer client until it restarts, which is the existing exact-equality check working as intended.
Soham Das (SohamDas2021)
changed the title
Stop state-aware exec failures from corrupting the output they report on
[WSLC] Stop state-aware exec failures from corrupting the output they report on
Sep 28, 2026
The combined truncation flag is ignored when the daemon reports TimedOut or Cancelled. If either bounded queue overflowed before one of those terminals, the piped SDK caller still receives an ordinary timeout or Exited(-1) and has no indication that stdout/stderr is incomplete—the same false completeness this change is intended to eliminate. Preserve both facts in the waiter result (for example with a completion type that can carry the terminal outcome plus truncation) rather than dropping truncated for these arms.
| Exec, dispatch failed | Script's stdout, up to the point of failure | Single JSON envelope (`{error}`), after the script's stderr and any buffered MXC diagnostics |
`stdout` is authoritative: for non-exec phases it carries exactly one envelope; for exec
it carries either the script's output (success) or exactly one envelope (failure).
`stderr` is informational. MXC routes its diagnostic logger output to `stderr` in
state-aware mode so `stdout` remains parseable without sentinels. (One-shot dispatch
keeps its existing `stdout` logger behaviour — the stricter routing applies to
state-aware only.)
`stdout` is authoritative: for non-exec phases and exec dry-run it carries exactly one
envelope; for a non-dry-run exec it carries the script's output and nothing else. An exec
can fail after the script has already streamed output, so its error envelope goes to
`stderr` — appending it to `stdout` would leave neither the script's output nor the
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
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.
📖 Description
Two ways a state-aware exec could report something untrue about a run it had already finished.
A failed exec corrupted its own output.
execstreams the script's stdout as it arrives. When the run then failed — a timeout, a daemon fault, output truncation — the executor appended its JSON error envelope to that same stdout. The result was neither valid JSON nor clean program output:PRE_FAILURE_MARKER{"error":{"code":"backend_error",...}}. The envelope now goes to stderr for a live exec, leaving stdout owned solely by the script. Non-exec phases and exec dry-run are unchanged, because they stream nothing and their envelope is the only thing on stdout.A truncated exec overwrote the reason it ended. When a caller does not drain output fast enough, the daemon drops bytes and latches a truncation flag. That flag then replaced the terminal outcome: a clean run lost the exit code it had produced, and a run that timed out or was cancelled lost the truncation notice entirely — the caller was told the output was complete when it was not. Truncation now travels alongside the outcome as its own frame, so a timeout still reports as a timeout, and a truncated clean run still fails the call but carries its exit code in
details.exitCode.Also corrects
docs/wsl/wslc-state-aware.md, which documented the old overwriting behaviour as intentional.🔗 References
Addresses defect 3 of #843. The truncation defects were surfaced while analysing it; the second one became reachable on every terminal variant when #1252 added
TimedOutandCancelled.Of #843's four defects: 1 is closed (#1187, #954), 2's output half is closed (#1252) and its stdin half is blocked on the WSLc SDK surfacing
WSLCProcessFlagsStdin(#1258), 4 is closed (#1167 → #1240). This is 3.🔍 Validation
cargo fmt --all -- --checkandcargo clippy --all-targets --features wslc -- -D warningsclean acrosswslc_common,wxc_wslc_daemon,wxc, andmxc_engine.cargo test --workspace, plus new unit tests at each seam: the stream-selection branch in the executor, the daemon's frame sequence for every terminal variant, and the exit code surviving a truncation failure.run_wslc_state_aware_tests.ps1, 82/82.PROTOCOL_VERSIONgoes 5 → 6 for the new frame. The persistent per-user daemon is refused by a newer client until it restarts, which is the existing exact-equality check working as intended.✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see docs/pull-requests.md)📋 Issue Type
Microsoft Reviewers: Open in CodeFlow