| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- /** Lazy Koffi bindings for generic Win32 process, stdio, and Job operations. */
- import koffi from 'koffi'
- import * as abi from './abi.ts'
- import { Win32Error } from './errors.ts'
- declare const nativePtr: unique symbol
- /** Koffi native pointer branded against accidental numeric use. */
- export type NativePtr = bigint & { readonly [nativePtr]: true }
- type Ptr = ReturnType<typeof koffi.pointer>
- const PVOID: Ptr = koffi.pointer('void')
- const PPVOID: Ptr = koffi.pointer(PVOID)
- /** 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<typeof koffi.load>
- /** Token and security APIs. */
- readonly advapi32: ReturnType<typeof koffi.load>
- /** Bind one stdcall function from a loaded Win32 library. */
- readonly bind: (
- library: ReturnType<typeof 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
- hStdInput: NativePtr
- hStdOutput: NativePtr
- hStdError: 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
- 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: null,
- commandLine: string,
- processAttributes: null,
- threadAttributes: null,
- inheritHandles: number,
- creationFlags: number,
- environment: 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
- assignProcessToJobObject(job: NativePtr, process: NativePtr): number
- resumeThread(thread: NativePtr): number
- terminateProcess(process: NativePtr, exitCode: number): number
- getStdHandle(stdHandle: number): NativePtr
- }
- /** Koffi STARTUPINFOW layout. */
- export 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,
- })
- /** Koffi PROCESS_INFORMATION layout. */
- export 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 */
- /**
- * Allocate a pointer-sized out-parameter slot.
- * @returns allocated native slot.
- */
- export function allocPtrSlot(): NativePtr {
- return koffi.alloc(PVOID, 1) as NativePtr
- }
- /**
- * Allocate a uint32 out-parameter slot.
- * @returns allocated native slot.
- */
- export function allocUint32(): NativePtr {
- return koffi.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 = koffi.decode(slot, 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 koffi.decode(slot, 'uint32') as number
- }
- /**
- * Allocate a zeroed STARTUPINFOW.
- * @returns allocated struct pointer.
- */
- export function allocStartupInfo(): NativePtr {
- return koffi.alloc(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 {
- koffi.encode(startupInfo, STARTUPINFOW, fields)
- }
- /**
- * Allocate a zeroed PROCESS_INFORMATION.
- * @returns allocated struct pointer.
- */
- export function allocProcessInfo(): NativePtr {
- return koffi.alloc(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 koffi.decode(processInfo, PROCESS_INFORMATION) as ProcessInfoOutput
- }
- let cachedContext: Win32BindingContext | undefined
- let cached: Win32ProcessBindings | undefined
- /* v8 ignore start -- exercised by native Windows ABI and sandbox jobs. */
- function bindingContext(): Win32BindingContext {
- if (cachedContext !== undefined) return cachedContext
- 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(): Win32ProcessBindings {
- if (cached !== undefined) return cached
- const { kernel32, advapi32, bind } = bindingContext()
- cached = {
- closeHandle: bind(kernel32, 'CloseHandle', 'int', [PVOID]),
- getLastError: bind(kernel32, 'GetLastError', 'uint32', []),
- 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),
- ]),
- 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']),
- assignProcessToJobObject: bind(kernel32, 'AssignProcessToJobObject', 'int', [PVOID, PVOID]),
- resumeThread: bind(kernel32, 'ResumeThread', 'uint32', [PVOID]),
- terminateProcess: bind(kernel32, 'TerminateProcess', 'int', [PVOID, 'uint32']),
- getStdHandle: bind(kernel32, 'GetStdHandle', PVOID, ['int']),
- } as unknown as Win32ProcessBindings
- 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,
- ): Win32ProcessBindings & Extension {
- return { ...bindings(), ...create(bindingContext()) }
- }
- /* 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))
- }
|