linux-execve.spec.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  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.each([undefined, 'pipe'] as const)('preserves inherited stdio with control %s and reports execve errno', async (control) => {
  8. const nativeExecve = vi.fn(() => -1)
  9. const nativeFcntl = vi.fn((fd: number, command: number) => {
  10. if (command === 2) return 0
  11. return fd === 7 ? 1 : [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: '' }, control)
  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. ...control === 'pipe' ? [[7, 1, 0], [7, 2, 0]] : [],
  40. ])
  41. expect(nativeExecve).toHaveBeenCalledExactlyOnceWith(
  42. '/missing/tool',
  43. ['tool', 'literal arg', null],
  44. ['A=1', 'EMPTY=', null],
  45. )
  46. expect(errno).toHaveBeenCalledOnce()
  47. expect(failure).toMatchObject({
  48. code: 'ENOENT',
  49. errno: -2,
  50. syscall: 'execve',
  51. path: '/missing/tool',
  52. })
  53. expect(failure).toBeInstanceOf(Error)
  54. expect((failure as Error).message).toContain("ENOENT: no such file or directory, execve '/missing/tool'")
  55. })
  56. it('reports failure to read descriptor flags before replacing the process', async () => {
  57. const nativeExecve = vi.fn()
  58. const nativeFcntl = vi.fn(() => -1)
  59. const func = vi.fn((declaration: string) => declaration.includes('execve')
  60. ? nativeExecve
  61. : nativeFcntl)
  62. const errno = vi.fn(() => 9)
  63. vi.doMock('koffi', () => ({ default: { errno, load: () => ({ func }) } }))
  64. const { loadLinuxExecve } = await import('../src/linux-execve.ts')
  65. expect(() => loadLinuxExecve()('/bin/tool', ['tool'], {})).toThrow(expect.objectContaining({
  66. code: 'EBADF',
  67. errno: -9,
  68. syscall: 'fcntl',
  69. }))
  70. expect(nativeFcntl).toHaveBeenCalledExactlyOnceWith(0, 1, 0)
  71. expect(nativeExecve).not.toHaveBeenCalled()
  72. expect(errno).toHaveBeenCalledOnce()
  73. })
  74. it('reports failure to clear close-on-exec before replacing the process', async () => {
  75. const nativeExecve = vi.fn()
  76. const nativeFcntl = vi.fn()
  77. .mockReturnValueOnce(1)
  78. .mockReturnValueOnce(-1)
  79. const func = vi.fn((declaration: string) => declaration.includes('execve')
  80. ? nativeExecve
  81. : nativeFcntl)
  82. const errno = vi.fn(() => 5)
  83. vi.doMock('koffi', () => ({ default: { errno, load: () => ({ func }) } }))
  84. const { loadLinuxExecve } = await import('../src/linux-execve.ts')
  85. expect(() => loadLinuxExecve()('/bin/tool', ['tool'], {})).toThrow(expect.objectContaining({
  86. code: 'EIO',
  87. errno: -5,
  88. syscall: 'fcntl',
  89. }))
  90. expect(nativeFcntl.mock.calls).toEqual([
  91. [0, 1, 0],
  92. [0, 2, 0],
  93. ])
  94. expect(nativeExecve).not.toHaveBeenCalled()
  95. expect(errno).toHaveBeenCalledOnce()
  96. })
  97. })