Files
deepseek-harness/packages/subprocess/win32-process/src/ffi.ts
T

384 lines
14 KiB
TypeScript

/** Lazy Koffi bindings for generic Win32 process, stdio, and Job operations. */
import * as abi from './abi.ts'
import { Win32Error } from './errors.ts'
import { requireKoffi, type Koffi } from './koffi.ts'
declare const nativePtr: unique symbol
/** Koffi native pointer branded against accidental numeric use. */
export type NativePtr = bigint & { readonly [nativePtr]: true }
type Ptr = ReturnType<Koffi['pointer']>
/** Loaded Win32 libraries and the shared stdcall binder used by process extensions. */
export interface Win32BindingContext {
/** Kernel process, handle, pipe, and Job APIs. */
readonly kernel32: ReturnType<Koffi['load']>
/** Token and security APIs. */
readonly advapi32: ReturnType<Koffi['load']>
/** Bind one stdcall function from a loaded Win32 library. */
readonly bind: (
library: ReturnType<Koffi['load']>,
name: string,
result: Ptr | string,
args: Array<Ptr | string>,
) => unknown
}
/**
* Return whether a Koffi pointer represents NULL.
* @param value - pointer value returned by Koffi or a Win32 call.
* @returns true for null, undefined, or address zero.
*/
export function isNullPtr(value: NativePtr | null | undefined): value is null | undefined {
return value === null || value === undefined || (value as bigint) === 0n
}
/** STARTUPINFOW fields used by inherited or piped stdio launches. */
export interface StartupInfoInput {
cb: number
dwFlags: number
wShowWindow: number
hStdInput: NativePtr
hStdOutput: NativePtr
hStdError: NativePtr
cbReserved2?: number
lpReserved2?: NativePtr
}
/** Decoded PROCESS_INFORMATION result. */
export interface ProcessInfoOutput {
hProcess: NativePtr | null
hThread: NativePtr | null
dwProcessId: number
dwThreadId: number
}
/** Generic Win32 calls consumed by restricted-token sandbox process operations. */
export interface Win32ProcessBindings {
closeHandle(handle: NativePtr): number
getLastError(): number
getFileType(handle: NativePtr): number
uvGetOsfhandle(fileDescriptor: number): NativePtr | null
formatMessageW(
flags: number,
source: null,
messageId: number,
languageId: number,
buffer: Buffer,
size: number,
args: null,
): number
createPipe(readHandle: NativePtr, writeHandle: NativePtr, attributes: null, size: number): number
setHandleInformation(handle: NativePtr, mask: number, flags: number): number
createProcessAsUserW(
token: NativePtr,
applicationName: string | null,
commandLine: string,
processAttributes: null,
threadAttributes: null,
inheritHandles: number,
creationFlags: number,
environment: null,
currentDirectory: string | null,
startupInfo: NativePtr,
processInfo: NativePtr,
): number
createProcessW(
applicationName: string | null,
commandLine: string,
processAttributes: null,
threadAttributes: null,
inheritHandles: number,
creationFlags: number,
environment: Buffer | null,
currentDirectory: string | null,
startupInfo: NativePtr,
processInfo: NativePtr,
): number
readFile(file: NativePtr, buffer: Buffer, count: number, bytesRead: NativePtr, overlapped: null): number
peekNamedPipe(
pipe: NativePtr,
buffer: null,
size: number,
bytesRead: NativePtr | null,
totalAvail: NativePtr,
leftThisMessage: NativePtr | null,
): number
waitForSingleObject(handle: NativePtr, milliseconds: number): number
getExitCodeProcess(process: NativePtr, exitCode: NativePtr): number
createJobObjectW(attributes: null, name: null): NativePtr
setInformationJobObject(job: NativePtr, cls: number, information: Buffer, length: number): number
queryInformationJobObject(
job: NativePtr,
cls: number,
information: Buffer,
length: number,
returnLength: null,
): number
assignProcessToJobObject(job: NativePtr, process: NativePtr): number
resumeThread(thread: NativePtr): number
terminateProcess(process: NativePtr, exitCode: number): number
terminateJobObject(job: NativePtr, exitCode: number): number
getStdHandle(stdHandle: number): NativePtr
}
/** Generic Win32 calls plus Node's libuv descriptor-to-handle bridge. */
export interface CurrentTokenProcessBindings extends Win32ProcessBindings {
uvGetOsfhandle(fileDescriptor: number): NativePtr | null
}
interface Win32Types {
PVOID: Ptr
PPVOID: Ptr
STARTUPINFOW: ReturnType<Koffi['struct']>
PROCESS_INFORMATION: ReturnType<Koffi['struct']>
}
let cachedTypes: Win32Types | undefined
/** Resolve Koffi pointer and process layouts on the first native operation. */
function win32Types(): Win32Types {
if (cachedTypes !== undefined) return cachedTypes
const koffi = requireKoffi()
const PVOID = koffi.pointer('void')
const PPVOID = koffi.pointer(PVOID)
const STARTUPINFOW = koffi.struct('DSH_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,
})
const PROCESS_INFORMATION = koffi.struct('DSH_PROCESS_INFORMATION', {
hProcess: PVOID, hThread: PVOID, dwProcessId: 'uint32', dwThreadId: 'uint32',
})
/* v8 ignore start -- ABI guards are pinned by native header probes. */
if (STARTUPINFOW.size !== abi.STARTUPINFOW_SIZE) {
throw new Error(`STARTUPINFOW layout mismatch: koffi computed ${STARTUPINFOW.size}, expected ${abi.STARTUPINFOW_SIZE}`)
}
if (PROCESS_INFORMATION.size !== abi.PROCESS_INFORMATION_SIZE) {
throw new Error(`PROCESS_INFORMATION layout mismatch: koffi computed ${PROCESS_INFORMATION.size}, expected ${abi.PROCESS_INFORMATION_SIZE}`)
}
/* v8 ignore stop */
return cachedTypes = { PVOID, PPVOID, STARTUPINFOW, PROCESS_INFORMATION }
}
/**
* Materialize the Koffi STARTUPINFOW layout on first native use.
* @returns the cached native struct type.
*/
export function startupInfoType(): ReturnType<Koffi['struct']> {
return win32Types().STARTUPINFOW
}
/**
* Materialize the Koffi PROCESS_INFORMATION layout on first native use.
* @returns the cached native struct type.
*/
export function processInformationType(): ReturnType<Koffi['struct']> {
return win32Types().PROCESS_INFORMATION
}
/**
* Allocate a pointer-sized out-parameter slot.
* @returns allocated native slot.
*/
export function allocPtrSlot(): NativePtr {
const { PVOID } = win32Types()
return requireKoffi().alloc(PVOID, 1) as NativePtr
}
/**
* Allocate a uint32 out-parameter slot.
* @returns allocated native slot.
*/
export function allocUint32(): NativePtr {
return requireKoffi().alloc('uint32', 1) as NativePtr
}
/**
* Decode a pointer out-parameter.
* @param slot - pointer-sized slot filled by Win32.
* @returns decoded pointer, or null for address zero.
*/
export function decodePtr(slot: NativePtr): NativePtr | null {
const value = requireKoffi().decode(slot, win32Types().PVOID) as NativePtr | null
return isNullPtr(value) ? null : value
}
/**
* Decode a uint32 out-parameter.
* @param slot - uint32 slot filled by Win32.
* @returns decoded unsigned value.
*/
export function decodeUint32(slot: NativePtr): number {
return requireKoffi().decode(slot, 'uint32') as number
}
/**
* Allocate a zeroed STARTUPINFOW.
* @returns allocated struct pointer.
*/
export function allocStartupInfo(): NativePtr {
return requireKoffi().alloc(win32Types().STARTUPINFOW, 1) as NativePtr
}
/**
* Encode the stdio-bearing STARTUPINFOW fields.
* @param startupInfo - allocated STARTUPINFOW pointer.
* @param fields - fields required for inherited stdio.
*/
export function encodeStartupInfo(startupInfo: NativePtr, fields: StartupInfoInput): void {
requireKoffi().encode(startupInfo, win32Types().STARTUPINFOW, fields)
}
/**
* Allocate a zeroed PROCESS_INFORMATION.
* @returns allocated struct pointer.
*/
export function allocProcessInfo(): NativePtr {
return requireKoffi().alloc(win32Types().PROCESS_INFORMATION, 1) as NativePtr
}
/**
* Decode PROCESS_INFORMATION.
* @param processInfo - struct pointer filled by CreateProcess.
* @returns process/thread handles and ids.
*/
export function decodeProcessInfo(processInfo: NativePtr): ProcessInfoOutput {
return requireKoffi().decode(processInfo, win32Types().PROCESS_INFORMATION) as ProcessInfoOutput
}
let cachedContext: Win32BindingContext | undefined
let cached: CurrentTokenProcessBindings | undefined
/* v8 ignore start -- exercised by native Windows ABI and sandbox jobs. */
function bindingContext(): Win32BindingContext {
if (cachedContext !== undefined) return cachedContext
const koffi = requireKoffi()
const kernel32 = koffi.load('kernel32.dll')
const advapi32 = koffi.load('advapi32.dll')
const bind = (
lib: ReturnType<typeof koffi.load>,
name: string,
result: Ptr | string,
args: Array<Ptr | string>,
): unknown => lib.func('__stdcall', name, result, args)
cachedContext = { kernel32, advapi32, bind }
return cachedContext
}
function bindings(): CurrentTokenProcessBindings {
if (cached !== undefined) return cached
const koffi = requireKoffi()
const { PVOID, PPVOID, STARTUPINFOW, PROCESS_INFORMATION } = win32Types()
const { kernel32, advapi32, bind } = bindingContext()
const node = koffi.load(null)
cached = {
closeHandle: bind(kernel32, 'CloseHandle', 'int', [PVOID]),
getLastError: bind(kernel32, 'GetLastError', 'uint32', []),
getFileType: bind(kernel32, 'GetFileType', 'uint32', [PVOID]),
formatMessageW: bind(kernel32, 'FormatMessageW', 'uint32', [
'uint32', PVOID, 'uint32', 'uint32', PVOID, '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),
]),
createProcessW: bind(kernel32, 'CreateProcessW', 'int', [
'str16', 'str16', PVOID, PVOID, 'int', 'uint32', PVOID, 'str16',
koffi.pointer(STARTUPINFOW), koffi.pointer(PROCESS_INFORMATION),
]),
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')]),
createJobObjectW: bind(kernel32, 'CreateJobObjectW', PVOID, [PVOID, 'str16']),
setInformationJobObject: bind(kernel32, 'SetInformationJobObject', 'int', [PVOID, 'int', PVOID, 'uint32']),
queryInformationJobObject: bind(kernel32, 'QueryInformationJobObject', 'int', [
PVOID, 'int', PVOID, 'uint32', PVOID,
]),
assignProcessToJobObject: bind(kernel32, 'AssignProcessToJobObject', 'int', [PVOID, PVOID]),
resumeThread: bind(kernel32, 'ResumeThread', 'uint32', [PVOID]),
terminateProcess: bind(kernel32, 'TerminateProcess', 'int', [PVOID, 'uint32']),
terminateJobObject: bind(kernel32, 'TerminateJobObject', 'int', [PVOID, 'uint32']),
getStdHandle: bind(kernel32, 'GetStdHandle', PVOID, ['int']),
uvGetOsfhandle: node.func('uv_get_osfhandle', PVOID, ['int']),
} as unknown as CurrentTokenProcessBindings
return cached
}
/**
* Extend the shared process table with caller-owned Win32 API families.
* @param create - binds only the caller-specific operations from the shared libraries.
* @returns generic process bindings combined with the caller-specific operations.
*/
export function extendWin32ProcessBindings<Extension extends object>(
create: (context: Win32BindingContext) => Extension,
): CurrentTokenProcessBindings & Extension {
return { ...bindings(), ...create(bindingContext()) }
}
/**
* Load the generic process binding table without policy-specific extensions.
* @returns shared Win32 process, stdio, and Job operations.
*/
export function loadWin32ProcessBindings(): CurrentTokenProcessBindings {
return bindings()
}
/* v8 ignore stop */
/**
* Format a Win32 error code through FormatMessageW.
* @param api - active binding table.
* @param win32Code - captured GetLastError value.
* @returns trimmed system message, or an empty string when unavailable.
*/
export function errorText(api: Win32ProcessBindings, 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,
)
return length === 0 ? '' : buffer.subarray(0, length * 2).toString('utf16le').trim()
}
/**
* Throw the current GetLastError value.
* @param api - active binding table.
* @param name - failing Win32 operation.
* @param detail - optional operation context.
* @returns never; always throws Win32Error.
*/
export function throwLastError(api: Win32ProcessBindings, name: string, detail?: string): never {
const win32Code = api.getLastError()
throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code))
}
/**
* Throw an explicitly captured Win32 error code.
* @param api - active binding table.
* @param name - failing Win32 operation.
* @param win32Code - error captured before cleanup.
* @param detail - optional operation context.
* @returns never; always throws Win32Error.
*/
export function throwWin32(
api: Win32ProcessBindings,
name: string,
win32Code: number,
detail?: string,
): never {
throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code))
}