From e47598470654bd3e68d01f79891bdb9719f9613a Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:16:34 +0800 Subject: [PATCH] test(sandbox): close Windows ACL coverage gaps --- .../sandbox/sandbox-windows-acl/src/index.ts | 2 + .../tests/index-failure-paths.spec.ts | 39 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/sandbox/sandbox-windows-acl/src/index.ts b/packages/sandbox/sandbox-windows-acl/src/index.ts index c3d302c43d..cf304b1904 100644 --- a/packages/sandbox/sandbox-windows-acl/src/index.ts +++ b/packages/sandbox/sandbox-windows-acl/src/index.ts @@ -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()) { diff --git a/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts b/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts index d9e5d06ab1..c171d30084 100644 --- a/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts +++ b/packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts @@ -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' }, ], }) })