fix(sandbox): address the ACL backend review findings — quoting, DACL merge+lock, failure paths

quoteArg doubles end-of-string backslashes (real CommandLineToArgvW round-trip test; first-token exemption noted); grantWrite merges into the current DACL instead of replacing it, and both grant/revoke hold a LockFileEx per-path lock under GetTempPathW/dsh-acl-locks (koffi crashes on NULL lpOverlapped — a zeroed OVERLAPPED is used); GRANT_MASK gains DELETE|FILE_DELETE_CHILD (never WRITE_DAC/WRITE_OWNER) though the win32 26200 second check only constrains the WRITE bit; failure paths close all handles (CreateProcessAsUserW pipe set, ResumeThread thread/process/job) with stub-api tests; getTempPathW refuses undersized buffers; drainPipe backs off; runner.spec pwshAvailable uses the resolvePwshPath probe; NTSTATUS exit codes mirror bit-exact (verified end-to-end); WinLocalSid JSDoc re-attributed to WinLocalLogonSid. All gates green: 42 passed/2 skipped, typecheck, oxlint, 0 clones, constraints, knip.
This commit is contained in:
Huanqi Cao
2026-08-08 14:32:01 +08:00
parent fa591b5ebd
commit 91d3ed6c5a
13 changed files with 805 additions and 83 deletions
@@ -29,7 +29,7 @@ import { resolve } from 'node:path'
import { grantWrite, revokeWrite } from './acl.ts'
import { Win32Error } from './errors.ts'
import { allocPtrSlot, decodePtr, isNullPtr, throwLastError, win32 } from './ffi.ts'
import { allocPtrSlot, decodePtr, getTempPath, isNullPtr, throwLastError, win32 } from './ffi.ts'
import type { NativePtr, Win32Bindings } from './ffi.ts'
import { drainPipe, spawnSandboxed, spawnSandboxedInherited, waitForExit } from './spawn.ts'
import { createRestrictedToken, findLogonSid, makeWellKnownSid, openCurrentProcessToken } from './token.ts'
@@ -88,13 +88,6 @@ function randomWriteSid(): string {
return `S-1-4-${randomInt(1, 2 ** 30)}-${randomInt(1, 2 ** 30)}`
}
function getTempPath(api: Win32Bindings): string {
const buffer = Buffer.alloc((abi.MAX_PATH + 1) * 2)
const length = api.getTempPathW(buffer.length / 2, buffer)
if (length === 0) throwLastError(api, 'GetTempPathW')
return buffer.subarray(0, length * 2).toString('utf16le')
}
/**
* One write-restricted sandbox instance: token + orphan-SID grants + spawn.
* `init()` is fail-closed — any Win32 failure revokes whatever was granted