ffi.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. /** Lazy Koffi bindings for generic Win32 process, stdio, and Job operations. */
  2. import koffi from 'koffi'
  3. import * as abi from './abi.ts'
  4. import { Win32Error } from './errors.ts'
  5. declare const nativePtr: unique symbol
  6. /** Koffi native pointer branded against accidental numeric use. */
  7. export type NativePtr = bigint & { readonly [nativePtr]: true }
  8. type Ptr = ReturnType<typeof koffi.pointer>
  9. const PVOID: Ptr = koffi.pointer('void')
  10. const PPVOID: Ptr = koffi.pointer(PVOID)
  11. /** Loaded Win32 libraries and the shared stdcall binder used by process extensions. */
  12. export interface Win32BindingContext {
  13. /** Kernel process, handle, pipe, and Job APIs. */
  14. readonly kernel32: ReturnType<typeof koffi.load>
  15. /** Token and security APIs. */
  16. readonly advapi32: ReturnType<typeof koffi.load>
  17. /** Bind one stdcall function from a loaded Win32 library. */
  18. readonly bind: (
  19. library: ReturnType<typeof koffi.load>,
  20. name: string,
  21. result: Ptr | string,
  22. args: Array<Ptr | string>,
  23. ) => unknown
  24. }
  25. /**
  26. * Return whether a Koffi pointer represents NULL.
  27. * @param value - pointer value returned by Koffi or a Win32 call.
  28. * @returns true for null, undefined, or address zero.
  29. */
  30. export function isNullPtr(value: NativePtr | null | undefined): value is null | undefined {
  31. return value === null || value === undefined || (value as bigint) === 0n
  32. }
  33. /** STARTUPINFOW fields used by inherited or piped stdio launches. */
  34. export interface StartupInfoInput {
  35. cb: number
  36. dwFlags: number
  37. hStdInput: NativePtr
  38. hStdOutput: NativePtr
  39. hStdError: NativePtr
  40. cbReserved2?: number
  41. lpReserved2?: NativePtr
  42. }
  43. /** Decoded PROCESS_INFORMATION result. */
  44. export interface ProcessInfoOutput {
  45. hProcess: NativePtr | null
  46. hThread: NativePtr | null
  47. dwProcessId: number
  48. dwThreadId: number
  49. }
  50. /** Generic Win32 calls consumed by restricted-token sandbox process operations. */
  51. export interface Win32ProcessBindings {
  52. closeHandle(handle: NativePtr): number
  53. getLastError(): number
  54. getFileType(handle: NativePtr): number
  55. uvGetOsfhandle(fileDescriptor: number): NativePtr | null
  56. formatMessageW(
  57. flags: number,
  58. source: null,
  59. messageId: number,
  60. languageId: number,
  61. buffer: Buffer,
  62. size: number,
  63. args: null,
  64. ): number
  65. createPipe(readHandle: NativePtr, writeHandle: NativePtr, attributes: null, size: number): number
  66. setHandleInformation(handle: NativePtr, mask: number, flags: number): number
  67. createProcessAsUserW(
  68. token: NativePtr,
  69. applicationName: string | null,
  70. commandLine: string,
  71. processAttributes: null,
  72. threadAttributes: null,
  73. inheritHandles: number,
  74. creationFlags: number,
  75. environment: null,
  76. currentDirectory: string | null,
  77. startupInfo: NativePtr,
  78. processInfo: NativePtr,
  79. ): number
  80. createProcessW(
  81. applicationName: string | null,
  82. commandLine: string,
  83. processAttributes: null,
  84. threadAttributes: null,
  85. inheritHandles: number,
  86. creationFlags: number,
  87. environment: Buffer | null,
  88. currentDirectory: string | null,
  89. startupInfo: NativePtr,
  90. processInfo: NativePtr,
  91. ): number
  92. readFile(file: NativePtr, buffer: Buffer, count: number, bytesRead: NativePtr, overlapped: null): number
  93. peekNamedPipe(
  94. pipe: NativePtr,
  95. buffer: null,
  96. size: number,
  97. bytesRead: NativePtr | null,
  98. totalAvail: NativePtr,
  99. leftThisMessage: NativePtr | null,
  100. ): number
  101. waitForSingleObject(handle: NativePtr, milliseconds: number): number
  102. getExitCodeProcess(process: NativePtr, exitCode: NativePtr): number
  103. createJobObjectW(attributes: null, name: null): NativePtr
  104. setInformationJobObject(job: NativePtr, cls: number, information: Buffer, length: number): number
  105. queryInformationJobObject(
  106. job: NativePtr,
  107. cls: number,
  108. information: Buffer,
  109. length: number,
  110. returnLength: null,
  111. ): number
  112. assignProcessToJobObject(job: NativePtr, process: NativePtr): number
  113. resumeThread(thread: NativePtr): number
  114. terminateProcess(process: NativePtr, exitCode: number): number
  115. terminateJobObject(job: NativePtr, exitCode: number): number
  116. getStdHandle(stdHandle: number): NativePtr
  117. }
  118. /** Generic Win32 calls plus Node's libuv descriptor-to-handle bridge. */
  119. export interface CurrentTokenProcessBindings extends Win32ProcessBindings {
  120. uvGetOsfhandle(fileDescriptor: number): NativePtr | null
  121. }
  122. /** Koffi STARTUPINFOW layout. */
  123. export const STARTUPINFOW = koffi.struct('DSH_STARTUPINFOW', {
  124. cb: 'uint32',
  125. lpReserved: 'str16',
  126. lpDesktop: 'str16',
  127. lpTitle: 'str16',
  128. dwX: 'uint32',
  129. dwY: 'uint32',
  130. dwXSize: 'uint32',
  131. dwYSize: 'uint32',
  132. dwXCountChars: 'uint32',
  133. dwYCountChars: 'uint32',
  134. dwFillAttribute: 'uint32',
  135. dwFlags: 'uint32',
  136. wShowWindow: 'uint16',
  137. cbReserved2: 'uint16',
  138. lpReserved2: koffi.pointer('uint8'),
  139. hStdInput: PVOID,
  140. hStdOutput: PVOID,
  141. hStdError: PVOID,
  142. })
  143. /** Koffi PROCESS_INFORMATION layout. */
  144. export const PROCESS_INFORMATION = koffi.struct('DSH_PROCESS_INFORMATION', {
  145. hProcess: PVOID,
  146. hThread: PVOID,
  147. dwProcessId: 'uint32',
  148. dwThreadId: 'uint32',
  149. })
  150. /* v8 ignore start -- ABI guards are pinned by native header probes. */
  151. if (STARTUPINFOW.size !== abi.STARTUPINFOW_SIZE) {
  152. throw new Error(`STARTUPINFOW layout mismatch: koffi computed ${STARTUPINFOW.size}, expected ${abi.STARTUPINFOW_SIZE}`)
  153. }
  154. if (PROCESS_INFORMATION.size !== abi.PROCESS_INFORMATION_SIZE) {
  155. throw new Error(`PROCESS_INFORMATION layout mismatch: koffi computed ${PROCESS_INFORMATION.size}, expected ${abi.PROCESS_INFORMATION_SIZE}`)
  156. }
  157. /* v8 ignore stop */
  158. /**
  159. * Allocate a pointer-sized out-parameter slot.
  160. * @returns allocated native slot.
  161. */
  162. export function allocPtrSlot(): NativePtr {
  163. return koffi.alloc(PVOID, 1) as NativePtr
  164. }
  165. /**
  166. * Allocate a uint32 out-parameter slot.
  167. * @returns allocated native slot.
  168. */
  169. export function allocUint32(): NativePtr {
  170. return koffi.alloc('uint32', 1) as NativePtr
  171. }
  172. /**
  173. * Decode a pointer out-parameter.
  174. * @param slot - pointer-sized slot filled by Win32.
  175. * @returns decoded pointer, or null for address zero.
  176. */
  177. export function decodePtr(slot: NativePtr): NativePtr | null {
  178. const value = koffi.decode(slot, PVOID) as NativePtr | null
  179. return isNullPtr(value) ? null : value
  180. }
  181. /**
  182. * Decode a uint32 out-parameter.
  183. * @param slot - uint32 slot filled by Win32.
  184. * @returns decoded unsigned value.
  185. */
  186. export function decodeUint32(slot: NativePtr): number {
  187. return koffi.decode(slot, 'uint32') as number
  188. }
  189. /**
  190. * Allocate a zeroed STARTUPINFOW.
  191. * @returns allocated struct pointer.
  192. */
  193. export function allocStartupInfo(): NativePtr {
  194. return koffi.alloc(STARTUPINFOW, 1) as NativePtr
  195. }
  196. /**
  197. * Encode the stdio-bearing STARTUPINFOW fields.
  198. * @param startupInfo - allocated STARTUPINFOW pointer.
  199. * @param fields - fields required for inherited stdio.
  200. */
  201. export function encodeStartupInfo(startupInfo: NativePtr, fields: StartupInfoInput): void {
  202. koffi.encode(startupInfo, STARTUPINFOW, fields)
  203. }
  204. /**
  205. * Allocate a zeroed PROCESS_INFORMATION.
  206. * @returns allocated struct pointer.
  207. */
  208. export function allocProcessInfo(): NativePtr {
  209. return koffi.alloc(PROCESS_INFORMATION, 1) as NativePtr
  210. }
  211. /**
  212. * Decode PROCESS_INFORMATION.
  213. * @param processInfo - struct pointer filled by CreateProcess.
  214. * @returns process/thread handles and ids.
  215. */
  216. export function decodeProcessInfo(processInfo: NativePtr): ProcessInfoOutput {
  217. return koffi.decode(processInfo, PROCESS_INFORMATION) as ProcessInfoOutput
  218. }
  219. let cachedContext: Win32BindingContext | undefined
  220. let cached: CurrentTokenProcessBindings | undefined
  221. /* v8 ignore start -- exercised by native Windows ABI and sandbox jobs. */
  222. function bindingContext(): Win32BindingContext {
  223. if (cachedContext !== undefined) return cachedContext
  224. const kernel32 = koffi.load('kernel32.dll')
  225. const advapi32 = koffi.load('advapi32.dll')
  226. const bind = (
  227. lib: ReturnType<typeof koffi.load>,
  228. name: string,
  229. result: Ptr | string,
  230. args: Array<Ptr | string>,
  231. ): unknown => lib.func('__stdcall', name, result, args)
  232. cachedContext = { kernel32, advapi32, bind }
  233. return cachedContext
  234. }
  235. function bindings(): CurrentTokenProcessBindings {
  236. if (cached !== undefined) return cached
  237. const { kernel32, advapi32, bind } = bindingContext()
  238. const node = koffi.load(null)
  239. cached = {
  240. closeHandle: bind(kernel32, 'CloseHandle', 'int', [PVOID]),
  241. getLastError: bind(kernel32, 'GetLastError', 'uint32', []),
  242. getFileType: bind(kernel32, 'GetFileType', 'uint32', [PVOID]),
  243. formatMessageW: bind(kernel32, 'FormatMessageW', 'uint32', [
  244. 'uint32', PVOID, 'uint32', 'uint32', PVOID, 'uint32', PVOID,
  245. ]),
  246. createPipe: bind(kernel32, 'CreatePipe', 'int', [PPVOID, PPVOID, PVOID, 'uint32']),
  247. setHandleInformation: bind(kernel32, 'SetHandleInformation', 'int', [PVOID, 'uint32', 'uint32']),
  248. createProcessAsUserW: bind(advapi32, 'CreateProcessAsUserW', 'int', [
  249. PVOID, 'str16', 'str16', PVOID, PVOID, 'int', 'uint32', PVOID, 'str16',
  250. koffi.pointer(STARTUPINFOW), koffi.pointer(PROCESS_INFORMATION),
  251. ]),
  252. createProcessW: bind(kernel32, 'CreateProcessW', 'int', [
  253. 'str16', 'str16', PVOID, PVOID, 'int', 'uint32', PVOID, 'str16',
  254. koffi.pointer(STARTUPINFOW), koffi.pointer(PROCESS_INFORMATION),
  255. ]),
  256. readFile: bind(kernel32, 'ReadFile', 'int', [PVOID, PVOID, 'uint32', koffi.pointer('uint32'), PVOID]),
  257. peekNamedPipe: bind(kernel32, 'PeekNamedPipe', 'int', [
  258. PVOID, PVOID, 'uint32', koffi.pointer('uint32'), koffi.pointer('uint32'), koffi.pointer('uint32'),
  259. ]),
  260. waitForSingleObject: bind(kernel32, 'WaitForSingleObject', 'uint32', [PVOID, 'uint32']),
  261. getExitCodeProcess: bind(kernel32, 'GetExitCodeProcess', 'int', [PVOID, koffi.pointer('uint32')]),
  262. createJobObjectW: bind(kernel32, 'CreateJobObjectW', PVOID, [PVOID, 'str16']),
  263. setInformationJobObject: bind(kernel32, 'SetInformationJobObject', 'int', [PVOID, 'int', PVOID, 'uint32']),
  264. queryInformationJobObject: bind(kernel32, 'QueryInformationJobObject', 'int', [
  265. PVOID, 'int', PVOID, 'uint32', PVOID,
  266. ]),
  267. assignProcessToJobObject: bind(kernel32, 'AssignProcessToJobObject', 'int', [PVOID, PVOID]),
  268. resumeThread: bind(kernel32, 'ResumeThread', 'uint32', [PVOID]),
  269. terminateProcess: bind(kernel32, 'TerminateProcess', 'int', [PVOID, 'uint32']),
  270. terminateJobObject: bind(kernel32, 'TerminateJobObject', 'int', [PVOID, 'uint32']),
  271. getStdHandle: bind(kernel32, 'GetStdHandle', PVOID, ['int']),
  272. uvGetOsfhandle: node.func('uv_get_osfhandle', PVOID, ['int']),
  273. } as unknown as CurrentTokenProcessBindings
  274. return cached
  275. }
  276. /**
  277. * Extend the shared process table with caller-owned Win32 API families.
  278. * @param create - binds only the caller-specific operations from the shared libraries.
  279. * @returns generic process bindings combined with the caller-specific operations.
  280. */
  281. export function extendWin32ProcessBindings<Extension extends object>(
  282. create: (context: Win32BindingContext) => Extension,
  283. ): CurrentTokenProcessBindings & Extension {
  284. return { ...bindings(), ...create(bindingContext()) }
  285. }
  286. /**
  287. * Load the generic process binding table without policy-specific extensions.
  288. * @returns shared Win32 process, stdio, and Job operations.
  289. */
  290. export function loadWin32ProcessBindings(): CurrentTokenProcessBindings {
  291. return bindings()
  292. }
  293. /* v8 ignore stop */
  294. /**
  295. * Format a Win32 error code through FormatMessageW.
  296. * @param api - active binding table.
  297. * @param win32Code - captured GetLastError value.
  298. * @returns trimmed system message, or an empty string when unavailable.
  299. */
  300. export function errorText(api: Win32ProcessBindings, win32Code: number): string {
  301. const buffer = Buffer.alloc(1024)
  302. const length = api.formatMessageW(
  303. abi.FORMAT_MESSAGE_FROM_SYSTEM | abi.FORMAT_MESSAGE_IGNORE_INSERTS,
  304. null,
  305. win32Code,
  306. 0,
  307. buffer,
  308. buffer.length / 2,
  309. null,
  310. )
  311. return length === 0 ? '' : buffer.subarray(0, length * 2).toString('utf16le').trim()
  312. }
  313. /**
  314. * Throw the current GetLastError value.
  315. * @param api - active binding table.
  316. * @param name - failing Win32 operation.
  317. * @param detail - optional operation context.
  318. * @returns never; always throws Win32Error.
  319. */
  320. export function throwLastError(api: Win32ProcessBindings, name: string, detail?: string): never {
  321. const win32Code = api.getLastError()
  322. throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code))
  323. }
  324. /**
  325. * Throw an explicitly captured Win32 error code.
  326. * @param api - active binding table.
  327. * @param name - failing Win32 operation.
  328. * @param win32Code - error captured before cleanup.
  329. * @param detail - optional operation context.
  330. * @returns never; always throws Win32Error.
  331. */
  332. export function throwWin32(
  333. api: Win32ProcessBindings,
  334. name: string,
  335. win32Code: number,
  336. detail?: string,
  337. ): never {
  338. throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code))
  339. }