fix(sandbox): drop Authenticated Users from both restricting lists — close the C:\\-root escape, CIM unavailable in every confined mode

workspace-write now runs [logon SID, Everyone, orphan]: the two lists differ only by the orphan, and the keep-alive group (logon SID + Everyone) is the single shared invariant. The WMI namespace security check fails in BOTH modes (0x80041003), so CIM/Get-ComputerInfo are unavailable everywhere — the price of closing the C:\\-root tree-creation escape (AU:(AD) + AU:(OI)(CI)(IO)(M)) in workspace-write too. The unused WinLocalSid/WinInteractiveSid/WinAuthenticatedUserSid constants and their ABI-probe prints are removed; the enforcement 'full' claim now stands on a closed NTFS surface. New regression: a C:\\Users\\Public subdirectory write is denied under BOTH modes (the ambient-writable blind spot the review flagged — INTERACTIVE is absent from both lists). FAT-class (non-ACL) targets outside the granted roots remain writable (no security descriptors to intersect) — documented as a legacy residue, warn-only, not engineered around. Docs/design note/PR body updated in both languages (list I/J terminology gone everywhere).
This commit is contained in:
Huanqi Cao
2026-08-08 21:55:02 +08:00
parent 9d10a1888b
commit 441927c526
14 changed files with 104 additions and 81 deletions

View File

@@ -101,44 +101,41 @@ function buildRestrictingSids(sids: readonly NativePtr[]): Buffer {
return buffer
}
/** The well-known SIDs packed into every restricted token's restricting list. */
/** The well-known SID packed into every restricted token's restricting list. */
export interface RestrictingSidSet {
world: NativePtr
authUser: NativePtr
}
/**
* Create the write-restricted token with the mode-selected restricting list
* (dual lists verified on Win11 26200, see the POC-worktree restrict-variant
* harness):
* - list I (read-only): [logon SID, EVERYONE]
* - list J (workspace-write): [logon SID, EVERYONE, Authenticated Users, orphan]
* (verified on Win11 26200, see the POC-worktree restrict-variant harness):
* - read-only: [logon SID, EVERYONE]
* - workspace-write: [logon SID, EVERYONE, orphan]
*
* The logon SID and EVERYONE are shared: they keep the early startup chain
* (0xC0000142 without them) and CNG (`\Device\CNG` write trustee — pwsh
* crashes 0xE0434352 without EVERYONE) alive. Authenticated Users exists in
* list J ONLY because the CIM path's WMI namespace security check requires it
* (0x80041003 otherwise) — read-only drops it for a zero ambient-write
* surface (it closes the host's C:\-root tree-creation escape, where
* `AU:(AD)` + `AU:(OI)(CI)(IO)(M)` ACEs stand) at the cost of CIM
* unavailability; documented in README. List I also carries NO orphan: a
* standing grant ACE from an earlier workspace-write period (a
* `/permission` mode downgrade, or a crash-resumed session) must stay INERT
* under read-only — the WRITE_RESTRICTED pass-2 check grants only what the
* restricting list carries, so omitting the orphan keeps read-only strictly
* zero-grant even with stale ACEs standing, while the unrevoked ACE keeps
* the re-upgrade free (the seam's grant map hits it — no re-propagation).
* INTERACTIVE/LOCAL are absent from BOTH lists — the host's Public tree
* grants write to INTERACTIVE, so removing it closes that escape. S-1-2-1
* (console logon) is intentionally absent: see win32-abi.ts for the
* verified failure modes. FAILS CLOSED: any failure throws — never spawn
* unrestricted.
* The logon SID + EVERYONE keep-alive group is shared by both modes: early
* DLL init dies with 0xC0000142 and CNG (`\Device\CNG` write trustee —
* pwsh crashes 0xE0434352) fails without them. The orphan SID joins ONLY
* workspace-write — read-only carries no orphan, so a standing grant ACE
* from an earlier workspace-write period (a `/permission` mode downgrade, or
* a crash-resumed session) stays INERT under read-only: the WRITE_RESTRICTED
* pass-2 check grants only what the restricting list carries, keeping
* read-only strictly zero-grant even with stale ACEs standing, while the
* unrevoked ACE keeps the re-upgrade free (the seam's grant map hits it — no
* re-propagation). Authenticated Users is absent from BOTH lists: the WMI
* namespace security check fails (0x80041003), so CIM is unavailable in
* every confined mode, and the C:\-root tree-creation escape (standing
* `AU:(AD)` + `AU:(OI)(CI)(IO)(M)` ACEs) is closed in both — documented in
* README. INTERACTIVE/LOCAL are absent from BOTH lists too — the host's
* Public tree grants write to INTERACTIVE, so removing it closes that
* escape. S-1-2-1 (console logon) is intentionally absent: see win32-abi.ts
* for the verified failure modes. FAILS CLOSED: any failure throws — never
* spawn unrestricted.
* @param api - the binding table.
* @param currentToken - the process token to restrict.
* @param logonSid - the copied logon session SID.
* @param writeSid - the orphan SID forming the write allowlist (list J only).
* @param writeSid - the orphan SID forming the write allowlist (workspace-write only).
* @param known - the well-known SIDs entering the restricting list.
* @param mode - selects the restricting list (I for read-only, J for workspace-write).
* @param mode - selects the restricting list (workspace-write adds the orphan).
* @returns the restricted token handle.
*/
export function createRestrictedToken(
@@ -151,7 +148,7 @@ export function createRestrictedToken(
): NativePtr {
const restrictingSids = buildRestrictingSids(mode === 'read-only'
? [logonSid, known.world]
: [logonSid, known.world, known.authUser, writeSid])
: [logonSid, known.world, writeSid])
const tokenSlot = allocPtrSlot()
const created = api.createRestrictedToken(
currentToken,