Skip to content

[WSLC] Reject remaining policy fields the backend cannot honor - #1058

Merged
Soham Das (SohamDas2021) merged 4 commits into
mainfrom
user/sodas/wslc-rejection-hardening
Sep 4, 2026
Merged

[WSLC] Reject remaining policy fields the backend cannot honor#1058
Soham Das (SohamDas2021) merged 4 commits into
mainfrom
user/sodas/wslc-rejection-hardening

Conversation

@SohamDas2021

@SohamDas2021 Soham Das (SohamDas2021) commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

📖 Description


Summary

WSLc accepted five policy fields, carried them into the runner, and never read them. A caller asking for a UI lockdown, a firewall enforcement mode, inbound local-network access, policy preservation, or a container that outlives the run got a container that silently did not have the posture they asked for. Each is now an explicit policy_validation rejection.

Field Before Now
ui accepted + ignored, every phase rejected on every phase, both surfaces
network.allowLocalNetwork ignored at state-aware provision rejected at provision
network.enforcementMode accepted + ignored firewall/both rejected; capabilities accepted
lifecycle.preservePolicy accepted + ignored on one-shot rejected on one-shot
lifecycle.destroyOnExit false accepted, behaved as true false rejected on one-shot; true accepted

Rejection means abort, not teardown. All three entry surfaces validate before anything is created: ScriptRunner::run ahead of execute, SandboxBackend::spawn ahead of start_container, and the state-aware dispatcher ahead of each phase body — and connect_daemon() lives inside provision(), so a refused provision never even spawns the daemon.

ui is rejected by presence, not value. The parser materializes a full-lockdown ui block into every request, so an explicitly supplied lockdown ui is indistinguishable by value from an absent one — a value-based check would reject every WSLc config in the repo while letting the most restrictive request a caller can write through unenforced. This uses the parse-derived ContainerPolicy::ui_specified flag, mirroring IsolationSession.

enforcementMode, preservePolicy, and destroyOnExit are rejected by value instead, because their defaults honestly describe WSLc's behavior — destroyOnExit defaults to true, so bare configs are untouched.

destroyOnExit: false corrects a claim this PR originally made. Selecting WSLC_CONTAINER_FLAG_AUTO_REMOVE is not the same as honoring the field: StartedContainer owns the WslcSessionGuard, whose Drop terminates the session — and with it the session-scoped container — regardless of the flag, and the WSLc SDK has no cross-process re-attach. true and false therefore produce an identical observable outcome, which the repo's own fixture had already admitted in a comment. Accepting false promised a container that was already gone, so it is now refused and that fixture converted into a rejection test.

No wire, schema, or parser-gating changes — this is a domain-model behavior change only, so it lands while WSLc is still experimental.

Validation

wslc_common 211 tests (with and without link-wslcsdk), wxc_common 983, clean fmt and clippy on wslc_common / wxc_common / wxc --features wslc. All 43 tests/configs/wslc*.json pushed through the real parser + validator: 26 accept, 17 reject. Of the rejections, 16 are pre-existing (13 {{SANDBOX_ID}} placeholders, 3 named *_rejected_*) and 1 is the new fixture this PR adds. No config regressed into an unintended rejection.

The full WSLc one-shot E2E suite was run on a WSL2 host and is green, including both lifecycle fixtures.

On E2E coverage, precisely. One rejection is now asserted positively end-to-end: wslc_destroy_on_exit_false_rejected.json runs in the nightly WSLc suite and requires exit -1 plus the rejection message, so the payload provably never runs. The other three are not — no fixture supplies a ui block, a firewall enforcement mode, or preservePolicy. What the config corpus covers for those is the inverse and, for this change, the higher risk: that none of the existing configs regressed into an over-rejection. The positive path is covered by unit tests instead, including the near-miss acceptances (capabilities, allowLocalNetwork: false, absent ui) that a gate flipping between value- and presence-based would break. Positive E2E fixtures for the remaining three are better added alongside the promotion in PR 3, where these fields move to the top-level surface and the configs have to be rewritten anyway.

Related Issues

📋 Issue Type

  • Bug fix
  • Feature
  • Task

GitHub Actions runs the PR validation build automatically. The ADO pipeline
(MXC-PR-Build) is the Azure version of the PR pipeline, kept in parity with the GitHub
Actions build; it runs on merge to main, and Microsoft reviewers with write access can trigger it
on a PR with /azp run. See docs/pull-requests.md.

If the dependency-feed-check check fails on a new dependency, the crate must be added to
the feed before the PR can pass. See docs/pull-requests.md
for the steps.

Microsoft Reviewers: Open in CodeFlow

WSLc accepted four policy fields, carried them into the runner, and then
never read them. A caller asking for a UI lockdown, a firewall enforcement
mode, inbound local-network access, or policy preservation got a container
that silently did not have the posture they asked for. Close each hole with
an explicit policy_validation rejection.

