feat(sandbox): per-session windows-acl write grant with dual-mode restricting lists and a private temp subdirectory

This commit is contained in:
Huanqi Cao
2026-08-08 17:29:43 +08:00
parent 91d3ed6c5a
commit abfb933620
40 changed files with 1239 additions and 118 deletions
@@ -10,7 +10,7 @@ The [sandbox decision](2026-07-06-sandbox.md) leaves `PLATFORM_CHAINS.win32` emp
## Decision
Implement the rung directly on the raw ACL mechanism: duplicate the caller's token into a `WRITE_RESTRICTED` token (`CreateRestrictedToken` with `WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`) whose restricting SIDs include a per-instance orphan SID (`S-1-4-x-y`); the orphan SID's Write ACEs on the workspace and temp roots are the entire write allowlist, because `WRITE_RESTRICTED` intersects write accesses only and reads keep the caller's full ambient access. The mechanism is the one huoyaoyuan/windows-acl-restrict-poc (`10e4dfb`) demonstrates; this port checks every API call and fails closed (the POC fail-opened on every ignored return value). It ships as [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.md) (backend plus the `./runner` argv-prefix entry), the `win32` chain rung of [`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.md), and [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/bash/pwsh-sandbox/README.md) as the confining executor; the Windows platform layer re-enables the full permission surface (sandbox/sandbox-policy/permission/approval/fs-sandbox) over the confined pwsh stack.
Implement the rung directly on the raw ACL mechanism: duplicate the caller's token into a `WRITE_RESTRICTED` token (`CreateRestrictedToken` with `WRITE_RESTRICTED` + `DISABLE_MAX_PRIVILEGE` + `LUA_TOKEN`) whose restricting SIDs include an orphan SID (`S-1-4-x-y`); the orphan SID's Write ACEs on the workspace and temp roots are the entire write allowlist, because `WRITE_RESTRICTED` intersects write accesses only and reads keep the caller's full ambient access. The mechanism is the one huoyaoyuan/windows-acl-restrict-poc (`10e4dfb`) demonstrates; this port checks every API call and fails closed (the POC fail-opened on every ignored return value). The orphan SID is PER SESSION, not per spawn: the seam provisions one SID per session as a log-only `sandbox/acl-session` event on the session log (fork mints a fresh one; resume replays the same one), materializes its ACEs lazily at the session's first confined execution, and holds them for the server process's lifetime (revoked on provider dispose; idempotent re-grant skips the eager full-tree re-propagation when the exact ACE survives a restart — no garbage collection). The token's restricting list is dual-mode: list I (`read-only` = logon SID, Everyone, orphan — no Authenticated Users, so CIM is unavailable but the ambient AU-writable surface, notably the C:\-root tree-creation escape, is closed) and list J (`workspace-write` = + Authenticated Users, keeping the CIM path alive at the cost of that residual surface); the verified keep-alive invariants are logon SID + Everyone for early DLL init and CNG, and Authenticated Users for the WMI namespace security check alone. Workspace-write children see a PRIVATE per-session temp subdirectory (`<temp>\dsh-<hash>`, TMP/TEMP rewritten by the runner — bwrap `--tmpfs /tmp` semantics). It ships as [`@deepseek-ai/dsh-sandbox-windows-acl`](../../../../packages/sandbox/sandbox-windows-acl/README.md) (backend plus the `./runner` argv-prefix entry), the `win32` chain rung of [`dsh-sandbox-local`](../../../../packages/sandbox/sandbox-local/README.md), and [`@deepseek-ai/dsh-pwsh-sandbox`](../../../../packages/bash/pwsh-sandbox/README.md) as the confining executor; the Windows platform layer re-enables the full permission surface (sandbox/sandbox-policy/permission/approval/fs-sandbox) over the confined pwsh stack.
## How the restriction works (why no new identity)
@@ -32,11 +32,11 @@ The [landstrip evaluation](../../rejected/feature/2026-07-26-evaluate-landstrip-
## Consequences
Bought: write-only confinement with no new OS floor (`CreateRestrictedToken` predates the mxc releases by two decades), reads/network/process visibility untouched exactly as the mode vocabulary requires, and fail-closed errors carrying the API name and the exact Win32 code. Cost: no read-side or network isolation; console isolation unavailable (hidden-console children die with `STATUS_DLL_INIT_FAILED`; children share the host console); standing ACE mutations on the granted roots (caller-owned directories, revoked by `dispose()`); the workspace-write temp grant is the real temp directory — the same backend-defined choice the Landlock rung makes.
Bought: write-only confinement with no new OS floor (`CreateRestrictedToken` predates the mxc releases by two decades), reads/network/process visibility untouched exactly as the mode vocabulary requires, and fail-closed errors carrying the API name and the exact Win32 code. Cost: no read-side or network isolation; console isolation unavailable (hidden-console children die with `STATUS_DLL_INIT_FAILED`; children share the host console); standing ACE mutations on the granted roots (caller-owned directories, revoked by provider dispose, self-healing across restarts via the durable per-session record); grant materialization is an EAGER full-tree propagation (`SetNamedSecurityInfoW` walks every descendant immediately — tens of seconds on large workspaces), paid once per session per server lifetime by the per-session reuse; `read-only` loses CIM (AuthUsers dropped — the WMI namespace security check fails, and `Get-ComputerInfo` silently returns incomplete results), while `workspace-write` retains the Authenticated-Users residual (a C:\-root tree-creation escape) as the price of a working CIM path; `whoami` and token-inspection cmdlets fail under the restricted token (diagnostic noise, documented).
## Testing
The product-visible Windows roster flip is win32-only, so the keyless snapshot fixtures — which must replay on macOS/Linux — cannot cover it; the bundle composition specs ([`base.spec.ts`](../../../../packages/bundle/base/tests/base.spec.ts), [`windows-shell.spec.ts`](../../../../apps/cli/tests/windows-shell.spec.ts)) plus the win32 real-runner suites (`packages/sandbox/sandbox-windows-acl/tests/`, `packages/bash/pwsh-sandbox/tests/`) are the substitute evidence, and the CI Windows lane owns the assembled signal.
The product-visible Windows roster flip is win32-only, so the keyless snapshot fixtures — which must replay on macOS/Linux — cannot cover it; the bundle composition specs ([`base.spec.ts`](../../../../packages/bundle/base/tests/base.spec.ts), [`windows-shell.spec.ts`](../../../../apps/cli/tests/windows-shell.spec.ts)) plus the win32 real-runner suites (`packages/sandbox/sandbox-windows-acl/tests/`, `packages/bash/pwsh-sandbox/tests/`) are the substitute evidence, and the CI Windows lane owns the assembled signal. The per-session grant machinery is pinned cross-platform by `packages/sandbox/sandbox-local/tests/acl-session.spec.ts` (record fold/provision, one-shot materialization, fork/resume SID reuse, dispose revocation — Win32 surface mocked) and on win32 by `grant.spec.ts` (real-DACL materialization), the `acl.spec.ts` idempotent-grant fast-path, and the `runner.spec.ts` `--write-sid` contract (caller-owned grants, private temp subdir through TMP/TEMP, dual-mode CIM probes).
## Related