fix(ci): restore the native Windows coverage denominator to green

The windows-native job has been red since #1990 put the sandbox-windows-acl sources into the Windows 100%-per-file denominator without tests carrying them, and #1543 dropped the authoring.ts V8 ignore for the POSIX-only owner-execute branch. Non-blocking at merge time, the red state has propagated to every later pull request.

Cover every in-process ACL-sandbox failure branch with stub-based failure-path suites (ffi/acl/token/spawn/index), following the package's existing failure-paths pattern; the package now measures 100% per file under the Windows denominator. Exclude only the runner entry from the win32 denominator: it executes exclusively as a spawned child outside the instrumented run, and its behavior is pinned end-to-end by the runner suite. Restore the authoring.ts narrow V8 ignore and add one for the dispose token guard whose absent-token arm is lifecycle-unreachable. Update the dual-lane Agent Note with the denominator composition.
This commit is contained in:
Huanqi Cao
2026-08-10 19:07:35 +08:00
parent d2321d210a
commit 59a2e4d825
12 changed files with 1806 additions and 5 deletions

View File

@@ -105,6 +105,7 @@ async function tightenModes(dir: string): Promise<void> {
if (entry.isDirectory()) {
await tightenModes(target)
} else {
/* v8 ignore next -- Windows exposes no POSIX owner-execute bit; the POSIX lane covers both file modes. */
await chmod(target, ((await stat(target)).mode & 0o100) === 0 ? 0o600 : 0o700)
}
}

View File

