Skip to content

feat(windows-sandbox): place sandboxed children on a private desktop - #3174

Merged
liugddx merged 12 commits into
apache:mainfrom
liugddx:feat/windows-sandbox-private-desktop
Aug 19, 2026
Merged

feat(windows-sandbox): place sandboxed children on a private desktop#3174
liugddx merged 12 commits into
apache:mainfrom
liugddx:feat/windows-sandbox-private-desktop

Conversation

@liugddx

@liugddx liugddx commented Aug 17, 2026

Copy link
Copy Markdown
Member

What & why

Closes the private-desktop half of the umbrella #2142 Phase 4 ③ sandbox hardening — specifically initial private-desktop placement plus a hardened per-launch desktop object. #3161 delivered the production-identity readiness probe and deferred the private desktop to a follow-up (RFC v1 §6.5); this is that follow-up.

Non-interactive AppContainer workers previously inherited the creator's interactive Winsta0\Default desktop as their initial desktop. AppContainer fences files, network and token, but does not choose a non-interactive desktop — so code that never re-attaches still ran amid the user's interactive windows. RFC v1 §6.3 promised a private desktop; it was designed but not enforced.

What this does

  • Each AppContainer launch — both the production create_appcontainer_child path and the probe_appcontainer_child readiness path — creates a per-launch alternate desktop on the existing window station (CreateDesktopExW, bounded 512 KiB heap so ten-way concurrency stays ~5 MiB of the documented 48 MiB system budget; name = maka-sandbox-desktop.<pid>.<128-bit CSPRNG nonce>, RNG failure fails the launch closed).
  • Its security descriptor: a leading deny ACE strips DESKTOP_SWITCHDESKTOP/HOOKCONTROL/journal (0x138) from the launching-user SID (which the AppContainer child carries as an effective SID); allow ACEs grant the launching user and Local System control for cleanup and the child's AppContainer SID only the minimal DESKTOP_* rights; an explicit Low no-write-up mandatory label makes the granted rights pass MIC at the child's Low IL.
  • STARTUPINFOEXW.lpDesktop points the child at it; the handle lives until the child settles, then CloseDesktop.
  • Fail closed: if the desktop, its DACL, or its name nonce can't be built, the launch fails — auto/require never fall back to the host desktop.
  • The --boundary-probe self-attests its initial desktop name against the launcher-owned maka-sandbox-desktop. prefix; --readiness-probe emits a machine-readable attestation (exact SID, specific Job, settlement, desktop placement) that readiness-probe-smoke.ps1 asserts field by field.
  • windows_launcher_desktop_tests.rs covers the SDDL builder (deny-before-allow, minimal app-SID mask, Low label), the placement predicate, and a real-OS test holding ten live desktops with ten distinct names inside the heap budget.

Scope of the guarantee (narrowed per review)

This PR ships initial-desktop placement plus a DACL/label-protected per-launch desktop object — not escape-proof confinement:

  • Enforced: the worker starts on a launcher-created private desktop, never the interactive Default; the desktop object is protected (deny ACE, minimal app mask, Low label, protected DACL) and per-launch unique (CSPRNG nonce; CreateDesktopExW would silently reopen an existing name, so uniqueness is part of the object contract).
  • ⚠️ Not claimed: lpDesktop selects only the initial desktop. Nothing structural stops in-process code from OpenDesktopW("Default") + SetThreadDesktop re-attaching — keeping a worker unable to enumerate/message/hook the user's windows after an escape attempt requires the deferred no-Win32k / dedicated window-station / token-boundary gates (§6.5).
  • ⏸️ Deferred (§6.5): clipboard isolation. The clipboard belongs to the window station, which both desktops still share.
  • ⏸️ Deferred (§6.5): full window-station separation and no-Win32k mitigation.
  • ⏸️ Deferred (§6.5): end-to-end Low-IL GUI proof. The Low label makes the granted create-window/write rights pass MIC, but no probe creates a window in-child; the worker does no GUI work, so this is labeled-usable, not proven.

Digest is unchanged — the desktop is a launch-time detail and does not enter the manifest, so no profile_digest_mismatch.

Stacked on #3161

This PR is stacked on #3161 (it modifies the probe_appcontainer_child code that #3161 introduces). Until #3161 merges, the diff here includes its commits; they drop out automatically once #3161 lands. Please merge #3161 first.

Verification (Windows 11 x64, local)

  • cargo fmt --check clean; cargo build --locked; cargo test --locked → 49 pass (incl. desktop SDDL/placement tests and the ten-distinct-desktops live test).
  • Runtime behavior (child initially on maka-sandbox-desktop.*, file/network/Job boundary intact, readiness attestation fields) is proven by appcontainer-smoke.ps1 and readiness-probe-smoke.ps1 on the real windows-sandbox-w0 CI lane.
  • @maka/core + @maka/runtime rebuild clean; default-sandbox-manager node test → 11 pass.

RFC

windows-sandbox-rfc-v1.md and .zh-CN.md §6.3/§6.4/§6.5 updated: the normative guarantee is stated as initial-desktop placement (no unconditional "cannot" claims); the deny ACE, Low label, heap bound, and readiness attestation are recorded as enforced; no-Win32k, dedicated window station, clipboard isolation, and in-child window-creation proof stay on the deferred-gates list.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Claude Fable 5 (Anthropic), driven by the human contributor of record, authored the private-desktop placement (create_confined_desktop, the two launch-path wirings, the boundary-probe self-attestation), the smoke assertions, the Rust unit tests, and the RFC updates, all under human review. The commit carries a Generated-by: Claude Fable 5 trailer; the final squash commit must retain it.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1beafad1-e9b0-4146-b9d5-770b88b67c19

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec0c33 and 3ec5183.

📒 Files selected for processing (1)
  • experiments/windows-sandbox/launcher/src/windows_launcher.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • experiments/windows-sandbox/launcher/src/windows_launcher.rs

Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.


📝 Walkthrough

Problem solved

The PR places Windows AppContainer children on a per-launch alternate desktop on the existing window station.

Production launches and readiness probes create the desktop with a protected DACL. They assign it through STARTUPINFOEXW.lpDesktop and retain the handle through launch settlement. Desktop or DACL creation failures fail closed. Launches do not fall back to Default.

The guarantee covers initial desktop placement and DACL protection. It does not provide escape-proof confinement. Clipboard isolation, dedicated window-station separation, token and Win32k escape prevention, and verified Low-IL desktop rights remain deferred.

The PR also adds a production-identity readiness probe. The probe verifies the AppContainer SID, Job membership, timeout handling, cleanup, and repeatable execution. Runtime availability now depends on a successful readiness probe instead of launcher file presence alone.

Source of truth and solution scope

The PR extends the existing Windows launcher and sandbox manager. It does not create a parallel launch path.

The readiness probe reuses the existing production identity, Job setup, launch protocol, and desktop-placement logic. The runtime uses the launcher probe before it reports Windows sandbox availability.

The manifest digest remains unchanged.

This is the smallest coherent solution for the stated initial-placement guarantee. Desktop creation, protected DACL configuration, desktop assignment, verification, and fail-closed handling are required to prevent fallback to the interactive desktop. The readiness cache adds required lifecycle handling, bounded negative caching, and synchronous availability checks.

Deletion or simplification opportunities

No safe deletion is evident from the supplied change summary.

The Rust tests cover desktop security helpers, AppContainer permissions, desktop-name validation, JSON escaping, readiness profile naming, mutex naming, and request validation. Removing them would weaken security and regression coverage.

The runtime tests cover probe success, failures, timeouts, memoization, negative-result expiry, permanent positive caching, cache-only reads, and missing launchers. Removing these cases would weaken readiness-cache coverage.

The smoke tests and workflow step validate the production-identity path, argument validation, denied network access, and non-default desktop placement. They should remain unless equivalent Windows CI coverage exists elsewhere.

Validation

  • Rust tests cover desktop security helpers and readiness behavior.
  • Runtime tests cover probe success, non-zero exits, spawn errors, timeouts, memoization, negative-result expiry, permanent positive caching, and missing launchers.
  • The readiness-probe smoke test runs the production-identity probe twice and checks argument validation.
  • The Windows sandbox smoke test requires denied network access and a launcher-owned maka-sandbox-desktop instead of Default.
  • A Windows workflow step runs the readiness-probe smoke test.
  • The RFC is updated in English and Chinese to document enforced guarantees and deferred gates.
  • The author reports that formatting, builds, tests, and Windows smoke checks passed after rebasing.
  • The manifest digest is unchanged.

Required-check status is unverified from the supplied evidence.

Complexity delta

  • Authorities: Adds desktop DACL construction and explicit AppContainer SID and Job verification. It reuses the existing launcher identity and Job authorities.
  • States: Adds readiness success, failure, timeout, cleanup, memoization, and private-desktop settlement states.
  • Branches: Adds fail-closed handling for missing launchers, probe failures, desktop failures, invalid desktop names, unexpected arguments, and cleanup uncertainty. It removes file-presence-only readiness.
  • Configuration: Adds a fixed readiness request identifier, readiness mutex naming, desktop security descriptors, and cache durations. It does not add manifest or persistent desktop configuration.
  • Public surface: Adds readiness and desktop-security functions, readiness constants, and runtime readiness types and helpers.
  • Test maintenance: Adds Rust, runtime, smoke-test, workflow, and bilingual RFC coverage for Windows-specific behavior.

Total maintenance complexity increases. The increase is justified by stronger launch-boundary enforcement and deterministic, fail-closed readiness checks. The implementation also removes file-presence-only readiness. Optional follow-up items remain separate and are not implementation requirements for this PR.

Review-relevant risks

  • The PR changes user-visible Windows sandbox availability. Readiness failures can make the sandbox unavailable. Material changes to sandbox availability require independent human review under repository policy.
  • The PR changes Windows process isolation through alternate-desktop placement and a restricted DACL. Material security changes require independent human review under repository policy.
  • The desktop DACL grants the user SID GA and uses a deny ACE for selected interactive-control rights. A review concern remains that GA may still permit rights such as SWITCHDESKTOP, hooks, journaling, or ownership in the AppContainer token. The final DACL behavior requires independent human review under repository policy.
  • The PR documents shared clipboard access and deferred window-station, escape-proof, and Low-IL guarantees. Material reliance on these boundaries requires independent human review under repository policy.
  • The PR changes Windows CI and smoke-test requirements. Material release or governance effects require independent human review under repository policy.
  • The PR adds public Rust functions, constants, and types. Material public-contract changes require independent human review under repository policy.
  • No licensing effect was identified in the current diff.

The person performing the merge reviews the final diff. A maintainer makes the final determination.

Walkthrough

The launcher adds production-identity readiness checks and private desktop enforcement. Runtime availability uses cached readiness results. Smoke tests, unit tests, CI, and RFC documentation cover the preview behavior and deferred capabilities.

Changes

Windows sandbox readiness

Layer / File(s) Summary
Launcher readiness probe
experiments/windows-sandbox/launcher/...
The launcher adds --readiness-probe. It creates a disposable AppContainer and kill-on-close Job, launches cmd.exe /d /c exit 0, verifies the exact SID and Job, enforces timeouts, and settles child processes.
Private desktop boundary
experiments/windows-sandbox/launcher/src/..., experiments/windows-sandbox/appcontainer-smoke.ps1
AppContainer workloads use per-launch desktops with restricted DACLs. Boundary probes report desktop names and reject interactive desktops. Tests validate desktop permissions and JSON escaping.
Runtime readiness gating
packages/runtime/src/sandbox/default-sandbox-manager.ts, packages/runtime/src/__tests__/default-sandbox-manager.test.ts
Runtime availability checks execute the launcher readiness probe with a 15-second timeout. Results are cached by launcher path. Missing launchers and probe failures fail closed. Negative results expire after 60 seconds, while positive results remain cached.
Smoke tests and RFC status
experiments/windows-sandbox/readiness-probe-smoke.ps1, .github/workflows/windows-sandbox-w0.yml, docs/architecture/windows-sandbox-rfc-v1*
PowerShell and CI validate repeated readiness probes, argument rejection, and desktop isolation. The RFC documents enforced preview capabilities and deferred gates.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 3ec51

The PR adds private-desktop confinement, but the current implementation still has an unsafe unaligned read in the AppContainer readiness path and can block availability checks for up to 15 seconds while permanently caching transient failures, so these issues should be fixed or explicitly accepted before merge.

Sequence Diagram(s)

sequenceDiagram
  participant Runtime as default-sandbox-manager
  participant Launcher as Windows launcher
  participant Sandbox as AppContainer child
  Runtime->>Launcher: Run --readiness-probe
  Launcher->>Sandbox: Start with exact AppContainer SID, Job, and private desktop
  Sandbox-->>Launcher: Return exit status and boundary state
  Launcher-->>Runtime: Report clean status or failure
  Runtime-->>Runtime: Cache readiness by launcher path
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Ai Use Disclosure ✅ Passed The PR selects substantive generative use, names Claude Fable 5 and its scope, and all 7 commits in the PR range contain the matching standalone Generated-by trailer.
Title check ✅ Passed The title clearly identifies the main change: placing Windows sandbox children on private desktops.
Description check ✅ Passed The description clearly explains the change, scope, deferred risks, issue linkage, verification, stacking order, and AI contribution.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Confine Windows AppContainer children to per-launch private desktops

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Launch AppContainer children on per-launch private desktops; fail closed on desktop/ACL errors.
• Add readiness probe and smoke/tests to assert desktop isolation and boundary enforcement.
• Update RFC docs and CI workflow to reflect enforced preview guarantees.
Diagram

graph TD
  RT["Runtime (TS)"] --> LP["Launcher exe"] --> DS["Private desktop"] --> CP["AppContainer child"] --> BP["Boundary probe"] --> SM["Smoke/CI asserts"]
  LP --> AC["AppContainer SID"] --> DS
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Dedicated window station (full winsta isolation) now
  • ➕ Stronger GUI isolation boundary than desktop-only isolation
  • ➕ Reduces reliance on current window station security defaults
  • ➖ Higher complexity and greater risk of breaking process creation/interop
  • ➖ More cleanup/ownership edge cases; harder to test and debug
  • ➖ Explicitly deferred by RFC §6.5 and may expand PR scope significantly
