linux-execve.ts 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. /** Lazy libc execve and descriptor bindings used by the one-shot Linux bootstrap. */
  2. import { getSystemErrorMessage, getSystemErrorName } from 'node:util'
  3. import koffi from 'koffi'
  4. import { SUBPROCESS_CONTROL_FD } from '@deepseek-ai/dsh-subprocess/control'
  5. /** Replace the current process image while preserving the supplied argv and environment. */
  6. export type LinuxExecve = (
  7. file: string,
  8. argv: string[],
  9. env: Record<string, string>,
  10. control?: 'pipe',
  11. ) => never
  12. type NativeExecve = (
  13. file: string,
  14. argv: Array<string | null>,
  15. envp: Array<string | null>,
  16. ) => number
  17. type NativeFcntl = (fd: number, command: number, argument: number) => number
  18. const STANDARD_FILE_DESCRIPTORS = [0, 1, 2] as const
  19. const F_GETFD = 1
  20. const F_SETFD = 2
  21. const FD_CLOEXEC = 1
  22. let cachedExecve: LinuxExecve | undefined
  23. function systemError(errno: number, syscall: string, path?: string): Error {
  24. const uvError = -errno
  25. const code = getSystemErrorName(uvError)
  26. const detail = getSystemErrorMessage(uvError)
  27. const subject = path === undefined ? syscall : `${syscall} '${path}'`
  28. const error = Object.assign(new Error(`${code}: ${detail}, ${subject}`), {
  29. code,
  30. errno: uvError,
  31. syscall,
  32. })
  33. return path === undefined ? error : Object.assign(error, { path })
  34. }
  35. /**
  36. * Load libc's execve and fcntl symbols on first use and retain the native bindings.
  37. * @returns a process-replacing execve operation that throws Node-style errors on failure.
  38. */
  39. export function loadLinuxExecve(): LinuxExecve {
  40. if (cachedExecve !== undefined) return cachedExecve
  41. const libc = koffi.load(null)
  42. const nativeExecve = libc.func(
  43. 'int execve(const char *pathname, const char **argv, const char **envp)',
  44. ) as NativeExecve
  45. const nativeFcntl = libc.func(
  46. 'int fcntl(int fd, int cmd, int arg)',
  47. ) as NativeFcntl
  48. cachedExecve = (file, argv, env, control) => {
  49. const descriptors = control === 'pipe'
  50. ? [...STANDARD_FILE_DESCRIPTORS, SUBPROCESS_CONTROL_FD]
  51. : STANDARD_FILE_DESCRIPTORS
  52. for (const fd of descriptors) {
  53. const flags = nativeFcntl(fd, F_GETFD, 0)
  54. if (flags === -1) throw systemError(koffi.errno(), 'fcntl')
  55. if ((flags & FD_CLOEXEC) === 0) continue
  56. if (nativeFcntl(fd, F_SETFD, flags & ~FD_CLOEXEC) === -1) {
  57. throw systemError(koffi.errno(), 'fcntl')
  58. }
  59. }
  60. nativeExecve(
  61. file,
  62. [...argv, null],
  63. [...Object.entries(env).map(([key, value]) => `${key}=${value}`), null],
  64. )
  65. throw systemError(koffi.errno(), 'execve', file)
  66. }
  67. return cachedExecve
  68. }