ffi.ts 12 KB

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