2. Job Object UI restrictions instead of a private desktop
  • ➕ Centralized restriction knob; no per-launch desktop objects
  • ➕ May reduce object/ACL management complexity
  • ➖ UI restrictions are easy to misconfigure and can be incomplete vs desktop scoping
  • ➖ Less direct guarantee against landing on interactive Default desktop
  • ➖ Harder to self-attest and validate via simple desktop-name checks

Recommendation: The PR’s approach (per-launch alternate desktop on the existing window station, protected DACL granting only Local System + current user + the AppContainer SID with a minimal DESKTOP_* mask) is the right incremental hardening step: it closes same-session GUI attack surface while keeping launch semantics stable. Window-station separation is a meaningful next step, but keeping it deferred avoids broad compatibility risk while still delivering a fail-closed, test-asserted guarantee that children never run on Winsta0\Default.

Files changed (10) +752 / -25

Enhancement (3) +533 / -15
main.rsAdd --readiness-probe and desktop self-attestation in boundary probe +87/-1

Add --readiness-probe and desktop self-attestation in boundary probe

• Wires a new --readiness-probe CLI entrypoint and extends boundary-probe output with the current desktop name + isolation predicate, including minimal JSON escaping and Win32 desktop-name querying.

experiments/windows-sandbox/launcher/src/main.rs

windows_launcher.rsCreate per-launch confined desktops and enforce readiness probe +393/-13

Create per-launch confined desktops and enforce readiness probe

• Implements production-identity readiness probe that creates a Job, per-invocation AppContainer profile, and launches a throwaway child. Adds private desktop creation via CreateDesktopW with a protected DACL granting minimal rights to the AppContainer SID, and sets STARTUPINFO.lpDesktop for both production and probe launches; adds token/job verification helpers to fail closed on spoofing or ambient-job confusion.

experiments/windows-sandbox/launcher/src/windows_launcher.rs

default-sandbox-manager.tsGate Windows backend availability on memoized readiness probe +53/-1

Gate Windows backend availability on memoized readiness probe

• Replaces file-presence-only availability with a cached spawnSync of the launcher’s --readiness-probe (15s timeout, hidden window, stdio ignored) so auto/require fail closed when the host cannot establish the boundary.

packages/runtime/src/sandbox/default-sandbox-manager.ts

Tests (3) +112 / -1
appcontainer-smoke.ps1Assert confined child runs on an isolated private desktop +7/-1

Assert confined child runs on an isolated private desktop

• Extends the smoke test to require desktop self-attestation (desktopIsolated + maka-sandbox-desktop.* prefix) and to fail if the child reports Default.

experiments/windows-sandbox/appcontainer-smoke.ps1

windows_launcher_desktop_tests.rsAdd unit tests for desktop SDDL and isolation predicates +62/-0

Add unit tests for desktop SDDL and isolation predicates

• Introduces pure-function tests that validate the generated desktop DACL (protected DACL, GA for owner/SYSTEM, minimal mask for AppContainer SID) and the desktop isolation/json escaping helpers.

experiments/windows-sandbox/launcher/src/windows_launcher_desktop_tests.rs

readiness-probe-smoke.ps1Add smoke test for --readiness-probe repeatability and arg rejection +43/-0

Add smoke test for --readiness-probe repeatability and arg rejection

• New script runs the launcher readiness probe twice to ensure teardown leaves no residue, and verifies the probe rejects unexpected arguments (fail closed).

experiments/windows-sandbox/readiness-probe-smoke.ps1

Documentation (2) +100 / -9
windows-sandbox-rfc-v1.mdAlign RFC with preview enforcement and private-desktop semantics +70/-4

Align RFC with preview enforcement and private-desktop semantics

• Updates status/updated dates, expands §6.3 and §6.4 with explicit private-desktop/readiness-probe enforcement details, and adds a new §6.5 preview implementation status section enumerating enforced vs deferred gates.

docs/architecture/windows-sandbox-rfc-v1.md

windows-sandbox-rfc-v1.zh-CN.mdMirror RFC alignment and preview status section (zh-CN) +30/-5

Mirror RFC alignment and preview status section (zh-CN)

• Brings the Chinese RFC in sync with the English changes: enforced private desktop and readiness probe details plus a preview implementation status section listing enforced vs deferred guarantees.

docs/architecture/windows-sandbox-rfc-v1.zh-CN.md

Other (2) +7 / -0
windows-sandbox-w0.ymlRun readiness-probe smoke test in W0 CI lane +3/-0

Run readiness-probe smoke test in W0 CI lane

• Adds a workflow step that executes the new readiness-probe smoke script, ensuring CI validates production-identity probing behavior on Windows.

.github/workflows/windows-sandbox-w0.yml

Cargo.tomlEnable windows-sys features for desktop APIs +4/-0

Enable windows-sys features for desktop APIs

• Adds Win32_Graphics_Gdi (for CreateDesktopW signature types) and Win32_System_StationsAndDesktops to allow CreateDesktopW/CloseDesktop usage.

experiments/windows-sandbox/launcher/Cargo.toml

@qodo-code-review

qodo-code-review Bot commented Aug 17, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Probe uses unsupported shell ✗ Dismissed 🐞 Bug ≡ Correctness
Description
Fix now — readiness_probe launches capability-less cmd.exe, although the runtime records that
cmd.exe fails DLL initialization in this AppContainer mode while the purpose-built worker is
supported. A host capable of running the production worker can therefore be rejected as unavailable
by an unrepresentative probe.
Code

experiments/windows-sandbox/launcher/src/windows_launcher.rs[214]

+    let mut command = wide(&format!("\"{cmd_path}\" /c exit 0"));
Relevance

●●● Strong

Readiness must use a supported production executable; this directly undermines the PR’s fail-closed
availability intent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The readiness implementation launches System32 cmd.exe without capabilities, while the runtime's
Windows sandbox selection explicitly documents that cmd.exe/pwsh fail DLL initialization in a
capability-less AppContainer and that only the purpose-built filesystem worker is supported.

experiments/windows-sandbox/launcher/src/windows_launcher.rs[163-218]
packages/runtime/src/builtin-tools.ts[734-741]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The readiness probe uses `cmd.exe`, which the repository identifies as incompatible with the capability-less AppContainer used by this backend, causing false unavailable results.

## Issue Context
Reuse the closest existing production-compatible worker/probe seam rather than adding another general-purpose shell path. A dedicated mode is justified only if the existing worker cannot perform a bounded no-op; that would add a new CLI branch and associated test burden.

## Fix Focus Areas
- experiments/windows-sandbox/launcher/src/windows_launcher.rs[163-218]
- packages/runtime/src/builtin-tools.ts[734-741]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Timeout leaks readiness profiles ✗ Dismissed 🐞 Bug ☼ Reliability
Description
Fix now — the 15-second spawnSync timeout can terminate the launcher after it creates a uniquely
named AppContainer profile, bypassing the only DeleteAppContainerProfile call in Drop. Repeated
hung probes then permanently accumulate profiles that neither later nonces nor ACL-ledger
reconciliation can discover.
Code

packages/runtime/src/sandbox/default-sandbox-manager.ts[R61-64]

