1
0

ffi.ts 11 KB

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