Every rejection aborts before anything is created. All three entry surfaces
validate first: ScriptRunner::run ahead of execute, SandboxBackend::spawn
ahead of start_container, and the state-aware dispatcher ahead of each phase
body -- and connect_daemon() lives inside provision(), so a refused provision
never even spawns the daemon.

  ui                          rejected on every phase, both surfaces
  network.allowLocalNetwork   rejected at state-aware provision (one-shot
                              already rejected it)
  network.enforcementMode     firewall/both rejected; capabilities accepted
  lifecycle.preservePolicy    rejected on one-shot (state-aware already
                              rejects the whole lifecycle section at parse)

ui is rejected by presence, not value. UiPolicy::default() is full lockdown,
so an explicitly supplied lockdown ui is indistinguishable by value from an
absent one -- a value-based check would let the single most restrictive
request a caller can write through unenforced. This uses the parse-derived
ContainerPolicy::ui_specified flag, mirroring IsolationSession.

enforcementMode and preservePolicy are rejected by value instead, because
their defaults honestly describe WSLc's behavior: an all-or-nothing container
network with nothing per-host to enforce, and auto-remove teardown. Refusing
those for mere presence would be dishonest.

destroyOnExit stays honored -- it selects WSLC_CONTAINER_FLAG_AUTO_REMOVE --
so only preservePolicy is refused. A blanket lifecycle rejection would have
broken the wslc_destroy_on_exit_{true,false} configs; a test pins both values
still passing.

The two allowLocalNetwork messages differ deliberately. One-shot points
callers at experimental.wslc portMappings, but WslcProvisionPhase has no
portMappings field at all, so repeating that advice on the state-aware
surface would be a lie.

Rejection ordering is filesystem -> ui -> network, documented in the policy.rs
module header and pinned by precedence tests.

No wire, schema, or parser-gating changes: this is a domain-model behavior
change only, so it lands while WSLc is still experimental and the nightly
WSLc suite exercises the new rejections before the surface moves.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c23ddc86-3848-452e-8355-e11d2ffa9b7f
@SohamDas2021
Soham Das (SohamDas2021) requested review from a team and a balanced review from Copilot August 27, 2026 20:32
@SohamDas2021
Soham Das (SohamDas2021) requested a review from a team as a code owner August 27, 2026 20:32
@azure-pipelines

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

@SohamDas2021 Soham Das (SohamDas2021) changed the title [WSLC] Reject policy fields the backend cannot honor 2 [WSLC] Reject policy fields the backend cannot honor Aug 27, 2026

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.

Pull request overview

Adds fail-closed WSLc validation for policy fields the backend cannot enforce.

Changes:

  • Rejects unsupported UI, network enforcement, local-network, and lifecycle settings.
  • Applies validation before one-shot, streaming, and state-aware execution.
  • Adds regression tests and updates WSLc documentation.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/core/wxc_common/src/models.rs Documents WSLc use of UI presence tracking.
src/backends/wslc/common/src/wsl_container_runner.rs Adds one-shot policy validation.
src/backends/wslc/common/src/state_aware.rs Tests state-aware validation hooks.
src/backends/wslc/common/src/sandbox.rs Tests pre-start streaming validation.
src/backends/wslc/common/src/policy.rs Implements shared WSLc policy rejection rules.
docs/wsl/wslc-state-aware.md Updates the state-aware policy matrix.
docs/wsl/wsl-container-getting-started.md Documents unsupported one-shot policy fields.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/backends/wslc/common/src/policy.rs Outdated

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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Suppressed comments (1)

