test(sandbox): close Windows ACL coverage gaps

This commit is contained in:
Tianyi Cui
2026-08-10 23:16:34 +08:00
parent d3715052ea
commit e475984706
2 changed files with 37 additions and 4 deletions
@@ -239,6 +239,8 @@ export class AclSandbox {
this.tempWriteSidPtr = this.tempWriteSid === undefined ? undefined : parseSid(this.tempWriteSid)
const tempDir = this.mode === 'read-only' || this.tempDirOption === null ? null : this.tempDirOption
/* v8 ignore next -- constructor validation requires workspace-write to supply
an explicit temp directory or null; the other branches normalize to null. */
if (tempDir === undefined) throw new Error('AclSandbox workspace-write temp directory was not resolved')
if (tempDir !== null) {
if (!existsSync(tempDir) || !statSync(tempDir).isDirectory()) {
@@ -186,6 +186,28 @@ describe('AclSandbox constructor validation', () => {
expect(sandbox.mode).toBe('read-only')
expect(sandbox.tempDir).toBeUndefined()
})
it('rejects temp authority under read-only', () => {
const workspace = scratch()
const temp = scratch()
expect(() => new AclSandbox({ writableDirs: [workspace], tempDir: temp, mode: 'read-only' }))
.toThrow(/read-only does not accept a temp directory/u)
expect(() => new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-1', mode: 'read-only' }))
.toThrow(/read-only does not accept write SIDs/u)
expect(() => new AclSandbox({ writableDirs: [workspace], tempDir: null, tempWriteSid: 'S-1-4-9000-1-1', mode: 'read-only' }))
.toThrow(/read-only does not accept write SIDs/u)
})
it('rejects a temp SID when temp writes are disabled', () => {
const workspace = scratch()
expect(() => new AclSandbox({
writableDirs: [workspace],
tempDir: null,
writeSid: 'S-1-4-9000-2',
tempWriteSid: 'S-1-4-9000-2-1',
mode: 'workspace-write',
})).toThrow(/temp write SID requires a temp directory/u)
})
})
describe('AclSandbox init', () => {
@@ -274,20 +296,29 @@ describe('AclSandbox init', () => {
await expect(sandbox.init()).rejects.toBeInstanceOf(Win32Error)
})
it('reports a failed close of the current process token', async () => {
const { closeHandle } = state.stubs as HappyStubs
it('aggregates failed current and restricted token closes after init', async () => {
const { closeHandle, createRestrictedToken } = state.stubs as HappyStubs
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-9', mode: 'workspace-write' })
const restrictedToken = 99n
createRestrictedToken.mockImplementation((
_existing: unknown, _flags: unknown, _dc: unknown, _ds: unknown, _pc: unknown, _pd: unknown,
_rc: unknown, _rs: unknown, slot: NativePtr,
) => {
koffi.encode(slot, PVOID, restrictedToken)
return 1
})
// fresh() hands out 1n to OpenProcess and 2n to OpenProcessToken; the
// token-layer close of 1n succeeds and init's close of 2n fails.
closeHandle.mockImplementation((handle: NativePtr) => (handle === 2n ? 0 : 1))
closeHandle.mockImplementation((handle: NativePtr) => (handle === 2n || handle === restrictedToken ? 0 : 1))
// The failure lands after this.token is stored but before this.api is
// assigned. Cleanup retries the still-open handle and reports both close
// failures after releasing the restricted token and parsed SIDs.
// failures plus the restricted-token close after releasing parsed SIDs.
await expect(sandbox.init()).rejects.toMatchObject({
errors: [
{ api: 'CloseHandle' },
{ api: 'CloseHandle' },
{ api: 'CloseHandle' },
],
})
})