+      const result = spawnSync(clientPath, ['--readiness-probe'], {
+        timeout: WINDOWS_READINESS_PROBE_TIMEOUT_MS,
+        windowsHide: true,
+        stdio: 'ignore',
Relevance

●●● Strong

The code documents profile leakage when externally killed; the synchronous timeout can trigger
exactly that cleanup bypass.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Node caller enforces an out-of-process kill timeout. The launcher itself acknowledges that
killing a probe before destruction leaks its profile, generates a fresh nonce specifically to avoid
the leaked name, and deletes the profile only from AppContainerProfile::drop; readiness does not
create an ACL ledger entry for later reconciliation.

packages/runtime/src/sandbox/default-sandbox-manager.ts[61-66]
experiments/windows-sandbox/launcher/src/windows_launcher.rs[174-196]
experiments/windows-sandbox/launcher/src/windows_launcher.rs[443-451]
experiments/windows-sandbox/launcher/src/acl_ledger.rs[438-455]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Externally timing out the readiness launcher bypasses profile destruction, and unique nonce-based names make leaked readiness profiles unrecoverable.

## Issue Context
First reuse the existing reconciliation lifecycle or avoid creating a registered throwaway profile if a derived identity is sufficient. Add a new readiness-profile registry only if those existing seams cannot recover profiles; doing so adds persistent state and cleanup-test burden.

## Fix Focus Areas
- packages/runtime/src/sandbox/default-sandbox-manager.ts[61-66]
- experiments/windows-sandbox/launcher/src/windows_launcher.rs[174-196]
- experiments/windows-sandbox/launcher/src/windows_launcher.rs[443-451]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Capability bypasses readiness probe ✗ Dismissed 🐞 Bug ≡ Correctness
Description
Fix now — the backend now uses the launcher readiness probe, but
isBuiltinFilesystemWorkerSandboxAvailable still reports Windows availability from file presence
alone. Runtime Host gates worker composition on that stale helper, so it can initialize and
advertise a managed worker even when the backend's readiness check fails.
Code

packages/runtime/src/sandbox/default-sandbox-manager.ts[88]

+    isAvailable: createWindowsReadinessProbe(clientPath),
Relevance

●●● Strong

Worker composition uses the file-presence helper separately, creating a concrete stale-availability
correctness mismatch.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed backend delegates availability to probeWindowsReadiness, but the sibling exported
helper returns true on Windows whenever builtinWindowsClientPath exists. Runtime Host uses that
helper—not SandboxManager.canEnforce—to decide whether to construct the filesystem worker.

packages/runtime/src/sandbox/default-sandbox-manager.ts[55-88]
packages/runtime/src/sandbox/default-sandbox-manager.ts[106-119]
packages/runtime-host/src/server/execution-composition.ts[305-320]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Windows worker composition and the Windows backend use inconsistent availability checks, allowing file presence to bypass the new readiness result.

## Issue Context
Consolidate duplicated authority: make worker capability gating reuse the backend/manager readiness seam. No new configuration or public surface is needed.

## Fix Focus Areas
- packages/runtime/src/sandbox/default-sandbox-manager.ts[55-88]
- packages/runtime/src/sandbox/default-sandbox-manager.ts[106-119]
- packages/runtime-host/src/server/execution-composition.ts[305-320]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Readiness probe leaves child running past failure ✓ Resolved 🐞 Bug ☼ Reliability
Description
In probe_appcontainer_child, once ResumeThread succeeds, any subsequent boundary verification
failure (SID match, specific-Job membership, or boundary mismatch) returns an error without
explicitly terminating and reaping the already-resumed child. This leaves a transient window where
the unverified child can continue executing and, in the child_in_job == false case, readiness
teardown may not kill it by merely closing the probe Job handle, unlike the explicit termination
used in the sibling production launch paths.
Code

experiments/windows-sandbox/launcher/src/windows_launcher.rs[R325-333]

+        let child_sid_matches =
+            unsafe { child_token_appcontainer_sid_matches(process.hProcess, app_container_sid) }?;
+        let child_in_job = unsafe { child_process_is_in_specific_job(process.hProcess, job) }?;
+        if !(child_sid_matches && child_in_job) {
+            return Err(
+                "readiness probe child did not run under the requested AppContainer SID and Job"
+                    .to_owned(),
+            );
+        }
Relevance

●●● Strong

PR #2961 explicitly accepted terminating resumed children when boundary verification fails,
including AppContainer paths.

PR-#2961

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited behavior is that the probe child is resumed before the SID and Job verification steps, and
on verification failure the function exits via an error path that performs handle closure rather
than terminating the process. Because readiness cleanup later relies on closing a kill-on-close Job,
that mechanism is only a backstop if the process is actually in that Job; when the verification
establishes the child is outside the specific Job, Job closure cannot guarantee termination,
violating the readiness teardown contract. This same “resumed but unverified child not explicitly
terminated on failure” cleanup gap was previously identified and fixed in the production
create_child/create_appcontainer_child paths by routing failures through explicit
termination/settlement logic rather than depending on Job-handle closure, and the probe path
reintroduces that risk.

experiments/windows-sandbox/launcher/src/windows_launcher.rs:341-346: TBD
experiments/windows-sandbox/launcher/src/windows_launcher.rs[317-345]
experiments/windows-sandbox/launcher/src/windows_launcher.rs[1085-1134]
PR-#2961

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `probe_appcontainer_child`, after `ResumeThread` succeeds, if any post-resume boundary verification fails (`child_token_appcontainer_sid_matches`, `child_process_is_in_specific_job`, or the combined mismatch condition), the function returns `Err` without explicitly terminating and waiting on the resumed child process. Cleanup currently relies on `readiness_probe` closing the kill-on-close Job later, which creates a transient execution window and is not a reliable backstop when the verification indicates the child is not in the specific Job (`child_in_job == false`).

## Issue Context
This mirrors a previously fixed launcher defect in the production `create_child`/`create_appcontainer_child` paths (referenced as PR #2961): a child was resumed, failed a post-launch boundary check, and was not explicitly terminated on failure, so the fix routed such failures through the existing settlement/termination logic (e.g., `settle_job`) instead of relying on Job-handle closure alone. For readiness-probe, the child is intended to be short-lived (`cmd.exe /c exit 0`), so the current behavior may often be a cleanup-latency/consistency issue, but it can become a correctness issue when the process is outside the specific Job and therefore not killable by closing the probe Job. Reuse the launcher’s existing termination/settlement seam (or add a local guard that uses it) rather than creating a separate cleanup policy; ensure every post-resume error path terminates and waits for the process before handles are closed.

## Fix Focus Areas
- experiments/windows-sandbox/launcher/src/windows_launcher.rs[317-346]
- experiments/windows-sandbox/launcher/src/windows_launcher.rs[1085-1134]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Synchronous readiness probe can block event loop ✓ Resolved 🐞 Bug ➹ Performance
Description
probeWindowsReadiness runs spawnSync (up to a 15s timeout) synchronously inside the
SandboxBackend.isAvailable() contract, which sandbox-manager.canEnforce/selectInitial and
diagnostics call synchronously; the first Windows sandbox availability check per launcher path can
therefore block the Node.js event loop for up to 15 seconds, a large behavioral regression from the
previous cheap existsSync check.
Code

packages/runtime/src/sandbox/default-sandbox-manager.ts[R61-66]

+      const result = spawnSync(clientPath, ['--readiness-probe'], {
+        timeout: WINDOWS_READINESS_PROBE_TIMEOUT_MS,
+        windowsHide: true,
+        stdio: 'ignore',
+      });
+      available = result.error === undefined && result.status === 0;
Relevance

●● Moderate

Blocking synchronous availability is a credible regression, but historical evidence does not
establish team treatment of this API tradeoff.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
isAvailable is declared and consumed as a synchronous, cheap check (types.ts declares
isAvailable?(platform?): boolean, and sandbox-manager.ts calls
backend.isAvailable?.(selected.platform) synchronously in canEnforce/selectInitial), so replacing
existsSync with a blocking spawnSync of up to 15s violates that 'cheap' contract on the first
invocation for each client path, potentially stalling all in-flight Node.js work in that process.

packages/runtime/src/sandbox/sandbox-manager.ts:128-136: TBD
packages/runtime/src/sandbox/windows-sandbox.ts:93-95: TBD

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`probeWindowsReadiness` uses `spawnSync` with up to a 15-second timeout, called synchronously from `isAvailable()`, which is consumed by `SandboxManager.canEnforce`/`selectInitial` as a fast, synchronous check. The first call for a given launcher path can block the calling Node.js thread/event loop for up to 15 seconds.

## Issue Context
Previously `isAvailable` was just `existsSync(clientPath)`, a fast filesystem stat. This PR replaces it with a child-process spawn that stands up the full AppContainer/Job/desktop boundary, and caches the boolean result at module scope for the process lifetime, but the very first call still pays the full synchronous cost, which is user-visible if it happens during interactive command handling.

## Fix Focus Areas
- packages/runtime/src/sandbox/default-sandbox-manager.ts[55-77]
- packages/runtime/src/sandbox/windows-sandbox.ts[93-95]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Desktop SDDL built via unescaped string interpolation ⊘ Outdated 🐞 Bug ⚙ Maintainability
Description
desktop_sddl builds the SDDL string by directly interpolating owner_sid and app_container_sid into
the ACE strings without validating that they are well-formed SID strings; both values currently
originate from ConvertSidToStringSidW-derived, launcher-controlled data so injection is unlikely,
but the function itself provides no defense if a future caller passes an unexpected value, which
would silently corrupt the DACL grant rather than fail closed at construction time.
Code

experiments/windows-sandbox/launcher/src/windows_launcher.rs[R479-483]

+pub(crate) fn desktop_sddl(owner_sid: &str, app_container_sid: &str) -> String {
+    format!(
+        "D:P(A;;GA;;;{owner_sid})(A;;GA;;;SY)(A;;0x{CONFINED_DESKTOP_APP_MASK:x};;;{app_container_sid})"
+    )
+}
Relevance

●● Moderate

Potential hardening is plausible, but evidence shows only controlled SID-derived inputs and no close
validation precedent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
desktop_sddl is a pub(crate) function taking arbitrary &str SID parameters and formatting them
directly into an SDDL string consumed by ConvertStringSecurityDescriptorToSecurityDescriptorW; there
is no validation that the inputs are syntactically valid SID strings before being embedded, so a
malformed or attacker-influenced value would either fail
ConvertStringSecurityDescriptorToSecurityDescriptorW (fail closed, likely benign) or, in the worst
case, alter the ACE structure.
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`desktop_sddl` interpolates `owner_sid` and `app_container_sid` directly into an SDDL string without validating their shape (e.g., that they start with `S-1-` and contain only digits/hyphens). Currently both values are produced internally via `ConvertSidToStringSidW`, so this is low risk, but the function itself provides no defense if a future caller passes an unexpected value.

## Issue Context
The function is `pub(crate)` and is exercised directly by tests with hard-coded SID literals, so it is a candidate seam for reuse elsewhere; without input validation, a bug in a future caller could silently produce a malformed DACL instead of failing loudly.

## Fix Focus Areas
- experiments/windows-sandbox/launcher/src/windows_launcher.rs[479-483]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Web pages:
  +14 more
Review mode: 🧠 Deep: This is a security-sensitive Windows sandbox hardening change with substantial new native logic across desktop ACLs, process creation, readiness probing, and runtime availability caching, creating multiple independent, easy-to-miss failure modes.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs Outdated
Comment thread packages/runtime/src/sandbox/default-sandbox-manager.ts
Comment thread packages/runtime/src/sandbox/default-sandbox-manager.ts Outdated

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

Request changes on the current head c999eefd4.

The alternate-desktop implementation is a useful partial hardening step, but the security guarantee currently documented by this PR is stronger than the mechanism:

CreateDesktopW creates the new desktop on the launcher's current window station, and the RFC explicitly defers a dedicated window station. On Windows, windows/messages/hooks are desktop-scoped, but the clipboard belongs to the window station. Therefore this change can isolate the child from Default windows and desktop hooks, but it does not by itself establish the claimed clipboard isolation while both desktops remain in the same window station.

The current desktopIsolated attestation only proves that the desktop name is non-empty and not Default; the smoke additionally proves the generated prefix. Neither test attempts OpenClipboard/GetClipboardData, proves a different window station, or proves that the child cannot reopen Winsta0\\Default.

Please either:

  • narrow the RFC/code comments and PR claim to the desktop-scoped guarantees actually enforced here, with clipboard isolation remaining deferred; or
  • create a dedicated window station plus desktop under one RAII lifecycle, pass the full winsta\\desktop name, and add negative tests for clipboard access, opening/switching to Default, child escape, and residual object cleanup.

This PR is stacked on #3161, whose readiness ownership and failure-cleanup blockers also need to be resolved before this can merge.

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

English

Follow-up review on current head c999eefd4.

The previously reported window-station/clipboard blocker remains and is not duplicated here. One additional blocker was found: the new desktop receives a DACL but no explicit Low mandatory integrity label. Since AppContainer processes run at Low IL, the current probes prove only desktop assignment and name visibility—not that the advertised create-window/write rights are usable by production children.

Please add and verify the appropriate integrity label, with a child-side window-creation test, or explicitly narrow the backend to a no-Win32k contract.

The commit also discloses material Claude authorship but contains only Co-Authored-By; project policy requires a Generated-by: <tool> trailer on every materially AI-authored commit.

All current GitHub checks pass, including 41 Rust tests and the Windows smoke, but those tests do not exercise clipboard access, reopening Winsta0\Default, desktop write/create access, or concurrent desktop-heap pressure.

简体中文

对当前 head c999eefd4 的补充审查。

先前指出的 window-station/clipboard blocker 仍然成立,此处不重复评论。另发现一个 blocker:新 desktop 配置了 DACL,却没有显式设置 Low mandatory integrity label。AppContainer 进程运行在 Low IL,因此当前 probe 只证明了 child 被分配到指定名称的 desktop,不能证明生产 child 实际拥有所声明的 create-window/write 权限。

请设置并验证正确的 integrity label,同时增加 child-side window-creation 测试;或者明确把 backend 收窄为禁止 Win32k 的合同。

此外,该提交披露了 Claude 的实质性参与,但只有 Co-Authored-By。项目政策要求每个包含实质性 AI 内容的提交带有 Generated-by: <tool> trailer。

当前 GitHub checks 全部通过,包括 41 个 Rust 测试和 Windows smoke;但这些测试没有覆盖 clipboard、重新打开 Winsta0\Default、desktop write/create 权限以及并发 desktop-heap 压力。

Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs
liugddx and others added 3 commits August 18, 2026 14:58
The Windows AppContainer backend advertised availability from file
existence alone (`existsSync(clientPath)`), which never proves the host
can actually stand up the sandbox identity. Replace that with a real
readiness probe (RFC §6.4) and align the RFC to the shipped behavior.

- launcher: add `--readiness-probe`, which creates the real AppContainer
  identity/token and a kill-on-close Job and launches a throwaway confined
  child, failing closed if the host cannot create or enforce the boundary.
- runtime: back the Windows backend's `isAvailable` with a memoized
  spawnSync of `--readiness-probe`, so `auto`/`require` fail closed on
  hosts where the OS cannot create the boundary rather than trusting the
  packaged binary's presence.
- CI: add readiness-probe-smoke.ps1 to the W0 lane (exit 0, repeatable,
  argument-rejecting).
- docs: align RFC §6.4/§6.5 (EN + zh-CN) — mark the readiness probe as
  implemented and enforced, and keep private desktop, full per-profile
  policy at readiness, and launcher signature/version as later gates.

Advances apache#2142 Phase 4 (align the RFC's guarantees with the shipped slice).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
…entity spoofing

Address adversarial review on the production-identity readiness probe:

- Make the throwaway AppContainer profile name unique per invocation with a
  monotonic nonce (SystemTime nanos + PID) so a recycled PID can never collide
  with a still-registered profile and deadlock the probe (finding A).
- Prove enforcement, not mere presence: verify the confined child runs under the
  SPECIFIC requested Job (IsProcessInJob against our handle) and carries the
  EXACT requested AppContainer SID (TokenAppContainerSid + EqualSid), failing
  closed otherwise. "some job / some AppContainer" is not evidence the boundary
  we asked for is real (finding B).
- Cache Windows readiness at module scope keyed by client path so backends
  sharing a launcher share one probe result; distinct paths stay independent
  (finding D).
- Sync RFC (EN + zh) Updated: date to 2026-08-17 (finding C).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
…eadiness probe deterministically

Address maintainer review on the readiness-probe PR:

- Availability: make `isBuiltinFilesystemWorkerSandboxAvailable(win32)` consume
  the memoized readiness probe instead of treating launcher-file presence as a
  second authority. Runtime Host composition calls this at startup, warming the
  readiness cache so the backend's later synchronous `isAvailable()` on the
  transform hot path hits the cache rather than spawning on the event loop.
  Export `probeWindowsReadiness` with an injectable spawn seam and cover it with
  unit tests (clean/non-zero/spawn-error/external-timeout/memoized/missing).

- Settlement: route the readiness child's post-resume outcomes (verification
  failure, timeout, clean exit) through explicit TerminateProcess plus the
  existing `terminate_and_drain_job` primitive, so a child found outside the Job
  is settled rather than relying on Job close as the only backstop.

- Cleanup: give the readiness AppContainer profile one fixed, self-reconciling
  name that best-effort-deletes any leftover before create, so a profile leaked
  by an externally-killed probe is reclaimed by the next probe. Replaces the
  PID+nonce scheme.

- Docs: mark the structured unavailable-reason / diagnostics surface as a
  deferred later gate in both RFC language variants (§6.4/§6.5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, leaving the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) open — AppContainer confines files/network/token but not the desktop
scope. Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

Enforce the desktop-scoped half under an explicit no-Win32k contract: each
AppContainer launch (production create + readiness probe) now stands up a
per-launch alternate desktop on the existing window station, with a protected
DACL that grants only the launching user, Local System, and the child's
AppContainer SID the minimal DESKTOP_* rights (create window/menu, read/write
objects, enumerate) and never DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal.
STARTUPINFOEXW.lpDesktop points the child at it. If the desktop cannot be
created or the DACL cannot be granted (CreateProcessW then fails ACCESS_DENIED),
the launch fails closed — auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- This keeps the worker off the interactive Default desktop, so it cannot
  enumerate/post messages to the user's interactive windows or install desktop
  hooks against them.
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work (no-Win32k contract). A verified Low mandatory-
  integrity label proving those rights are usable at AppContainer's Low IL is
  deferred (§6.5).

The boundary probe self-attests its own desktop name and refuses to run on
Default; appcontainer-smoke asserts the private-desktop name prefix and rejects
Default. Digest is unchanged: the desktop is a launch-time detail and does not
enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx
liugddx force-pushed the feat/windows-sandbox-private-desktop branch from c999eef to 0df8547 Compare August 18, 2026 07:11
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Thanks both — pushed 0df8547d7, taking the narrowing path you offered rather than building a full window station in this PR.

@hqhq1025 — I took option (a): narrow the claim to the desktop-scoped guarantees actually enforced, with clipboard deferred.

  • The RFC (§6.3, both languages), the create_confined_desktop doc comment, and the PR body now claim only that the worker is confined off the interactive Default desktop — it cannot enumerate/post messages to the user's interactive windows or install desktop hooks against them.
  • Clipboard isolation is explicitly not claimed here: the RFC now states the clipboard is window-station-scoped and stays shared, and §6.5 lists "full window-station separation and clipboard isolation" as the later gate. The desktopIsolated attestation is scoped to exactly what it proves (name is non-empty and not Default) — a confinement check, not a clipboard claim.

@M4n5ter — the DACL-vs-Low-IL / integrity-label finding: I took your "explicitly narrow the backend to a no-Win32k contract" option.

  • The shipped contract is now stated as confinement only — the worker performs no Win32k/GUI work. The create-window/write rights in the desktop DACL are granted but explicitly not relied upon; §6.5 (both languages) adds a deferred gate "Usable Low-IL desktop rights / no-Win32k contract" noting the desktop carries no explicit Low mandatory-integrity label, so a verified label plus a child-side window-creation test is deferred. So the probes now prove only what the contract claims (desktop assignment + off-Default), matching the narrowed guarantee.
  • AI attribution: the commit now carries Generated-by: Claude Fable 5, and the PR template's AI use section is filled in with tool and scope.

Both the readiness-ownership and failure-cleanup blockers you referenced on #3161 are addressed in 4506446f2 there (single availability authority warmed at composition; settlement-path reuse; deterministic self-reconciling cleanup). This PR stays stacked on #3161 — please merge #3161 first.

简体中文

感谢两位 —— 已推 0df8547d7,采用你们给出的收窄方案,而不是在本 PR 里建完整 window station。

@hqhq1025 —— 采用 option (a):把声明收窄到实际强制的 desktop 作用域,clipboard 延后。

  • RFC(§6.3,双语)、create_confined_desktop doc 注释、PR 正文现在只声明 worker 被挡在交互 Default 桌面之外 —— 不能枚举/向用户交互窗口发消息,或对其装 desktop hook。
  • 此处明确声明 clipboard 隔离:RFC 现说明 clipboard 是 window-station 作用域、仍为共用,§6.5 把"完整 window-station 分离与 clipboard 隔离"列为后续门禁。desktopIsolated 断言严格对应它所证明的(名字非空且非 Default)—— 是 confinement 检查,不是 clipboard 声明。

@M4n5ter —— DACL vs Low-IL / integrity-label 那条:采用你的"明确把 backend 收窄为 no-Win32k 契约"选项。

  • 落地契约现在表述为仅 confinement —— worker 不做任何 Win32k/GUI 工作。desktop DACL 里的 create-window/write 权限被授予但明确不依赖;§6.5(双语)新增延后门禁"可用的 Low-IL 桌面权限 / no-Win32k 契约",说明该桌面没有显式 Low mandatory-integrity label,故经验证的 label 加 child 侧 window-creation 测试暂缓。这样 probe 只证明契约所声明的内容(桌面分配 + 非 Default),与收窄后的保证一致。
  • AI attribution:该 commit 现带 Generated-by: Claude Fable 5,PR 模板 AI use 段已按工具与范围填好。

你在 #3161 提到的 readiness-ownership 与 failure-cleanup blocker,已在那边 4506446f2 处理(availability 收敛为单一 authority 并在 composition warm;复用 settlement path;确定性自我调和清理)。本 PR 仍 stack 在 #3161 上 —— 请先合 #3161

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

Follow-up review on current head 0df8547d7.

The private-desktop-specific concerns from my previous review are resolved at the contract level:

  • the RFC and code comments now explicitly state that the alternate desktop does not isolate the window-station clipboard;
  • dedicated window-station/clipboard isolation is correctly deferred;
  • Low-IL create-window/write usability is no longer claimed, and the shipped behavior is narrowed to a no-Win32k confinement contract;
  • the AI attribution trailers are present.

I would accept this PR's private-desktop delta on that narrowed scope. I cannot approve the stacked revision yet because it includes #3161 head 4506446f2, whose fixed readiness-profile implementation still has blocking namespace, cross-process ownership, unsettled-cleanup, and permanent-negative-cache issues. See my follow-up review on #3161 for the exact failure paths.

Once #3161 lands with those lifecycle issues resolved and this stack is rebased onto that accepted head, I will re-check the final desktop delta and CI. The current #3174 Windows W0 check is green; some package/E2E checks are still running at review time.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/architecture/windows-sandbox-rfc-v1.zh-CN.md (1)

128-172: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

中文版 §6.5 结尾缺少英文版的最后一句,且新增段落混用半角逗号。

两处与英文版不一致:

  1. 第 172 行只保留了「暂缓收窄的是 readiness 丰富度……绝不回退到宿主执行」。英文版 docs/architecture/windows-sandbox-rfc-v1.md 第 252-254 行还有一句:cancellation、parent-death、concurrency、process-drain 与残留 ACL/state 释放的生命周期证据由 W1(§9)与 Phase 4(#2142)跟踪,属于发布证据而非假设。中文读者会漏掉这条发布 gate。
  2. 新增文本在中文句中使用半角逗号 ,(如第 128、137、140-141、143、158-159、164-166、168-170 行),与文档其余部分的全角 不一致。

处置:第 1 点 fix-now(翻译内容缺失会改变含义);第 2 点 optional。

🧹 Nitpick comments (9)
experiments/windows-sandbox/launcher/src/windows_launcher.rs (2)

471-507: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: consolidate the two CreateAppContainerProfile call sites.

create_readiness repeats the full creation sequence from create. Only the display strings, the pre-delete reclaim, and the error prefix differ. Two call sites now own the profile-creation contract, so a later change to that contract can drift.

Disposition: optional. There is no correctness defect today.

♻️ Suggested shape
impl AppContainerProfile {
    unsafe fn create_named(
        name: Vec<u16>,
        display_name: &str,
        description: &str,
        label: &str,
    ) -> Result<Self, String> {
        let display_name = wide(display_name);
        let description = wide(description);
        let mut sid = null_mut();
        let result = unsafe {
            CreateAppContainerProfile(
                name.as_ptr(),
                display_name.as_ptr(),
                description.as_ptr(),
                null(),
                0,
                &mut sid,
            )
        };
        if result < 0 {
            return Err(format!(
                "CreateAppContainerProfile{label} failed closed: HRESULT 0x{:08x}",
                result as u32
            ));
        }
        Ok(Self { sid, name })
    }
}

create calls it with ""; create_readiness calls DeleteAppContainerProfile first and passes "(readiness)".

Source: Path instructions


596-645: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Derive the desktop name from request_id.

CreateDesktopW opens an existing desktop and ignores the supplied security descriptor. A clock rollback or coarse clock resolution can reuse this name while the old desktop remains referenced. The AppContainer child can then fail with ACCESS_DENIED because its SID is absent from the existing DACL. Hash request_id for the desktop name, as for the AppContainer profile.

experiments/windows-sandbox/launcher/src/main.rs (2)

286-298: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: make the child-side predicate the single authority on desktop identity.

desktop_is_isolated rejects only an empty name and Default. experiments/windows-sandbox/appcontainer-smoke.ps1 Lines 84 and 88 assert the stronger property, that the name starts with maka-sandbox-desktop.. Two places now define what an isolated desktop is, and the weaker definition is the one the launcher enforces.

If the child must always land on a launcher-created desktop, assert the prefix here and let the smoke script rely on desktopIsolated.

Disposition: optional.

♻️ Suggested change
-/// A confined worker must run on a private desktop, never the shared
-/// interactive `Default` desktop, and never an empty/unnamed one.
-fn desktop_is_isolated(name: &str) -> bool {
-    !name.is_empty() && !name.eq_ignore_ascii_case("Default")
-}
+/// A confined worker must run on a desktop this launcher created, never the
+/// shared interactive `Default` desktop, and never an empty/unnamed one.
+fn desktop_is_isolated(name: &str) -> bool {
+    name.starts_with("maka-sandbox-desktop.")
+}

The desktop-name prefix is produced by create_confined_desktop in experiments/windows-sandbox/launcher/src/windows_launcher.rs Lines 603-606, so extract it into one shared constant if you take this.

Source: Path instructions


300-319: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse serde_json instead of a hand-written JSON escaper.

main.rs already depends on serde_json (see serde_json::from_str at Line 137 and serde_json::to_vec at Line 385). serde_json::to_string(&desktop) produces the same quoted, escaped string for any &str. That deletes this function and the desktop_name_is_json_escaped test in experiments/windows-sandbox/launcher/src/windows_launcher_desktop_tests.rs.

Disposition: optional. The current implementation is correct for the desktop-name input.

♻️ Suggested change
-        "{{...,\"desktop\":{desktop_json},\"desktopIsolated\":{desktop_isolated}}}",
-        desktop_json = json_string(&desktop)
+        "{{...,\"desktop\":{desktop_json},\"desktopIsolated\":{desktop_isolated}}}",
+        desktop_json = serde_json::to_string(&desktop).map_err(|error| error.to_string())?

Source: Path instructions

experiments/windows-sandbox/launcher/src/windows_launcher_tests.rs (1)

102-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test does not protect the behavior its comment describes.

appcontainer_profile_name is a pure SHA-256 of its input. assert_eq!(first, second) therefore asserts that a pure function is deterministic, and the assert_ne! against "request-one" asserts SHA-256 collision resistance. Neither statement can fail while the function is a hash of its argument.

The invariant the comment states — a leaked readiness profile is reclaimed rather than accumulated — lives in AppContainerProfile::create_readiness, which calls DeleteAppContainerProfile before creating. This test does not reach that code.

The distinctness and name-format properties are already covered by appcontainer_profile_identity_is_unique_and_bounded_per_request on Lines 92-100.

Disposition: optional. Consider deleting this test, or replacing it with an assertion that the readiness name is stable across builds by pinning the expected literal, which would catch an accidental change to the fixed request id.

Source: Path instructions

experiments/windows-sandbox/appcontainer-smoke.ps1 (1)

87-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This check cannot fire. Delete it.

Line 84 already requires "desktop":"maka-sandbox-desktop\., so a "desktop":"Default" payload throws at Line 85 before reaching Line 88. The launcher also rejects a non-isolated desktop itself and exits non-zero, which Line 75 catches. Three layers assert the same invariant and this one is unreachable.

Disposition: optional.

🧹 Proposed deletion
-  # The confined child must never land on the shared interactive desktop.
-  if ($rendered -match '"desktop":"Default"') {
-    throw "Confined child ran on the interactive Default desktop: $rendered"
-  }

Source: Path instructions

packages/runtime/src/__tests__/default-sandbox-manager.test.ts (1)

153-164: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the cache-warming claim across both entry points.

This test proves memoization within probeWindowsReadiness. The load-bearing claim in packages/runtime/src/sandbox/default-sandbox-manager.ts Lines 134-139 is stronger: a call through isBuiltinFilesystemWorkerSandboxAvailable warms the cache so the backend's later synchronous isAvailable never spawns. No test asserts that the two entry points share one cache entry for the same resolved launcher path.

Disposition: optional. It protects the property that keeps the 15-second spawn off the operation path.

experiments/windows-sandbox/readiness-probe-smoke.ps1 (1)

17-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the em dashes to clear the PSScriptAnalyzer BOM warning.

Lines 19 and 20 contain em dash characters, which makes the file non-ASCII. PSScriptAnalyzer reports PSUseBOMForUnicodeEncodedFile for it. Windows PowerShell 5.1 reads a BOM-less UTF-8 file as ANSI and mangles those characters. The CI step uses shell: pwsh, so CI is unaffected, and the characters appear only in comments.

Either use ASCII hyphens or save the file with a UTF-8 BOM.

Disposition: optional.

Source: Linters/SAST tools

packages/runtime/src/sandbox/default-sandbox-manager.ts (1)

74-91: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Do not cache timeout results permanently

probeWindowsReadiness() caches false for timeouts (status === null). A transient startup timeout disables Windows sandboxing until process restart. Keep permanent caching for missing launchers and non-timeout failures, but retry timeouts with a bounded policy.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 99058c3f-425a-491b-b950-2f910b0f32de

📥 Commits

Reviewing files that changed from the base of the PR and between 19ccf0b and 0df8547.

📒 Files selected for processing (12)
  • .github/workflows/windows-sandbox-w0.yml
  • docs/architecture/windows-sandbox-rfc-v1.md
  • docs/architecture/windows-sandbox-rfc-v1.zh-CN.md
  • experiments/windows-sandbox/appcontainer-smoke.ps1
  • experiments/windows-sandbox/launcher/Cargo.toml
  • experiments/windows-sandbox/launcher/src/main.rs
  • experiments/windows-sandbox/launcher/src/windows_launcher.rs
  • experiments/windows-sandbox/launcher/src/windows_launcher_desktop_tests.rs
  • experiments/windows-sandbox/launcher/src/windows_launcher_tests.rs
  • experiments/windows-sandbox/readiness-probe-smoke.ps1
  • packages/runtime/src/__tests__/default-sandbox-manager.test.ts
  • packages/runtime/src/sandbox/default-sandbox-manager.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.

Comment thread docs/architecture/windows-sandbox-rfc-v1.md Outdated
Comment thread experiments/windows-sandbox/launcher/src/main.rs
Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs Outdated
Address four readiness-probe lifecycle blockers on the availability path:

1. Reserve the `readiness-probe` request_id in protocol and reject it in
   LaunchRequest::validate; derive the probe profile under a disjoint
   `maka.readiness.` namespace so no production launch can resolve to the
   profile the probe deletes and recreates.
2. Serialize the whole delete->create->probe->settle->drop window across
   processes with the DACL-hardened named mutex the ACL ledger uses
   (LedgerLock), scoped by SID under
   Global\Maka.WindowsSandbox.ReadinessProfile.v1, failing closed on timeout.
3. Preserve the AppContainer registration on drop when the Job could not be
   proven empty (keep_on_drop + preserve()), mirroring the production
   Unsettled contract, instead of deleting a possibly-live identity.
4. Cache negative Windows readiness for a bounded TTL (60s) rather than the
   process lifetime, and make the synchronous isAvailable hot path strictly
   cache-only (readCachedWindowsReadiness) so it never spawns on the event
   loop; positive results stay cached permanently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, leaving the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) open — AppContainer confines files/network/token but not the desktop
scope. Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

Enforce the desktop-scoped half under an explicit no-Win32k contract: each
AppContainer launch (production create + readiness probe) now stands up a
per-launch alternate desktop on the existing window station, with a protected
DACL that grants only the launching user, Local System, and the child's
AppContainer SID the minimal DESKTOP_* rights (create window/menu, read/write
objects, enumerate) and never DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal.
STARTUPINFOEXW.lpDesktop points the child at it. If the desktop cannot be
created or the DACL cannot be granted (CreateProcessW then fails ACCESS_DENIED),
the launch fails closed — auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- This keeps the worker off the interactive Default desktop, so it cannot
  enumerate/post messages to the user's interactive windows or install desktop
  hooks against them.
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work (no-Win32k contract). A verified Low mandatory-
  integrity label proving those rights are usable at AppContainer's Low IL is
  deferred (§6.5).

The boundary probe self-attests its own desktop name and refuses to run on
Default; appcontainer-smoke asserts the private-desktop name prefix and rejects
Default. Digest is unchanged: the desktop is a launch-time detail and does not
enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx
liugddx force-pushed the feat/windows-sandbox-private-desktop branch from 0df8547 to d861e1e Compare August 18, 2026 08:45
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Rebased onto the updated #3161 head (c8b6527d2) — the private-desktop delta itself is unchanged; only the readiness-probe base commit moved to pick up the four lifecycle fixes reviewed on #3161. Re-validated the launcher on the rebased head: cargo fmt --check, cargo build --locked, and cargo test --locked (45 passing, including the private-desktop desktop tests). No content change to the desktop scope reviewed here.

中文

已 rebase 到更新后的 #3161 head(c8b6527d2):private-desktop 改动本身不变,仅 readiness-probe 基座 commit 前移以纳入 #3161 上评审的四条 lifecycle 修复。在 rebase 后的 head 复验 launcher:cargo fmt/build/test(45 通过,含 private-desktop 桌面测试)。此处评审的 desktop 作用域无内容变化。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
experiments/windows-sandbox/launcher/src/windows_launcher.rs (1)

618-640: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

One constant serves two different authorities. Split it, and drop the rights the no-Win32k contract does not use.

CONFINED_DESKTOP_APP_MASK is used for two unrelated decisions:

  • Line 638: the access mask granted to the AppContainer package SID in the desktop DACL.
  • Line 719: the dwDesiredAccess the launcher requests for its own HDESK.

The launcher only needs a handle that keeps the desktop alive and can be closed. It does not need DESKTOP_CREATEWINDOW, DESKTOP_CREATEMENU, DESKTOP_WRITEOBJECTS, or DESKTOP_ENUMERATE. Coupling the two means narrowing the child's grant also narrows the launcher's handle, so neither can be tightened independently.

The doc comment at Lines 667-670 states the create-window and write rights are granted but not relied upon, because the worker performs no GUI work. Under that stated contract those grants are unused authority in a security boundary. Granting DESKTOP_READOBJECTS alone matches the shipped guarantee and removes the ambiguity the past Low-IL review raised.

Disposition: follow-up. The current code is coherent, but it grants rights the PR explicitly says it does not depend on.

♻️ Proposed split
-const CONFINED_DESKTOP_APP_MASK: u32 = DESKTOP_READOBJECTS
-    | DESKTOP_CREATEWINDOW
-    | DESKTOP_CREATEMENU
-    | DESKTOP_WRITEOBJECTS
-    | DESKTOP_ENUMERATE;
+/// Rights granted to the confined child. Under the no-Win32k contract the
+/// worker performs no GUI work, so only the read right is granted.
+const CONFINED_DESKTOP_CHILD_MASK: u32 = DESKTOP_READOBJECTS;
+
+/// Rights the launcher requests for its own handle: enough to keep the desktop
+/// alive and close it.
+const CONFINED_DESKTOP_OWNER_MASK: u32 = DESKTOP_READOBJECTS;

Then use CONFINED_DESKTOP_CHILD_MASK in desktop_sddl and CONFINED_DESKTOP_OWNER_MASK in the CreateDesktopW call.

Also applies to: 710-722

Source: Path instructions

experiments/windows-sandbox/launcher/src/acl_ledger.rs (1)

219-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The lease kind is encoded twice: in the name generator and in this substring match.

acquire infers the lease kind from name.contains(".ReadinessProfile."). readiness_mutex_name at Lines 54-56 is the source of truth for that substring. If the object name changes, this branch silently falls through to "acquire ACL ledger mutex timed out" and the diagnostic becomes wrong. The compiler cannot catch it.

Passing the label explicitly consolidates the authority in one place.

Disposition: optional. The existing .AclLease. branch has the same shape, so this is not a regression introduced here.

♻️ Proposed consolidation
-    pub(crate) fn acquire(name: &str, user_sid: &str, timeout_ms: u32) -> Result<Self, String> {
-        Self::try_acquire(name, user_sid, timeout_ms)?.ok_or_else(|| {
-            if name.contains(".AclLease.") {
-                "acquire ACL ledger lease timed out".to_owned()
-            } else if name.contains(".ReadinessProfile.") {
-                "acquire readiness profile lease timed out".to_owned()
-            } else {
-                "acquire ACL ledger mutex timed out".to_owned()
-            }
-        })
+    pub(crate) fn acquire(
+        name: &str,
+        user_sid: &str,
+        timeout_ms: u32,
+        kind: &'static str,
+    ) -> Result<Self, String> {
+        Self::try_acquire(name, user_sid, timeout_ms)?
+            .ok_or_else(|| format!("acquire {kind} timed out"))
     }

Callers then pass "readiness profile lease", "ACL ledger lease", or "ACL ledger mutex".

Source: Path instructions

packages/runtime/src/__tests__/default-sandbox-manager.test.ts (1)

211-231: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test duplicates coverage already asserted at Lines 203-207.

The preceding test ends by advancing the clock ten minutes and asserting calls stays at 2. That proves the spawn is not called at all after a positive result. This test asserts the weaker consequence that a failing spawn is not consulted, which follows from the spawn never running.

Deleting it removes one test to maintain without losing an invariant.

Disposition: optional.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 54c9eb67-1666-41be-aa25-c87d5ceec9f3

📥 Commits

Reviewing files that changed from the base of the PR and between 0df8547 and d861e1e.

📒 Files selected for processing (8)
  • docs/architecture/windows-sandbox-rfc-v1.md
  • docs/architecture/windows-sandbox-rfc-v1.zh-CN.md
  • experiments/windows-sandbox/launcher/src/acl_ledger.rs
  • experiments/windows-sandbox/launcher/src/protocol.rs
  • experiments/windows-sandbox/launcher/src/windows_launcher.rs
  • experiments/windows-sandbox/launcher/src/windows_launcher_tests.rs
  • packages/runtime/src/__tests__/default-sandbox-manager.test.ts
  • packages/runtime/src/sandbox/default-sandbox-manager.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/architecture/windows-sandbox-rfc-v1.md
  • docs/architecture/windows-sandbox-rfc-v1.zh-CN.md

Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.

Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs Outdated

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

Follow-up review on current head d861e1ee7.

The earlier clipboard and Low-IL overclaims are fixed, but the private-desktop delta still does not establish confinement from the interactive desktop.

STARTUPINFO.lpDesktop selects the child's initial desktop; it does not prevent code inside that child from calling OpenDesktopW("Default") and moving a thread with no windows/hooks via SetThreadDesktop. This PR creates and protects the private desktop, but it neither structurally denies access to Winsta0\Default nor enables a no-Win32k mitigation. The current smoke proves only the initial desktop name, so desktopIsolated overstates what was verified.

Please make one of the intended boundaries real: preferably enable and validate the no-Win32k mitigation for the Node worker, or use a token/window-station design that prevents reopening Default. Add child-side negative tests for OpenDesktop(Default), SetThreadDesktop, window enumeration, and hooks. Until then this is initial desktop placement, not malicious-code desktop confinement.

This stack also includes #3161, which still has the readiness retry and unsettled-profile lifecycle blockers noted in my latest review there. All current CI is green, including the Windows W0 lane, but it does not cover the escape path above. Not ready to merge.

…to what the code enforces

hqhq1025's round-3 review is factually correct on both points; this converges the
claims rather than building the heavier machinery, matching the preview's fail-closed
scope.

Blocker 1 (TTL recovery overclaim): the filesystem worker is published once when a
composition is built, so a negative readiness result is not recovered within a running
host — the 60s negative TTL only bounds how long a stale negative poisons the module
cache so the *next* composition build re-probes. De-claim the RFC (EN+zh §6.4/§6.5) and
default-sandbox-manager.ts comments from 're-probes on the next composition rather than
disabling until restart' to honest new-composition/restart-scoped recovery, and mark an
active running-host readiness retry with dynamic worker publication as a deferred gate.
Add a test asserting the hot path never self-recovers a negative; only an explicit
re-probe (a new composition build) can.

Blocker 2 (ineffective preserve/keep_on_drop): create_readiness deletes the fixed
identity unconditionally next cycle, so preserve() only deferred deletion by one probe
and never durably quarantined. Remove preserve()/keep_on_drop and the unsettled
out-param; the settlement-failure surfacing in the match stays, so an unsettled probe
still fails closed (reports unavailable). Document that cleanup relies on the
kill-on-close Job's tree termination and a zero-filesystem-root identity, and mark
durable quarantine (or unique probe identities plus a reconciliation ledger) as a
deferred gate (RFC §6.5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5

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

English

Request changes on current head d861e1ee7.

I found one additional blocker not covered by the existing threads: allocating a default-size WinSta0 desktop for every live launch puts ordinary sustained parallel workloads close to the system desktop-heap limit.

I am not repeating hqhq1025’s valid current-head finding that lpDesktop only selects the initial desktop and that no Win32k/UI restriction prevents malicious code from reopening Default.

No other new material correctness or design findings remain after reviewing the private-desktop delta, its composition with stacked #3161, existing discussions, and the simplification surface. The simplest coherent design may be to enforce the stated no-Win32k boundary and remove the per-launch desktop state, subject to Node-worker compatibility evidence.

Verification on d861e1ee7:

  • All current GitHub checks are successful, including the Windows W0 lane.
  • git diff --check and cargo fmt --all -- --check pass.
  • Windows-target production and test code compile with cargo check.
  • Desktop-heap exhaustion was established from the documented Windows resource contract and reachable sustained-concurrency path; the current CI does not reproduce it because its ten child processes exit quickly.
简体中文

在当前 head d861e1ee7 上请求修改。

我发现了一个现有讨论尚未覆盖的 blocker:每个存活的 launch 都在 WinSta0 上分配默认大小的 desktop,会让普通的持续并发 workload 很快逼近系统 desktop-heap 上限。

hqhq1025 已经准确指出:lpDesktop 只选择初始 desktop,当前也没有 Win32k/UI restriction 阻止恶意代码重新打开 Default。这里不重复该 finding。

检查 private-desktop 增量、它与 stacked #3161 的组合、全部现有讨论和简化空间后,没有其他新的实质性 correctness/design finding。一个可能更简单且完整的方向是强制落实文档声明的 no-Win32k boundary,并删除按 launch 创建 desktop 的状态;前提是补齐 Node worker 的兼容性证据。

d861e1ee7 上的验证:

  • 当前所有 GitHub checks 均成功,包括 Windows W0 lane。
  • git diff --checkcargo fmt --all -- --check 通过。
  • Windows target 的生产代码和测试代码均通过 cargo check
  • Desktop-heap 问题基于微软公开的资源契约和可达的持续并发路径确认;当前 CI 的十个 child 很快退出,因此没有复现持有全部 desktop 时的耗尽场景。

Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs Outdated
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL that grants only the launching user,
Local System, and the child's AppContainer SID the minimal DESKTOP_* rights
(create window/menu, read/write objects, enumerate) and never
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal. STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be granted
(CreateProcessW then fails ACCESS_DENIED), the launch fails closed — auto/require
never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name (desktopPrivatePlacement)
and refuses to run on Default; appcontainer-smoke asserts the private-desktop
name prefix and rejects Default. Digest is unchanged: the desktop is a
launch-time detail and does not enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx
liugddx force-pushed the feat/windows-sandbox-private-desktop branch from d861e1e to c000c64 Compare August 18, 2026 10:13
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

@hqhq1025 Your latest point is correct and I've converged on de-claiming rather than shipping the escape-proof boundary in this PR. New head: c000c6423 (rebased onto the fixed #3161 head 8b03f9f61).

lpDesktop is initial placement, not confinement — confirmed. STARTUPINFOW.lpDesktop selects only the child's initial desktop; in-process code can still OpenDesktopW("Default") + SetThreadDesktop to re-attach, because no no-Win32k mitigation, dedicated window station, or token boundary is enforced. The old desktopIsolated attestation proved only the initial desktop name, so it overstated. Changes:

  • Renamed the attestation desktopIsolateddesktopPrivatePlacement (JSON key, desktop_is_isolateddesktop_is_private_placement, comments, smoke assertion, unit test), so the name matches what it proves: initial-desktop placement + a DACL-protected private desktop.
  • RFC §6.3 and §6.5 (EN+zh) now state plainly this is placement + DACL protection, not escape-proof confinement, and explicitly note the OpenDesktopW("Default")+SetThreadDesktop re-attach path.
  • Moved the intended boundaries — no-Win32k process mitigation, dedicated window station (which also isolates the clipboard), token boundary, and the child-side negative tests (OpenDesktop(Default), SetThreadDesktop, window enumeration, hooks) — into explicit §6.5 deferred gates.
  • Rewrote the commit subject confine …place … on a private desktop and its body to match.

Why de-claim rather than enable no-Win32k now: making one boundary real (no-Win32k for the Node worker, or a token/window-station design) plus child-side escape tests is a genuine hardening slice with its own compatibility surface across Node/PowerShell/cmd/Git. Bundling it here would blow this PR's scope; the honest shipped guarantee is placement + DACL, and the worker does no GUI work. I'd rather ship that truthfully now and land the escape-proof boundary as its own gated slice than overclaim.

Since your prior review already accepted the private-desktop delta on the narrowed scope, and the stack is now rebased onto the fixed #3161 head, this should be re-checkable once #3161 lands. Not self-merging.

中文

你的最新意见正确,本 PR 选择降级声明而非在此塞入防逃逸边界。新 head:c000c6423(已 rebase 到修好的 #3161 head 8b03f9f61)。

lpDesktop 是初始放置、不是 confinement —— 属实。 lpDesktop 只选初始桌面;进程内代码仍可 OpenDesktopW("Default") + SetThreadDesktop 重新挂回,因为没有 no-Win32k mitigation、独立 window station 或 token 边界。旧的 desktopIsolated 只证了初始桌面名,故 overclaim。改动:把断言 desktopIsolateddesktopPrivatePlacement(JSON key、desktop_is_isolateddesktop_is_private_placement、注释、冒烟断言、单测),让命名与所证一致:初始桌面 placement + DACL 保护的私有桌面;RFC §6.3/§6.5(EN+zh)明确为 placement + DACL 保护、非防逃逸 confinement,并写明 OpenDesktopW("Default")+SetThreadDesktop 重挂路径;把预期边界(no-Win32k mitigation、独立 window station 兼隔离 clipboard、token 边界,及 child 侧 OpenDesktop(Default)/SetThreadDesktop/窗口枚举/hook 负测)移入 §6.5 明确延后门禁;commit 主题 confine …place … on a private desktop 并同步正文。

为何不在此启用 no-Win32k:把某条边界做实(Node worker 的 no-Win32k,或 token/window-station 设计)加 child 侧逃逸负测,是一块有自身跨 Node/PowerShell/cmd/Git 兼容面的硬化切片,塞进本 PR 会撑爆作用域;当前如实交付的保证是 placement + DACL,且 worker 不做任何 GUI 工作。我宁愿现在如实交付它、把防逃逸边界作为独立门禁切片落地,也不 overclaim。

你上一轮已在收窄作用域上接受 private-desktop delta,现栈已 rebase 到修好的 #3161 head,待 #3161 落地即可复检。不自合。

@Astro-Han

Copy link
Copy Markdown
Contributor

Thanks for the rework — the desktop confinement is genuinely the right mechanism for the gap (AppContainer fences files/network/token but not desktop scope: window-message shatter and desktop hooks stay open on the interactive desktop), and the implementation is lean: ~120 lines of Rust (create_confined_desktop + lpDesktop on both launch paths + child-side self-attestation) extending the existing create_appcontainer_child/probe_appcontainer_child seam, no parallel machinery. I verified the details: CreateDesktopW with per-object protected DACL (D:P) matches the #3182 lesson, handles close via RAII on all paths, both paths build the desktop before CreateProcessW and set lpDesktop before launch, the probe child also lands on the private desktop with mutex lease covering the whole cycle, failure is fail-closed (no fallback to Default), the private desktop sits on the launcher's winsta with clipboard sharing declared, and the windows-2025 CI lane genuinely runs cargo test + smoke scripts (asserting desktopPrivatePlacement:true, prefix match, rejecting "desktop":"Default") green on real Windows. Both broker paths route through launch_appcontainer, so production coverage is complete.

Conclusion: PASS — no P0/P1.

P2-1 (handle or explicitly defer): the DACL claim vs the child's effective permissions don't match. The desktop_sddl grants GA to the user SID (D:P(A;;GA;;;{owner_sid})(A;;GA;;;SY)(A;;0xc7;;;{app_sid})), and the commit/RFC declare the child "never" gets DESKTOP_SWITCHDESKTOP/HOOKCONTROL/journal. But an AppContainer token keeps the user SID as an effective SID (only Everyone→ etc. become deny-only), so the first ACE gives the child full desktop rights — including SWITCHDESKTOP, HOOKCONTROL, journal recording/replay, WRITE_DAC/WRITE_OWNER. The "minimal non-interactive, never SWITCHDESKTOP/HOOKCONTROL/journal" guarantee only holds at the app-SID ACE level, not for the child's effective access, and the test desktop_dacl_grants_owner_and_system_full_control bakes the GA-for-user into intent. Impact is limited (the child having GA on its own desktop crosses no desktop boundary — switching requires permission on the target/input desktop, whose DACL is system-owned), so this is a claim-vs-effective-access inconsistency rather than an exploitable hole — but future additions of user privileges would silently flow into the child. Fix (either): append a deny ACE for the user SID covering 0x138 (SWITCHDESKTOP|HOOKCONTROL|JOURNALRECORD|JOURNALPLAYBACK) after the grant ACE (launcher doesn't need those; CreateProcessW needs only the 0xc7 subset), or correct the commit/RFC/comment wording and defer explicitly.

P3 (optional): desktop_is_private_placement only excludes Default/empty — any other desktop name passes the child-side self-attestation; match the maka-sandbox-desktop. prefix instead (the smoke script's prefix assertion is the real backstop); the readiness gate proves SID+Job but not desktop placement (the probe child is cmd /c exit 0 — a future refactor dropping lpDesktop would stay green on the gate and only the CI smoke would catch it); the legacy restricted-token paths (--atomic/create_child_atomic via CreateProcessWithTokenW/CreateProcessAsUserW) still run on the interactive desktop with no flag noting the gap — production broker doesn't use them, but a comment or reusing create_confined_desktop there would harden the invitation-to-misuse.


AI-assisted review disclosure: this review was produced with AI assistance (pi review subagent on opencode-go/deepseek-v4-flash), which traced the DACL effective-permission semantics, the launch paths, and the CI lane from the PR head. P2-1 is an access-control analysis (AppContainer token retains user SID as effective), not an observed exploit. Please weigh these findings with your own judgment.

中文摘要(AI 辅助审查)

结论:PASS(无 P0/P1)。桌面隔离正是这个缺口的正确机制(AppContainer 只围文件/网络/token,不围 desktop scope:window-message shatter 与 desktop hooks 在交互桌面仍开放),实现精简:~120 行 Rust(create_confined_desktop + 两条 launch 路径接线 lpDesktop + 子进程侧自证)扩展现有 create_appcontainer_child/probe_appcontainer_child seam,无并行机制。已核实:CreateDesktopW+逐对象受保护 DACL(D:P)符合 #3182 教训;handle 全路径 RAII 关闭;两条路径都在 CreateProcessW 前建桌面并设 lpDesktop;探针 child 同落私有桌面且 mutex lease 覆盖全程;失败 fail-closed 无 fallback;私有桌面在 launcher 的 winsta 上、剪贴板共享已声明;windows-2025 CI lane 真跑 cargo test + smoke(断言 desktopPrivatePlacement:true、前缀匹配、拒绝 "desktop":"Default")在真实 Windows 全绿;broker 两条生产路径都经 launch_appcontainer 全覆盖。P2-1:DACL 声明与子进程有效权限不符——desktop_sddl 给 user SID 授 GA(D:P(A;;GA;;;owner)(A;;GA;;;SY)(A;;0xc7;;;app)),commit/RFC 声明"绝不给 SWITCHDESKTOP/HOOKCONTROL/journal";但 AppContainer token 保留 user SID 为有效 SID(仅 Everyone 等为 deny-only),首条 ACE 即给 child 全部桌面权限(含 SWITCHDESKTOP/HOOKCONTROL/journal 录制回放/WRITE_DAC/WRITE_OWNER);"最小权限绝不给"只在 app SID ACE 层面成立,测试还把 GA-for-user 固化为意图。影响有限(child 对自身桌面 GA 不跨任何桌面边界——切换需目标/输入桌面的权限,其 DACL 系统所有),是声明-有效权限不一致而非可利用漏洞,但未来给 user 增权会静默流入 child。修复二选一:grant ACE 后追加 user SID deny ACE 覆盖 0x138(SWITCHDESKTOP|HOOKCONTROL|JOURNALRECORD|JOURNALPLAYBACK,launcher 不需这些权限、CreateProcessW 只需 0xc7 子集),或修正 commit/RFC/注释措辞并显式延后。P3(可选):desktop_is_private_placement 只排除 Default/空名,任何其他桌面名都通过子进程自证——建议直接匹配 maka-sandbox-desktop. 前缀(冒烟脚本前缀断言才是真兜底);readiness gate 只证 SID+Job 不证桌面放置(探针 child 是 cmd /c exit 0,未来重构丢 lpDesktop 门禁仍绿仅 CI 冒烟能抓);遗留 restricted-token 路径(--atomic/CreateProcessWithTokenW/CreateProcessAsUserW 无 lpDesktop)仍在交互桌面裸奔——生产 broker 不用它们,但建议加注释或复用 create_confined_desktop。

@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Thanks @Astro-Han — took the deny-ACE option and swept the whole PR; head is now 3ec518309 (rebased onto #3161's /d fix), point by point:

P2-1 (owner GA is effective for the AppContainer child) — fixed with the deny ACE. You're right that the launching-user SID stays effective in the AppContainer token, so the owner full-control allow named the child as a grantee of the interactive-control rights the commit/RFC said it would "never" hold — a claim-vs-DACL mismatch even where the lowbox dual access check (user grant ∩ package grant = 0xc7) would have intersected them away in practice. Rather than de-claim, I made the DACL say what the docs say: desktop_sddl now emits D:P(D;;0x138;;;owner)(A;;GA;;;owner)(A;;GA;;;SY)(A;;0xc7;;;app) — a leading deny ACE (canonical order) stripping SWITCHDESKTOP | HOOKCONTROL | JOURNALRECORD | JOURNALPLAYBACK from the user SID, with a named DENIED_DESKTOP_INTERACTIVE_MASK const documenting why. The launcher is unaffected (it only ever requests the 0xc7 app mask, disjoint from the deny), and future user-SID privilege additions can no longer silently flow into the child. New test desktop_dacl_denies_interactive_control_to_the_effective_user_sid asserts the deny ACE exists and precedes every allow ACE; commit message and RFC §6.3/§6.5 (EN + zh) now describe the deny. Real-machine appcontainer-smoke.ps1 green under the new DACL (child still starts, attests the private desktop).

P3 attestation prefix — fixed. desktop_is_private_placement now requires the launcher-owned maka-sandbox-desktop. prefix (case-insensitive, non-empty suffix) instead of merely "not Default/empty" — Winlogon, screensaver desktops, or another product's alternate desktop now fail the child-side check too; the test covers those cases and the smoke prefix assertion remains the CI backstop.

P3 legacy restricted-token paths — documented. create_child / create_child_atomic now carry explicit doc comments: W0 diagnostic paths, NOT production (the packaged broker routes only through launch_appcontainer), children run on the interactive desktop with no placement, and wiring them through create_confined_desktop is deliberately out of scope while they remain negative-evidence prototypes.

P3 readiness gate doesn't prove placement — acknowledged, deferred. True: the probe child (cmd /d /c exit 0) reports nothing, so a refactor dropping lpDesktop would keep the gate green and only the CI smoke would catch it. Teaching the probe child to self-attest its desktop belongs with the §6.5 "full-policy readiness coverage" gate; not expanding the probe protocol in this PR.

Self-sweep result (beyond the quoted lines): your P2-1 plus the pattern across previous rounds — claims exceeding enforcement — prompted a full sweep of this PR instead of another spot-fix. The sweep caught one more instance we had missed: the create_confined_desktop doc comment still called this "a confinement boundary under a no-Win32k contract … the shipped guarantee is confinement." That comment (and two "confine the child" call-site comments) are now placement-scoped like the RFC and commit message.

中文

P2-1(owner GA 对 AppContainer 子进程有效)— 以 deny ACE 修复。 你说得对:发起用户 SID 在 AppContainer token 中仍有效,owner 全控 allow 因而把子进程列为了 commit/RFC 声称"绝不给"的交互控制权限的被授予者——即使 lowbox 双重访问检查(user 授权 ∩ package 授权 = 0xc7)实际会交集掉它们,声明与 DACL 本身仍不一致。这次不降级措辞,而是让 DACL 与文档一致:desktop_sddl 现在输出 D:P(D;;0x138;;;owner)(A;;GA;;;owner)(A;;GA;;;SY)(A;;0xc7;;;app)——前置 deny ACE(规范顺序)从用户 SID 剥离 SWITCHDESKTOP|HOOKCONTROL|JOURNALRECORD|JOURNALPLAYBACK,并有命名常量 DENIED_DESKTOP_INTERACTIVE_MASK 说明缘由。launcher 不受影响(只申请与 deny 不相交的 0xc7),未来给用户 SID 增权也不会再静默流入子进程。新测试断言 deny ACE 存在且在所有 allow 之前;commit/RFC §6.3/§6.5(中英)同步描述 deny。真机 appcontainer-smoke.ps1 在新 DACL 下全绿。

P3 自证前缀 — 已修。 desktop_is_private_placement 改为要求 launcher 自有的 maka-sandbox-desktop. 前缀(大小写不敏感、后缀非空),不再是"非 Default/非空"——Winlogon、屏保桌面或第三方 alternate desktop 现在同样通不过子进程侧自证;测试覆盖这些用例,冒烟前缀断言仍是 CI 兜底。

P3 遗留 restricted-token 路径 — 已注释。 create_child/create_child_atomic 现有显式文档注释:W0 诊断路径、非生产(打包 broker 只走 launch_appcontainer)、子进程在交互桌面上、无 placement;接入 create_confined_desktop 在其仍为负面证据原型期间刻意不做。

P3 readiness gate 不证 placement — 认同并延后。 探针子进程(cmd /d /c exit 0)不汇报任何东西,丢掉 lpDesktop 的重构确实只有 CI 冒烟能抓。让探针子进程自证桌面归入 §6.5 "full-policy readiness coverage" 门禁,本 PR 不扩探针协议。

自查结果(超出被引用行): 你的 P2-1 加上前几轮反复出现的同一模式——声明超出强制——促使这次对整个 PR 做全量清扫而非再打一个点补丁。清扫抓到一处我们此前漏掉的实例:create_confined_desktop 的文档注释仍写着 "confinement boundary under a no-Win32k contract … the shipped guarantee is confinement"。该注释(及两处 "confine the child" 调用点注释)现已与 RFC/commit 一致收窄为 placement。

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

The private-desktop direction strengthens the production AppContainer boundary, and the protected DACL, exact SID/Job checks, separate readiness namespace, and fail-closed Runtime cache are well aligned. Existing public reviews already identify the unaligned token-information UB, WinSta0 desktop-heap exhaustion, and possible existing-desktop reuse; I am not duplicating those inline threads, but they remain blockers.

From first principles, release evidence must attest the boundary properties, not only the launcher's success bit. The additional inline finding asks the independent readiness path to emit and verify its exact desktop/SID/Job attestation, matching what production confinement claims.

Review performed with three Codex reviewer agents and DeepSeek V4 Flash as advisory tools; I verified the novel finding against the latest head and live green CI.

中文评论

private desktop 方向强化了生产 AppContainer 边界;protected DACL、exact SID/Job 校验、独立 readiness namespace 和 fail-closed Runtime cache 均符合职责边界。现有公开 review 已指出 token information 未对齐读取 UB、WinSta0 desktop heap 耗尽和旧 desktop 复用风险,我不重复这些行内线程,但它们仍是 blocker。

从第一性原理看,release evidence 必须证明边界属性,而不只是 launcher 返回成功。以下新增行内问题建议让独立 readiness 路径输出并验证其 exact desktop/SID/Job attestation,与生产 confinement 的声明一致。

本次审查使用了三位 Codex reviewer agents 与 DeepSeek V4 Flash 作为辅助工具;我已依据最新 head 和实时绿色 CI 复核新增问题。

Comment thread experiments/windows-sandbox/readiness-probe-smoke.ps1
The named-lock comment claimed a squatted Global\ name "fails closed at
acquisition", but that only held for a squatter with a restrictive DACL:
CreateMutexW ignores the supplied security descriptor when the name
already exists, so a local user pre-creating the predictable mutex name
with a *permissive* DACL would hand the launcher an attacker-owned
arbitration object -- readiness and ACL-ledger operations would then
block on a mutex the squatter can hold forever (availability DoS), while
the code read as if the DACL had rejected them.

Close the gap where it is checkable: when CreateMutexW reports
ERROR_ALREADY_EXISTS (also the normal same-user contention path), read
the existing object's owner via GetSecurityInfo before any wait and
require it to be the current user or SYSTEM. Ownership is the one
property a permissive squatter cannot forge -- re-owning an object to
another SID requires SeTakeOwnership/SeRestore, which standard users do
not hold. Any other owner fails closed with an explicit
squatted-mutex error instead of blocking. The 10-way concurrency smoke
(same-user contention) passes unchanged; the cross-user negative path
cannot be exercised by an unprivileged CI runner and is enforced by the
owner check itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL: a leading deny ACE strips
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal record+playback (0x138) from the
launching-user SID — which the AppContainer child's token still carries as an
effective SID, so without the deny the owner's full-control allow would name
the child as a grantee of those rights (maintainer review, P2-1) — then allow
ACEs grant the launching user and Local System control for cleanup and the
child's AppContainer SID only the minimal DESKTOP_* rights (create
window/menu, read/write objects, enumerate). STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be
granted (CreateProcessW then fails ACCESS_DENIED), the launch fails closed —
auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name
(desktopPrivatePlacement) and requires the launcher-owned
maka-sandbox-desktop. prefix — not merely "not Default", so landing on any
other pre-existing desktop also fails the placement check; appcontainer-smoke
asserts the same prefix and rejects Default. Digest is unchanged: the desktop
is a launch-time detail and does not enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
…ttested

Follow-ups from inline review on apache#3174, each closing a spot where the
desktop's documented properties exceeded (or under-stated) what the code
enforced:

- Pin a Low no-write-up mandatory label (S:(ML;;NW;;;LW)) on the private
  desktop. Without it the desktop inherited the creator's Medium
  integrity, and because MIC is evaluated before the DACL, the Low-IL
  AppContainer child's granted create-window/write rights were unusable
  in exactly the case the DACL claimed to grant them. The rights are now
  labeled-usable; an in-child window-creation check proving them end to
  end stays a deferred gate (RFC S6.5) - the worker still does no GUI
  work, so the shipped guarantee remains placement.
- Bound the per-launch desktop heap: CreateDesktopExW with a 512 KiB
  budget instead of CreateDesktopW's default 3,072 KiB interactive
  allocation, so the supported ten-way concurrency costs ~5 MiB of the
  documented 48 MiB system desktop heap instead of ~30 MiB. A real-OS
  test holds ten confined desktops live simultaneously to prove the
  budget.
- Fix an unaligned read: TokenAppContainerSid was read through a Vec<u8>
  buffer cast to TOKEN_APPCONTAINER_INFORMATION, whose leading pointer
  field requires pointer alignment - UB in Rust. The buffer is now sized
  in usize words (same pattern as current_user_sid_string).
- Attest the readiness boundary instead of exiting 0: --readiness-probe
  now emits a machine-readable JSON of the facts it verified (exact-SID
  match, specific-Job membership, settlement drain, private-desktop
  placement), and readiness-probe-smoke.ps1 asserts those fields, so
  removing any verification would turn release evidence red rather than
  leaving a hollow exit-0 gate green.

RFC (EN + zh) updated to match: label and heap bound recorded as
enforced, the deferred gate narrowed to no-Win32k plus in-child
window-creation coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx
liugddx force-pushed the feat/windows-sandbox-private-desktop branch from 3ec5183 to 70eea18 Compare August 18, 2026 14:36
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Head is now 70eea182a (rebased onto #3161's a59c2554e, plus one commit), closing all four open inline threads — details and per-thread replies inline: Low no-write-up mandatory label pinned on the desktop (M4n5ter P1; in-child window probe stays deferred per RFC §6.5), desktop heap bounded via CreateDesktopExW at 512 KiB with a real-OS test holding ten confined desktops live simultaneously (M4n5ter P1), the TOKEN_APPCONTAINER_INFORMATION unaligned-read UB fixed with a word-aligned buffer (CodeRabbit), and --readiness-probe now emits a machine-readable attestation (exact SID, specific Job, settlement, desktop placement) that readiness-probe-smoke.ps1 asserts field by field (Astro-Han P2). RFC EN+zh updated to record label + heap bound as enforced and to narrow the deferred gate to no-Win32k + in-child window-creation coverage. Local: 49 cargo tests + both real-machine smokes green.

中文

head 更新为 70eea182a(rebase 到 #3161 a59c2554e 并加一个 commit),关闭全部四条行内线程:桌面钉 Low no-write-up 标签(child 内建窗探针按 §6.5 延后);CreateDesktopExW 限 512 KiB heap + 真机十桌面并存测试;TOKEN_APPCONTAINER_INFORMATION 未对齐读取 UB 改字对齐缓冲;--readiness-probe 输出机器可读 attestation(精确 SID/特定 Job/settlement/桌面放置)且冒烟逐字段断言。RFC 中英同步。本地 49 cargo 测试 + 两冒烟全绿。

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

The latest readiness attestation, Low-IL label, aligned token storage, and bounded desktop heap address the earlier findings. The new mutex-owner hardening currently rejects a legitimate elevated owner in required CI, and the desktop name still lacks a fail-closed collision contract.

The simplest authority is explicit and consistent identity: create the mutex with the exact owner SID the validator accepts, and use a GUID-quality desktop nonce while rejecting/retrying ERROR_ALREADY_EXISTS. Do not broaden trust to every Administrators-group owner merely to make CI pass.

Reviewed with Codex using two independent review passes and DeepSeek V4 Flash as an external adversarial pass; I verified the Windows object-identity paths and live required-check failure against this exact head.

中文

最新 readiness attestation、Low-IL label、token 对齐存储和 desktop heap 上限已修复旧问题。新的 mutex owner 加固在 required CI 中拒绝了合法 elevated owner;desktop 名称也仍缺少 fail-closed 的冲突契约。

最小权威方案是显式且一致的 identity:创建 mutex 时设置 validator 接受的精确 owner SID;desktop 使用 GUID 级 nonce,并在 ERROR_ALREADY_EXISTS 时拒绝或重试。不要为了 CI 直接信任所有 Administrators group owner。

本次由 Codex 两轮独立审查,并使用 DeepSeek V4 Flash 做外部对抗审查;我核对了当前 head 和 required check。

Comment thread experiments/windows-sandbox/launcher/src/acl_ledger.rs
Comment thread experiments/windows-sandbox/launcher/src/windows_launcher.rs Outdated
…ting locks

The squatted-lock owner check rejected locks owned by
BUILTIN\Administrators (S-1-5-32-544), which broke on elevated hosts:
an elevated administrator token stamps the Administrators group -- not
the user SID -- as the default owner on objects it creates, so a mutex
this very code created earlier on the elevated CI runner failed the
{user, SYSTEM} check and the W0 protocol lane went red
(unsettled_launch_preserves_grants_and_quarantines_the_ledger).

Accept the process token's default-owner SID (TokenOwner) as a third
legitimate owner. This stays inside the threat model: only an elevated
administrator can create objects owned by Administrators, and RFC
S1/S5 explicitly does not defend against administrators. Standard-user
squatters are still rejected -- they cannot forge either the user SID
or the Administrators owner. Adds a unit test pinning the helper to
{user SID, S-1-5-32-544} so both elevation states stay covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL: a leading deny ACE strips
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal record+playback (0x138) from the
launching-user SID — which the AppContainer child's token still carries as an
effective SID, so without the deny the owner's full-control allow would name
the child as a grantee of those rights (maintainer review, P2-1) — then allow
ACEs grant the launching user and Local System control for cleanup and the
child's AppContainer SID only the minimal DESKTOP_* rights (create
window/menu, read/write objects, enumerate). STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be
granted (CreateProcessW then fails ACCESS_DENIED), the launch fails closed —
auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name
(desktopPrivatePlacement) and requires the launcher-owned
maka-sandbox-desktop. prefix — not merely "not Default", so landing on any
other pre-existing desktop also fails the placement check; appcontainer-smoke
asserts the same prefix and rejects Default. Digest is unchanged: the desktop
is a launch-time detail and does not enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
liugddx added a commit to liugddx/maka-agent that referenced this pull request Aug 18, 2026
…ttested

Follow-ups from inline review on apache#3174, each closing a spot where the
desktop's documented properties exceeded (or under-stated) what the code
enforced:

- Pin a Low no-write-up mandatory label (S:(ML;;NW;;;LW)) on the private
  desktop. Without it the desktop inherited the creator's Medium
  integrity, and because MIC is evaluated before the DACL, the Low-IL
  AppContainer child's granted create-window/write rights were unusable
  in exactly the case the DACL claimed to grant them. The rights are now
  labeled-usable; an in-child window-creation check proving them end to
  end stays a deferred gate (RFC S6.5) - the worker still does no GUI
  work, so the shipped guarantee remains placement.
- Bound the per-launch desktop heap: CreateDesktopExW with a 512 KiB
  budget instead of CreateDesktopW's default 3,072 KiB interactive
  allocation, so the supported ten-way concurrency costs ~5 MiB of the
  documented 48 MiB system desktop heap instead of ~30 MiB. A real-OS
  test holds ten confined desktops live simultaneously to prove the
  budget.
- Fix an unaligned read: TokenAppContainerSid was read through a Vec<u8>
  buffer cast to TOKEN_APPCONTAINER_INFORMATION, whose leading pointer
  field requires pointer alignment - UB in Rust. The buffer is now sized
  in usize words (same pattern as current_user_sid_string).
- Attest the readiness boundary instead of exiting 0: --readiness-probe
  now emits a machine-readable JSON of the facts it verified (exact-SID
  match, specific-Job membership, settlement drain, private-desktop
  placement), and readiness-probe-smoke.ps1 asserts those fields, so
  removing any verification would turn release evidence red rather than
  leaving a hollow exit-0 gate green.

RFC (EN + zh) updated to match: label and heap bound recorded as
enforced, the deferred gate narrowed to no-Win32k plus in-child
window-creation coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx
liugddx force-pushed the feat/windows-sandbox-private-desktop branch from 70eea18 to 2797e0e Compare August 18, 2026 15:22
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Rebased onto #3161's 0df509bc3 (elevated-host fix for the lock-owner check that turned windows_sandbox_w0_protocol red); head is now 2797e0eb4, desktop delta unchanged. 中文:已 rebase 到 #3161 的提权宿主修复上,head 2797e0eb4,桌面增量不变。

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

Follow-up review on current head 2797e0eb4.

Findings

  1. P2 — The RFC still states an escape-proof result before immediately disclaiming it.

docs/architecture/windows-sandbox-rfc-v1.md:148-170 normatively says workers “cannot enumerate or post window messages to the user's interactive windows, or install desktop hooks against them,” but the same paragraph correctly explains that lpDesktop is only initial placement and in-process code can reopen Winsta0\Default with OpenDesktopW + SetThreadDesktop. The PR description also still calls this “desktop-scoped confinement” and says the worker is kept off Default, despite the latest commit deliberately narrowing the implementation to placement. This PR exists partly to align the RFC with shipped behavior, so the remaining top-level guarantee must be narrowed to “starts on a launcher-created alternate desktop” and must not use an unconditional “cannot” claim. Apply the same correction to the Chinese RFC and PR description/title.

  1. P2 — Desktop creation has no fail-closed uniqueness contract.

experiments/windows-sandbox/launcher/src/windows_launcher.rs:779-826 derives the object name from PID plus wall-clock nanoseconds and collapses clock failure to 0. Microsoft documents that CreateDesktopExW succeeds by returning the existing desktop when the specified name already exists; the supplied security descriptor and heap parameters therefore do not establish a new per-launch object in that case. PID reuse, clock rollback/failure, or same-process calls at the same clock tick can reopen an older desktop while the code and attestation accept the launcher prefix as proof of this launch's object. Use a GUID/cryptographic nonce and avoid the unwrap_or(0) fallback. The Windows test should retain ten handles and assert ten distinct names/objects, not only that ten calls returned handles.

Problem and architecture assessment

The underlying problem is correctly narrowed now: AppContainer alone does not choose a non-interactive initial desktop, and this PR adds initial placement plus a protected per-launch DACL. That is a useful defense-in-depth layer, but it is not confinement from Default; no-Win32k or a dedicated window-station/token boundary remains the correct deeper architecture for that stronger requirement. The current implementation is otherwise direct and uses the existing launch paths rather than introducing a parallel broker.

Deletion opportunities

The hand-written json_string helper and its test can be replaced with the existing serde_json::to_string. The explicit "desktop":"Default" smoke rejection is redundant after the stronger launcher-prefix assertion. These are non-blocking cleanup items.

Verification

All live GitHub checks are green. On this exact head I ran cargo fmt --check, Windows MSVC cross-target cargo check --locked, the full repository build, focused runtime readiness/Windows sandbox tests (22 passed), Biome, diff checks, and merge-conflict checks. Real Windows CI exercises creation, the Low label, the 512 KiB heap, and the smoke paths, but it does not exercise name collision or escape back to Default.

Merge verdict: not ready until the two claim/object-identity issues above are fixed. After that, the remaining no-Win32k/window-station work can stay explicitly deferred.

@liugddx liugddx changed the title feat(windows-sandbox): confine sandboxed children to a private desktop feat(windows-sandbox): place sandboxed children on a private desktop Aug 18, 2026
@liugddx

liugddx commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Thanks @hqhq1025 — both points fixed at head cecc80542, and both are accepted as valid, not argued down:

1. Normative claim vs disclaimed reality — fixed at the sentence that mattered. You're right that the earlier narrowing only fixed the annotation layer while the top-level §6.3 guarantee still said workers "cannot enumerate or post window messages … or install desktop hooks." The normative sentence now reads: workers start on a launcher-created private (alternate) desktop, never the interactive Default, with enumerate/message/hook resistance explicitly tied to the deferred no-Win32k/window-station gates — no unconditional "cannot" remains (EN + zh). The PR title is now "place sandboxed children on a private desktop" (was "confine…"), and the description's "Scope of the guarantee" section was rewritten the same way, including an explicit Not claimed bullet for the OpenDesktopW("Default") + SetThreadDesktop escape.

2. Desktop-name uniqueness — now fail-closed. Accepted in full: CreateDesktopExW opens an existing desktop on name collision (silently ignoring the supplied DACL and heap size), and PID + wall-clock nanoseconds with unwrap_or(0) was both predictable and clock-failure-degenerate. The nonce is now 128 bits from the OS CSPRNG (BCryptGenRandom, BCRYPT_USE_SYSTEM_PREFERRED_RNG); RNG failure fails the launch closed instead of collapsing to a constant name. ten_confined_desktops_can_be_held_live now retains all ten handles and asserts ten distinct names that each pass the placement predicate, not merely ten returned handles.

Deletion opportunities — both taken. json_string now delegates to serde_json::to_string (hand-rolled escaping and its test deleted), and the redundant "desktop":"Default" rejections were removed from both smokes — the launcher-prefix assertions already exclude Default.

Local verification on cecc80542: cargo fmt --check, cargo build --locked, 49 tests pass (including the ten-distinct-desktops live test), and both real-machine smokes green — the attestation output now shows the 128-bit hex nonce (maka-sandbox-desktop.<pid>.<32 hex>).

中文

1. 规范句与免责说明矛盾 — 已在真正的规范句上修复。 此前的收窄只改了注解层,§6.3 顶层保证仍写"cannot enumerate/post/hook"。现在规范句为:worker 以 launcher 创建的 private (alternate) desktop 作为初始桌面,绝不落在交互 Default;枚举/消息/钩子抗性显式挂到暂缓的 no-Win32k/window-station 门禁,全文无无条件 "cannot"(中英同步)。PR 标题改为 "place…"(原 "confine…"),描述的保证范围段同步重写并加入显式 Not claimed 条目(OpenDesktopW("Default") + SetThreadDesktop 逃逸)。

2. 桌面名唯一性 — 已 fail-closed。 完全接受:CreateDesktopExW 同名时打开旧桌面并静默忽略传入 DACL/heap,而 PID+时钟纳秒+unwrap_or(0) 既可预测又在时钟失败时退化。nonce 改为 OS CSPRNG 128 位(BCryptGenRandom),RNG 失败即启动 fail closed。十桌面测试持有全部句柄并断言十个互异且通过 placement 谓词的名字

删除机会 — 两条都采纳。 json_string 委托 serde_json::to_string(手写转义及其测试已删);两个冒烟中冗余的 "desktop":"Default" 拒绝已删(前缀断言已覆盖)。

本机 cecc80542:fmt/build 干净,49 测试过(含十互异桌面真机测试),两冒烟绿,attestation 已显示 128 位十六进制 nonce。

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

Follow-up review on current head cecc80542.

No actionable finding remains in the latest delta. The two blockers from my previous review are resolved.

Problem, root cause, and mechanism

The PR now defines the problem correctly: AppContainer does not choose a non-interactive initial desktop, so production children previously started on Winsta0\Default. The patch adds initial placement on a per-launch alternate desktop with a protected DACL, Low mandatory label, bounded heap, fail-closed creation, and placement attestation. It explicitly does not claim escape-proof confinement; no-Win32k and dedicated window-station/token boundaries remain deferred.

The latest commit fixes the object-identity gap directly. Desktop names now use a 128-bit nonce from BCryptGenRandom; RNG failure aborts the launch instead of falling back to a predictable value, and the real-Windows test holds ten desktops live while asserting ten distinct launcher-owned names. The RFC, Chinese RFC, PR title, and description now consistently describe initial placement rather than an unconditional inability to reopen Default.

First-principles and optimality assessment

For the stated placement guarantee, this is the simplest coherent architecture: reuse the existing production/readiness launch paths, create one scoped desktop object per launch, keep its handle alive through settlement, and fail closed on every setup error. Adding a no-Win32k state machine or dedicated window station here would solve a stronger problem than this PR claims and should remain a separate compatibility-tested hardening slice.

Deletion and test quality

The hand-written JSON escaper and duplicate Default smoke assertions were correctly deleted. The remaining SDDL, placement, heap, distinct-name, production smoke, and readiness-attestation tests protect observable boundary properties rather than mirroring implementation details. No further production code or low-quality test needs deletion before merge.

Verification

On this exact head, git diff --check, merge-conflict inspection, cargo fmt --check, and Windows MSVC cross-target cargo check --locked pass. Live Windows W0, Windows recovery, installed Windows CLI, package, typecheck, and CodeRabbit checks are green. At review time, test_workspaces, test_runtime_host, storybook, and e2e are still queued or running; they must finish successfully before merge.

Residual risks

The delivered boundary is initial placement, not prevention of OpenDesktopW("Default")/SetThreadDesktop; clipboard remains window-station scoped; Low-IL GUI rights are labeled but not exercised by an in-child window test; and the readiness lifecycle retains the documented #3161 deferred gaps. These are explicitly documented and do not contradict the current guarantee.

Merge verdict: the reviewed revision is ready once the remaining required checks complete successfully.

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

The current head resolves the earlier private-desktop blockers while keeping the security claim narrow and accurate. It proves initial placement on a launcher-created desktop, uses unpredictable names, protects the DACL and Low-IL label, verifies the exact AppContainer SID/Job identity, and keeps the desktop handle owned through child settlement. The RFC correctly leaves window-station clipboard isolation and stronger escape resistance out of this W0 claim.

I found no reproducible P0–P3 issue in the code on this head. The implementation extends the existing Windows launcher/readiness path and does not create a second sandbox authority.

CI note: the required run is not currently green. The E2E failure is the unrelated slash-command-menu.spec.ts compact-session assertion (Fake backend received: after compact was not observed), and Storybook remains stuck in progress. This approval is for the reviewed code; the PR is not merge-ready until required CI is resolved.

AI-assisted review disclosure: Codex performed the final review using two independent reviewer passes and OpenCode Go DeepSeek V4 Flash (high effort) as an adversarial advisory pass. I verified the desktop security/lifecycle paths against exact head cecc80542 and inspected the live E2E log. No local tests were run.

中文评论

当前 head 已解决此前 private desktop 的阻塞项,同时把安全声明保持在准确、有限的范围:它证明 child 初始位于 launcher 创建的 desktop,使用不可预测名称,保护 DACL 与 Low-IL label,校验精确 AppContainer SID/Job 身份,并由 launcher 持有 desktop handle 直到 child settle。RFC 也正确地没有把 window-station clipboard isolation 和更强 escape resistance 纳入本 W0 声明。

当前 head 的代码未发现可复现 P0–P3。实现扩展既有 Windows launcher/readiness 路径,没有新增第二个 sandbox authority。

CI 说明:必需 run 目前不是绿色。E2E 失败来自无关的 slash-command-menu.spec.ts compact-session 断言(未观察到 Fake backend received: after compact),Storybook 仍卡在 in progress。本次 approve 针对已审代码;必需 CI 解决前不能合并。

AI 辅助审查说明:Codex 使用两轮独立 reviewer 审查,并以 OpenCode Go DeepSeek V4 Flash(high effort)进行对抗性辅助审查;我已针对精确 head cecc80542 核对 desktop security/lifecycle 路径并检查实时 E2E 日志。本轮未运行本地测试。

liugddx and others added 4 commits August 19, 2026 08:11
The owner check accepted {user SID, SYSTEM, this token's default owner},
but the default owner differs between elevation states: a mutex created
by an *elevated* instance is owned by BUILTIN\Administrators, while a
concurrent *non-elevated* instance of the same user resolves its own
token owner to the user SID -- so it rejected the elevated instance's
legitimate lock and readiness/ACL-ledger acquisition failed closed
whenever elevated and unelevated Maka processes overlapped.

Make the lock's identity elevation-independent at the source: the lock
security descriptor now pins an explicit owner (O:<user SID> -- always
an assignable owner for the user's own token, elevated or not) ahead of
the protected DACL, so every lock this code creates carries the same
owner in every elevation state. Validation of a pre-existing lock
accepts exactly {user SID, SYSTEM, BUILTIN\Administrators} -- the last
for locks created by builds that predate the pinning during side-by-side
overlap, and safe because only an elevated administrator (outside the
RFC S1/S5 threat model) can create Administrators-owned objects. The
per-token default-owner query is deleted. Adds a lock_sddl unit test
asserting the pinned owner precedes the protected DACL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
Non-interactive AppContainer workers previously inherited the creator's
interactive Winsta0\Default desktop, exposing the same-session GUI attack
surface (window-message shatter into the user's interactive windows, desktop
hooks) — AppContainer confines files/network/token but not the desktop scope.
Per RFC v1 §6.3 the private-desktop promise was designed but deferred.

This change enforces initial-desktop *placement* plus DACL protection, not
escape-proof confinement. Each AppContainer launch (production create +
readiness probe) stands up a per-launch alternate desktop on the existing
window station, with a protected DACL: a leading deny ACE strips
DESKTOP_SWITCHDESKTOP / HOOKCONTROL / journal record+playback (0x138) from the
launching-user SID — which the AppContainer child's token still carries as an
effective SID, so without the deny the owner's full-control allow would name
the child as a grantee of those rights (maintainer review, P2-1) — then allow
ACEs grant the launching user and Local System control for cleanup and the
child's AppContainer SID only the minimal DESKTOP_* rights (create
window/menu, read/write objects, enumerate). STARTUPINFOEXW.lpDesktop points
the child at it. If the desktop cannot be created or the DACL cannot be
granted (CreateProcessW then fails ACCESS_DENIED), the launch fails closed —
auto/require never fall back to the host desktop.

Scope, stated honestly (per maintainer review on apache#3174):
- lpDesktop selects only the child's *initial* desktop. This places the worker
  off the interactive Default desktop and DACL-protects the private one, but it
  does NOT structurally confine: absent a no-Win32k mitigation, a dedicated
  window station, or a token boundary, in-process code can still
  OpenDesktopW("Default") + SetThreadDesktop to re-attach. Enforcing a no-Win32k
  mitigation is a deferred gate (§6.5).
- It does NOT isolate the clipboard: the clipboard belongs to the window
  station, which both desktops still share. Clipboard isolation with a dedicated
  window station is a later hardening gate (§6.5).
- The create-window/write DACL rights are granted but not relied upon; the
  worker does no GUI work. A verified Low mandatory-integrity label proving
  those rights are usable at AppContainer's Low IL is deferred (§6.5).

The boundary probe self-attests its own initial desktop name
(desktopPrivatePlacement) and requires the launcher-owned
maka-sandbox-desktop. prefix — not merely "not Default", so landing on any
other pre-existing desktop also fails the placement check; appcontainer-smoke
asserts the same prefix and rejects Default. Digest is unchanged: the desktop
is a launch-time detail and does not enter the manifest.

Stacked on apache#3161 (readiness probe).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
…ttested

Follow-ups from inline review on apache#3174, each closing a spot where the
desktop's documented properties exceeded (or under-stated) what the code
enforced:

- Pin a Low no-write-up mandatory label (S:(ML;;NW;;;LW)) on the private
  desktop. Without it the desktop inherited the creator's Medium
  integrity, and because MIC is evaluated before the DACL, the Low-IL
  AppContainer child's granted create-window/write rights were unusable
  in exactly the case the DACL claimed to grant them. The rights are now
  labeled-usable; an in-child window-creation check proving them end to
  end stays a deferred gate (RFC S6.5) - the worker still does no GUI
  work, so the shipped guarantee remains placement.
- Bound the per-launch desktop heap: CreateDesktopExW with a 512 KiB
  budget instead of CreateDesktopW's default 3,072 KiB interactive
  allocation, so the supported ten-way concurrency costs ~5 MiB of the
  documented 48 MiB system desktop heap instead of ~30 MiB. A real-OS
  test holds ten confined desktops live simultaneously to prove the
  budget.
- Fix an unaligned read: TokenAppContainerSid was read through a Vec<u8>
  buffer cast to TOKEN_APPCONTAINER_INFORMATION, whose leading pointer
  field requires pointer alignment - UB in Rust. The buffer is now sized
  in usize words (same pattern as current_user_sid_string).
- Attest the readiness boundary instead of exiting 0: --readiness-probe
  now emits a machine-readable JSON of the facts it verified (exact-SID
  match, specific-Job membership, settlement drain, private-desktop
  placement), and readiness-probe-smoke.ps1 asserts those fields, so
  removing any verification would turn release evidence red rather than
  leaving a hollow exit-0 gate green.

RFC (EN + zh) updated to match: label and heap bound recorded as
enforced, the deferred gate narrowed to no-Win32k plus in-child
window-creation coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
…ive claim

Maintainer follow-up on the private-desktop delta, both points fixing a
gap between what was stated and what the object model guarantees:

- Desktop-name uniqueness is now fail-closed. CreateDesktopExW *opens*
  an existing desktop when the name collides -- silently ignoring the
  supplied DACL and heap budget -- and the old nonce was wall-clock
  nanoseconds with a unwrap_or(0) fallback, so PID reuse, clock
  rollback, same-tick calls, or a failed clock could reopen an older
  desktop while the prefix attestation still passed. The nonce is now
  128 bits from the OS CSPRNG (BCryptGenRandom), collision is
  cryptographically negligible, and RNG failure fails the launch closed
  instead of collapsing to a constant name. The ten-desktop live test
  now also asserts ten *distinct* names that each pass the placement
  attestation, not merely ten returned handles.
- The RFC's top-level S6.3 guarantee no longer states an unconditional
  "cannot": it now reads "workers start on a launcher-created private
  desktop, never Default", with the enumerate/message/hook resistance
  explicitly tied to the deferred no-Win32k/window-station gates (EN +
  zh). The PR title/description are narrowed the same way.

Cleanups from the same review: json_string now delegates to serde_json
(hand-rolled escaping and its test deleted), and the redundant
'"desktop":"Default"' smoke rejections are removed -- the stronger
launcher-prefix assertions already exclude Default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx
liugddx force-pushed the feat/windows-sandbox-private-desktop branch from cecc805 to e7f560e Compare August 19, 2026 00:13
@liugddx
liugddx merged commit 57593a9 into apache:main Aug 19, 2026
27 of 28 checks passed
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.

4 participants