@@ -168,12 +168,14 @@ export const PROCESS_INFORMATION = koffi.struct('PROCESS_INFORMATION', {
dwThreadId: 'uint32',
})
/* v8 ignore start -- layout-mismatch guards fire only on ABI breakage; verify/abi-probe.cpp pins both sizes. */
if (STARTUPINFOW.size !== abi.STARTUPINFOW_SIZE) {
throw new Error(`STARTUPINFOW layout mismatch: koffi computed ${STARTUPINFOW.size}, header probe says ${abi.STARTUPINFOW_SIZE}`)
}
if (PROCESS_INFORMATION.size !== abi.PROCESS_INFORMATION_SIZE) {
throw new Error(`PROCESS_INFORMATION layout mismatch: koffi computed ${PROCESS_INFORMATION.size}, header probe says ${abi.PROCESS_INFORMATION_SIZE}`)
}
/* v8 ignore stop */
/**
* Allocate one pointer-sized slot (for `T **` out-parameters).

View File

@@ -360,6 +360,8 @@ export class AclSandbox {
}
}
const token = this.token
/* v8 ignore next -- init assigns this.api only after this.token, so an initialized instance always
has its token; the guard mirrors the write-SID guard's defensive shape. */
if (token !== undefined) {
try {
if (api.closeHandle(token) === 0) throwLastError(api, 'CloseHandle', 'restricted token')

View File

@@ -0,0 +1,456 @@
/**
* ACL failure-path tests with stub binding tables (the failure-paths.spec.ts
* pattern): every checked Win32 call in the lock, read-merge-write, and
* grant-skip sequence has a failing counterpart, and each failure closes the
* handles it created before throwing. The exact-ACE skip and the DACL-walk
* defenses are driven through crafted in-memory ACL/SID buffers. Pure
* stubs — no real Win32 calls, so these run on every platform; the
* real-FFI round-trip lives in acl.spec.ts (win32 only).
*/
import { tmpdir } from 'node:os'
import { describe, expect, it, vi } from 'vitest'
import koffi from 'koffi'
import { grantWrite, revokeWrite, withPathLock } from '../src/acl.ts'
import { allocBytes, ptrAddress } from '../src/ffi.ts'
import type { NativePtr, Win32Bindings } from '../src/ffi.ts'
import { Win32Error } from '../src/errors.ts'
import * as abi from '../src/win32-abi.ts'
const PVOID = koffi.pointer('void')
/** The stub the grant/revoke happy path needs; every call succeeds until a field is overridden per test. */
function aclApi(overrides: Partial<Win32Bindings> = {}): Win32Bindings {
return {
getTempPathW: vi.fn((_length: number, buffer: Buffer) => {
const temp = tmpdir().replace(/[\\/]$/u, '')
buffer.write(temp, 'utf16le')
return temp.length
}),
createFileW: vi.fn(() => 7n),
lockFileEx: vi.fn(() => 1),
unlockFileEx: vi.fn(() => 1),
closeHandle: vi.fn(() => 1),
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, 0n) // no explicit DACL: the merge builds one
koffi.encode(descriptor, PVOID, 0n)
return 0
}),
setEntriesInAclW: vi.fn((_count: unknown, _entries: unknown, _old: unknown, newAcl: NativePtr) => {
koffi.encode(newAcl, PVOID, 9n)
return 0
}),
setNamedSecurityInfoW: vi.fn(() => 0),
localFree: vi.fn(() => 0n as NativePtr),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
...overrides,
} as unknown as Win32Bindings
}
/** One SID allocation: revision@0, subAuthorityCount@1, identifierAuthority@2 (6 bytes), subauthorities@8. */
function craftSid(revision: number, count: number, authority: number[] = [0, 0, 0, 0, 0, 5]): NativePtr {
const sid = allocBytes(8)
koffi.encode(sid, 'uint8', revision)
koffi.encode(sid, 1, 'uint8', count)
authority.forEach((byte, index) => {
koffi.encode(sid, 2 + index, 'uint8', byte)
})
return sid
}
/**
* One in-memory ACL carrying the exact grant ACE the skip checks for:
* header (AclRevision@0, AclSize@2, AceCount@4) then one ACCESS_ALLOWED_ACE
* (AceType@0, AceFlags@1, AceSize@2, Mask@4, inline SID@8). `match` selects
* whether the inline SID bytes equal `sid`.
*/
function craftAclWithGrant(sid: NativePtr, match: boolean): NativePtr {
const acl = allocBytes(32)
koffi.encode(acl, 'uint8', 2) // AclRevision
koffi.encode(acl, 2, 'uint16', 16) // AclSize: header + one 8-byte-SID ACE
koffi.encode(acl, 4, 'uint16', 1) // AceCount
const ace = 8
koffi.encode(acl, ace + 0, 'uint8', abi.ACCESS_ALLOWED_ACE_TYPE)
koffi.encode(acl, ace + 1, 'uint8', abi.SUB_CONTAINERS_AND_OBJECTS_INHERIT)
koffi.encode(acl, ace + 2, 'uint16', 8)
koffi.encode(acl, ace + 4, 'uint32', abi.GRANT_MASK)
const inlineSid = ace + 8
for (let offset = 0; offset < 8; offset++) {
koffi.encode(acl, inlineSid + offset, 'uint8', match
? koffi.decode(sid, offset, 'uint8') as number
: offset === 0 ? 9 : 0)
}
return acl
}
describe('withPathLock failure paths', () => {
it('fails closed when CreateFileW returns an invalid handle', () => {
const api = aclApi({ createFileW: vi.fn(() => 0n as NativePtr) })
let caught: unknown
try {
withPathLock(api, 'C:\\locked', () => {})
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('CreateFileW')
})
it('closes the handle and reports when LockFileEx fails', () => {
const closeHandle = vi.fn(() => 1)
const api = aclApi({ lockFileEx: vi.fn(() => 0), closeHandle })
let caught: unknown
try {
withPathLock(api, 'C:\\locked', () => {})
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('LockFileEx')
expect(closeHandle).toHaveBeenCalledWith(7n)
})
it('closes the handle and reports when UnlockFileEx fails', () => {
const closeHandle = vi.fn(() => 1)
const api = aclApi({ unlockFileEx: vi.fn(() => 0), closeHandle })
let caught: unknown
try {
withPathLock(api, 'C:\\locked', () => {})
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('UnlockFileEx')
expect(closeHandle).toHaveBeenCalledWith(7n)
})
it('reports a failed CloseHandle after a successful action', () => {
const api = aclApi({ closeHandle: vi.fn(() => 0) })
let caught: unknown
try {
withPathLock(api, 'C:\\locked', () => {})
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('CloseHandle')
})
})
describe('mergeAndApply failure paths', () => {
it('reports a SetEntriesInAclW failure when the directory carries no descriptor to free', () => {
const api = aclApi({ setEntriesInAclW: vi.fn(() => 5) }) // default descriptor: none
const sid = craftSid(1, 0)
let caught: unknown
try {
grantWrite(api, 'C:\\granted', sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('SetEntriesInAclW')
})
it('reports a NULL merged ACL when there is no descriptor to free', () => {
const api = aclApi({ setEntriesInAclW: vi.fn(() => 0) }) // no out slot write, no descriptor
const sid = craftSid(1, 0)
let caught: unknown
try {
grantWrite(api, 'C:\\granted', sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('SetEntriesInAclW')
})
it('frees the descriptor and reports when SetEntriesInAclW fails', () => {
const localFree = vi.fn(() => 0n as NativePtr)
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, 0n)
koffi.encode(descriptor, PVOID, 6n) // an existing explicit DACL
return 0
}),
setEntriesInAclW: vi.fn(() => 5),
localFree,
})
const sid = craftSid(1, 0)
let caught: unknown
try {
grantWrite(api, 'C:\\granted', sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('SetEntriesInAclW')
expect(localFree).toHaveBeenCalledWith(6n)
})
it('frees the descriptor and reports a NULL merged ACL', () => {
const localFree = vi.fn(() => 0n as NativePtr)
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, 0n)
koffi.encode(descriptor, PVOID, 6n)
return 0
}),
setEntriesInAclW: vi.fn(() => 0), // success without writing the out slot
localFree,
})
const sid = craftSid(1, 0)
let caught: unknown
try {
grantWrite(api, 'C:\\granted', sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('SetEntriesInAclW')
expect(localFree).toHaveBeenCalledWith(6n)
})
it('frees the merged ACL and reports when SetNamedSecurityInfoW fails', () => {
const localFree = vi.fn(() => 0n as NativePtr)
const api = aclApi({ setNamedSecurityInfoW: vi.fn(() => 5), localFree })
const sid = craftSid(1, 0)
let caught: unknown
try {
grantWrite(api, 'C:\\granted', sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('SetNamedSecurityInfoW')
expect(localFree).toHaveBeenCalledWith(9n)
})
it('reports a failed descriptor LocalFree after a successful apply', () => {
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, 0n)
koffi.encode(descriptor, PVOID, 6n)
return 0
}),
localFree: vi.fn(() => 1n as NativePtr), // both frees "fail"; the first is checked
})
const sid = craftSid(1, 0)
let caught: unknown
try {
grantWrite(api, 'C:\\granted', sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('LocalFree')
})
it('reports a failed merged-ACL LocalFree after a successful apply', () => {
// No existing descriptor (the default stub): the merge's only LocalFree
// is the merged ACL's, which "fails" and is checked after the apply.
const api = aclApi({ localFree: vi.fn(() => 1n as NativePtr) })
const sid = craftSid(1, 0)
let caught: unknown
try {
grantWrite(api, 'C:\\granted', sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('LocalFree')
})
})
describe('the exact-ACE skip and DACL-walk defenses', () => {
it('grantWrite skips the apply when the standing exact ACE matches (descriptor freed, nothing merged)', () => {
const sid = craftSid(1, 0)
const localFree = vi.fn(() => 0n as NativePtr)
const setNamedSecurityInfoW = vi.fn(() => 0)
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, ptrAddress(craftAclWithGrant(sid, true)))
koffi.encode(descriptor, PVOID, 6n)
return 0
}),
localFree,
setNamedSecurityInfoW,
})
grantWrite(api, 'C:\\granted', sid)
expect(setNamedSecurityInfoW).not.toHaveBeenCalled()
expect(localFree).toHaveBeenCalledWith(6n)
})
it('grantWrite skips the apply without freeing when the exact ACE stands but no descriptor owns it', () => {
const sid = craftSid(1, 0)
const localFree = vi.fn(() => 0n as NativePtr)
const setNamedSecurityInfoW = vi.fn(() => 0)
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, ptrAddress(craftAclWithGrant(sid, true)))
koffi.encode(descriptor, PVOID, 0n) // the read "returned" a bare ACL with no descriptor
return 0
}),
localFree,
setNamedSecurityInfoW,
})
grantWrite(api, 'C:\\granted', sid)
expect(setNamedSecurityInfoW).not.toHaveBeenCalled()
expect(localFree).not.toHaveBeenCalled()
})
it('grantWrite reports a failed descriptor LocalFree on the exact-ACE skip path', () => {
const sid = craftSid(1, 0)
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, ptrAddress(craftAclWithGrant(sid, true)))
koffi.encode(descriptor, PVOID, 6n)
return 0
}),
localFree: vi.fn(() => 1n as NativePtr),
})
let caught: unknown
try {
grantWrite(api, 'C:\\granted', sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('LocalFree')
})
it('falls back to the merge path when the standing ACE names a different SID', () => {
const sid = craftSid(1, 0)
const setNamedSecurityInfoW = vi.fn(() => 0)
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, ptrAddress(craftAclWithGrant(sid, false)))
koffi.encode(descriptor, PVOID, 6n)
return 0
}),
setNamedSecurityInfoW,
})
grantWrite(api, 'C:\\granted', sid)
expect(setNamedSecurityInfoW).toHaveBeenCalledTimes(1)
})
it('treats an implausibly small ACL size as no exact grant', () => {
const sid = craftSid(1, 0)
const acl = allocBytes(32)
koffi.encode(acl, 'uint8', 2)
koffi.encode(acl, 2, 'uint16', 4) // smaller than the 8-byte ACL header
koffi.encode(acl, 4, 'uint16', 1)
const setNamedSecurityInfoW = vi.fn(() => 0)
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, ptrAddress(acl))
koffi.encode(descriptor, PVOID, 6n)
return 0
}),
setNamedSecurityInfoW,
})
grantWrite(api, 'C:\\granted', sid)
expect(setNamedSecurityInfoW).toHaveBeenCalledTimes(1)
})
it('treats an ACE that would overrun the ACL as no exact grant', () => {
const sid = craftSid(1, 0)
const acl = allocBytes(32)
koffi.encode(acl, 'uint8', 2)
koffi.encode(acl, 2, 'uint16', 8) // header only: no room for any ACE
koffi.encode(acl, 4, 'uint16', 1)
koffi.encode(acl, 10, 'uint16', 100) // the walk reads a lying ACE size
const setNamedSecurityInfoW = vi.fn(() => 0)
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, ptrAddress(acl))
koffi.encode(descriptor, PVOID, 6n)
return 0
}),
setNamedSecurityInfoW,
})
grantWrite(api, 'C:\\granted', sid)
expect(setNamedSecurityInfoW).toHaveBeenCalledTimes(1)
})
})
describe('revokeWrite no-DACL path', () => {
it('reports nothing to revoke when the read yields neither DACL nor descriptor', () => {
// The default stub encodes a NULL DACL and a NULL descriptor.
const api = aclApi()
const sid = craftSid(1, 0)
expect(revokeWrite(api, 'C:\\granted', sid)).toBe(false)
})
it('frees a descriptor that carries no DACL and reports nothing to revoke', () => {
const localFree = vi.fn(() => 0n as NativePtr)
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, 0n)
koffi.encode(descriptor, PVOID, 6n) // descriptor WITHOUT a DACL
return 0
}),
localFree,
})
const sid = craftSid(1, 0)
expect(revokeWrite(api, 'C:\\granted', sid)).toBe(false)
expect(localFree).toHaveBeenCalledWith(6n)
})
it('reports a failed descriptor LocalFree on the no-DACL path', () => {
const api = aclApi({
getNamedSecurityInfoW: vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, 0n)
koffi.encode(descriptor, PVOID, 6n)
return 0
}),
localFree: vi.fn(() => 1n as NativePtr),
})
const sid = craftSid(1, 0)
let caught: unknown
try {
revokeWrite(api, 'C:\\granted', sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('LocalFree')
})
})

