fix(sandbox): harden the per-session record and the ACL runner failure paths (review round v6)
Durable record: bound to the owning session id and validated at the fold (orphan-SID shape, temp path inside the host temp root) — a fork's copied parent record no longer provisions the child, and a tampered record fails loud. Private temp dir: random unguessable name persisted in the record, created exclusively (pre-existing entries and reparse points fail EEXIST). Persistence: a fresh provision kicks an immediate flush (no write-behind debounce), narrowing the crash window to the flush latency — documented as the one self-healing gap. Runner-failure rules: exit-gated on 127 so a confined command that prints the signature on a non-127 exit is never misclassified. Spawn: AssignProcessToJobObject failure terminates the suspended child (no hanging orphans). SandboxExecutionPolicy.sessionId is the branded SessionId. Boundary docs: qualifying clause on the absolutist sentences, NULL-DACL Known Limitation, 'full' scoped to the supported NTFS surface, CLM gate comment.
This commit is contained in:
@@ -102,6 +102,32 @@ describe('spawn failure paths close their handles', () => {
|
||||
expect(closeHandle).toHaveBeenCalledTimes(3)
|
||||
expect(closed).toEqual([201n, 200n, 100n])
|
||||
})
|
||||
|
||||
it('spawnSandboxedInherited TERMINATES the suspended child before closing handles when AssignProcessToJobObject fails', () => {
|
||||
// The child is created suspended and is NOT in the kill-on-close job when
|
||||
// the assignment fails: closing the job cannot kill it, so the failure
|
||||
// branch must TerminateProcess first or every failure strands a hanging
|
||||
// orphan forever.
|
||||
const { api: baseApi, closeHandle } = resumeFailureApi()
|
||||
type JobFailureApi = Win32Bindings & {
|
||||
assignProcessToJobObject: ReturnType<typeof vi.fn>
|
||||
terminateProcess: ReturnType<typeof vi.fn>
|
||||
}
|
||||
const api = baseApi as JobFailureApi
|
||||
api.assignProcessToJobObject = vi.fn(() => 0)
|
||||
api.terminateProcess = vi.fn(() => 1)
|
||||
let caught: unknown
|
||||
try {
|
||||
spawnSandboxedInherited(api, token, { command: 'probe.exe', args: [], cwd: 'C:\\' })
|
||||
} catch (error) {
|
||||
caught = error
|
||||
}
|
||||
expect(caught).toBeInstanceOf(Win32Error)
|
||||
expect((caught as Win32Error).api).toBe('AssignProcessToJobObject')
|
||||
expect(api.terminateProcess).toHaveBeenCalledExactlyOnceWith(200n, 1)
|
||||
// thread, process, job — and the child is already dead before they close.
|
||||
expect(closeHandle).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getTempPath buffer defense', () => {
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('windows-acl win32 chain (LocalSandboxProvider)', () => {
|
||||
])
|
||||
expect(confined.enforcement).toBe('full')
|
||||
expect(confined.denialSignatures).toEqual(['access is denied', 'access to the path', 'permission denied'])
|
||||
expect(confined.runnerFailureRules).toEqual([{ fatalSignatures: ['windows-acl-run: '] }])
|
||||
expect(confined.runnerFailureRules).toEqual([{ allowedExitCodes: [127], fatalSignatures: ['windows-acl-run: '] }])
|
||||
// A sole candidate is selected unprobed.
|
||||
expect(probeWindowsAcl).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -53,6 +53,6 @@ describe('windows-acl win32 chain (LocalSandboxProvider)', () => {
|
||||
const confined = sandbox.confine(['true'], RO)
|
||||
expect(confined.argv.slice(-4)).toEqual(['--mode', 'read-only', '--', 'true'])
|
||||
expect(confined.enforcement).toBe('full')
|
||||
expect(confined.runnerFailureRules).toEqual([{ fatalSignatures: ['windows-acl-run: '] }])
|
||||
expect(confined.runnerFailureRules).toEqual([{ allowedExitCodes: [127], fatalSignatures: ['windows-acl-run: '] }])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user