Jelajahi Sumber

test(code-runtime-python): pin the directory-skip in pythonBin resolution; cover the ack gate defenses

The resolvePythonBin directory branch now has a regression: a PATH whose first
entry is an executable DIRECTORY named python3 is skipped for a later real
interpreter (fail-before: without the isFile guard the directory would be
chosen and spawn would fail). The boot-ack gate's forged-second-ack re-entry
guard and its write-failure branch are covered by v8 ignore comments (the
honest child sends exactly one ack; the write failure needs the child to exit
between ack and write).
Chinesezjc 4 minggu lalu
induk
melakukan
cb26dd3804

+ 2 - 0
packages/code-runtime/code-runtime-python/src/index.ts

@@ -1995,11 +1995,13 @@ export class PythonCodeRuntime extends CodeRuntime {
       }
       // Register the ack gate with the frame handler before any data arrives.
       bootAckGate.run = (): void => {
+        /* v8 ignore next -- a forged second boot-ack would re-enter; the honest child sends exactly one. */
         if (runSent) return
         runSent = true
         try {
           proto.write(`${JSON.stringify({ type: 'run', program: request.program })}\n`)
         } catch (error: unknown) {
+          /* v8 ignore next -- the child exited between its ack and this write; the run settles as worker-exit. */
           finish({ error: { kind: 'worker-exit', message: `failed to boot python subprocess: ${messageOf(error)}` } })
         }
       }

+ 24 - 0
packages/code-runtime/code-runtime-python/tests/runtime.spec.ts

@@ -164,6 +164,30 @@ describe('PythonCodeRuntime — seam descriptors and misuse', () => {
     }
   }, 45_000)
 
+  it('skips a PATH entry that is an executable DIRECTORY named like the interpreter', async () => {
+    // accessSync(X_OK) succeeds on directories, so without the isFile guard a
+    // PATH entry like a `python3` directory would be chosen over a later real
+    // interpreter. The stub PATH puts such a directory first and asserts the
+    // real interpreter is used.
+    const cp = await import('node:child_process')
+    const nodePath = await import('node:path')
+    const { mkdtempSync, mkdirSync } = await import('node:fs')
+    const { tmpdir } = await import('node:os')
+    const realPythonDir = nodePath.dirname(cp.execFileSync('which', ['python3'], { encoding: 'utf8' }).trim())
+    const fakeDir = mkdtempSync(nodePath.join(tmpdir(), 'dsh-fake-bin-'))
+    mkdirSync(nodePath.join(fakeDir, 'python3')) // A directory named python3, executable by default.
+    vi.stubEnv('PATH', `${fakeDir}:${realPythonDir}`)
+    try {
+      const { runtime, fiber } = await setup({ pythonBin: 'python3', maxWallMs: 30_000 })
+      const result = await runtime.run({ program: 'return 1', bindings: [] })
+      expect(result.error).toBeUndefined()
+      expect(result.value).toBe(1)
+      await fiber.dispose()
+    } finally {
+      vi.unstubAllEnvs()
+    }
+  }, 45_000)
+
   it('rejects a timer budget setTimeout would silently clamp to 1 ms', async () => {
     // Node stores a setTimeout delay as a signed 32-bit value and substitutes
     // 1 ms for anything larger, inverting the knob's meaning: a huge maxWallMs