Skip to content

[WSLC] (2/2) Forward host stdin to create-and-run execution - #1564

Merged
Soham Das (SohamDas2021) merged 2 commits into
mainfrom
sohamdas2021-wslc-one-shot-stdin
Oct 27, 2026
Merged

Soham Das (SohamDas2021) merged 2 commits into
mainfrom
sohamdas2021-wslc-one-shot-stdin

Conversation

@SohamDas2021

@SohamDas2021 Soham Das (SohamDas2021) commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

📖 Description

WSLc create-and-run execution can now forward stdin. Before this change, a WSLc command started with wxc-exec <config>, spawn, or run always 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-exec forwards its own stdin when it is a pipe, a file, or NUL, for example type input.txt | wxc-exec config.json or wxc-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 / SpawnWithStdinAsync and IContainerRunner.SpawnWithStdin / SpawnWithStdinAsync (.NET), and spawnWithStdin (Node) return a writable stdin, through a new FFI entry point mxc_spawn_with_stdin_json. This mirrors [WSLC] (1/2) Forward host stdin to state-aware exec #1562's spawn_in_container_with_stdin. Closing stdin gives the command end-of-file, and waiting on the process closes a stdin the caller never took. Plain spawn / Spawn / spawn and run / Run / run are 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.

    • If the container starts but its stdin cannot be obtained, the container is stopped and the run fails, rather than leaving a command waiting for input nobody can send.
    • In the CLI, if reading the executor's stdin or writing the container's stdin fails while the command runs, the container is stopped before the command sees end-of-file, and the run is reported as failed, even if it also timed out. A failure after the run is over is ignored.
    • In the SDKs, a failure forwarding Node's native stdin, other than the command closing its stdin, is reported by wait.
  • .NET Kill during cleanup. MxcProcess.Kill returns 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, and Kill used 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 warnings or output metadata in Node passed a Koffi temporary copy to mxc_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 MxcContainerWslcE2ETests and Node wslc-e2e.test.js create-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.

Command Result
tests\scripts\run_wslc_all_tests.ps1 before the change Passed: create-and-run 40/40, container lifecycle 137/137
tests\scripts\run_wslc_all_tests.ps1 after the change Passed: create-and-run 49/49 (9 stdin cases), container lifecycle 139/139 (#1562's current suite)
cargo fmt --all -- --check Passed
cargo clippy --workspace --all-targets -- -D warnings Passed
cargo clippy -p mxc-sdk -p mxc_ffi --features wslc --all-targets -- -D warnings Passed
cargo test --workspace Passed
cargo test -p mxc-sdk --features wslc --lib Passed: 2833 passed, 0 failed
cargo test -p mxc_ffi --features wslc Passed
dotnet test --solution Microsoft.Mxc.Sdk.slnx -p:MxcWithWslc=true Passed: 324 passed, 0 failed, 47 skipped (live-host tests)
Same, with MXC_ENABLE_WSLC_TESTS=1 and --filter-class "*WslcE2ETests" Passed: 16/16, 0 skipped (9 create-and-run, 7 lifecycle)
npm run typecheck (sdk/node) Passed
npm test (sdk/node) Passed: 432 passed, 0 failed, 17 skipped
Node WSLc integration (wslc-e2e, wslc-state-aware) with MXC_ENABLE_WSLC_TESTS=1 Passed: 9/9. The unread-stdin test also passed 3 more times in isolation, twice on the late-exit path that crashed before the binding fix.
PowerShell parse check of build-windows-sdk-test-bundle.ps1, run_backend_validation_tests.ps1, run_wslc_all_tests.ps1 Passed

The 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 with cat and 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 whole wxc-exec process tree on timeout and bounds its output reads.

The live .NET and Node tests cover:

  • byte-exact forwarding;
  • wait closing an untaken stdin;
  • plain Spawn / spawn and Run giving end-of-file;
  • a command that exits with 4 MiB of stdin unread under a 40 s timeout, which must not be reported as timed out;
  • a write blocked on stdin being released by the command exiting before any wait, by Kill, and by Dispose;
  • Kill returning 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.exe failure for wxc-windows-sandbox-daemon under MSBuild. I worked around it by building that native target with cargo first; the failure isn't caused by this change.

✅ Checklist

📋 Issue Type

  • Bug fix
  • Feature
  • Task
Microsoft Reviewers: Open in CodeFlow

@SohamDas2021
Soham Das (SohamDas2021) requested a review from a team as a code owner October 22, 2026 22:11
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@MGudgin Gudge (MGudgin) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_stdin uses 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 at container-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.cs is 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.ps1 is byte-identical. Finding 5 is newly_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.

Comment thread src/mxc-sdk/src/backends/wslc/common/sandbox.rs Outdated
Comment thread src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs
Comment thread src/mxc-sdk/src/backends/wslc/common/sandbox.rs
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() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/mxc-sdk/src/backends/wslc/common/sandbox.rs
Comment thread src/mxc-sdk/src/core/mxc_common/process_util.rs Outdated
Comment thread tests/scripts/run_wslc_all_tests.ps1 Outdated
Comment thread src/mxc-sdk/src/backends/wslc/common/sandbox.rs
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the sohamdas2021-wslc-one-shot-stdin branch from e7afc86 to 83c4058 Compare October 23, 2026 22:44
Copilot AI balanced review requested due to automatic review settings October 23, 2026 22:44

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

Existing V1 compatibility and stdin lifecycle handling remain unresolved, and scheduled validation omits the new SDK scenarios.

8 open issues
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

Comment thread src/mxc-sdk/src/backends/wslc/common/sandbox.rs
Comment thread src/mxc-sdk/src/backends/wslc/common/sandbox.rs Outdated
Comment thread src/mxc-sdk/src/backends/wslc/common/sandbox.rs
Comment thread sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerWslcE2ETests.cs
Comment on lines +487 to +492
// 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();
Comment thread src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs Outdated
Comment thread src/mxc-sdk/src/core/mxc_common/process_util.rs Outdated
Comment thread tests/scripts/run_wslc_all_tests.ps1 Outdated
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the sohamdas2021-wslc-one-shot-stdin branch from 83c4058 to e313f46 Compare October 23, 2026 23:20
Copilot AI balanced review requested due to automatic review settings October 23, 2026 23: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.

Comment thread sdk/node/tests/integration/wslc-e2e.test.ts Outdated
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the sohamdas2021-wslc-one-shot-stdin branch from e313f46 to 0684e32 Compare October 26, 2026 17:41
Copilot AI balanced review requested due to automatic review settings October 26, 2026 17:41

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.

Comment thread src/mxc-sdk/src/backends/wslc/common/sandbox.rs
Copilot AI balanced review requested due to automatic review settings October 26, 2026 19:50

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.

Comment on lines +563 to +566
_finishingExitedWait = true;
try
{
return WaitBlocking();

@MGudgin Gudge (MGudgin) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Base automatically changed from sohamdas2021-wslc-stdin-forwarding to main October 27, 2026 15:52
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
@SohamDas2021
Soham Das (SohamDas2021) merged commit 48e0920 into main Oct 27, 2026
30 checks passed
@SohamDas2021
Soham Das (SohamDas2021) deleted the sohamdas2021-wslc-one-shot-stdin branch October 27, 2026 16:18
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.

[WSLC] Allow commands to receive input

3 participants