Files
deepseek-harness/packages/sandbox/sandbox-windows-acl/tests/probe.spec.ts
Huanqi Cao 5fea4b7c4b feat(sandbox): derive the windows-acl write SID per workspace, not per session
The per-session random write SID forced a full tree propagation per
session per server lifetime (minutes on large workspaces). The write
SID is now the per-workspace identity derived from the canonical
workspace path (workspaceWriteSid: sha256 -> S-1-4-x-y), stored
nowhere: the workspace-root ACE materializes once per workspace per
machine and every later provision hits the exact-ACE skip.

- workspace ACEs are STANDING (never revoked - the reuse cache); temp
  ACEs stay revocable (disposed with the provider), so an inheritable
  ACE never outlives its session's temp dir on the ambient temp root
- AclSandbox requires the write SID under workspace-write; read-only
  parses/grants nothing; the runner derives the SID itself (the
  --write-sid flag's presence still marks the seam-managed contract)
- the acl-session record drops writeSid (sessionId/workspace/tempDir
  remain): the SID-tamper surface and its validation are gone
- sandbox-local holds two grant maps: standing workspace grants and
  revocable per-session temp grants

Docs (README pair, design note pair, catalogs, type-equiv) and the
acl-session/grant/acl/probe/runner suites updated; workspace-sid.spec
pins the derivation contract.
2026-08-09 10:44:35 +08:00

98 lines
4.5 KiB
TypeScript

/**
* End-to-end probe of the ACL write-restriction sandbox, using the same
* probes as the POC verification harness: the confined child must be able to
* write into the granted target and temp directories, must be DENIED writing
* anywhere else, and (documented boundary) may still READ outside — the
* WRITE_RESTRICTED token intersects write accesses only.
*
* The escape target sits in its own scratch dir under the system temp
* directory, OUTSIDE both granted trees: tempDir is passed EXPLICITLY (never
* defaulted through GetTempPathW, whose grant would inherit (OI)(CI) over the
* whole real temp tree) and the writable dir is a separate mkdtemp directory
* that contains neither sibling. Nothing under the user profile is touched.
*/
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { AclSandbox } from '../src/index.ts'
const isWin32 = process.platform === 'win32'
function pwshAvailable(): boolean {
try {
execFileSync('where.exe', ['pwsh'], { stdio: 'ignore' })
return true
} catch {
return false
}
}
describe.skipIf(!isWin32 || !pwshAvailable())('AclSandbox write restriction', () => {
let scratchRoot!: string
let writableDir!: string
let isolatedTemp!: string
let secretFile!: string
let escapeFile!: string
let sandbox: AclSandbox
beforeAll(async () => {
scratchRoot = mkdtempSync(join(tmpdir(), 'dsh-acl-sandbox-'))
writableDir = join(scratchRoot, 'writable')
mkdirSync(writableDir)
isolatedTemp = mkdtempSync(join(tmpdir(), 'dsh-acl-sandbox-temp-'))
secretFile = join(scratchRoot, 'secret.txt')
writeFileSync(secretFile, 'top secret - must stay readable to prove the read boundary')
escapeFile = join(scratchRoot, 'escaped.txt')
// tempDir is passed explicitly: GetTempPathW reads the native environment
// block, which host runtimes (vitest worker pools) may not keep in sync
// with process.env — and a real-temp grant would inherit over every
// temp subdirectory, including this test's scratch dir.
sandbox = new AclSandbox({ writableDirs: [writableDir], tempDir: isolatedTemp, writeSid: 'S-1-4-9000-4', mode: 'workspace-write' })
await sandbox.init()
})
afterAll(() => {
sandbox.dispose()
rmSync(scratchRoot, { recursive: true, force: true })
rmSync(isolatedTemp, { recursive: true, force: true })
})
it('allows writes only in granted directories and denies the escape write', async () => {
const probe = [
"$ErrorActionPreference='SilentlyContinue';",
`try{Set-Content -Path '${writableDir}\\child-wrote.txt' -Value ok -ErrorAction Stop;'TARGET-WRITE: OK'}catch{'TARGET-WRITE: DENIED'};`,
`try{Set-Content -Path '${isolatedTemp}\\child-wrote.txt' -Value ok -ErrorAction Stop;'TEMP-WRITE: OK'}catch{'TEMP-WRITE: DENIED'};`,
`try{Set-Content -Path '${escapeFile}' -Value ok -ErrorAction Stop;'ESCAPE-WRITE: OK (ESCAPE!)'}catch{'ESCAPE-WRITE: DENIED'};`,
`try{Get-Content '${secretFile}' -ErrorAction Stop | Out-Null;'SECRET-READ: OK'}catch{'SECRET-READ: DENIED'}`,
].join('')
const child = sandbox.spawn({
command: 'pwsh',
args: ['/NoLogo', '/NonInteractive', '/NoProfile', '/Command', probe],
cwd: writableDir,
})
const result = await child.wait()
const output = result.stdout.toString('utf8') + result.stderr.toString('utf8')
expect(result.exitCode, `child output:\n${output}`).toBe(0)
expect(output, `child output:\n${output}`).toContain('TARGET-WRITE: OK')
expect(output, `child output:\n${output}`).toContain('TEMP-WRITE: OK')
expect(output, `child output:\n${output}`).toContain('ESCAPE-WRITE: DENIED')
// Documented boundary: WRITE_RESTRICTED intersects write accesses only,
// so reads outside the allowlist still succeed.
expect(output, `child output:\n${output}`).toContain('SECRET-READ: OK')
expect(existsSync(escapeFile)).toBe(false)
expect(existsSync(join(writableDir, 'child-wrote.txt'))).toBe(true)
}, 30_000)
it('fails closed when the write SID cannot be parsed (no unrestricted fallback)', async () => {
// A malformed SID makes ConvertStringSidToSidW fail; init must throw
// before any grant is applied and never spawn unrestricted.
const broken = new AclSandbox({ writableDirs: [writableDir], writeSid: 'S-1-4-abc-1', mode: 'workspace-write' })
await expect(broken.init()).rejects.toThrow(/ConvertStringSidToSidW/u)
}, 15_000)
})