|
|
@@ -1,512 +1,299 @@
|
|
|
-/**
|
|
|
- * Lazy koffi bindings for the Win32 ACL-sandbox backend. Koffi loads lazily so
|
|
|
- * non-Windows processes never open Win32 libraries. Every function signature
|
|
|
- * below was verified against the MinGW Windows headers on this machine
|
|
|
- * (winnt.h / accctrl.h / aclapi.h / securitybaseapi.h / sddl.h /
|
|
|
- * processthreadsapi.h / fileapi.h / namedpipeapi.h / synchapi.h / winbase.h);
|
|
|
- * struct layouts are asserted at load time against verify/abi-probe.cpp.
|
|
|
- * @module @deepseek-ai/dsh-sandbox-windows-acl/ffi
|
|
|
- */
|
|
|
+/** ACL/token bindings layered on the shared Win32 process owner. */
|
|
|
|
|
|
import koffi from 'koffi'
|
|
|
-import { Win32Error } from './errors.ts'
|
|
|
+import {
|
|
|
+ ERROR_INSUFFICIENT_BUFFER,
|
|
|
+ Win32Error,
|
|
|
+ extendWin32ProcessBindings,
|
|
|
+ isNullPtr,
|
|
|
+ throwLastError,
|
|
|
+} from '@deepseek-ai/dsh-win32-process'
|
|
|
+import type { NativePtr, Win32ProcessBindings } from '@deepseek-ai/dsh-win32-process'
|
|
|
import * as abi from './win32-abi.ts'
|
|
|
|
|
|
-/** Branded koffi 3 native pointer. Koffi 3 pointers are BigInt values; the brand keeps them out of numeric contexts. */
|
|
|
-declare const nativePtr: unique symbol
|
|
|
-/** Koffi 3 native pointer (a BigInt address), branded so it cannot silently enter numeric contexts. */
|
|
|
-export type NativePtr = bigint & { readonly [nativePtr]: true }
|
|
|
-
|
|
|
-/**
|
|
|
- * True for NULL pointers, however koffi returns them (null or 0n).
|
|
|
- * @param value - a pointer as koffi may hand it back (pointer, null, or 0n).
|
|
|
- * @returns a type guard narrowing to the NULL shapes.
|
|
|
- */
|
|
|
-export function isNullPtr(value: NativePtr | null | undefined): value is null | undefined {
|
|
|
- return value === null || value === undefined || (value as bigint) === 0n
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * True for CreateFileW's INVALID_HANDLE_VALUE failure marker (-1, which
|
|
|
- * koffi hands back as the unsigned 64-bit all-ones pointer).
|
|
|
- * @param handle - the handle CreateFileW returned.
|
|
|
- * @returns whether the handle signals failure.
|
|
|
- */
|
|
|
-export function isInvalidHandle(handle: NativePtr | null | undefined): boolean {
|
|
|
- if (isNullPtr(handle)) return true
|
|
|
- return (handle as bigint) === 0xFFFFFFFFFFFFFFFFn || (handle as bigint) === -1n
|
|
|
-}
|
|
|
+export {
|
|
|
+ allocPtrSlot,
|
|
|
+ allocUint32,
|
|
|
+ decodePtr,
|
|
|
+ decodeUint32,
|
|
|
+ isNullPtr,
|
|
|
+ throwLastError,
|
|
|
+ throwWin32,
|
|
|
+} from '@deepseek-ai/dsh-win32-process'
|
|
|
+export type { NativePtr } from '@deepseek-ai/dsh-win32-process'
|
|
|
|
|
|
type Ptr = ReturnType<typeof koffi.pointer>
|
|
|
+const PVOID: Ptr = koffi.pointer('void')
|
|
|
+const PPVOID: Ptr = koffi.pointer(PVOID)
|
|
|
|
|
|
-/** Field subset written into a zeroed STARTUPINFOW (layout verified: size 104). */
|
|
|
-export interface StartupInfoInput {
|
|
|
- cb: number
|
|
|
- dwFlags: number
|
|
|
- hStdInput: NativePtr
|
|
|
- hStdOutput: NativePtr
|
|
|
- hStdError: NativePtr
|
|
|
-}
|
|
|
-
|
|
|
-/** Decoded PROCESS_INFORMATION (layout verified: size 24). */
|
|
|
-export interface ProcessInfoOutput {
|
|
|
- hProcess: NativePtr | null
|
|
|
- hThread: NativePtr | null
|
|
|
- dwProcessId: number
|
|
|
- dwThreadId: number
|
|
|
-}
|
|
|
-
|
|
|
-/** The lazy koffi binding table: every Win32 call the ACL backend uses, signature-verified against the real headers. */
|
|
|
-export interface Win32Bindings {
|
|
|
- // ---- process / token handles --------------------------------------------
|
|
|
+/** ACL/token calls composed with the generic Win32 process binding table. */
|
|
|
+export interface Win32Bindings extends Win32ProcessBindings {
|
|
|
openProcess(desiredAccess: number, inheritHandle: number, pid: number): NativePtr
|
|
|
openProcessToken(process: NativePtr, desiredAccess: number, tokenHandle: NativePtr): number
|
|
|
- closeHandle(handle: NativePtr): number
|
|
|
- // ---- errors / diagnostics ------------------------------------------------
|
|
|
- getLastError(): number
|
|
|
- formatMessageW(flags: number, source: null, messageId: number, languageId: number, buffer: Buffer, size: number, args: null): number
|
|
|
- // ---- memory --------------------------------------------------------------
|
|
|
localAlloc(flags: number, bytes: number): NativePtr
|
|
|
localFree(memory: NativePtr): NativePtr
|
|
|
- // ---- SIDs ----------------------------------------------------------------
|
|
|
convertStringSidToSidW(stringSid: string, sid: NativePtr): number
|
|
|
createWellKnownSid(type: number, domainSid: null, sid: NativePtr, size: NativePtr): number
|
|
|
isValidSid(sid: NativePtr): number
|
|
|
getLengthSid(sid: NativePtr): number
|
|
|
copySid(length: number, destination: NativePtr, source: NativePtr): number
|
|
|
- // ---- token information ---------------------------------------------------
|
|
|
getTokenInformation(token: NativePtr, cls: number, info: Buffer | null, length: number, needed: NativePtr): number
|
|
|
setTokenInformation(token: NativePtr, cls: number, info: Buffer, length: number): number
|
|
|
- // ---- restricted token ----------------------------------------------------
|
|
|
createRestrictedToken(
|
|
|
- existing: NativePtr, flags: number,
|
|
|
- disableCount: number, disableSids: null,
|
|
|
- deletePrivilegeCount: number, privilegesToDelete: null,
|
|
|
- restrictCount: number, restrictingSids: Buffer,
|
|
|
+ existing: NativePtr,
|
|
|
+ flags: number,
|
|
|
+ disableCount: number,
|
|
|
+ disableSids: null,
|
|
|
+ deletePrivilegeCount: number,
|
|
|
+ privilegesToDelete: null,
|
|
|
+ restrictCount: number,
|
|
|
+ restrictingSids: Buffer,
|
|
|
newToken: NativePtr,
|
|
|
): number
|
|
|
- // ---- ACL editing ---------------------------------------------------------
|
|
|
setEntriesInAclW(count: number, entries: Buffer, oldAcl: NativePtr | null, newAcl: NativePtr): number
|
|
|
setNamedSecurityInfoW(
|
|
|
- path: string, objectType: number, information: number,
|
|
|
- owner: null, group: null, dacl: NativePtr | null, sacl: null,
|
|
|
+ path: string,
|
|
|
+ objectType: number,
|
|
|
+ information: number,
|
|
|
+ owner: null,
|
|
|
+ group: null,
|
|
|
+ dacl: NativePtr | null,
|
|
|
+ sacl: null,
|
|
|
): number
|
|
|
getNamedSecurityInfoW(
|
|
|
- path: string, objectType: number, information: number,
|
|
|
- owner: NativePtr, group: NativePtr, dacl: NativePtr, sacl: NativePtr, descriptor: NativePtr,
|
|
|
+ path: string,
|
|
|
+ objectType: number,
|
|
|
+ information: number,
|
|
|
+ owner: NativePtr,
|
|
|
+ group: NativePtr,
|
|
|
+ dacl: NativePtr,
|
|
|
+ sacl: NativePtr,
|
|
|
+ descriptor: NativePtr,
|
|
|
): number
|
|
|
- // ---- environment / io ----------------------------------------------------
|
|
|
getTempPathW(length: number, buffer: Buffer): number
|
|
|
+ setEnvironmentVariableW(name: string, value: string): number
|
|
|
+ setConsoleCtrlHandler(handler: null, add: number): number
|
|
|
createFileW(
|
|
|
- fileName: string, desiredAccess: number, shareMode: number, attributes: null,
|
|
|
- creationDisposition: number, flagsAndAttributes: number, templateFile: null,
|
|
|
+ fileName: string,
|
|
|
+ desiredAccess: number,
|
|
|
+ shareMode: number,
|
|
|
+ attributes: null,
|
|
|
+ creationDisposition: number,
|
|
|
+ flagsAndAttributes: number,
|
|
|
+ templateFile: null,
|
|
|
): NativePtr
|
|
|
- lockFileEx(file: NativePtr, flags: number, reserved: number, bytesLow: number, bytesHigh: number, overlapped: NativePtr): number
|
|
|
- unlockFileEx(file: NativePtr, reserved: number, bytesLow: number, bytesHigh: number, overlapped: NativePtr): number
|
|
|
- createPipe(readHandle: NativePtr, writeHandle: NativePtr, attributes: null, size: number): number
|
|
|
- setHandleInformation(handle: NativePtr, mask: number, flags: number): number
|
|
|
- createProcessAsUserW(
|
|
|
- token: NativePtr, applicationName: null, commandLine: string,
|
|
|
- processAttributes: null, threadAttributes: null,
|
|
|
- inheritHandles: number, creationFlags: number, environment: null,
|
|
|
- currentDirectory: string | null, startupInfo: NativePtr, processInfo: NativePtr,
|
|
|
+ lockFileEx(
|
|
|
+ file: NativePtr,
|
|
|
+ flags: number,
|
|
|
+ reserved: number,
|
|
|
+ bytesLow: number,
|
|
|
+ bytesHigh: number,
|
|
|
+ overlapped: NativePtr,
|
|
|
): number
|
|
|
- setEnvironmentVariableW(name: string, value: string): number
|
|
|
- readFile(file: NativePtr, buffer: Buffer, count: number, bytesRead: NativePtr, overlapped: null): number
|
|
|
- peekNamedPipe(
|
|
|
- pipe: NativePtr, buffer: null, size: number,
|
|
|
- bytesRead: NativePtr, totalAvail: NativePtr, leftThisMessage: NativePtr,
|
|
|
+ unlockFileEx(
|
|
|
+ file: NativePtr,
|
|
|
+ reserved: number,
|
|
|
+ bytesLow: number,
|
|
|
+ bytesHigh: number,
|
|
|
+ overlapped: NativePtr,
|
|
|
): number
|
|
|
- waitForSingleObject(handle: NativePtr, milliseconds: number): number
|
|
|
- getExitCodeProcess(process: NativePtr, exitCode: NativePtr): number
|
|
|
- resumeThread(thread: NativePtr): number
|
|
|
- // ---- job object (runner kill-on-close) -----------------------------------
|
|
|
- createJobObjectW(attributes: null, name: null): NativePtr
|
|
|
- setInformationJobObject(job: NativePtr, cls: number, information: Buffer, length: number): number
|
|
|
- assignProcessToJobObject(job: NativePtr, process: NativePtr): number
|
|
|
- // Terminate a suspended child that could not be placed in the kill-on-close
|
|
|
- // job — closing handles alone would leave it hanging forever.
|
|
|
- terminateProcess(process: NativePtr, exitCode: number): number
|
|
|
- // ---- console -------------------------------------------------------------
|
|
|
- // HandlerRoutine=null + add=1 makes this process ignore CTRL+C (wincon.h):
|
|
|
- // the runner survives console Ctrl+C so the child handles its own and the
|
|
|
- // runner can clean up grants after the child exits.
|
|
|
- setConsoleCtrlHandler(handler: null, add: number): number
|
|
|
- getStdHandle(stdHandle: number): NativePtr
|
|
|
}
|
|
|
|
|
|
-const PVOID: Ptr = koffi.pointer('void')
|
|
|
-const PPVOID: Ptr = koffi.pointer(PVOID)
|
|
|
-
|
|
|
-/** koffi STARTUPINFOW layout; its size is asserted against abi.STARTUPINFOW_SIZE at load. */
|
|
|
-export const STARTUPINFOW = koffi.struct('STARTUPINFOW', {
|
|
|
- cb: 'uint32',
|
|
|
- lpReserved: 'str16',
|
|
|
- lpDesktop: 'str16',
|
|
|
- lpTitle: 'str16',
|
|
|
- dwX: 'uint32',
|
|
|
- dwY: 'uint32',
|
|
|
- dwXSize: 'uint32',
|
|
|
- dwYSize: 'uint32',
|
|
|
- dwXCountChars: 'uint32',
|
|
|
- dwYCountChars: 'uint32',
|
|
|
- dwFillAttribute: 'uint32',
|
|
|
- dwFlags: 'uint32',
|
|
|
- wShowWindow: 'uint16',
|
|
|
- cbReserved2: 'uint16',
|
|
|
- lpReserved2: koffi.pointer('uint8'),
|
|
|
- hStdInput: PVOID,
|
|
|
- hStdOutput: PVOID,
|
|
|
- hStdError: PVOID,
|
|
|
-})
|
|
|
-
|
|
|
-/** koffi PROCESS_INFORMATION layout; its size is asserted against abi.PROCESS_INFORMATION_SIZE at load. */
|
|
|
-export const PROCESS_INFORMATION = koffi.struct('PROCESS_INFORMATION', {
|
|
|
- hProcess: PVOID,
|
|
|
- hThread: PVOID,
|
|
|
- dwProcessId: 'uint32',
|
|
|
- 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).
|
|
|
- * @returns the allocated slot pointer.
|
|
|
+ * Return whether CreateFileW produced INVALID_HANDLE_VALUE.
|
|
|
+ * @param handle - handle returned by CreateFileW.
|
|
|
+ * @returns true for null, zero, or the all-bits-one sentinel.
|
|
|
*/
|
|
|
-export function allocPtrSlot(): NativePtr {
|
|
|
- const value: unknown = koffi.alloc(PVOID, 1)
|
|
|
- return value as NativePtr
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Allocate one uint32 slot.
|
|
|
- * @returns the allocated slot pointer.
|
|
|
- */
|
|
|
-export function allocUint32(): NativePtr {
|
|
|
- const value: unknown = koffi.alloc('uint32', 1)
|
|
|
- return value as NativePtr
|
|
|
+export function isInvalidHandle(handle: NativePtr | null | undefined): boolean {
|
|
|
+ if (isNullPtr(handle)) return true
|
|
|
+ return (handle as bigint) === 0xFFFFFFFFFFFFFFFFn || (handle as bigint) === -1n
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Write a uint32 value into a slot pointer.
|
|
|
- * @param slot - the slot allocated by {@link allocUint32}.
|
|
|
- * @param value - the uint32 to encode.
|
|
|
+ * Encode a uint32 into an allocated slot.
|
|
|
+ * @param slot - slot allocated by allocUint32.
|
|
|
+ * @param value - unsigned value to store.
|
|
|
*/
|
|
|
export function encodeUint32(slot: NativePtr, value: number): void {
|
|
|
koffi.encode(slot, 'uint32', value)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Decode the pointer stored in a pointer-sized slot (NULL becomes null).
|
|
|
- * @param slot - the pointer-sized slot holding the out-parameter value.
|
|
|
- * @returns the decoded pointer, or null for NULL.
|
|
|
- */
|
|
|
-export function decodePtr(slot: NativePtr): NativePtr | null {
|
|
|
- const value: unknown = koffi.decode(slot, PVOID)
|
|
|
- if (isNullPtr(value as NativePtr | null | undefined)) return null
|
|
|
- return value as NativePtr
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Decode a uint32 at a slot pointer.
|
|
|
- * @param slot - the uint32 slot holding the out-parameter value.
|
|
|
- * @returns the decoded uint32.
|
|
|
- */
|
|
|
-export function decodeUint32(slot: NativePtr): number {
|
|
|
- const value: unknown = koffi.decode(slot, 'uint32')
|
|
|
- return value as number
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Cast a koffi pointer to its numeric address (bigint, used for raw struct packing).
|
|
|
- * @param ptr - the koffi pointer.
|
|
|
- * @returns the pointer's numeric address.
|
|
|
+ * Return a Koffi pointer's numeric address for struct packing.
|
|
|
+ * @param ptr - native pointer.
|
|
|
+ * @returns pointer address.
|
|
|
*/
|
|
|
export function ptrAddress(ptr: NativePtr): bigint {
|
|
|
return koffi.address(ptr)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Allocate a raw byte block (used for SID copies and variable-length arrays).
|
|
|
- * @param length - the block size in bytes.
|
|
|
- * @returns the allocated block pointer.
|
|
|
+ * Allocate a raw byte block.
|
|
|
+ * @param length - byte count.
|
|
|
+ * @returns allocated pointer.
|
|
|
*/
|
|
|
export function allocBytes(length: number): NativePtr {
|
|
|
- const value: unknown = koffi.alloc('uint8', length)
|
|
|
- return value as NativePtr
|
|
|
+ return koffi.alloc('uint8', length) as NativePtr
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Allocate one zeroed OVERLAPPED (32 bytes on x64: Internal@0, InternalHigh@8,
|
|
|
- * Offset@16, OffsetHigh@20, hEvent@24). LockFileEx/UnlockFileEx receive this
|
|
|
- * instead of a NULL lpOverlapped: koffi 3.1.1 crashes on NULL there, and a
|
|
|
- * zeroed OVERLAPPED on a synchronous file handle is the documented equivalent
|
|
|
- * (the byte range locks from offset 0, hEvent stays NULL).
|
|
|
- * @returns the zeroed block pointer.
|
|
|
+ * Allocate one zeroed x64 OVERLAPPED record.
|
|
|
+ * @returns allocated pointer.
|
|
|
+ * @remarks Koffi 3.1.1 crashes when LockFileEx or UnlockFileEx receives NULL;
|
|
|
+ * a zeroed OVERLAPPED is equivalent for the synchronous lock-file handle.
|
|
|
*/
|
|
|
export function allocOverlapped(): NativePtr {
|
|
|
return allocBytes(32)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Decode a pointer VALUE stored in memory at `buffer[offset]` (e.g. TOKEN_GROUPS entries).
|
|
|
- * @param buffer - the buffer holding the pointer value.
|
|
|
- * @param offset - byte offset of the pointer inside the buffer.
|
|
|
- * @returns the decoded pointer, or null for NULL.
|
|
|
+ * Decode a pointer value from a Buffer field.
|
|
|
+ * @param buffer - encoded native record.
|
|
|
+ * @param offset - pointer field byte offset.
|
|
|
+ * @returns decoded pointer, or null for address zero.
|
|
|
*/
|
|
|
export function decodePtrAt(buffer: Buffer, offset: number): NativePtr | null {
|
|
|
- const value: unknown = koffi.decode(buffer, offset, PVOID)
|
|
|
- if (isNullPtr(value as NativePtr | null | undefined)) return null
|
|
|
- return value as NativePtr
|
|
|
+ const value = koffi.decode(buffer, offset, PVOID) as NativePtr | null
|
|
|
+ return isNullPtr(value) ? null : value
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Decode a uint8 at a native pointer plus byte offset — the ACL walk's
|
|
|
- * field-read primitive (koffi.decode with an offset, no memcpy, no pointer
|
|
|
- * arithmetic).
|
|
|
- * @param ptr - the native pointer to read from.
|
|
|
- * @param offset - byte offset from the pointer.
|
|
|
- * @returns the decoded uint8.
|
|
|
+ * Decode a uint8 field at a native pointer offset.
|
|
|
+ * @param ptr - native record pointer.
|
|
|
+ * @param offset - field byte offset.
|
|
|
+ * @returns decoded value.
|
|
|
*/
|
|
|
export function decodeUint8At(ptr: NativePtr, offset: number): number {
|
|
|
- const value: unknown = koffi.decode(ptr, offset, 'uint8')
|
|
|
- return value as number
|
|
|
+ return koffi.decode(ptr, offset, 'uint8') as number
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Decode a uint16 at a native pointer plus byte offset (see {@link decodeUint8At}).
|
|
|
- * @param ptr - the native pointer to read from.
|
|
|
- * @param offset - byte offset from the pointer.
|
|
|
- * @returns the decoded uint16.
|
|
|
+ * Decode a uint16 field at a native pointer offset.
|
|
|
+ * @param ptr - native record pointer.
|
|
|
+ * @param offset - field byte offset.
|
|
|
+ * @returns decoded value.
|
|
|
*/
|
|
|
export function decodeUint16At(ptr: NativePtr, offset: number): number {
|
|
|
- const value: unknown = koffi.decode(ptr, offset, 'uint16')
|
|
|
- return value as number
|
|
|
+ return koffi.decode(ptr, offset, 'uint16') as number
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Decode a uint32 at a native pointer plus byte offset (see {@link decodeUint8At}).
|
|
|
- * @param ptr - the native pointer to read from.
|
|
|
- * @param offset - byte offset from the pointer.
|
|
|
- * @returns the decoded uint32.
|
|
|
+ * Decode a uint32 field at a native pointer offset.
|
|
|
+ * @param ptr - native record pointer.
|
|
|
+ * @param offset - field byte offset.
|
|
|
+ * @returns decoded value.
|
|
|
*/
|
|
|
export function decodeUint32At(ptr: NativePtr, offset: number): number {
|
|
|
- const value: unknown = koffi.decode(ptr, offset, 'uint32')
|
|
|
- return value as number
|
|
|
+ return koffi.decode(ptr, offset, 'uint32') as number
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Compare two SIDs field-by-field via BOUNDED offset reads (revision, count,
|
|
|
- * identifier authority, subauthorities up to the count) — never a fixed-size
|
|
|
- * struct decode, which would read past a short SID allocation (a SID with
|
|
|
- * fewer than 8 subauthorities is smaller than `SID_STRUCT`). An implausible
|
|
|
- * subauthority count reads as unequal.
|
|
|
- * @param left - pointer to one SID (offset 0).
|
|
|
- * @param leftOffset - byte offset of the SID structure within `left`.
|
|
|
- * @param right - pointer to the other SID.
|
|
|
- * @param rightOffset - byte offset of the SID structure within `right`.
|
|
|
- * @returns whether the SIDs are identical.
|
|
|
+ * Compare two in-memory SID records without allocating strings.
|
|
|
+ * @param left - first native buffer.
|
|
|
+ * @param leftOffset - first SID byte offset.
|
|
|
+ * @param right - second native buffer.
|
|
|
+ * @param rightOffset - second SID byte offset.
|
|
|
+ * @returns true when revision, authority, and every sub-authority match.
|
|
|
*/
|
|
|
-export function sameSidAt(left: NativePtr, leftOffset: number, right: NativePtr, rightOffset: number): boolean {
|
|
|
- const leftRevision = decodeUint8At(left, leftOffset)
|
|
|
- const rightRevision = decodeUint8At(right, rightOffset)
|
|
|
- if (leftRevision !== rightRevision) return false
|
|
|
+export function sameSidAt(
|
|
|
+ left: NativePtr,
|
|
|
+ leftOffset: number,
|
|
|
+ right: NativePtr,
|
|
|
+ rightOffset: number,
|
|
|
+): boolean {
|
|
|
+ if (decodeUint8At(left, leftOffset) !== decodeUint8At(right, rightOffset)) return false
|
|
|
const leftCount = decodeUint8At(left, leftOffset + 1)
|
|
|
const rightCount = decodeUint8At(right, rightOffset + 1)
|
|
|
if (leftCount !== rightCount || leftCount > abi.SID_MAX_SUB_AUTHORITIES) return false
|
|
|
- for (let index = 0; index < 6; index++) {
|
|
|
- if (decodeUint8At(left, leftOffset + 2 + index) !== decodeUint8At(right, rightOffset + 2 + index)) return false
|
|
|
+ for (let index = 0; index < 6; index += 1) {
|
|
|
+ if (decodeUint8At(left, leftOffset + 2 + index) !== decodeUint8At(right, rightOffset + 2 + index)) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
}
|
|
|
- for (let index = 0; index < leftCount; index++) {
|
|
|
- if (decodeUint32At(left, leftOffset + 8 + index * 4) !== decodeUint32At(right, rightOffset + 8 + index * 4)) return false
|
|
|
+ for (let index = 0; index < leftCount; index += 1) {
|
|
|
+ if (decodeUint32At(left, leftOffset + 8 + index * 4) !==
|
|
|
+ decodeUint32At(right, rightOffset + 8 + index * 4)) return false
|
|
|
}
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Allocate a zeroed STARTUPINFOW.
|
|
|
- * @returns the allocated struct pointer.
|
|
|
- */
|
|
|
-export function allocStartupInfo(): NativePtr {
|
|
|
- const value: unknown = koffi.alloc(STARTUPINFOW, 1)
|
|
|
- return value as NativePtr
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Write the stdio-relevant fields into a zeroed STARTUPINFOW (others stay default-initialized).
|
|
|
- * @param startupInfo - the allocated STARTUPINFOW to encode into.
|
|
|
- * @param fields - the field subset to write.
|
|
|
- */
|
|
|
-export function encodeStartupInfo(startupInfo: NativePtr, fields: StartupInfoInput): void {
|
|
|
- koffi.encode(startupInfo, STARTUPINFOW, fields)
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Allocate a zeroed PROCESS_INFORMATION.
|
|
|
- * @returns the allocated struct pointer.
|
|
|
- */
|
|
|
-export function allocProcessInfo(): NativePtr {
|
|
|
- const value: unknown = koffi.alloc(PROCESS_INFORMATION, 1)
|
|
|
- return value as NativePtr
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Decode a PROCESS_INFORMATION after CreateProcessAsUserW.
|
|
|
- * @param processInfo - the PROCESS_INFORMATION filled by the spawn call.
|
|
|
- * @returns the decoded handle/id fields.
|
|
|
- */
|
|
|
-export function decodeProcessInfo(processInfo: NativePtr): ProcessInfoOutput {
|
|
|
- const value: unknown = koffi.decode(processInfo, PROCESS_INFORMATION)
|
|
|
- return value as ProcessInfoOutput
|
|
|
-}
|
|
|
-
|
|
|
let cached: Win32Bindings | undefined
|
|
|
|
|
|
function bindings(): Win32Bindings {
|
|
|
if (cached !== undefined) return cached
|
|
|
- const kernel32 = koffi.load('kernel32.dll')
|
|
|
- const advapi32 = koffi.load('advapi32.dll')
|
|
|
-
|
|
|
- // Each binding shape is verified by verify/abi-probe.cpp against the real
|
|
|
- // Windows headers and exercised end-to-end by tests/probe.spec.ts; the
|
|
|
- // single cast keeps the per-binding noise out of this table.
|
|
|
- const bind = (lib: ReturnType<typeof koffi.load>, name: string, result: Ptr | string, args: Array<Ptr | string>): unknown =>
|
|
|
- lib.func('__stdcall', name, result, args)
|
|
|
-
|
|
|
- cached = {
|
|
|
+ cached = extendWin32ProcessBindings(({ kernel32, advapi32, bind }) => ({
|
|
|
openProcess: bind(kernel32, 'OpenProcess', PVOID, ['uint32', 'int', 'uint32']),
|
|
|
openProcessToken: bind(advapi32, 'OpenProcessToken', 'int', [PVOID, 'uint32', PPVOID]),
|
|
|
- closeHandle: bind(kernel32, 'CloseHandle', 'int', [PVOID]),
|
|
|
- getLastError: bind(kernel32, 'GetLastError', 'uint32', []),
|
|
|
- formatMessageW: bind(kernel32, 'FormatMessageW', 'uint32', ['uint32', PVOID, 'uint32', 'uint32', PVOID, 'uint32', PVOID]),
|
|
|
localAlloc: bind(kernel32, 'LocalAlloc', PVOID, ['uint32', 'size_t']),
|
|
|
localFree: bind(kernel32, 'LocalFree', PVOID, [PVOID]),
|
|
|
convertStringSidToSidW: bind(advapi32, 'ConvertStringSidToSidW', 'int', ['str16', PPVOID]),
|
|
|
- createWellKnownSid: bind(advapi32, 'CreateWellKnownSid', 'int', ['int', PVOID, PVOID, koffi.pointer('uint32')]),
|
|
|
+ createWellKnownSid: bind(advapi32, 'CreateWellKnownSid', 'int', [
|
|
|
+ 'int', PVOID, PVOID, koffi.pointer('uint32'),
|
|
|
+ ]),
|
|
|
isValidSid: bind(advapi32, 'IsValidSid', 'int', [PVOID]),
|
|
|
getLengthSid: bind(advapi32, 'GetLengthSid', 'uint32', [PVOID]),
|
|
|
copySid: bind(advapi32, 'CopySid', 'int', ['uint32', PVOID, PVOID]),
|
|
|
- getTokenInformation: bind(advapi32, 'GetTokenInformation', 'int', [PVOID, 'int', PVOID, 'uint32', koffi.pointer('uint32')]),
|
|
|
+ getTokenInformation: bind(advapi32, 'GetTokenInformation', 'int', [
|
|
|
+ PVOID, 'int', PVOID, 'uint32', koffi.pointer('uint32'),
|
|
|
+ ]),
|
|
|
setTokenInformation: bind(advapi32, 'SetTokenInformation', 'int', [PVOID, 'int', PVOID, 'uint32']),
|
|
|
- createRestrictedToken: bind(advapi32, 'CreateRestrictedToken', 'int', [PVOID, 'uint32', 'uint32', PVOID, 'uint32', PVOID, 'uint32', PVOID, PPVOID]),
|
|
|
+ createRestrictedToken: bind(advapi32, 'CreateRestrictedToken', 'int', [
|
|
|
+ PVOID, 'uint32', 'uint32', PVOID, 'uint32', PVOID, 'uint32', PVOID, PPVOID,
|
|
|
+ ]),
|
|
|
setEntriesInAclW: bind(advapi32, 'SetEntriesInAclW', 'uint32', ['uint32', PVOID, PVOID, PPVOID]),
|
|
|
- setNamedSecurityInfoW: bind(advapi32, 'SetNamedSecurityInfoW', 'uint32', ['str16', 'int', 'uint32', PVOID, PVOID, PVOID, PVOID]),
|
|
|
- getNamedSecurityInfoW: bind(advapi32, 'GetNamedSecurityInfoW', 'uint32', ['str16', 'int', 'uint32', PPVOID, PPVOID, PPVOID, PPVOID, PPVOID]),
|
|
|
- getTempPathW: bind(kernel32, 'GetTempPathW', 'uint32', ['uint32', PVOID]),
|
|
|
- // fileapi.h line ~64: HANDLE CreateFileW(LPCWSTR, DWORD, DWORD,
|
|
|
- // LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE).
|
|
|
- createFileW: bind(kernel32, 'CreateFileW', PVOID, ['str16', 'uint32', 'uint32', PVOID, 'uint32', 'uint32', PVOID]),
|
|
|
- // fileapi.h lines ~177/~185: BOOL LockFileEx(HANDLE, DWORD, DWORD, DWORD,
|
|
|
- // DWORD, LPOVERLAPPED); BOOL UnlockFileEx(HANDLE, DWORD, DWORD, DWORD,
|
|
|
- // LPOVERLAPPED). lpOverlapped is NULL for synchronous locking.
|
|
|
- lockFileEx: bind(kernel32, 'LockFileEx', 'int', [PVOID, 'uint32', 'uint32', 'uint32', 'uint32', PVOID]),
|
|
|
- unlockFileEx: bind(kernel32, 'UnlockFileEx', 'int', [PVOID, 'uint32', 'uint32', 'uint32', PVOID]),
|
|
|
- createPipe: bind(kernel32, 'CreatePipe', 'int', [PPVOID, PPVOID, PVOID, 'uint32']),
|
|
|
- setHandleInformation: bind(kernel32, 'SetHandleInformation', 'int', [PVOID, 'uint32', 'uint32']),
|
|
|
- createProcessAsUserW: bind(advapi32, 'CreateProcessAsUserW', 'int', [
|
|
|
- PVOID, 'str16', 'str16', PVOID, PVOID, 'int', 'uint32', PVOID, 'str16',
|
|
|
- koffi.pointer(STARTUPINFOW), koffi.pointer(PROCESS_INFORMATION),
|
|
|
+ setNamedSecurityInfoW: bind(advapi32, 'SetNamedSecurityInfoW', 'uint32', [
|
|
|
+ 'str16', 'int', 'uint32', PVOID, PVOID, PVOID, PVOID,
|
|
|
+ ]),
|
|
|
+ getNamedSecurityInfoW: bind(advapi32, 'GetNamedSecurityInfoW', 'uint32', [
|
|
|
+ 'str16', 'int', 'uint32', PPVOID, PPVOID, PPVOID, PPVOID, PPVOID,
|
|
|
]),
|
|
|
+ getTempPathW: bind(kernel32, 'GetTempPathW', 'uint32', ['uint32', PVOID]),
|
|
|
setEnvironmentVariableW: bind(kernel32, 'SetEnvironmentVariableW', 'int', ['str16', 'str16']),
|
|
|
- readFile: bind(kernel32, 'ReadFile', 'int', [PVOID, PVOID, 'uint32', koffi.pointer('uint32'), PVOID]),
|
|
|
- peekNamedPipe: bind(kernel32, 'PeekNamedPipe', 'int', [PVOID, PVOID, 'uint32', koffi.pointer('uint32'), koffi.pointer('uint32'), koffi.pointer('uint32')]),
|
|
|
- waitForSingleObject: bind(kernel32, 'WaitForSingleObject', 'uint32', [PVOID, 'uint32']),
|
|
|
- getExitCodeProcess: bind(kernel32, 'GetExitCodeProcess', 'int', [PVOID, koffi.pointer('uint32')]),
|
|
|
- resumeThread: bind(kernel32, 'ResumeThread', 'uint32', [PVOID]),
|
|
|
- createJobObjectW: bind(kernel32, 'CreateJobObjectW', PVOID, [PVOID, 'str16']),
|
|
|
- setInformationJobObject: bind(kernel32, 'SetInformationJobObject', 'int', [PVOID, 'int', PVOID, 'uint32']),
|
|
|
- assignProcessToJobObject: bind(kernel32, 'AssignProcessToJobObject', 'int', [PVOID, PVOID]),
|
|
|
- terminateProcess: bind(kernel32, 'TerminateProcess', 'int', [PVOID, 'uint32']),
|
|
|
setConsoleCtrlHandler: bind(kernel32, 'SetConsoleCtrlHandler', 'int', [PVOID, 'int']),
|
|
|
- getStdHandle: bind(kernel32, 'GetStdHandle', PVOID, ['int']),
|
|
|
- } as unknown as Win32Bindings
|
|
|
+ createFileW: bind(kernel32, 'CreateFileW', PVOID, [
|
|
|
+ 'str16', 'uint32', 'uint32', PVOID, 'uint32', 'uint32', PVOID,
|
|
|
+ ]),
|
|
|
+ lockFileEx: bind(kernel32, 'LockFileEx', 'int', [
|
|
|
+ PVOID, 'uint32', 'uint32', 'uint32', 'uint32', PVOID,
|
|
|
+ ]),
|
|
|
+ unlockFileEx: bind(kernel32, 'UnlockFileEx', 'int', [
|
|
|
+ PVOID, 'uint32', 'uint32', 'uint32', PVOID,
|
|
|
+ ]),
|
|
|
+ })) as unknown as Win32Bindings
|
|
|
return cached
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Resolve the lazy Win32 bindings (throws the first binding failure, fail-closed).
|
|
|
- * @returns the cached binding table.
|
|
|
+ * Resolve the cached ACL/token binding table asynchronously.
|
|
|
+ * @returns generic process plus ACL/token bindings.
|
|
|
*/
|
|
|
export function win32(): Promise<Win32Bindings> {
|
|
|
return Promise.resolve(bindings())
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Resolve the lazy Win32 bindings SYNCHRONOUSLY — the sandbox seam's
|
|
|
- * server-side per-session grant materializes ACEs inside the synchronous
|
|
|
- * `confine()` call, which cannot await. Same cached table as {@link win32}
|
|
|
- * (the underlying koffi loads are synchronous; the async wrapper exists for
|
|
|
- * the runner's await-shaped call sites).
|
|
|
- * @returns the cached binding table.
|
|
|
+ * Resolve the cached ACL/token binding table synchronously.
|
|
|
+ * @returns generic process plus ACL/token bindings.
|
|
|
*/
|
|
|
export function win32Sync(): Win32Bindings {
|
|
|
return bindings()
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Turn a Win32 error code into readable text via FormatMessageW.
|
|
|
- * @param api - the binding table.
|
|
|
- * @param win32Code - the error code to format.
|
|
|
- * @returns the formatted message text, or '' when formatting fails.
|
|
|
- */
|
|
|
-export function errorText(api: Win32Bindings, win32Code: number): string {
|
|
|
- const buffer = Buffer.alloc(1024)
|
|
|
- const length = api.formatMessageW(
|
|
|
- abi.FORMAT_MESSAGE_FROM_SYSTEM | abi.FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
|
- null, win32Code, 0, buffer, buffer.length / 2, null,
|
|
|
- )
|
|
|
- if (length === 0) return ''
|
|
|
- return buffer.subarray(0, length * 2).toString('utf16le').trim()
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Read the process temp directory via GetTempPathW (fileapi.h line ~188).
|
|
|
- * Defensive against an overlong system temp path: GetTempPathW reports the
|
|
|
- * REQUIRED length (including NUL) without writing the buffer when it is too
|
|
|
- * small, so a reported length beyond the buffer's capacity means the buffer
|
|
|
- * was never filled and must not be decoded.
|
|
|
- * @param api - the binding table.
|
|
|
- * @returns the NUL-terminated temp path decoded as a string.
|
|
|
+ * Resolve the current Windows temporary directory.
|
|
|
+ * @param api - active ACL/token binding table.
|
|
|
+ * @returns UTF-16 path reported by GetTempPathW.
|
|
|
*/
|
|
|
export function getTempPath(api: Win32Bindings): string {
|
|
|
const buffer = Buffer.alloc((abi.MAX_PATH + 1) * 2)
|
|
|
const length = api.getTempPathW(buffer.length / 2, buffer)
|
|
|
if (length === 0) throwLastError(api, 'GetTempPathW')
|
|
|
if (length > buffer.length / 2) {
|
|
|
- throw new Win32Error('GetTempPathW', abi.ERROR_INSUFFICIENT_BUFFER, `required ${length} chars exceed the ${buffer.length / 2}-char buffer; nothing was written`)
|
|
|
+ throw new Win32Error(
|
|
|
+ 'GetTempPathW',
|
|
|
+ ERROR_INSUFFICIENT_BUFFER,
|
|
|
+ `required ${length} chars exceed the ${buffer.length / 2}-char buffer; nothing was written`,
|
|
|
+ )
|
|
|
}
|
|
|
return buffer.subarray(0, length * 2).toString('utf16le')
|
|
|
}
|
|
|
-
|
|
|
-/**
|
|
|
- * Throw a Win32Error for a BOOL-style API failure. MUST be called immediately
|
|
|
- * after the failed call so GetLastError is not clobbered by other Win32 calls.
|
|
|
- * @param api - the binding table.
|
|
|
- * @param name - the failed API's name for the error message.
|
|
|
- * @param detail - optional detail overriding the formatted system message.
|
|
|
- * @returns never — always throws.
|
|
|
- */
|
|
|
-export function throwLastError(api: Win32Bindings, name: string, detail?: string): never {
|
|
|
- const win32Code = api.getLastError()
|
|
|
- throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code))
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Throw a Win32Error for an HRESULT-style API return value (the value IS the error code).
|
|
|
- * @param api - the binding table.
|
|
|
- * @param name - the failed API's name for the error message.
|
|
|
- * @param win32Code - the API's returned error code.
|
|
|
- * @param detail - optional detail overriding the formatted system message.
|
|
|
- * @returns never — always throws.
|
|
|
- */
|
|
|
-export function throwWin32(api: Win32Bindings, name: string, win32Code: number, detail?: string): never {
|
|
|
- throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code))
|
|
|
-}
|