Skip to content

[WSLC] Pull a missing image instead of failing the run - #1318

Open
Soham Das (SohamDas2021) wants to merge 9 commits into
mainfrom
sohamdas2021-wslc-image-prepull
Open

Soham Das (SohamDas2021) wants to merge 9 commits into
mainfrom
sohamdas2021-wslc-image-prepull

Conversation

@SohamDas2021

@SohamDas2021 Soham Das (SohamDas2021) commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📖 Description

Running a config whose image wasn't already cached failed, and told you to run wxc-exec.exe --setup-wslc --image <name> and try again. The run now pulls the image and carries on, so a config naming a fresh image works on the first invocation.

Pull failures split by whether a retry could succeed. A reference the registry refuses and a registry the host is barred from are Rejected; no network, DNS failure, or a registry that is down is Host and retryable.

--setup-wslc stays. It is no longer a prerequisite, but it still moves the download off the critical path and is how you populate a cache for a host that cannot reach a registry.

A pull only happens where it is allowed. A config that denies egress is refused rather than pulled. An administrative policy can restrict which registries a pull may reach, and an unreadable policy refuses the pull rather than ignoring it. A pull that stops making progress is abandoned at a deadline instead of holding the host indefinitely, and a pull that resolves a tag records the digest it produced.

A reference naming a digest always goes to the registry, and records none. The digest a reference carries is not the digest the local store reports, and nothing exposes a mapping between the two, so matching such a reference against the cache could serve content nobody asked for, and reporting the store's digest against it would name content the caller did not pin. An image supplied as a tar records none either, since nothing was resolved.

On the state-aware path a pull no longer occupies the daemon's single lifecycle worker: deciding what to do with the image stays on the worker, and a pull parks the request until it lands. Measured against a pull that never responds, with the daemon identity pinned so both requests provably share one worker, a cached provision takes 0.1s whether or not the stalled pull is running.

🔗 References

No existing issue. Supersedes the two-call pattern introduced by #165.

🔍 Validation

  • cargo fmt --all --check, cargo clippy --workspace --all-targets --features wslc -- -D warnings
  • 315 unit tests in wslc_common, 44 in the daemon
  • WSLC E2E on a WSL2 host: 116/116 (35 one-shot + 81 state-aware)
  • Cold cache pulls then executes in one invocation; a warm cache reuses without pulling; an unreachable registry fails in ~5s with offline guidance; a nonexistent image is rejected without it
  • Live-checked on a cold cache that a denied egress posture refuses the pull, that a registry outside the allowlist is refused, and that the deadline abandons a pull that never responds
  • Node state-aware SDK suite passes against the in-process library. The Rust and .NET SDK suites pass but stop at the request boundary, so they show this breaks no contract rather than exercising the pull

Commits 1 and 2 claim no behavior change; that was checked by normalizing both original function bodies and the merged one and diffing — after substituting the config accessors and the log prefix, the remainder is identical.

Cargo.lock gains one line. winreg was already a workspace dependency and is now used to read the registry policy, so no new external dependency enters the graph.

The later commits address review findings. A provision whose client timed out mid-pull left a container nobody could reach, holding the daemon's idle watchdog above zero forever — and the client's own timeout is what hid it, because the abandoned reader kept the connection open and the daemon never learned its reply had not arrived. The client now closes that connection, and a sandbox that still cannot be released after repeated attempts stops counting against the watchdog so the daemon can shut down, deleting the container when it does. The offline advice pointed at --setup-wslc, which shares the same pull and fails identically on that host. The cache-miss path had no committed test, since the E2E preflight pre-pulls every image.

✅ Checklist

📋 Issue Type

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

Copilot AI balanced review requested due to automatic review settings September 28, 2026 21:26
@SohamDas2021
Soham Das (SohamDas2021) requested a review from a team as a code owner September 28, 2026 21:26
@azure-pipelines

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

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@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

Requesting changes for three verified blockers in the automatic image-pull path:

  1. Registry pulling is an unbounded synchronous SDK call on the daemon's single lifecycle worker.
  2. The new abandoned-provision cleanup observes the internal worker reply channel, which remains open during real named-pipe client timeouts/disconnects.
  3. A request declaring network.egress.default: deny can now cause a configuration-directed host-network registry fetch before container enforcement exists.

