From cb26dd3804ce7ec7e70185faf8e497f511f35697 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 26 Aug 2026 02:57:00 +0800 Subject: [PATCH] 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). --- .../code-runtime-python/src/index.ts | 2 ++ .../code-runtime-python/tests/runtime.spec.ts | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/code-runtime/code-runtime-python/src/index.ts b/packages/code-runtime/code-runtime-python/src/index.ts index d1f795ff15..eebc1efcbb 100644 --- a/packages/code-runtime/code-runtime-python/src/index.ts +++ b/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)}` } }) } } diff --git a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts index b12171ce11..61f2d68a5e 100644 --- a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts +++ b/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