View File

@@ -11,7 +11,8 @@ import koffi from 'koffi'
import { PROCESS_INFORMATION, getTempPath } from '../src/ffi.ts'
import type { NativePtr, Win32Bindings } from '../src/ffi.ts'
import { Win32Error } from '../src/errors.ts'
import { spawnSandboxed, spawnSandboxedInherited } from '../src/spawn.ts'
import { drainPipe, spawnSandboxed, spawnSandboxedInherited, waitForExit } from '../src/spawn.ts'
import * as abi from '../src/win32-abi.ts'
const PVOID = koffi.pointer('void')
@@ -136,3 +137,318 @@ describe('getTempPath buffer defense', () => {
expect(() => getTempPath(api)).toThrow(/GetTempPathW failed \(Win32 122\): required 300/u)
})
})
/** The stub the pipe-happy path needs: CreatePipe fills both out slots with fresh handles. */
function pipeOkApi(overrides: Partial<Win32Bindings> = {}): {
api: Win32Bindings
closed: bigint[]
closeHandle: ReturnType<typeof vi.fn>
} {
const closed: bigint[] = []
let next = 1n
const closeHandle = vi.fn((handle: NativePtr) => {
closed.push(handle)
return 1
})
const api = {
createPipe: vi.fn((readSlot: NativePtr, writeSlot: NativePtr) => {
koffi.encode(readSlot, PVOID, next++)
koffi.encode(writeSlot, PVOID, next++)
return 1
}),
setHandleInformation: vi.fn(() => 1),
createProcessAsUserW: vi.fn((
_token: unknown, _app: unknown, _cmd: unknown, _pa: unknown, _ta: unknown,
_inherit: unknown, _flags: unknown, _env: unknown, _cwd: unknown, _si: unknown, processInfo: NativePtr,
) => {
koffi.encode(processInfo, PROCESS_INFORMATION, { hProcess: 200n, hThread: 201n, dwProcessId: 1234, dwThreadId: 5678 })
return 1
}),
getLastError: vi.fn(() => 5),
closeHandle,
formatMessageW: vi.fn(() => 0),
...overrides,
} as unknown as Win32Bindings
return { api, closed, closeHandle }
}
describe('spawn pipe failures close their handles', () => {
const token = 1n as NativePtr
it('spawnSandboxed reports a CreatePipe failure', () => {
const api = { createPipe: vi.fn(() => 0), getLastError: vi.fn(() => 5), formatMessageW: vi.fn(() => 0) } as unknown as Win32Bindings
let caught: unknown
try {
spawnSandboxed(api, token, { command: 'probe.exe', args: [], cwd: 'C:\\' })
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('CreatePipe')
})
it('spawnSandboxed reports a NULL pipe handle after CreatePipe succeeds', () => {
const api = { createPipe: vi.fn(() => 1), getLastError: vi.fn(() => 5), formatMessageW: vi.fn(() => 0) } as unknown as Win32Bindings
let caught: unknown
try {
spawnSandboxed(api, token, { command: 'probe.exe', args: [], cwd: 'C:\\' })
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('CreatePipe')
})
it('spawnSandboxed reports a SetHandleInformation failure', () => {
const { api } = pipeOkApi({ setHandleInformation: vi.fn(() => 0) })
let caught: unknown
try {
spawnSandboxed(api, token, { command: 'probe.exe', args: [], cwd: 'C:\\' })
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('SetHandleInformation')
})
it('spawnSandboxed rejects NULL process/thread handles after a successful spawn', () => {
const { api } = pipeOkApi({
createProcessAsUserW: vi.fn((
_token: unknown, _app: unknown, _cmd: unknown, _pa: unknown, _ta: unknown,
_inherit: unknown, _flags: unknown, _env: unknown, _cwd: unknown, _si: unknown, processInfo: NativePtr,
) => {
koffi.encode(processInfo, PROCESS_INFORMATION, { hProcess: null, hThread: null, dwProcessId: 1234, dwThreadId: 5678 })
return 1
}),
})
expect(() => spawnSandboxed(api, token, { command: 'probe.exe', args: [], cwd: 'C:\\' }))
.toThrow(/null process\/thread handles/u)
})
})
describe('spawnSandboxedInherited failure paths', () => {
const token = 1n as NativePtr
/** The stub the inherited-happy path needs; overrides flip one call per test. */
function inheritedApi(overrides: Partial<Win32Bindings> = {}): {
api: Win32Bindings
closed: bigint[]
closeHandle: ReturnType<typeof vi.fn>
} {
const closed: bigint[] = []
let std = 50n
const closeHandle = vi.fn((handle: NativePtr) => {
closed.push(handle)
return 1
})
const api = {
createJobObjectW: vi.fn(() => 100n),
setInformationJobObject: vi.fn(() => 1),
getStdHandle: vi.fn(() => std++),
setHandleInformation: vi.fn(() => 1),
createProcessAsUserW: vi.fn((
_token: unknown, _app: unknown, _cmd: unknown, _pa: unknown, _ta: unknown,
_inherit: unknown, _flags: unknown, _env: unknown, _cwd: unknown, _si: unknown, processInfo: NativePtr,
) => {
koffi.encode(processInfo, PROCESS_INFORMATION, { hProcess: 200n, hThread: 201n, dwProcessId: 1234, dwThreadId: 5678 })
return 1
}),
assignProcessToJobObject: vi.fn(() => 1),
resumeThread: vi.fn(() => 0),
getLastError: vi.fn(() => 5),
closeHandle,
formatMessageW: vi.fn(() => 0),
...overrides,
} as unknown as Win32Bindings
return { api, closed, closeHandle }
}
it('closes the job and reports when GetStdHandle yields a NULL handle', () => {
const { api, closeHandle } = inheritedApi({ getStdHandle: vi.fn(() => 0n as NativePtr) })
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('GetStdHandle')
expect(closeHandle).toHaveBeenCalledWith(100n)
})
it('reports a SetHandleInformation failure while enabling stdio inheritance', () => {
const { api } = inheritedApi({ setHandleInformation: vi.fn(() => 0) })
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('SetHandleInformation')
})
it('closes the job and reports when CreateProcessAsUserW fails', () => {
const { api, closeHandle } = inheritedApi({ createProcessAsUserW: vi.fn(() => 0) })
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('CreateProcessAsUserW')
expect(closeHandle).toHaveBeenCalledWith(100n)
})
it('closes the job and rejects NULL process/thread handles after a successful spawn', () => {
const { api, closeHandle } = inheritedApi({
createProcessAsUserW: vi.fn((
_token: unknown, _app: unknown, _cmd: unknown, _pa: unknown, _ta: unknown,
_inherit: unknown, _flags: unknown, _env: unknown, _cwd: unknown, _si: unknown, processInfo: NativePtr,
) => {
koffi.encode(processInfo, PROCESS_INFORMATION, { hProcess: null, hThread: null, dwProcessId: 1234, dwThreadId: 5678 })
return 1
}),
})
expect(() => spawnSandboxedInherited(api, token, { command: 'probe.exe', args: [], cwd: 'C:\\' }))
.toThrow(/null process\/thread handles/u)
expect(closeHandle).toHaveBeenCalledWith(100n)
})
it('closes the job and reports when SetInformationJobObject fails', () => {
const { api, closeHandle } = inheritedApi({ setInformationJobObject: vi.fn(() => 0) })
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('SetInformationJobObject')
expect(closeHandle).toHaveBeenCalledWith(100n)
})
it('closes the job and reports a NULL job object', () => {
const { api } = inheritedApi({ createJobObjectW: vi.fn(() => 0n as NativePtr) })
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('CreateJobObjectW')
})
it('returns the pid, process handle, and kill-on-close job when every call succeeds', () => {
const { api, closeHandle } = inheritedApi()
const spawned = spawnSandboxedInherited(api, token, { command: 'probe.exe', args: [], cwd: 'C:\\' })
expect(spawned.pid).toBe(1234)
expect(spawned.process).toBe(200n)
expect(spawned.job).toBe(100n)
// thread handle closed by the spawn; process and job handles stay with the caller.
expect(closeHandle).toHaveBeenCalledWith(201n)
expect(closeHandle).not.toHaveBeenCalledWith(200n)
expect(closeHandle).not.toHaveBeenCalledWith(100n)
})
})
describe('drainPipe', () => {
it('stops at ERROR_NO_DATA and closes the read end', () => {
const closeHandle = vi.fn(() => 1)
const api = {
peekNamedPipe: vi.fn(() => 0),
getLastError: vi.fn(() => abi.ERROR_NO_DATA),
closeHandle,
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
return drainPipe(api, 30n as NativePtr).then((buffer) => {
expect(buffer.length).toBe(0)
expect(closeHandle).toHaveBeenCalledWith(30n)
})
})
it('reports a PeekNamedPipe failure that is not a clean EOF', () => {
const api = {
peekNamedPipe: vi.fn(() => 0),
getLastError: vi.fn(() => 5),
closeHandle: vi.fn(() => 1),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
return expect(drainPipe(api, 30n as NativePtr)).rejects.toMatchObject({ api: 'PeekNamedPipe' })
})
it('reports a ReadFile failure after data was reported available', () => {
const api = {
peekNamedPipe: vi.fn((_pipe: unknown, _buffer: unknown, _size: unknown, _read: unknown, totalAvail: NativePtr) => {
koffi.encode(totalAvail, 'uint32', 4)
return 1
}),
readFile: vi.fn(() => 0),
getLastError: vi.fn(() => 5),
closeHandle: vi.fn(() => 1),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
return expect(drainPipe(api, 30n as NativePtr)).rejects.toMatchObject({ api: 'ReadFile' })
})
it('drains one chunk and stops at ERROR_BROKEN_PIPE', () => {
let peeks = 0
const api = {
peekNamedPipe: vi.fn((_pipe: unknown, _buffer: unknown, _size: unknown, _read: unknown, totalAvail: NativePtr) => {
peeks++
if (peeks > 1) return 0
koffi.encode(totalAvail, 'uint32', 4)
return 1
}),
readFile: vi.fn((_file: unknown, chunk: Buffer, _count: unknown, read: NativePtr) => {
chunk.write('ab', 0, 'utf8')
koffi.encode(read, 'uint32', 2)
return 1
}),
getLastError: vi.fn(() => abi.ERROR_BROKEN_PIPE),
closeHandle: vi.fn(() => 1),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
return drainPipe(api, 30n as NativePtr).then((buffer) => {
expect(buffer.toString('utf8')).toBe('ab')
})
})
})
describe('waitForExit', () => {
it('reports a WaitForSingleObject failure', () => {
const api = {
waitForSingleObject: vi.fn(() => 0xFFFFFFFF),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
expect(() => waitForExit(api, 200n as NativePtr)).toThrow(Win32Error)
})
it('reports a GetExitCodeProcess failure', () => {
const api = {
waitForSingleObject: vi.fn(() => 0),
getExitCodeProcess: vi.fn(() => 0),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
expect(() => waitForExit(api, 200n as NativePtr)).toThrow(Win32Error)
})
it('returns the exit code and closes the process handle', () => {
const closeHandle = vi.fn(() => 1)
const api = {
waitForSingleObject: vi.fn(() => 0),
getExitCodeProcess: vi.fn((_process: unknown, slot: NativePtr) => {
koffi.encode(slot, 'uint32', 42)
return 1
}),
closeHandle,
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
expect(waitForExit(api, 200n as NativePtr)).toBe(42)
expect(closeHandle).toHaveBeenCalledWith(200n)
})
})

View File

@@ -0,0 +1,190 @@
/**
* FFI helper tests with stub binding tables (the failure-paths.spec.ts
* pattern): error formatting and temp-path decoding defenses, the
* last-error throwers' detail fallback, pointer decode NULL handling, and
* the bounded SID comparison's early exits. Pure stubs — no real Win32
* calls, so these run on every platform; the real-FFI round-trip lives in
* acl.spec.ts and probe.spec.ts (win32 only).
*/
import { describe, expect, it, vi } from 'vitest'
import koffi from 'koffi'
import { Win32Error } from '../src/errors.ts'
import {
allocBytes, decodePtr, decodePtrAt, errorText, getTempPath,
isInvalidHandle, isNullPtr, sameSidAt, throwLastError, throwWin32,
} from '../src/ffi.ts'
import type { NativePtr, Win32Bindings } from '../src/ffi.ts'
import * as abi from '../src/win32-abi.ts'
const PVOID = koffi.pointer('void')
/** A stub whose formatMessageW writes real UTF-16 text (the errorText round-trip). */
function formatApi(): { api: Win32Bindings; formatMessageW: ReturnType<typeof vi.fn> } {
const formatMessageW = vi.fn((_flags: number, _source: null, _id: number, _lang: number, buffer: Buffer, _size: number, _args: null) => {
const text = 'access denied'
buffer.write(text, 'utf16le')
return text.length
})
const api = {
formatMessageW,
getLastError: vi.fn(() => 5),
} as unknown as Win32Bindings
return { api, formatMessageW }
}
/** A minimal SID allocation: revision@0, subAuthorityCount@1, identifierAuthority@2, subauthorities@8. */
function craftSid(revision: number, count: number, authority: number[] = [0, 0, 0, 0, 0, 0], subs: number[] = []): NativePtr {
const sid = allocBytes(8 + subs.length * 4)
koffi.encode(sid, 'uint8', revision)
koffi.encode(sid, 1, 'uint8', count)
authority.forEach((byte, index) => {
koffi.encode(sid, 2 + index, 'uint8', byte)
})
subs.forEach((sub, index) => {
koffi.encode(sid, 8 + index * 4, 'uint32', sub)
})
return sid
}
describe('errorText', () => {
it('decodes the formatted UTF-16 message and trims it', () => {
const { api } = formatApi()
expect(errorText(api, 5)).toBe('access denied')
})
it('returns an empty string when FormatMessageW formats nothing', () => {
const api = { formatMessageW: vi.fn(() => 0) } as unknown as Win32Bindings
expect(errorText(api, 5)).toBe('')
})
})
describe('getTempPath', () => {
it('decodes the NUL-terminated temp path GetTempPathW wrote', () => {
const api = {
getTempPathW: vi.fn((_length: number, buffer: Buffer) => {
buffer.write('C:\\TEMP', 'utf16le')
return 7
}),
} as unknown as Win32Bindings
expect(getTempPath(api)).toBe('C:\\TEMP')
})
it('reports the Win32 failure when GetTempPathW writes nothing', () => {
const { api } = formatApi()
const failing = { ...api, getTempPathW: vi.fn(() => 0) } as Win32Bindings
let caught: unknown
try {
getTempPath(failing)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('GetTempPathW')
})
})
describe('throwLastError and throwWin32', () => {
it('throwLastError formats the system message when no detail is given', () => {
const { api } = formatApi()
let caught: unknown
try {
throwLastError(api, 'Probe')
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).message).toContain('Probe failed (Win32 5): access denied')
})
it('throwWin32 formats the system message when no detail is given', () => {
const { api } = formatApi()
let caught: unknown
try {
throwWin32(api, 'Probe', 5)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).message).toContain('Probe failed (Win32 5): access denied')
})
it('Win32Error appends the detail when one is given', () => {
const error = new Win32Error('Probe', 5, 'the lock file path')
expect(error.name).toBe('Win32Error')
expect(error.api).toBe('Probe')
expect(error.win32Code).toBe(5)
expect(error.message).toBe('Probe failed (Win32 5): the lock file path')
})
it('Win32Error omits the detail suffix when none is given', () => {
const error = new Win32Error('Probe', 5)
expect(error.message).toBe('Probe failed (Win32 5)')
})
})
describe('pointer NULL handling', () => {
it('isNullPtr accepts null, undefined, and the zero pointer', () => {
expect(isNullPtr(null)).toBe(true)
expect(isNullPtr(undefined)).toBe(true)
expect(isNullPtr(0n as NativePtr)).toBe(true)
expect(isNullPtr(42n as NativePtr)).toBe(false)
})
it('isInvalidHandle treats NULL as failure', () => {
expect(isInvalidHandle(null)).toBe(true)
expect(isInvalidHandle(undefined)).toBe(true)
expect(isInvalidHandle(0n as NativePtr)).toBe(true)
expect(isInvalidHandle(42n as NativePtr)).toBe(false)
})
it('decodePtrAt returns null for a NULL pointer stored in a buffer', () => {
const buffer = Buffer.alloc(8)
buffer.writeBigUInt64LE(0n, 0)
expect(decodePtrAt(buffer, 0)).toBeNull()
})
it('decodePtrAt returns the stored pointer value', () => {
const buffer = Buffer.alloc(8)
buffer.writeBigUInt64LE(42n, 0)
expect(decodePtrAt(buffer, 0)).toBe(42n)
})
it('decodePtr returns null for an unset out-parameter slot', () => {
const slot = koffi.alloc(PVOID, 1) as unknown as NativePtr
expect(decodePtr(slot)).toBeNull()
})
})
describe('sameSidAt bounded comparison', () => {
it('rejects a revision mismatch before comparing anything else', () => {
const left = craftSid(1, 0)
const right = craftSid(2, 0)
expect(sameSidAt(left, 0, right, 0)).toBe(false)
})
it('rejects a subauthority-count mismatch', () => {
const left = craftSid(1, 1, [0, 0, 0, 0, 0, 5], [42])
const right = craftSid(1, 2, [0, 0, 0, 0, 0, 5], [42, 43])
expect(sameSidAt(left, 0, right, 0)).toBe(false)
})
it('rejects an implausible subauthority count', () => {
const left = craftSid(1, abi.SID_MAX_SUB_AUTHORITIES + 1)
const right = craftSid(1, abi.SID_MAX_SUB_AUTHORITIES + 1)
expect(sameSidAt(left, 0, right, 0)).toBe(false)
})
it('rejects a differing identifier authority byte', () => {
const left = craftSid(1, 0, [0, 0, 0, 0, 0, 5])
const right = craftSid(1, 0, [0, 0, 0, 0, 0, 6])
expect(sameSidAt(left, 0, right, 0)).toBe(false)
})
it('accepts identical SIDs at nonzero offsets', () => {
const left = craftSid(1, 1, [0, 0, 0, 0, 0, 5], [42])
const right = craftSid(1, 1, [0, 0, 0, 0, 0, 5], [42])
expect(sameSidAt(left, 4, right, 4)).toBe(true)
})
})

View File

@@ -0,0 +1,388 @@
/**
* AclSandbox orchestration failure-path tests: the win32 resolver is mocked
* to hand each test a stub binding table, so every checked Win32 call in
* init/spawn/dispose has a failing counterpart without opening real token or
* ACL handles. Constructor validation, the fail-closed init cleanup, and the
* dispose aggregation use the same stubs. Pure stubs — no real Win32 calls,
* so these run on every platform; the real-FFI round-trip lives in
* acl.spec.ts and runner.spec.ts (win32 only).
*/
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
import koffi from 'koffi'
import { PROCESS_INFORMATION } from '../src/ffi.ts'
import type { NativePtr, Win32Bindings } from '../src/ffi.ts'
import { Win32Error } from '../src/errors.ts'
import { AclSandbox } from '../src/index.ts'
import * as abi from '../src/win32-abi.ts'
const PVOID = koffi.pointer('void')
type MockFn = ReturnType<typeof vi.fn>
/** The stub binding table plus the mocks the assertions inspect directly. */
interface HappyStubs {
api: Win32Bindings
setNamedSecurityInfoW: MockFn
convertStringSidToSidW: MockFn
closeHandle: MockFn
localFree: MockFn
createRestrictedToken: MockFn
createJobObjectW: MockFn
getNamedSecurityInfoW: MockFn
}
const state = vi.hoisted(() => ({ stubs: undefined as HappyStubs | undefined }))
vi.mock('../src/ffi.ts', async (importOriginal) => {
const actual = await importOriginal<typeof import('../src/ffi.ts')>()
return {
...actual,
win32: () => Promise.resolve(state.stubs?.api as Win32Bindings),
win32Sync: () => state.stubs?.api as Win32Bindings,
}
})
const scratchDirs: string[] = []
afterAll(() => {
for (const dir of scratchDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
})
function scratch(): string {
const dir = mkdtempSync(join(tmpdir(), 'dsh-acl-index-'))
scratchDirs.push(dir)
return dir
}
/**
* The stub the whole happy pipeline needs: token opening, write-SID parse,
* workspace+temp grants, logon-SID scan, well-known SID, restricted token,
* default-DACL merge, piped/inherited spawns, drains, and exit waits all
* succeed. Every test flips one call per branch.
*/
function happyStubs(): HappyStubs {
let next = 0n
const fresh = () => ++next
const openProcess = vi.fn(() => fresh())
const openProcessToken = vi.fn((_process: unknown, _access: unknown, slot: NativePtr) => {
koffi.encode(slot, PVOID, fresh())
return 1
})
const convertStringSidToSidW = vi.fn((_sid: string, slot: NativePtr) => {
koffi.encode(slot, PVOID, fresh())
return 1
})
const getTempPathW = vi.fn((_length: number, buffer: Buffer) => {
const temp = tmpdir().replace(/[\\/]$/u, '')
buffer.write(temp, 'utf16le')
return temp.length
})
const createFileW = vi.fn(() => fresh())
const getNamedSecurityInfoW = vi.fn((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
koffi.encode(dacl, PVOID, 0n)
koffi.encode(descriptor, PVOID, 0n)
return 0
})
const setEntriesInAclW = vi.fn((_count: unknown, _entries: unknown, _old: unknown, newAcl: NativePtr) => {
koffi.encode(newAcl, PVOID, fresh())
return 0
})
const setNamedSecurityInfoW = vi.fn(() => 0)
const getTokenInformation = vi.fn((_token: unknown, cls: number, info: Buffer | null, _length: number, needed: NativePtr) => {
if (info === null) {
koffi.encode(needed, 'uint32', cls === abi.TokenGroups ? 24 : 8)
return 0 // the size probe is expected to "fail"
}
if (cls === abi.TokenGroups) {
info.writeUInt32LE(1, 0)
info.writeBigUInt64LE(77n, abi.TOKEN_GROUPS_OFFSET)
info.writeUInt32LE(abi.SE_GROUP_LOGON_ID, abi.TOKEN_GROUPS_OFFSET + 8)
} else {
info.writeBigUInt64LE(88n, 0) // the token's current default DACL
}
return 1
})
const getLengthSid = vi.fn(() => 12)
const copySid = vi.fn(() => 1)
const createWellKnownSid = vi.fn(() => 1)
const isValidSid = vi.fn(() => 1)
const createRestrictedToken = vi.fn((
_existing: unknown, _flags: unknown, _dc: unknown, _ds: unknown, _pc: unknown, _pd: unknown,
_rc: unknown, _rs: unknown, slot: NativePtr,
) => {
koffi.encode(slot, PVOID, fresh())
return 1
})
const setTokenInformation = vi.fn(() => 1)
const createPipe = vi.fn((readSlot: NativePtr, writeSlot: NativePtr) => {
koffi.encode(readSlot, PVOID, fresh())
koffi.encode(writeSlot, PVOID, fresh())
return 1
})
const setHandleInformation = vi.fn(() => 1)
const createProcessAsUserW = vi.fn((
_token: unknown, _app: unknown, _cmd: unknown, _pa: unknown, _ta: unknown,
_inherit: unknown, _flags: unknown, _env: unknown, _cwd: unknown, _si: unknown, processInfo: NativePtr,
) => {
koffi.encode(processInfo, PROCESS_INFORMATION, { hProcess: fresh(), hThread: fresh(), dwProcessId: 1234, dwThreadId: 5678 })
return 1
})
const peekNamedPipe = vi.fn(() => 0)
const readFile = vi.fn(() => 1)
const waitForSingleObject = vi.fn(() => 0)
const getExitCodeProcess = vi.fn((_process: unknown, slot: NativePtr) => {
koffi.encode(slot, 'uint32', 42)
return 1
})
const createJobObjectW = vi.fn(() => fresh())
const setInformationJobObject = vi.fn(() => 1)
const assignProcessToJobObject = vi.fn(() => 1)
const resumeThread = vi.fn(() => 0)
const getStdHandle = vi.fn(() => fresh())
const localFree = vi.fn(() => 0n)
const closeHandle = vi.fn(() => 1)
const getLastError = vi.fn(() => abi.ERROR_BROKEN_PIPE) // the drains' clean EOF
const formatMessageW = vi.fn(() => 0)
const api = {
openProcess, openProcessToken, convertStringSidToSidW, getTempPathW, createFileW,
lockFileEx: vi.fn(() => 1), unlockFileEx: vi.fn(() => 1),
getNamedSecurityInfoW, setEntriesInAclW, setNamedSecurityInfoW, getTokenInformation,
getLengthSid, copySid, createWellKnownSid, isValidSid, createRestrictedToken,
setTokenInformation, createPipe, setHandleInformation, createProcessAsUserW,
peekNamedPipe, readFile, waitForSingleObject, getExitCodeProcess, createJobObjectW,
setInformationJobObject, assignProcessToJobObject, resumeThread, getStdHandle,
localFree, closeHandle, getLastError, formatMessageW,
} as unknown as Win32Bindings
return {
api, setNamedSecurityInfoW, convertStringSidToSidW, closeHandle, localFree,
createRestrictedToken, createJobObjectW, getNamedSecurityInfoW,
}
}
beforeEach(() => {
state.stubs = happyStubs()
})
describe('AclSandbox constructor validation', () => {
it('rejects a writable directory that does not exist', () => {
const missing = join(scratch(), 'missing')
expect(() => new AclSandbox({ writableDirs: [missing], tempDir: null, mode: 'read-only' }))
.toThrow(/writable dir does not exist/u)
})
it('resolves relative writable directories to absolute paths', () => {
const dir = scratch()
const sandbox = new AclSandbox({ writableDirs: [dir], tempDir: null, mode: 'read-only' })
expect(sandbox.writableDirs).toEqual([resolve(dir)])
expect(sandbox.mode).toBe('read-only')
expect(sandbox.tempDir).toBeUndefined()
})
})
describe('AclSandbox init', () => {
it('completes the happy workspace-write pipeline: workspace and temp grants, restricted token, resolved temp dir', async () => {
const { setNamedSecurityInfoW } = state.stubs as HappyStubs
const workspace = scratch()
const temp = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: temp, writeSid: 'S-1-4-9000-1', mode: 'workspace-write' })
await sandbox.init()
expect(sandbox.tempDir).toBe(resolve(temp))
expect(setNamedSecurityInfoW).toHaveBeenCalledTimes(2)
})
it('defaults the temp dir to GetTempPathW when no tempDir option is given', async () => {
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], writeSid: 'S-1-4-9000-2', mode: 'workspace-write' })
await sandbox.init()
expect(sandbox.tempDir).toBe(tmpdir().replace(/[\\/]$/u, ''))
})
it('applies no grants when the temp dir option is null', async () => {
const { setNamedSecurityInfoW } = state.stubs as HappyStubs
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-3', mode: 'workspace-write' })
await sandbox.init()
expect(setNamedSecurityInfoW).toHaveBeenCalledTimes(1) // workspace only
})
it('rejects a temp dir that does not exist', async () => {
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: join(scratch(), 'missing'), writeSid: 'S-1-4-9000-4', mode: 'workspace-write' })
await expect(sandbox.init()).rejects.toThrow(/temp dir does not exist/u)
})
it('builds a read-only token without parsing a write SID or applying grants', async () => {
const { convertStringSidToSidW, setNamedSecurityInfoW } = state.stubs as HappyStubs
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, mode: 'read-only' })
await sandbox.init()
expect(convertStringSidToSidW).not.toHaveBeenCalled()
expect(setNamedSecurityInfoW).not.toHaveBeenCalled()
expect(() => { sandbox.dispose() }).not.toThrow() // no write SID: nothing to revoke or free
})
it('applies no grants when the caller owns the DACLs (manageDacls: false)', async () => {
const { setNamedSecurityInfoW } = state.stubs as HappyStubs
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-5', mode: 'workspace-write', manageDacls: false })
await sandbox.init()
expect(setNamedSecurityInfoW).not.toHaveBeenCalled()
expect(() => { sandbox.dispose() }).not.toThrow() // caller-owned DACLs: nothing to revoke
})
it('refuses a second init on the same instance', async () => {
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-6', mode: 'workspace-write' })
await sandbox.init()
await expect(sandbox.init()).rejects.toThrow(/already initialized/u)
})
it('reports a ConvertStringSidToSidW failure before granting anything', async () => {
const { convertStringSidToSidW, setNamedSecurityInfoW } = state.stubs as HappyStubs
convertStringSidToSidW.mockReturnValue(0)
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-7', mode: 'workspace-write' })
await expect(sandbox.init()).rejects.toMatchObject({ api: 'ConvertStringSidToSidW' })
expect(setNamedSecurityInfoW).not.toHaveBeenCalled()
})
it('rejects a NULL write SID after ConvertStringSidToSidW succeeds', async () => {
const { convertStringSidToSidW } = state.stubs as HappyStubs
convertStringSidToSidW.mockImplementation(() => 1) // no out slot write
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-8', mode: 'workspace-write' })
await expect(sandbox.init()).rejects.toBeInstanceOf(Win32Error)
})
it('reports a failed close of the current process token', async () => {
const { closeHandle } = state.stubs as HappyStubs
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-9', mode: 'workspace-write' })
// 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))
await expect(sandbox.init()).rejects.toMatchObject({ api: 'CloseHandle' })
// The failed init never stored a restricted token: dispose skips the
// token close and the already-drained allocations.
expect(() => { sandbox.dispose() }).not.toThrow()
})
it('revokes the revocable grants and aggregates cleanup failures when the token pipeline fails', async () => {
const { createRestrictedToken, localFree, getNamedSecurityInfoW } = state.stubs as HappyStubs
const workspace = scratch()
const temp = scratch()
let inCleanup = false
createRestrictedToken.mockImplementation(() => {
inCleanup = true // the grants already landed: every later call is the cleanup's
return 0
})
localFree.mockImplementation(() => (inCleanup ? 1n : 0n))
getNamedSecurityInfoW.mockImplementation((
_path: unknown, _type: unknown, _info: unknown, _owner: unknown, _group: unknown,
dacl: NativePtr, _sacl: unknown, descriptor: NativePtr,
) => {
if (inCleanup) return 2 // the cleanup's revocation read fails too
koffi.encode(dacl, PVOID, 0n)
koffi.encode(descriptor, PVOID, 0n)
return 0
})
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: temp, writeSid: 'S-1-4-9000-10', mode: 'workspace-write' })
await expect(sandbox.init()).rejects.toThrow(/3 grant revocation\(s\) also failed/u)
})
})
describe('AclSandbox spawn', () => {
it('refuses to spawn before init', () => {
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-11', mode: 'workspace-write' })
expect(() => sandbox.spawn({ command: 'probe.exe' })).toThrow(/not initialized/u)
})
it('pipe spawn drains empty pipes and settles with the child exit code', async () => {
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-12', mode: 'workspace-write' })
await sandbox.init()
const child = sandbox.spawn({ command: 'probe.exe', args: ['--flag'], cwd: workspace })
expect(child.pid).toBe(1234)
const expected = { stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), exitCode: 42 }
await expect(child.wait()).resolves.toEqual(expected)
// The second wait reuses the settled exit-code promise instead of re-waiting.
await expect(child.wait()).resolves.toEqual(expected)
})
it('inherit spawn settles with empty stdio and closes the kill-on-close job', async () => {
const { closeHandle } = state.stubs as HappyStubs
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-13', mode: 'workspace-write' })
await sandbox.init()
const child = sandbox.spawn({ command: 'probe.exe', stdio: 'inherit' })
await expect(child.wait()).resolves.toEqual({ stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), exitCode: 42 })
expect(closeHandle).toHaveBeenCalled()
})
it('inherit spawn reports a failed close of the kill-on-close job', async () => {
const { closeHandle, createJobObjectW } = state.stubs as HappyStubs
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-14', mode: 'workspace-write' })
await sandbox.init()
let jobHandle = 0n
closeHandle.mockImplementation((handle: NativePtr) => (handle === jobHandle ? 0 : 1))
const child = sandbox.spawn({ command: 'probe.exe', stdio: 'inherit' })
jobHandle = createJobObjectW.mock.results.at(-1)?.value as NativePtr
await expect(child.wait()).rejects.toMatchObject({ api: 'CloseHandle' })
})
})
describe('AclSandbox dispose', () => {
it('is a no-op before init', () => {
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-15', mode: 'workspace-write' })
expect(() => { sandbox.dispose() }).not.toThrow()
})
it('aggregates a failing temp revocation into an AggregateError', async () => {
const { getNamedSecurityInfoW } = state.stubs as HappyStubs
const workspace = scratch()
const temp = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: temp, writeSid: 'S-1-4-9000-16', mode: 'workspace-write' })
await sandbox.init()
getNamedSecurityInfoW.mockReturnValue(2)
expect(() => { sandbox.dispose() }).toThrow(/1 cleanup failure/u)
})
it('aggregates SID and token cleanup failures into an AggregateError', async () => {
const { localFree } = state.stubs as HappyStubs
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-17', mode: 'workspace-write' })
await sandbox.init()
localFree.mockReturnValue(1n)
expect(() => { sandbox.dispose() }).toThrow(AggregateError)
})
it('reports a failed close of the restricted token', async () => {
const { createRestrictedToken, closeHandle } = state.stubs as HappyStubs
const workspace = scratch()
const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-18', mode: 'workspace-write' })
let restrictedToken = 0n
createRestrictedToken.mockImplementation((
_existing: unknown, _flags: unknown, _dc: unknown, _ds: unknown, _pc: unknown, _pd: unknown,
_rc: unknown, _rs: unknown, slot: NativePtr,
) => {
restrictedToken = 99n
koffi.encode(slot, PVOID, restrictedToken)
return 1
})
closeHandle.mockImplementation((handle: NativePtr) => (handle === restrictedToken ? 0 : 1))
await sandbox.init()
expect(() => { sandbox.dispose() }).toThrow(AggregateError)
})
})