Verification receipts: reviewed head d1688d35cdbc; all three anchors are added lines in the 19-file PR patch. The head is behind the current base (merge-base 86fb3d2ab, current base snapshot 3b0ac9c25), but the reviewed head and findings are unchanged.

The move/deduplication itself looked sound; these requests are about the new pull and ownership semantics.

Comment thread src/backends/wslc/common/src/image.rs
Comment thread src/backends/wslc/daemon/src/session_manager.rs Outdated
Comment thread tests/configs/wslc_cold_cache_pull.json Outdated
"commandLine": "echo COLD_CACHE_PULL_OK"
},
"network": {
"egress": { "default": "deny" }

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.

High (security/policy) — Settle the deny-policy contract before locking this behavior in.

Attribution: introduced_by_change — this new fixture intentionally demonstrates that a request declaring deny egress may still trigger a host-side pull on cache miss.

The pull happens before the container exists, using the host/session network and a destination selected by wslc.image. The validator still accepts egress.default: deny as the isolated WSLC posture, so documenting this exception does not provide enforcement or explicit caller authorization.

Fix: Either reject cache-miss pulls under isolated posture and require a prewarmed cache/imageTarPath, or add an explicit policy field authorizing host-side registry access and include it in validation, documentation, and policy identity.

@SohamDas2021 Soham Das (SohamDas2021) Sep 29, 2026 •

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.

Taken the fail-closed option. resolve_image now takes a RegistryAccess that each caller derives from the posture it already computes (policy::network_is_isolated one-shot, NetworkMode on the daemon), so a cache miss under a deny posture is rejected, naming the three ways forward: warm the cache, set imageTarPath, or allow egress. --setup-wslc is deliberately unaffected.

Fixture split accordingly: wslc_cold_cache_pull.json declares allow and covers the pull, and a new wslc_cold_cache_denied_egress.json asserts the refusal and that the workload never runs. Verified live.

One thing to weigh: a config that omits network is isolated by default, so this refuses those too — including wslc_python_hello.json in the getting-started guide, which passes E2E only because the suite pre-pulls.

A wslc.allowImagePull opt-in is the proper fix, but stateAwareWslc is pinned at 0.9.0-alpha, so it needs a version promotion rather than an added field — its own PR. Happy either way: land this now, or hold for the opt-in.

@microsoft-github-policy-service microsoft-github-policy-service Bot added the Needs-Author-Feedback Waiting for additional information or action from the issue or pull-request author. label Sep 28, 2026

@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.

Supplementary Medium and Low findings

These are additional non-blocking findings from the adversarial review. The existing requested-changes verdict remains based on the three High findings.

Findings outside the diff / claim locations

Medium (documentation drift) — Canonical policy documentation still describes the deny posture as isolated without the provision-time pull exception. docs/wsl/wslc-state-aware.md:119-139 — Attribution: newly_exposed_by_change. The PR changes provision so a cache miss contacts the registry before container enforcement, but only updates the test-prerequisite line in this lifecycle guide; the policy matrix remains unchanged. docs/schema.md is also outside the PR patch and has no caveat near wslc.image. Add the host-side pull exception to both authoritative locations.

Low (proportionality) — Roughly half the changed LOC is an optional pure relocation. commit 84fe92c94851 message — Attribution: introduced_by_change. The move is verified as effectively verbatim and is not a correctness concern, but it substantially increases review volume for the feature. No fix is required; consider separating such moves in future changes.

Attribution notes

  • docs/schema.md and the policy-matrix lines are not modified by the PR; they are retained only because the new behavior makes their existing wording newly incomplete.
  • The stale tests/scripts/README.md wording is byte-identical and too minor to post as a separate finding.

Comment thread src/backends/wslc/daemon/src/session_manager.rs Outdated
Comment thread src/backends/wslc/common/src/image.rs
Comment thread src/backends/wslc/common/src/image.rs
Comment thread src/backends/wslc/common/src/image.rs Outdated
Comment thread tests/scripts/run_wslc_all_tests.ps1 Outdated
Comment thread src/backends/wslc/common/src/image.rs
Comment thread tests/scripts/run_wslc_all_tests.ps1 Outdated
Comment thread src/backends/wslc/common/src/image.rs Outdated
Comment thread src/backends/wslc/common/src/image.rs Outdated
Comment thread tests/scripts/run_wslc_all_tests.ps1
Copilot AI review requested due to automatic review settings September 29, 2026 00:04
@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs-Attention Requires attention or a decision from the MXC maintainers. and removed Needs-Author-Feedback Waiting for additional information or action from the issue or pull-request author. labels Sep 29, 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.

Copilot review overview

🟡 Changes recommended

The actual client-timeout path can still orphan a provisioned sandbox, and the cleanup test does not exercise that path.

Review effort: Balanced
Findings: 2 High severity · 1 Medium severity · 1 Low severity

Open (4)

Comment thread src/backends/wslc/daemon/src/control_server.rs Outdated
Comment thread src/backends/wslc/daemon/src/control_server.rs Outdated
Comment thread src/backends/wslc/daemon/src/control_server.rs
Comment thread src/backends/wslc/common/src/wsl_container_runner.rs
Comment thread sdk/node/tests/integration/wslc-state-aware.test.ts
Copilot AI review requested due to automatic review settings September 29, 2026 03:00

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/backends/wslc/common/src/registry_policy.rs Outdated
Comment thread src/backends/wslc/common/src/image.rs Outdated
Comment thread src/backends/wslc/common/src/image.rs Outdated
Comment thread tests/scripts/run_wslc_all_tests.ps1
Comment thread src/backends/wslc/common/src/daemon_client.rs
Comment thread src/backends/wslc/common/src/image.rs
Comment thread src/backends/wslc/common/src/registry_policy.rs Outdated

@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.

Re-review after e9972527da9c

The update addresses or makes moot 12 of the 15 original findings. The pure-relocation proportionality note required no code change. I am keeping changes requested for two blockers: the pull deadline is not a hard wall-clock deadline, and the new test override bypasses the administrative registry policy it is meant to enforce.

Original finding resolution

# Status Verification
1 Partially addressed — still blocking Added a 540s progress-callback deadline, but WSLC invokes the callback only for Docker response-body progress chunks; a silent DNS/connect/header stall produces no callback and can still hold the single worker indefinitely.
2 Addressed Cleanup moved to response delivery; client timeout cancels the blocked pipe read; delivery and vanished-client tests pass.
3 Addressed Cache misses under deny posture now fail closed; dedicated E2E fixture asserts refusal.
4 Addressed Undelivered cleanup retries, then retires the entry from the live count while preserving shutdown cleanup.
5 Partially addressed — replacement has a new blocker Registry allowlist and digest audit were added, but the production environment override can replace HKLM policy.
6 Addressed Unknown HRESULT remediation is now conditional rather than diagnosed as network failure.
7 Addressed Untagged references normalize to :latest; unit and live evidence cover reuse.
8 Addressed E2E now distinguishes pull from cache hit and asserts no second pull.
9 Addressed Pure action-selection and name-matching functions have unit coverage.
10 Addressed E2E asserts image-not-found-specific guidance; classification unit tests remain.
11 Moot after redesign Deny posture no longer pulls, so the isolation documentation is accurate again.
12 Addressed SDK initialization moved to sdk_init, breaking the module cycle.
13 Addressed SDK text is control-character-sanitized and character-boundary capped.
14 Addressed Cold-cache purge refuses a reparse-point root.
15 Acknowledged Optional relocation remains non-blocking and needs no code change.

Verification

  • cargo test -p wslc_common: 296 passed.
  • cargo test -p wxc_wslc_daemon: 41 passed, 2 host-only ignored; daemon IPC 1 passed, 1 host-only ignored.
  • Diff formatting check passed.
  • The current LXC CI failure is unrelated to this PR: the version-comparison fixture now differs only in $schema; the latest main LXC workflow subsequently passed.

Comment thread src/backends/wslc/common/src/image.rs
Comment thread src/backends/wslc/common/src/registry_policy.rs Outdated
@microsoft-github-policy-service microsoft-github-policy-service Bot added the Needs-Author-Feedback Waiting for additional information or action from the issue or pull-request author. label Sep 29, 2026
Copilot AI balanced review requested due to automatic review settings September 29, 2026 22:21

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/backends/wslc/common/src/wsl_container_runner.rs Outdated
Comment thread src/backends/wslc/common/src/image.rs Outdated
Comment thread src/backends/wslc/daemon/src/session_manager.rs Outdated
Comment thread src/backends/wslc/daemon/tests/daemon_ipc.rs Outdated
Copilot AI balanced review requested due to automatic review settings September 29, 2026 23:08

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.

Copilot review overview

🟡 Changes recommended

Timeout races and incomplete deadline enforcement can still orphan state or hang pulls, while new E2E coverage is nondeterministic.

Review effort: Balanced
Findings: 2 High severity · 4 Medium severity

Open (6)
Resolved since last review (1)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Short client timeouts leave no pull deadline headroom

src/​backends/​wslc/​common/​src/​image.rs:64

For every supported client timeout of 60 seconds or less, subtracting PULL_HEADROOM yields zero and this fallback returns the entire client timeout. The daemon's pull deadline then fires at the same instant as the client's response deadline, leaving no time to deliver the pull failure and defeating this function's invariant. Reserve a positive fraction of short timeouts as headroom as well, and update the short-deadline tests accordingly.

Medium severity Synchronous pulls can exceed the documented timeout indefinitely

src/​backends/​wslc/​common/​src/​image.rs:172

The timeout is checked only from pull_progress, while this SDK call blocks synchronously. As the new start_pull documentation notes, a transfer stalled before its next progress callback cannot be stopped this way. The one-shot runner and --setup-wslc both call pull_image directly, so they can exceed the documented 540-second bound indefinitely. These paths need an external deadline with session-lifetime handling, not only a callback deadline.

Medium severity Offline warm-cache runs incorrectly require a registry pull

tests/​scripts/​run_wslc_all_tests.ps1:405

This always purges a dedicated store and then requires a successful registry pull. Consequently the documented offline warm-cache invocation (-SkipSetup, or a host with only the default cache populated) now fails here despite the script prerequisites saying registry access or cached images is sufficient. Gate the cold-cache block behind an explicit network-enabled option/probe, or update the runner's prerequisites so registry access is mandatory.

Comment thread src/backends/wslc/common/src/daemon_client.rs
Comment thread src/backends/wslc/daemon/src/session_manager.rs Outdated
Copilot AI balanced review requested due to automatic review settings September 30, 2026 18:27
Pure code move, no behavior change. Collects the WSLc image-handling code
into `wslc_common::image` so the one-shot runner and the state-aware daemon
read it from one place.

Moved verbatim:
  wsl_container_runner.rs -> image.rs   TarFormat, detect_tar_format,
                                        import_image_from_tar, and the tar
                                        detection tests
  container_steps.rs      -> image.rs   resolve_image

The only edits to moved lines are the path qualifiers that had to drop when
associated functions became free functions (`Self::detect_tar_format` ->
`detect_tar_format`, `WSLContainerRunner::import_image_from_tar` ->
`import_image_from_tar`), plus one rustfmt reflow that fits `let
first_component = entry_path` on a single line at the shallower indent.

`setup_pull_image` stays in `wsl_container_runner.rs` for now: it reaches the
private `init_and_load_sdk`, and moving it would widen that visibility for no
present gain. It moves in the commit that extracts the pull primitive.

Review with `--color-moved=zebra --color-moved-ws=allow-indentation-change`;
git will not detect this as a rename because the functions come from the
middle of two files that both survive.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
The one-shot runner and the state-aware daemon each carried their own copy
of resolve_image. The two bodies were the same cache-lookup, tar-import and
rejection logic, down to the error string, so a fix to one silently left the
other behind. This deletes the one-shot copy and points both callers at the
shared function.

Reconciling the two cost three things:

  - The one-shot copy read `image`, `image_tar_path` and `storage_path` off
    `self.config`. The shared function already took all three as arguments,
    so the one-shot call site now passes them explicitly and the function
    needs no receiver.

  - The prefix each copy logged under, `[WSLC]` against `[WSLC][daemon]`, is
    now an argument. Both callers pass what they printed before, so the
    output is unchanged. Note this tag was already unreliable: the daemon's
    own tar-import lines print `[WSLC]`, as do the container-settings lines
    in `create_daemon_container`. Worth settling separately.

  - The one-shot copy branched with if/else-if/else and a trailing `Ok(())`;
    the shared one returns early. Kept the early returns, which leaves the
    rejection as the function's tail expression, where the pull will go.

Also restores the comment about `info.name` being a fixed-size,
possibly-unterminated C buffer, which explains the manual NUL scan and had
survived only in the one-shot copy.

No behavior change. Verified by normalizing both original bodies and the
merged one and diffing: after substituting the config accessors and the log
prefix, the remainder is identical.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Running a config whose image was not already cached failed, and told the
operator to run `wxc-exec.exe --setup-wslc --image <name>` and try again.
Two commands to do one thing, and the first one is only discoverable by
hitting the error. The run now pulls the image and carries on, so a config
referencing a fresh image works on the first invocation.

Pull failures split by whether a retry could succeed. A reference the
registry refuses (WSLC_E_IMAGE_NOT_FOUND) and a registry the host is barred
from (WSLC_E_REGISTRY_BLOCKED_BY_POLICY) are Rejected: the same input will
fail again. Everything else — no network, DNS failure, a registry that is
down — is Host, and retryable. The header declares both codes with
MAKE_HRESULT, which bindgen cannot evaluate, so the facade derives them from
the generated WSLC_E_BASE.

The retryable message keeps the cache-warming guidance the old rejection
carried, including the storagePath override, and adds imageTarPath. Losing
the network should not also lose the instructions for working without it.

`--setup-wslc` stays. It is no longer a prerequisite, but it still moves the
download off the critical path, and it is how you populate a cache for a
host that cannot reach a registry. `setup_pull_image` moves to image.rs and
now shares `pull_image`, so WslcPullSessionImage has one call site.

Bring-up reaching the network is new and deliberate. A config setting
`network.egress.default` to `deny` constrains the container once it is
running; it does not constrain the pull that precedes it. Verified on a live
host: the pull completes and the container is still sealed. Documented in
the getting-started guide, since someone reviewing sandbox posture will ask.

Progress reporting is not wired up — `progressCallback` stays null, so a
large pull is silent until it finishes. A private registry still cannot be
reached: `registryAuth` is null, and such a pull surfaces as
WSLC_E_IMAGE_NOT_FOUND with the SDK's own docker-login wording.

Validated on a WSL2 host: cold cache pulls then executes in one invocation,
a warm cache reuses without pulling, an unreachable registry fails in ~5s
with the offline guidance, and a nonexistent image is rejected without it.
Full WSLC E2E 111/111.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Three findings from the adversarial review of this branch.

A provision that pulls a cold image can outlast the client's deadline. The
worker still created the container and filed it under a sandbox_id nobody
would ever receive, so it could not be deprovisioned, and its entry held the
idle watchdog's container count above zero — the daemon could never idle out
and the WSL utility VM leaked for the life of the process. The Provision
dispatch arm now releases a container whose reply channel has closed, which
is what the Exec arm already does for a disconnected client.

The unreachable-registry message told the operator to run `--setup-wslc`.
That shares `pull_image`, so on the host that just failed it is the same
fetch failing again. It now leads with "Restore network access and retry",
offers imageTarPath first, and qualifies the cache-warming command as
something to run from a connected machine. Both rejection arms also name a
way forward instead of stopping at the diagnosis; the PowerShell spelling of
the command is dropped, since repeating a long image reference four times
made the message harder to read, not more useful.

The cache-miss path had no committed test: the E2E preflight pre-pulls every
image, so nothing reached the branch this branch rewrote. Two configs pin it
against a storage path the runner purges first — one asserts pull-then-run
and reuse on a second pass, one asserts an unresolvable reference is
rejected.

Also folds `StorageArg` back into a plain function now that only one command
spelling is rendered.

WSLC E2E 114/114 on a WSL2 host (was 111; the three new cases are the
cold-cache ones). The daemon fix carries an #[ignore]d regression test —
it needs a live host, since a provision cannot succeed without one.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Addresses the PR review. Three behaviour changes and the tests that were
asserting less than they claimed.

A cache miss no longer pulls when the request declares no egress. The fetch
runs on the host's network before the container exists, so the declared
posture cannot constrain it; a config that asks for isolation now gets a
rejection naming the warm-cache, imageTarPath and allow-egress routes instead
of a silent fetch. `--setup-wslc` is unaffected: an operator warming a cache
is not a sandbox with a policy.

The orphan cleanup moved to where the failure is observable. Checking
`reply.is_closed()` on the worker never fired: `SessionHandle::provision`
holds the receiver and the control-server task stays parked on it, so a pipe
client timing out leaves it open and the failure only surfaces at the later
`write_frame`. Cleanup now lives there, with bounded retry, because a
transient delete leaves a container nobody else can name.

An untagged reference re-pulled on every run. The store spells out
`busybox:latest` while the request said `busybox`, and the comparison was
literal, so the cache never matched. Matching now accounts for the implicit
tag, leaving explicit tags, digests, and `host:port/` prefixes alone.

The catch-all failure arm no longer asserts a network cause -- a full disk
reaches it too -- and registry-supplied text is stripped of control
characters and capped before it reaches a log, a message, or the pipe.

The new E2E cases were checking too little: both cold-cache runs asserted
only the workload marker, so a needless re-pull passed, and the unresolvable
case asserted `could not be pulled`, which every arm says. They now assert
the pull and cache-hit signals, and the wording unique to the non-retryable
arm. The store they purge is checked for a reparse point first.

`resolve_image`'s branch selection and the name matching are now pure
functions with unit coverage, so cache-hit, tar, pull and refuse are testable
without a live host.

WSLC E2E 115/115 on a WSL2 host. 275 unit tests. Verified live: a deny
posture refuses rather than pulls, and an untagged reference now hits the
cache on its second run.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Bound the runtime pull, gated which registries it may reach, and broke the
cycle between image handling and the runner:

- A pull now runs under a 540s deadline, overridable with
  MXC_WSLC_PULL_TIMEOUT_SECS. The SDK progress callback returns E_ABORT
  past the deadline, which aborts the pull rather than only abandoning
  the wait.
- An administrative allowlist (SOFTWARE\Policies\Mxc,
  WslcAllowedImageRegistries) decides which registries a runtime pull may
  reach, and the resolved digest is recorded on every resolution. Pinning
  a digest from cache is not implementable: a reference carries the
  manifest digest while the store reports the config digest, and the SDK
  exposes no mapping between them, so digest references always go to the
  registry.
- SDK bootstrap moved into sdk_init, so image no longer calls back into
  wsl_container_runner.

Closed the orphaned-sandbox path a client timeout leaves behind:

- On timeout the client cancels its blocked read instead of abandoning
  the reader thread. The reader held the pipe open, so the daemon's write
  succeeded and it never learned the reply was undelivered.
- A sandbox whose release fails terminally is retired from the live
  count, so the idle watchdog can shut the daemon down. Its handle stays
  in the map and the container is still deleted at shutdown.
- Extracted deliver_provision_reply so the undelivered path is covered
  without a live session. The previous test passed whenever provisioning
  failed for unrelated reasons, without entering the cleanup branch.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Administrative registry policy can no longer be relaxed by user state:

- Removed the environment override that replaced the allowlist before HKLM
  was read, so any standard user could widen an administrator's
  restriction. Tests now redirect the policy read to a per-test HKCU key,
  paired with an owner PID, and that redirect is compiled out of release
  builds - the same shape the telemetry policy uses.
- The read path itself had no coverage: every earlier test either built a
  policy value directly or went through the override. All four documented
  states now drive the real registry, and each was checked by reverting
  the behaviour and confirming the test fails.
- An allowlist an administrator deliberately emptied now reports as a
  policy that was read and permits nothing, rather than one that could not
  be read and should be corrected.
- Added the administrator-facing reference the value was missing; it lives
  under the same key as the telemetry policy and needs the same
  documentation.

The pull budget is bounded by the deadline its caller is waiting on:

- Raising MXC_WSLC_PULL_TIMEOUT_SECS past the client's response deadline
  produced exactly the abandoned container the budget exists to prevent -
  the error message advising the operator to raise it made that reachable
  by following instructions. The effective budget is now capped under the
  caller's deadline, and a value too large for the clock falls back to the
  default rather than panicking.
- The pull runs on a thread of its own, so a registry that stops
  responding cannot sit on the daemon's single lifecycle worker for the
  whole budget. The SDK reports progress only as response chunks arrive,
  so a transfer stalled before its next chunk cannot be cancelled from
  outside; teardown therefore waits for an abandoned pull before releasing
  the session it is still using.

Two fixes to what a run reports and what a test proves:

- The digest recorded after a pull was matched by repository, so a store
  holding several tags of one image could attribute the wrong digest to
  the run. It now matches the exact tag, and a reference pinning a digest
  reports nothing rather than the store's unrelated one.
- The timeout test slept instead of blocking on I/O, so it never reached
  the cancellation path it was meant to cover. It now blocks a real pipe
  read and asserts the server's reply cannot land, which is what lets the
  daemon release an undelivered sandbox.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
A pull no longer occupies the daemon's single lifecycle worker.

Bounding the pull was not enough: the worker still waited on it, so a
registry that stopped responding delayed start, exec, stop, deprovision
and teardown for every other sandbox until the budget ran out. Provision
now splits at the network boundary. Deciding what to do with the image
touches only the local store and stays on the worker; when that decision
is a pull, the request parks and the worker returns to its queue. The
pull reports back as another command, and a timer posts the same command
if the budget expires first, so whichever lands first answers the caller
and the other finds nothing to answer.

Measured against a pull that never responds, with the daemon identity
pinned so both requests provably share one worker: a cached provision
takes 0.1s idle and 0.1s while the pull is stuck, where it previously
took 4.6s and 10.1s.

Two consequences worth naming. A parked provision has no container yet,
so it now counts toward the idle watchdog - otherwise the daemon could
retire itself out from under the client still waiting. And shutdown
answers a parked provision rather than dropping its reply, which would
otherwise reach the client as a bare "worker gone".

The pull still cannot be stopped from outside. The SDK reports progress
only as response chunks arrive and exposes no cancellation handle, so a
transfer stalled before its next chunk ends when the SDK's own network
timeouts fire. What changed is that nothing waits on it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6

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.

Copilot review overview

🟡 Changes recommended

Non-Windows builds/tests regress, and setup pulls can still hang beyond the documented deadline.

Review effort: Balanced
Findings: 2 High severity

Open (2)
Resolved since last review (6)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Enforce setup pull deadline via off-thread execution

src/​backends/​wslc/​common/​src/​image.rs:993

--setup-wslc still calls the synchronous pull directly. Its deadline is checked only from pull_progress, so the no-progress stall described above start_pull never invokes the callback and can block this command indefinitely; the advertised 540-second bound is not enforced here. Run setup through the off-thread deadline path too, while preserving the session handle if the timed-out SDK call remains in flight.

Comment thread src/backends/wslc/common/Cargo.toml
Comment thread src/backends/wslc/common/src/daemon_client.rs
Copilot AI balanced review requested due to automatic review settings September 30, 2026 18:35
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the sohamdas2021-wslc-image-prepull branch from 398b884 to f71d9a4 Compare September 30, 2026 18:35

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.

Copilot review overview

🟡 Changes recommended

Setup pulls can still hang indefinitely, and the added timeout tests contain cross-platform and determinism issues.

Review effort: Balanced
Findings: 3 High severity · 1 Medium severity · 2 Low severity

Open (6)

Comment thread src/backends/wslc/common/src/image.rs Outdated
Comment thread src/backends/wslc/common/src/image.rs Outdated
Comment thread sdk/node/tests/integration/wslc-state-aware.test.ts Outdated
Comment thread tests/scripts/run_wslc_state_aware_tests.ps1 Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e1e6a680-2715-49df-ac0e-9df5352325b6
Copilot AI balanced review requested due to automatic review settings September 30, 2026 19:09
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the sohamdas2021-wslc-image-prepull branch from f71d9a4 to d997d30 Compare September 30, 2026 19:09

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.

Copilot review overview

🟡 Changes recommended

Concurrent use of the worker-owned SDK session and daemon-scoped timeout overrides can violate lifecycle and deadline guarantees.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (6)

Comment on lines +214 to +218
// the spawned one join the MTA, where a handle may be used from any member
// thread. Measured against a live 31s pull: 298 concurrent `WslcListSessionImages`
// calls on the same session all returned inside 29ms, so the SDK neither rejects
// nor serializes the overlap.
unsafe impl Send for PullJob {}
Comment on lines +62 to +64
pub fn daemon_pull_budget() -> Duration {
capped_for_daemon(pull_budget(), crate::daemon_client::call_timeout())
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs-Attention Requires attention or a decision from the MXC maintainers. Needs-Author-Feedback Waiting for additional information or action from the issue or pull-request author.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants