linux-execve.spec.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { afterEach, describe, expect, it, vi } from 'vitest'
  2. afterEach(() => {
  3. vi.doUnmock('koffi')
  4. vi.resetModules()
  5. })
  6. describe.skipIf(process.platform !== 'linux')('Linux libc execve binding', () => {
  7. it('preserves inherited stdio, null-terminates argv and envp, and reports execve errno', async () => {
  8. const nativeExecve = vi.fn(() => -1)
  9. const nativeFcntl = vi.fn((fd: number, command: number) => {
  10. if (command === 2) return 0
  11. return [1, 0, 5][fd]
  12. })
  13. const func = vi.fn((declaration: string) => declaration.includes('execve')
  14. ? nativeExecve
  15. : nativeFcntl)
  16. const load = vi.fn(() => ({ func }))
  17. const errno = vi.fn(() => 2)
  18. vi.doMock('koffi', () => ({ default: { errno, load } }))
  19. const { loadLinuxExecve } = await import('../src/linux-execve.ts')
  20. const execve = loadLinuxExecve()
  21. expect(loadLinuxExecve()).toBe(execve)
  22. expect(load).toHaveBeenCalledExactlyOnceWith(null)
  23. expect(func.mock.calls).toEqual([
  24. ['int execve(const char *pathname, const char **argv, const char **envp)'],
  25. ['int fcntl(int fd, int cmd, int arg)'],
  26. ])
  27. let failure: unknown
  28. try {
  29. execve('/missing/tool', ['tool', 'literal arg'], { A: '1', EMPTY: '' })
  30. } catch (error) {
  31. failure = error
  32. }
  33. expect(nativeFcntl.mock.calls).toEqual([
  34. [0, 1, 0],
  35. [0, 2, 0],
  36. [1, 1, 0],
  37. [2, 1, 0],
  38. [2, 2, 4],
  39. ])
  40. expect(nativeExecve).toHaveBeenCalledExactlyOnceWith(
  41. '/missing/tool',
  42. ['tool', 'literal arg', null],
  43. ['A=1', 'EMPTY=', null],
  44. )
  45. expect(errno).toHaveBeenCalledOnce()
  46. expect(failure).toMatchObject({
  47. code: 'ENOENT',
  48. errno: -2,
  49. syscall: 'execve',
  50. path: '/missing/tool',
  51. })
  52. expect(failure).toBeInstanceOf(Error)
  53. expect((failure as Error).message).toContain("ENOENT: no such file or directory, execve '/missing/tool'")
  54. })
  55. it('reports failure to read descriptor flags before replacing the process', async () => {
  56. const nativeExecve = vi.fn()
  57. const nativeFcntl = vi.fn(() => -1)
  58. const func = vi.fn((declaration: string) => declaration.includes('execve')
  59. ? nativeExecve
  60. : nativeFcntl)
  61. const errno = vi.fn(() => 9)
  62. vi.doMock('koffi', () => ({ default: { errno, load: () => ({ func }) } }))
  63. const { loadLinuxExecve } = await import('../src/linux-execve.ts')
  64. expect(() => loadLinuxExecve()('/bin/tool', ['tool'], {})).toThrow(expect.objectContaining({
  65. code: 'EBADF',
  66. errno: -9,
  67. syscall: 'fcntl',
  68. }))
  69. expect(nativeFcntl).toHaveBeenCalledExactlyOnceWith(0, 1, 0)
  70. expect(nativeExecve).not.toHaveBeenCalled()
  71. expect(errno).toHaveBeenCalledOnce()
  72. })
  73. it('reports failure to clear close-on-exec before replacing the process', async () => {
  74. const nativeExecve = vi.fn()
  75. const nativeFcntl = vi.fn()
  76. .mockReturnValueOnce(1)
  77. .mockReturnValueOnce(-1)
  78. const func = vi.fn((declaration: string) => declaration.includes('execve')
  79. ? nativeExecve
  80. : nativeFcntl)
  81. const errno = vi.fn(() => 5)
  82. vi.doMock('koffi', () => ({ default: { errno, load: () => ({ func }) } }))
  83. const { loadLinuxExecve } = await import('../src/linux-execve.ts')
  84. expect(() => loadLinuxExecve()('/bin/tool', ['tool'], {})).toThrow(expect.objectContaining({
  85. code: 'EIO',
  86. errno: -5,
  87. syscall: 'fcntl',
  88. }))
  89. expect(nativeFcntl.mock.calls).toEqual([
  90. [0, 1, 0],
  91. [0, 2, 0],
  92. ])
  93. expect(nativeExecve).not.toHaveBeenCalled()
  94. expect(errno).toHaveBeenCalledOnce()
  95. })
  96. })