ソースを参照

test(code-runtime-python): cover the boot-ack gate's re-entry guard and run-write failure

The review rejected the v8-ignore defense for the ack gate: a forged second
boot-ack is deterministically constructible (one os.write on fd 3) and the
run-write failure is deterministically constructible with the boot-write-failure
mock pattern. A program that forges an extra boot-ack asserts the run still
completes once (the gate does not re-send the run frame); a mocked child whose
fd-3 pipe accepts the boot frame but rejects the run write resolves a
worker-exit.
Chinesezjc 2 週間 前
コミット
3e0055edaf

+ 40 - 0
packages/code-runtime/code-runtime-python/tests/boot-write-failure.spec.ts

@@ -47,6 +47,30 @@ afterEach(() => {
   spawnMock.mockReset()
 })
 
+/** A child whose fd-3 pipe accepts the boot write, then rejects the run write. */
+function fakeChildWithAckThenThrowingFd3(): EventEmitter {
+  const child = new EventEmitter() as EventEmitter & {
+    pid?: number
+    stdout: PassThrough
+    stderr: PassThrough
+    stdio: unknown[]
+  }
+  child.stdout = new PassThrough()
+  child.stderr = new PassThrough()
+  const proto = new PassThrough()
+  let writes = 0
+  proto.write = () => {
+    writes += 1
+    if (writes === 1) return true // The boot frame goes out.
+    throw Object.assign(new Error('EPIPE: broken pipe, write'), { code: 'EPIPE' })
+  }
+  child.stdio = [new PassThrough(), child.stdout, child.stderr, proto]
+  // Emit the boot-ack after the boot write, so the run-frame write fires and
+  // hits the throwing pipe.
+  setImmediate(() => proto.emit('data', Buffer.from('{"type":"boot-ack"}\n')))
+  return child
+}
+
 describe('PythonCodeRuntime — boot-write failure', () => {
   it('resolves a worker-exit when the fd-3 boot write throws (no TDZ ReferenceError)', async () => {
     // Before the fix, the boot-write block ran BEFORE `wallTimer`, `onAbort`,
@@ -98,4 +122,20 @@ describe('PythonCodeRuntime — boot-write failure', () => {
     expect(existsSync(dirname(stagedBootstrap as string))).toBe(false)
     await fiber.dispose()
   })
+
+  it('resolves a worker-exit when the run write after boot-ack throws', async () => {
+    // The run frame goes out from the boot-ack handler; a pipe that accepts
+    // the boot frame but rejects the run write must settle the run as a
+    // worker-exit rather than reject run() or leave it hanging.
+    spawnMock.mockImplementation(() => fakeChildWithAckThenThrowingFd3())
+    const ctx = new Context()
+    const fiber = await ctx.plugin(PythonCodeRuntime)
+    const runtime = ctx.codeRuntime as InstanceType<typeof PythonCodeRuntime>
+
+    const result = await runtime.run({ program: 'return 1', bindings: [] })
+
+    expect(result.error?.kind).toBe('worker-exit')
+    expect(result.error?.message).toContain('failed to boot python subprocess')
+    await fiber.dispose()
+  })
 })

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

@@ -164,6 +164,27 @@ describe('PythonCodeRuntime — seam descriptors and misuse', () => {
     }
   }, 45_000)
 
+  it('ignores a forged second boot-ack without re-sending the run frame', async () => {
+    // The run frame is sent once, from the first boot-ack; a program that
+    // forges an extra boot-ack frame on fd 3 must not re-enter the gate (a
+    // second run frame would confuse the child's frame reader). The honest
+    // child sends exactly one ack; the forged one exercises the re-entry
+    // guard.
+    const { runtime } = await setup()
+    const result = await runtime.run({
+      program: [
+        'import os',
+        // One forged boot-ack after the program starts; the run already went
+        // out on the real ack.
+        "os.write(3, b'{\"type\":\"boot-ack\"}\\n')",
+        'return "done"',
+      ].join('\n'),
+      bindings: [],
+    })
+    expect(result.error).toBeUndefined()
+    expect(result.value).toBe('done')
+  }, 15_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