View File

@@ -0,0 +1,436 @@
/**
* Restricted-token failure-path tests with stub binding tables (the
* failure-paths.spec.ts pattern): every checked Win32 call in the token
* pipeline — open, logon-SID scan, well-known SID creation, default-DACL
* merge, restricted-token creation — has a failing counterpart, and each
* failure closes or frees what it created before throwing. Pure stubs — no
* real Win32 calls, so these run on every platform; the real-FFI round-trip
* lives in acl.spec.ts (win32 only).
*/
import { describe, expect, it, vi } from 'vitest'
import koffi from 'koffi'
import { allocBytes, isNullPtr } from '../src/ffi.ts'
import type { NativePtr, Win32Bindings } from '../src/ffi.ts'
import { Win32Error } from '../src/errors.ts'
import {
createRestrictedToken, findLogonSid, makeWellKnownSid, openCurrentProcessToken, setTokenDefaultDaclGrant,
} from '../src/token.ts'
import * as abi from '../src/win32-abi.ts'
const PVOID = koffi.pointer('void')
describe('openCurrentProcessToken failure paths', () => {
it('reports when OpenProcess yields no handle', () => {
const api = {
openProcess: vi.fn(() => 0n),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
let caught: unknown
try {
openCurrentProcessToken(api)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('OpenProcess')
})
it('closes the process handle and reports when OpenProcessToken fails', () => {
const closeHandle = vi.fn(() => 1)
const api = {
openProcess: vi.fn(() => 7n),
openProcessToken: vi.fn(() => 0),
closeHandle,
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
let caught: unknown
try {
openCurrentProcessToken(api)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('OpenProcessToken')
expect(closeHandle).toHaveBeenCalledWith(7n)
})
it('reports a failed CloseHandle of the process handle', () => {
const api = {
openProcess: vi.fn(() => 7n),
openProcessToken: vi.fn((_process: unknown, _access: unknown, slot: NativePtr) => {
koffi.encode(slot, PVOID, 9n)
return 1
}),
closeHandle: vi.fn(() => 0),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
let caught: unknown
try {
openCurrentProcessToken(api)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('CloseHandle')
})
it('rejects a NULL token handle after a successful OpenProcessToken', () => {
const api = {
openProcess: vi.fn(() => 7n),
openProcessToken: vi.fn(() => 1), // succeeds without writing the out slot
closeHandle: vi.fn(() => 1),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
let caught: unknown
try {
openCurrentProcessToken(api)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('OpenProcessToken')
})
})
/**
* The stub the logon-SID scan needs: the size probe writes `needed`, the
* second call fills a TOKEN_GROUPS buffer (GroupCount@0, SID pointer@8,
* attributes@16) with the state's one group. The CopySid mock comes back
* beside the table for the one test that asserts on its arguments.
*/
function logonApi(state: {
needed: number
groupCount: number
sidPtr: bigint
logon: boolean
secondOk?: boolean
sidLength?: number
copyOk?: boolean
}): { api: Win32Bindings; copySid: ReturnType<typeof vi.fn> } {
const copySid = vi.fn(() => (state.copyOk === false ? 0 : 1))
const api = {
getTokenInformation: vi.fn((_token: unknown, cls: number, info: Buffer | null, _length: number, needed: NativePtr) => {
if (cls !== abi.TokenGroups) throw new Error(`unexpected token information class ${cls}`)
if (info === null) {
koffi.encode(needed, 'uint32', state.needed)
return 0 // the size probe is expected to "fail"
}
if (state.secondOk === false) return 0
info.writeUInt32LE(state.groupCount, 0)
if (state.groupCount > 0) {
info.writeBigUInt64LE(state.sidPtr, abi.TOKEN_GROUPS_OFFSET)
info.writeUInt32LE(state.logon ? abi.SE_GROUP_LOGON_ID : 0, abi.TOKEN_GROUPS_OFFSET + 8)
}
return 1
}),
getLengthSid: vi.fn(() => state.sidLength ?? 12),
copySid,
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
return { api, copySid }
}
describe('findLogonSid failure paths', () => {
const token = 9n as NativePtr
it('reports a size probe that wrote nothing', () => {
const { api } = logonApi({ needed: 0, groupCount: 0, sidPtr: 0n, logon: false })
let caught: unknown
try {
findLogonSid(api, token)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('GetTokenInformation')
})
it('rejects an implausibly small TokenGroups size', () => {
const { api } = logonApi({ needed: 4, groupCount: 0, sidPtr: 0n, logon: false })
let caught: unknown
try {
findLogonSid(api, token)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('GetTokenInformation')
})
it('reports a failed TokenGroups read', () => {
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: true, secondOk: false })
let caught: unknown
try {
findLogonSid(api, token)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('GetTokenInformation')
})
it('skips a NULL group SID pointer and throws when no logon SID remains', () => {
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 0n, logon: true })
expect(() => findLogonSid(api, token)).toThrow(/no logon SID found/u)
})
it('skips a non-logon group and throws when no logon SID remains', () => {
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: false })
expect(() => findLogonSid(api, token)).toThrow(/no logon SID found/u)
})
it('reports a zero logon-SID length', () => {
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: true, sidLength: 0 })
let caught: unknown
try {
findLogonSid(api, token)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('GetLengthSid')
})
it('reports a failed CopySid of the logon SID', () => {
const { api } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: true, copyOk: false })
let caught: unknown
try {
findLogonSid(api, token)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('CopySid')
})
it('copies the logon SID and returns the new allocation', () => {
const { api, copySid } = logonApi({ needed: 24, groupCount: 1, sidPtr: 77n, logon: true })
const copy = findLogonSid(api, token)
expect(isNullPtr(copy)).toBe(false)
expect(copySid).toHaveBeenCalledWith(12, copy, 77n)
})
})
describe('makeWellKnownSid failure paths', () => {
it('reports when CreateWellKnownSid fails', () => {
const api = {
createWellKnownSid: vi.fn(() => 0),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
let caught: unknown
try {
makeWellKnownSid(api, abi.WinWorldSid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('CreateWellKnownSid')
})
it('reports when the created well-known SID is invalid', () => {
const api = {
createWellKnownSid: vi.fn(() => 1),
isValidSid: vi.fn(() => 0),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
let caught: unknown
try {
makeWellKnownSid(api, abi.WinWorldSid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('IsValidSid')
})
})
/**
* The stub the default-DACL merge needs: the size probe writes `needed`, the
* second call fills the DACL pointer slot, and the merge/apply calls follow
* the state's results.
*/
function daclApi(state: {
needed: number
currentDacl: bigint
secondOk?: boolean
mergeResult?: number
newDacl: bigint
setTokenInfo?: number
}): Win32Bindings {
const api = {
getTokenInformation: vi.fn((_token: unknown, cls: number, info: Buffer | null, _length: number, needed: NativePtr) => {
if (cls !== abi.TokenDefaultDacl) throw new Error(`unexpected token information class ${cls}`)
if (info === null) {
koffi.encode(needed, 'uint32', state.needed)
return 0 // the size probe is expected to "fail"
}
if (state.secondOk === false) return 0
info.writeBigUInt64LE(state.currentDacl, 0)
return 1
}),
setEntriesInAclW: vi.fn((_count: unknown, _entries: unknown, _old: unknown, newAcl: NativePtr) => {
if (state.mergeResult !== undefined && state.mergeResult !== 0) return state.mergeResult
koffi.encode(newAcl, PVOID, state.newDacl)
return 0
}),
setTokenInformation: vi.fn(() => state.setTokenInfo ?? 1),
localFree: vi.fn(() => 0n),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
return api
}
describe('setTokenDefaultDaclGrant failure paths', () => {
const token = 9n as NativePtr
const sid = 77n as NativePtr
it('reports a size probe that wrote nothing', () => {
const api = daclApi({ needed: 0, currentDacl: 0n, newDacl: 0n })
let caught: unknown
try {
setTokenDefaultDaclGrant(api, token, sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('GetTokenInformation')
})
it('reports a failed default-DACL read', () => {
const api = daclApi({ needed: 8, currentDacl: 88n, secondOk: false, newDacl: 0n })
let caught: unknown
try {
setTokenDefaultDaclGrant(api, token, sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('GetTokenInformation')
})
it('rejects a token that carries no default DACL', () => {
const api = daclApi({ needed: 8, currentDacl: 0n, newDacl: 0n })
expect(() => { setTokenDefaultDaclGrant(api, token, sid) }).toThrow(/no default DACL/u)
})
it('reports a failed SetEntriesInAclW merge', () => {
const api = daclApi({ needed: 8, currentDacl: 88n, mergeResult: 5, newDacl: 0n })
let caught: unknown
try {
setTokenDefaultDaclGrant(api, token, sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('SetEntriesInAclW')
})
it('rejects a NULL merged default DACL', () => {
const api = daclApi({ needed: 8, currentDacl: 88n, newDacl: 0n })
let caught: unknown
try {
setTokenDefaultDaclGrant(api, token, sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('SetEntriesInAclW')
})
it('frees the merged DACL and reports when SetTokenInformation fails', () => {
const localFree = vi.fn(() => 0n)
const api = daclApi({ needed: 8, currentDacl: 88n, newDacl: 99n, setTokenInfo: 0 })
;(api.localFree as unknown as ReturnType<typeof vi.fn>).mockImplementation(localFree)
let caught: unknown
try {
setTokenDefaultDaclGrant(api, token, sid)
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('SetTokenInformation')
expect(localFree).toHaveBeenCalledWith(99n)
})
it('frees the merged DACL after a successful apply', () => {
const localFree = vi.fn(() => 0n)
const api = daclApi({ needed: 8, currentDacl: 88n, newDacl: 99n })
;(api.localFree as unknown as ReturnType<typeof vi.fn>).mockImplementation(localFree)
setTokenDefaultDaclGrant(api, token, sid)
expect(localFree).toHaveBeenCalledWith(99n)
})
})
describe('createRestrictedToken failure paths', () => {
it('builds the read-only restricting list without a write SID', () => {
const create = vi.fn((
_existing: unknown, _flags: unknown, _dc: unknown, _ds: unknown, _pc: unknown, _pd: unknown,
count: number, _sids: unknown, slot: NativePtr,
) => {
koffi.encode(slot, PVOID, 9n)
expect(count).toBe(2)
return 1
})
const api = { createRestrictedToken: create } as unknown as Win32Bindings
const logon = allocBytes(12)
expect(createRestrictedToken(api, 1n as NativePtr, logon, undefined, { world: 2n as NativePtr }, 'read-only')).toBe(9n)
})
it('builds the workspace-write restricting list with the write SID', () => {
const create = vi.fn((
_existing: unknown, _flags: unknown, _dc: unknown, _ds: unknown, _pc: unknown, _pd: unknown,
count: number, _sids: unknown, slot: NativePtr,
) => {
koffi.encode(slot, PVOID, 9n)
expect(count).toBe(3)
return 1
})
const api = { createRestrictedToken: create } as unknown as Win32Bindings
const logon = allocBytes(12)
expect(createRestrictedToken(api, 1n as NativePtr, logon, 3n as NativePtr, { world: 2n as NativePtr }, 'workspace-write')).toBe(9n)
})
it('reports when CreateRestrictedToken fails', () => {
const api = {
createRestrictedToken: vi.fn(() => 0),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
const logon = allocBytes(12)
let caught: unknown
try {
createRestrictedToken(api, 1n as NativePtr, logon, undefined, { world: 2n as NativePtr }, 'read-only')
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('CreateRestrictedToken')
})
it('rejects a NULL token handle after a successful CreateRestrictedToken', () => {
const api = {
createRestrictedToken: vi.fn(() => 1), // succeeds without writing the out slot
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
} as unknown as Win32Bindings
const logon = allocBytes(12)
let caught: unknown
try {
createRestrictedToken(api, 1n as NativePtr, logon, undefined, { world: 2n as NativePtr }, 'read-only')
} catch (error) {
caught = error
}
expect(caught).toBeInstanceOf(Win32Error)
expect((caught as Win32Error).api).toBe('CreateRestrictedToken')
})
})