Repository navigation
[WSLC] (2/2) Forward host stdin to create-and-run execution - #1564
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Gudge (MGudgin)
left a comment
There was a problem hiding this comment.
Summary
Review of this PR's own delta over #1562: base 019591ce27910c4cdf1e331faff855669bc38ec9, head e7afc864b8311b8be1e8196c99d7e2624ad13203. The base is the merge base and the local/GitHub source diffs match. Defects inherited unchanged from #1562 are not filed again here.
Nine findings survive the attribution pass: one P0 / critical SDK compatibility alert, four additional High findings, and four Medium findings. Eight are inline; original finding 6 is the description-only item below. Original numbering is retained for traceability. P0 is MXC's SDK compatibility category, not a security-vulnerability rating. Claim mismatches are non-blocking by themselves.
Checked with receipts
- The redirected CLI cases are present in
tests/scripts/run_wslc_all_tests.ps1:561-667, including binary-byte integrity, EOF timing, never-closing stdin, timeout, flood-warning and terminal cases. The existing scheduled WSLc dispatcher invokes this script; finding 5 specifically distinguishes the SDK path those CLI cases do not reach. forward_stdinuses a fixed 64 KiB buffer and explicitly treats BrokenPipe as benign (wsl_container_runner.rs:1463-1486). Finding 2 concerns other forwarding errors and lifetime coordination, not deliberate early reader closure.- The existing SDK timeout wrappers preserve the timeout flag: .NET at
MxcProcess.cs:523-525, and Node atcontainer-process.ts:327-330. That counter-evidence caused original finding 7 to be withdrawn rather than charged to this PR.
Finding 6 - Medium, non-blocking (description accuracy)
Attribution: claim_mismatch - two current PR-description claims do not match the implementation.
The description says, "Without stdin, nothing changes: the container's process is created exactly as before." SDK run still goes through piped spawn, which now selects InitStdin::Piped, and only then closes untaken input during wait. An empty redirected CLI pipe/NUL also selects piped creation. No input data is therefore not the same as preserving the old no-stdin process flags.
It also says, "Native stdin for .NET and Node is a local pipe forwarded to the container." .NET's StandardInput getter calls mxc_sandbox_take_stdin (MxcProcess.cs:189-205) and writes through the direct interruptible writer. Node's native-stdio transfer uses the added bridge_native_stdin local pipe (sandbox.rs:375-390). These are different ownership/exit-release paths.
Fix: distinguish no data from no input pipe, and describe the .NET/Node paths accurately; alternatively preserve closed process creation for APIs that explicitly supply no input. This claim-only finding is not a merge condition by itself.
Attribution and withdrawals
Original finding 7 (timeout finalization/warning classification) is withdrawn, final attribution invalid. The outer SDKs explicitly restore timeout status, the warning describes genuinely delayed exit callbacks rather than claiming normal completion, and a signalled process-exit event is positive termination evidence. I could not establish the asserted incorrect public timeout result or unsafe termination classification. It is not posted as a finding.
Verified pre-existing - not charged as existing defects
sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcProcess.csis byte-identical at this base and head. Finding 4 concerns the new early WSLc poll making its existing short-finalization assumption wrong; it does not refile the V1 wait change from #1562.scripts/ci/run_backend_validation_tests.ps1is byte-identical. Finding 5 isnewly_exposed_by_change: the new SDK bridge is not covered by the existing CLI-only dispatch, not an allegation that the old dispatcher was already defective.
Verification limits
This filing rechecked source, current claims, attribution, added-line anchors, existing tests and CI wiring. It did not rerun live WSLc or cross-platform .NET suites. The runtime findings describe verified code paths; the two coverage findings identify missing validation evidence rather than a claimed live test failure.
| return Ok(None); | ||
| // The exit event can signal long before the SDK's exit callback, | ||
| // and a poller must not wait out that gap. | ||
| if !self.started.exit_signalled() { |
There was a problem hiding this comment.
Finding 4 - High (correctness): early exit polling makes .NET finalization block cancellation and control.
Attribution: newly_exposed_by_change - .NET wait code is byte-identical in this PR, but this new event-based poll reports exit before the SDK callback, newly violating its short-finalization assumption.
MxcProcess.WaitCore sees that terminal poll and calls WaitBlocking (MxcProcess.cs:477-483). That holds _controlLock across mxc_sandbox_wait (:532-543). Native WSLc finalization can still perform two 30-second callback waits (wsl_container_runner.rs:699-723) after this earlier exit event. While inside that call, WaitAsync no longer observes its cancellation token and concurrent Kill/Dispose/warning access cannot take the same lock. Previously the poll waited for has_exited, so it did not hand off during this callback gap.
Fix: keep managed cancellation/control responsive during backend finalization, or distinguish root exit from completed finalization so polling does not imply a short native wait. Cover delayed callback completion while cancelling WaitAsync and attempting concurrent control.
There was a problem hiding this comment.
Kill is fixed in bd3b854. MxcProcess.Kill now returns at once when a wait has already seen the process exit and is finishing the native WSLc cleanup, instead of queuing behind _controlLock. The live test SpawnWithStdin_KillDoesNotWaitForTheCleanupOfAnExitedCommand covers it on the delayed-callback path.
WaitAsync cancellation, Dispose, and warnings access still wait for that cleanup. Making them independent of it needs a native change so kill and wait can run concurrently on one handle, which needs to be tracked as a follow-up.
e7afc86 to
83c4058
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Existing V1 compatibility and stdin lifecycle handling remain unresolved, and scheduled validation omits the new SDK scenarios.
8 open issues
Preserve V1 stdin EOF behavior by requiring explicit writable-stdin opt-in · New Propagate native stdin forwarding failures through process wait · New Close untaken stdin before managed exit polling begins · New Include .NET and stdin SDK suites in scheduled WSLc validation · New Keep wait cancellation and control operations responsive during callback finali… · New Close stdin on process exit to unblock pre-wait writes · New Bound blocked-write cleanup before joining the worker thread · New Kill the full redirected-input process tree on watchdog timeout · New
What changed in this PR
Extends #1562’s WSLc stdin support from container lifecycle execution to create-and-run execution through the CLI and SDKs.
Changes:
- Adds host stdin forwarding, writable SDK stdin, and interruptible pipe handling.
- Adds CLI, .NET, Node, and Rust coverage for input delivery, EOF, cancellation, and early exit.
- Updates backend guidance, API comments, and SDK changelogs.
| File | Description |
|---|---|
tests/scripts/run_wslc_all_tests.ps1 |
Adds nine CLI stdin scenarios. |
tests/configs/wslc_stdin.json |
Adds the shared stdin test configuration. |
src/mxc-sdk/src/lib.rs |
Updates Rust stdin availability documentation. |
src/mxc-sdk/src/core/mxc_common/process_util.rs |
Adds an interruptible pipe writer and tests. |
src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs |
Implements stdin forwarding, cleanup, and failure reporting. |
src/mxc-sdk/src/backends/wslc/common/sandbox.rs |
Exposes SDK stdin and bridges native pipe handles. |
sdk/node/tests/integration/wslc-e2e.test.ts |
Adds create-and-run stdin integration tests. |
sdk/node/src/v1/container-process.ts |
Updates the stdin property documentation. |
sdk/node/CHANGELOG.md |
Records writable WSLc stdin support. |
sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerWslcE2ETests.cs |
Adds live WSLc stdin and wait tests. |
sdk/dotnet/CHANGELOG.md |
Records WSLc spawn stdin behavior. |
docs/backends/wslc/wslc-state-aware.md |
Links to create-and-run stdin guidance. |
docs/backends/wslc/wsl-container-getting-started.md |
Documents stdin usage, EOF behavior, and limitations. |
Review effort: Balanced
| // The exit event can signal long before the SDK's exit callback, | ||
| // and a poller must not wait out that gap. | ||
| if !self.started.exit_signalled() { | ||
| return Ok(None); | ||
| } | ||
| self.started.close_stdin(); |
83c4058 to
e313f46
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The new Node stdin scenarios are neither packaged nor dispatched for scheduled WSLc host validation.
9 open issues
Close untaken stdin before managed exit polling begins Propagate native stdin forwarding failures through process wait Preserve V1 stdin EOF behavior by requiring explicit writable-stdin opt-in Schedule Node stdin cases in WSLc validation · New Kill the full redirected-input process tree on watchdog timeout Bound blocked-write cleanup before joining the worker thread Close stdin on process exit to unblock pre-wait writes Keep wait cancellation and control operations responsive during callback finali… Include .NET and stdin SDK suites in scheduled WSLc validation
Review effort: Balanced
e313f46 to
0684e32
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The new .NET and Node create-and-run stdin scenarios are not packaged and selected for scheduled WSLc validation.
10 open issues
Close untaken stdin before managed exit polling begins Propagate native stdin forwarding failures through process wait Preserve V1 stdin EOF behavior by requiring explicit writable-stdin opt-in WSLc validation omits new SDK create-and-run tests · New Schedule Node stdin cases in WSLc validation Kill the full redirected-input process tree on watchdog timeout Bound blocked-write cleanup before joining the worker thread Close stdin on process exit to unblock pre-wait writes Keep wait cancellation and control operations responsive during callback finali… Include .NET and stdin SDK suites in scheduled WSLc validation
Review effort: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved .NET cancellation and disposal blocking, alongside native handle and callback lifetime changes, require fixes and final human review.
2 open issues
9 resolved since last review
Close untaken stdin before managed exit polling begins Propagate native stdin forwarding failures through process wait Preserve V1 stdin EOF behavior by requiring explicit writable-stdin opt-in WSLc validation omits new SDK create-and-run tests Schedule Node stdin cases in WSLc validation Kill the full redirected-input process tree on watchdog timeout Bound blocked-write cleanup before joining the worker thread Close stdin on process exit to unblock pre-wait writes Include .NET and stdin SDK suites in scheduled WSLc validation
Review effort: Balanced
| _finishingExitedWait = true; | ||
| try | ||
| { | ||
| return WaitBlocking(); |
Gudge (MGudgin)
left a comment
There was a problem hiding this comment.
Approved with the remaining items from the October 26 disposition retained for follow-up: finding 2 still ignores an unsuccessful or unconfirmed container stop before closing stdin; finding 4 fixes Kill responsiveness but not wait cancellation, Dispose or warning access during native cleanup; finding 6 remains a PR-description mismatch. This approval does not mark those items fixed. No tracking issue was linked for the remaining finalization work when verified.
The other six filed findings are addressed. Original finding 7 remains withdrawn. The head and base are unchanged since re-verification; 24 targeted create-and-run, exit-watch, forwarding and pipe-cancellation tests passed. Live WSLc SDK scenarios could not be run locally because sandbox policy denied WSL access.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a9788898-f8e4-4968-a5ea-7e427ce1be93
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a9788898-f8e4-4968-a5ea-7e427ce1be93
28c1f32
bd3b854 to
28c1f32
Compare


📖 Description
WSLc create-and-run execution can now forward stdin. Before this change, a WSLc command started with
wxc-exec <config>,spawn, orrunalways saw end-of-file on stdin. ProcessContainer, Bubblewrap, LXC, Seatbelt, and IsolationSession already forward stdin, and Windows Sandbox buffers it up front, so WSLc was the one container backend that dropped it. #1562 added stdin to execution in an existing WSLc container; this PR covers create-and-run.What changes for users:
CLI.
wxc-execforwards its own stdin when it is a pipe, a file, orNUL, for exampletype input.txt | wxc-exec config.jsonorwxc-exec config.json < input.txt. When stdin is a terminal it is not forwarded: the command sees end-of-file at once, and a warning is printed to stderr, matching container lifecycle exec and Windows Sandbox.SDKs: stdin is opt-in. New
v1::spawn_with_stdin(Rust),MxcContainer.SpawnWithStdin/SpawnWithStdinAsyncandIContainerRunner.SpawnWithStdin/SpawnWithStdinAsync(.NET), andspawnWithStdin(Node) return a writable stdin, through a new FFI entry pointmxc_spawn_with_stdin_json. This mirrors [WSLC] (1/2) Forward host stdin to state-aware exec #1562'sspawn_in_container_with_stdin. Closing stdin gives the command end-of-file, and waiting on the process closes a stdin the caller never took. Plainspawn/Spawn/spawnandrun/Run/runare unchanged: a WSLc command still sees end-of-file at once.Ending input when the command stops. Stdin is closed as soon as the command exits (even before any wait), is killed, or is abandoned, so a write blocked on it fails instead of hanging. .NET writes the container's stdin directly; Node's native stdin is a local pipe that a pump forwards to the container, and the same rule applies to it.
Failure handling.
wait..NET
Killduring cleanup.MxcProcess.Killreturns at once when a wait has already seen the process exit and is finishing its cleanup. On WSLc, a command that leaves a large amount of stdin unread has its exit reported by the SDK about 50 seconds late, andKillused to queue behind that cleanup.Dispose, WaitAsync cancellation, and warnings access still wait for that cleanup; making them independent needs a native change so kill and wait can run concurrently on one handle, which needs to be tracked as a follow-up.Node warnings crash (existing bug). Reading a process's
warningsor output metadata in Node passed a Koffi temporary copy tomxc_string_free, which corrupted the native heap and crashed Node whenever the native side returned a value. The bindings now keep owned strings as opaque pointers, as the telemetry bindings already do. The WSLc late-exit warning made this reachable in the tests.CI. The WSLc scheduled validation now runs the .NET
MxcContainerWslcE2ETestsand Nodewslc-e2e.test.jscreate-and-run suites from the SDK bundle, alongside the lifecycle suites.A command that waits on a stdin that never closes is still ended by its
timeout, and a stdin that never closes does not delay a command that does not read it. Callers that do not opt in see no change: the container's process is created exactly as before.Known WSLc SDK limitation: a command that exits with more than about 1 MiB of stdin unread has its exit reported about 50 seconds late, and output it wrote after its input backed up may be lost. When that happens, a warning is reported.
🔗 References
Fixes #1258.
🔍 Validation
Release x64 build with
wslc, on Windows with WSL 3.0.4 and WSLc SDK 3.0.3. The build uses the vendored SDK because the MxcDependencies feed returned 401; that is expected.tests\scripts\run_wslc_all_tests.ps1before the changetests\scripts\run_wslc_all_tests.ps1after the changecargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo clippy -p mxc-sdk -p mxc_ffi --features wslc --all-targets -- -D warningscargo test --workspacecargo test -p mxc-sdk --features wslc --libcargo test -p mxc_ffi --features wslcdotnet test --solution Microsoft.Mxc.Sdk.slnx -p:MxcWithWslc=trueMXC_ENABLE_WSLC_TESTS=1and--filter-class "*WslcE2ETests"npm run typecheck(sdk/node)npm test(sdk/node)wslc-e2e,wslc-state-aware) withMXC_ENABLE_WSLC_TESTS=1build-windows-sdk-test-bundle.ps1,run_backend_validation_tests.ps1,run_wslc_all_tests.ps1The create-and-run E2E cases cover piped input, a redirected file, 1 MiB of every byte value checked with
sha256sum, end-of-file arriving only once host stdin closes, empty stdin, a host stdin that never closes with a command that ignores it and withcatand a timeout, a 32 MiB flood where output loss must come with the warning, and a terminal stdin that must warn and give end-of-file. The E2E helper kills the wholewxc-execprocess tree on timeout and bounds its output reads.The live .NET and Node tests cover:
Spawn/spawnandRungiving end-of-file;Kill, and byDispose;Killreturning promptly while a wait is finishing the late-exit cleanup.Unit tests cover the stdin writer and its cancellation (with bounded waits), the native stdin bridge (including releasing a blocked write and reporting write failures), the exit watcher, and CLI forwarding failures before and after the run ends.
The .NET build hit an existing
rc.exefailure forwxc-windows-sandbox-daemonunder MSBuild. I worked around it by building that native target with cargo first; the failure isn't caused by this change.✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see pull request builds)📋 Issue Type
Microsoft Reviewers: Open in CodeFlow