src/backends/wslc/common/src/wsl_container_runner.rs:2600

  • This test locks in incorrect behavior: destroyOnExit=false only omits WSLC_CONTAINER_FLAG_AUTO_REMOVE; the one-shot WslcSessionGuard still terminates the session at the end of the run and reaps the session-scoped container. Update this case to expect a policy rejection for false, while retaining the acceptance check for true.
        for destroy_on_exit in [true, false] {

Comment thread docs/wsl/wsl-container-getting-started.md Outdated
Comment thread src/backends/wslc/common/src/wsl_container_runner.rs
Comment thread .github/copilot-instructions.md Outdated
Copilot AI review requested due to automatic review settings August 27, 2026 21:57
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the user/sodas/wslc-rejection-hardening branch from cc7007a to fa97faa Compare August 27, 2026 21:57

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread src/backends/wslc/common/src/wsl_container_runner.rs
@SohamDas2021 Soham Das (SohamDas2021) changed the title [WSLC] Reject policy fields the backend cannot honor [WSLC] Reject remaining policy fields the backend cannot honor Aug 27, 2026
Copilot AI review requested due to automatic review settings August 28, 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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Comment thread .github/copilot-instructions.md Outdated
Comment thread docs/wsl/wsl-container-getting-started.md
Comment thread docs/wsl/wsl-container-getting-started.md
Comment thread docs/wsl/wslc-state-aware.md Outdated
Comment thread docs/schema.md Outdated
Comment thread src/backends/wslc/common/src/wsl_container_runner.rs Outdated
Comment thread src/backends/wslc/common/src/wsl_container_runner.rs Outdated
Comment thread src/core/wxc_common/src/models.rs Outdated
Comment thread tests/configs/wslc_destroy_on_exit_false_rejected.json
Comment thread src/backends/wslc/common/src/policy.rs Outdated
Comment thread docs/wsl/wsl-container-getting-started.md Outdated

@dhoehna Darren Hoehna (dhoehna) 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.

Look through some of the bigger comment sections and reduce them down.

Copilot AI review requested due to automatic review settings September 1, 2026 20:16

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.

🔵 Needs a closer look

The shared parser preempts the new state-aware allowLocalNetwork validator and still returns inappropriate one-shot guidance.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/backends/wslc/common/src/policy.rs:83

  • The real state-aware provision path never reaches this surface-specific check. convert_wire_config still rejects allowLocalNetwork=true for every WSLc request in config_parser.rs:1070-1095, before state-aware dispatch invokes validate_provision; that earlier error also tells state-aware callers to use experimental.wslc.portMappings, the dead-end this helper is intended to avoid. Exclude state-aware provision from the shared parser gate (while retaining the one-shot gate), then add a parsed state-aware request test so the externally observable path receives this validator's message.
  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@bbonaby Branden Bonaby (bbonaby) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

mostly still about comment clean up, other than that we should be ok

Comment thread src/backends/wslc/common/src/policy.rs Outdated
Comment thread src/backends/wslc/common/src/sandbox.rs Outdated
Comment thread src/backends/wslc/common/src/wsl_container_runner.rs Outdated
Comment thread src/backends/wslc/common/src/wsl_container_runner.rs Outdated
Comment thread src/backends/wslc/common/src/wsl_container_runner.rs Outdated
Comment thread src/backends/wslc/common/src/wsl_container_runner.rs Outdated
Comment thread src/backends/wslc/common/src/wsl_container_runner.rs Outdated
Copilot AI review requested due to automatic review settings September 3, 2026 15:45
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the user/sodas/wslc-rejection-hardening branch from 62ed1cd to cd8c127 Compare September 3, 2026 15:45

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

The lifecycle E2E test does not assert that the rejected payload never executes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment on lines +357 to +359
$null = $results.Add((Run-WslcTest "wslc_destroy_on_exit_false_rejected.json" `
-ExpectedExit -1 `
-OutputContains "destroyOnExit=false"))
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c23ddc86-3848-452e-8355-e11d2ffa9b7f

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.

🔵 Needs a closer look

The new E2E rejection test does not assert that the forbidden payload remains unexecuted.

Review details

Suppressed comments (1)

tests/scripts/run_wslc_all_tests.ps1:359

  • This E2E case does not actually enforce the stated “payload must never run” condition: Run-WslcTest only checks the exit code and required substring, so output containing both the rejection and the FAIL: marker would still pass. Add an explicit negative output assertion via the existing PostExitCheck hook.
$null = $results.Add((Run-WslcTest "wslc_destroy_on_exit_false_rejected.json" `
    -ExpectedExit -1 `
    -OutputContains "destroyOnExit=false"))
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@SohamDas2021
Soham Das (SohamDas2021) merged commit ab8930d into main Sep 4, 2026
27 checks passed
@SohamDas2021
Soham Das (SohamDas2021) deleted the user/sodas/wslc-rejection-hardening branch September 4, 2026 00:07
Soham Das (SohamDas2021) added a commit that referenced this pull request Sep 4, 2026
Resolves the conflicts caused by #1058 (WSLc rejection hardening) being
squash-merged to main as ab8930d while this branch already carried its
original commits, plus four other changes that landed alongside it.

Twelve files conflicted. Most were "both sides added a sibling", so the
resolution keeps both additions rather than either side alone:

- probe.rs / main.rs / platform.ts (+ tests): keep both the wslc_available
  gate from this branch and the hyperlight_available gate from #1059.
- platform.test.ts: split the merged block into separate "wslc availability
  gate" and "hyperlight availability gate" describes.
- sandbox.test.ts: keep main's new Linux backend-failure-reason test
  alongside the renamed windows_sandbox test.
- docs/schema.md: combine two independent promotions -- top-level wslc from
  this branch and top-level seatbelt (with extraMachLookups) from #1033 --
  leaving experimental holding only telemetry.
- sdk/node/README.md: keep both the Windows wslc-gating sentence and main's
  Linux unavailableReasons sentence.

The remaining five (policy.rs, wsl_container_runner.rs, the two WSLc test
configs, copilot-instructions.md) are pure promotion conflicts where this
branch's form -- wslc.portMappings, schema 0.9.0-alpha, the top-level wslc
block -- is the correct one.

Verified: cargo check/clippy/fmt/test --workspace --features wslc, the six
versioning and codegen gates, npm run build + npm test (331 tests), and
dotnet build of the C# SDK all pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c28d1c5e-fe3e-4634-9dd7-246528cc9edd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Copilot-Instructions PR modifies Copilot instruction files (.github/copilot-instructions.md or .github/instructions/)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants