Jelajahi Sumber

fix(subprocess): isolate native runner bootstrap

pku-xht 1 Minggu lalu
induk
melakukan
d82fd86c5c

+ 11 - 2
packages/subprocess/subprocess-local/src/runner-launch.ts

@@ -18,6 +18,7 @@ export const WINDOWS_RUNNER_SELECTION = 'windows' as const
 export type RunnerInvocation = [string, ...string[]]
 
 const SOURCE_TSCONFIG_PATH = fileURLToPath(new URL('../../../../tsconfig.base.json', import.meta.url))
+const RUNNER_CONTROL_ENV_PREFIXES = ['NODE_', 'TSX_'] as const
 
 /**
  * Resolve the source, built, or packaged entry that calls the same runner core.
@@ -67,11 +68,19 @@ export function runnerEnvironment(
   invocation?: RunnerInvocation,
 ): NodeJS.ProcessEnv {
   const entry = invocation?.at(-1)
-  return childEnv({
+  const env = childEnv()
+  for (const name of Object.keys(env)) {
+    const normalized = name.toUpperCase()
+    if (RUNNER_CONTROL_ENV_PREFIXES.some(prefix => normalized.startsWith(prefix))) {
+      Reflect.deleteProperty(env, name)
+    }
+  }
+  return {
+    ...env,
     [SUBPROCESS_RUNNER_ENV]: selection,
     SYSTEMD_LOG_TARGET: 'null',
     ...entry?.endsWith('.ts') === true ? { TSX_TSCONFIG_PATH: SOURCE_TSCONFIG_PATH } : {},
-  })
+  }
 }
 
 /**

+ 1 - 1
packages/subprocess/subprocess-local/src/spawn-runner.ts

@@ -150,7 +150,7 @@ function execLinuxTarget(
 ): never {
   const program = argv[0] as string
   if (program.includes('/')) return execLinuxFile(program, argv, request.env, internals)
-  const path = request.env.PATH ?? '/bin:/usr/bin'
+  const path = request.env.PATH ?? '/usr/bin:/bin'
   let permissionFailure: Error | undefined
   for (const directory of path.split(':')) {
     const root = directory.startsWith('/')

+ 14 - 5
packages/subprocess/subprocess-local/tests/native-containment.spec.ts

@@ -149,11 +149,20 @@ describe.skipIf(!linuxNative)('Linux user-systemd native containment', () => {
     const command = `setsid sh -c 'echo $$ > "$1"; trap "" TERM; while :; do sleep 60; done' sh ${JSON.stringify(pidFile)} & wait`
     const request = spec(['bash', '-c', command], 80)
     const handle = bindManagedProcess(request, launchLinuxScope(request, targetEnvironment(request)))
-    const descendant = await waitForPid(pidFile)
-    handle.terminate()
-    await handle.done
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    await waitGone(descendant)
+    let descendant: number | undefined
+    try {
+      descendant = await waitForPid(pidFile)
+      handle.terminate()
+      await handle.done
+      await expect(handle.waitForExit()).resolves.toBe(true)
+      await waitGone(descendant)
+    } finally {
+      handle.terminate()
+      await Promise.allSettled([handle.done, handle.waitForExit()])
+      if (descendant !== undefined) {
+        try { process.kill(descendant, 'SIGKILL') } catch { /* already contained */ }
+      }
+    }
   })
 
   it('preserves Node-shaped ENOENT and EACCES spawn failures without replay', async () => {

+ 29 - 1
packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts

@@ -276,6 +276,34 @@ describe('runner launch inputs', () => {
     }, true, 17)).toEqual(['ignore', 'ignore', 'ignore', 'ipc', 17, 1, 'pipe'])
   })
 
+  it('removes ambient Node and tsx controls from the bootstrap environment only', () => {
+    vi.stubEnv('NODE_OPTIONS', '--require /tmp/runner-bootstrap-control.cjs')
+    vi.stubEnv('NODE_DEBUG', 'esm')
+    vi.stubEnv('TSX_DISABLE_CACHE', '1')
+    vi.stubEnv('TSX_TSCONFIG_PATH', '/ambient/tsconfig.json')
+    try {
+      const sourceEnv = runnerEnvironment('/tmp/request', [process.execPath, '/repo/bin.ts'])
+      const builtEnv = runnerEnvironment('/tmp/request', [process.execPath, '/repo/runner.js'])
+      expect(sourceEnv.NODE_OPTIONS).toBeUndefined()
+      expect(sourceEnv.NODE_DEBUG).toBeUndefined()
+      expect(sourceEnv.TSX_DISABLE_CACHE).toBeUndefined()
+      expect(sourceEnv.TSX_TSCONFIG_PATH)
+        .toBe(resolve(import.meta.dirname, '../../../..', 'tsconfig.base.json'))
+      expect(builtEnv.NODE_OPTIONS).toBeUndefined()
+      expect(builtEnv.NODE_DEBUG).toBeUndefined()
+      expect(builtEnv.TSX_DISABLE_CACHE).toBeUndefined()
+      expect(builtEnv.TSX_TSCONFIG_PATH).toBeUndefined()
+      expect(targetEnvironment(spec)).toMatchObject({
+        NODE_OPTIONS: '--require /tmp/runner-bootstrap-control.cjs',
+        NODE_DEBUG: 'esm',
+        TSX_DISABLE_CACHE: '1',
+        TSX_TSCONFIG_PATH: '/ambient/tsconfig.json',
+      })
+    } finally {
+      vi.unstubAllEnvs()
+    }
+  })
+
   it('validates every Node-baseline NUL location before launch', () => {
     expect(targetEnvironment(spec)).toMatchObject({ EXPLICIT: 'yes' })
     expect(targetEnvironment({ ...spec, env: { '=C:': 'C:\\target' } }))
@@ -527,7 +555,7 @@ describe('Linux one-shot exec bootstrap', () => {
       throw Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES', errno: -13 })
     })
     await runSpawnRunner(files.requestPath, ['--', 'tool'], hostArgument(new FakeRunnerHost()), internals({ execve }))
-    expect(execve.mock.calls.map(call => call[0])).toEqual(['/bin/tool', '/usr/bin/tool'])
+    expect(execve.mock.calls.map(call => call[0])).toEqual(['/usr/bin/tool', '/bin/tool'])
     expect(readLinuxStartupError(files.startupErrorPath)).toMatchObject({
       type: 'error',
       error: {