Просмотр исходного кода

fix(code-runtime-python): skip relative PATH entries in pythonBin resolution; pin the sealing-threshold rejection

The review's remaining code items:
- resolvePythonBin now skips RELATIVE PATH segments (a bare 'bin' or '.'): the
  returned candidate must be absolute, because spawn() resolves a relative
  pythonBin against the host CWD, outside the seam contract.
- A deterministic-ish regression pins the sealing-threshold corner: 64 MiB of
  4 KiB (<= PIPE_BUF, atomic) newline-free writes plus 12289 more A's before
  the first newline make the first frame exceed FRAME_PARSE_CAP_BYTES; the
  newline-bearing chunk reaches the first-frame check (sealing is the ELSE
  half of the newline branch), so the run reports worker-exit with the
  protocol-frame-exceeded message.
Chinesezjc 1 месяц назад
Родитель
Сommit
d62b63d529

+ 7 - 5
packages/code-runtime/code-runtime-python/src/index.ts

@@ -410,11 +410,13 @@ function resolvePythonBin(bin: string): string {
   /* v8 ignore next -- PATH is set in every environment the runtime boots in; the guard is defensive. */
   if (path === undefined) return bin
   for (const dir of path.split(delimiter)) {
-    // An empty PATH segment (a `::`, implicitly CWD on POSIX) is skipped so a
-    // basename never resolves against the working directory; normal PATHs
-    // carry no empty segment.
-    /* v8 ignore next -- normal PATHs carry no empty segment. */
-    if (dir === '') continue
+    // An empty PATH segment (a `::`, implicitly CWD on POSIX) and a RELATIVE
+    // segment (`bin` or `.`) are skipped: a basename must never resolve against
+    // the working directory, and the returned candidate must be an absolute
+    // path — spawn() resolves a relative pythonBin against the host CWD, which
+    // is outside the seam contract.
+    /* v8 ignore next -- normal PATHs carry no empty or relative segment. */
+    if (dir === '' || !isAbsolute(dir)) continue
     const candidate = join(dir, bin)
     try {
       accessSync(candidate, fsConstants.X_OK)

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

@@ -4901,4 +4901,37 @@ describe('PythonCodeRuntime — hostile peer', () => {
     expect(result.logs).toContain('after-cap-frames')
   }, 120_000)
 
+  it('rejects an oversized first frame that lands on the sealing threshold with a newline', async () => {
+    // The fragment-count seal runs only on newline-free chunks (the ELSE half
+    // of the newline branch), so a chunk that carries the first newline always
+    // reaches the join and its first-frame check. Fail-before: sealing that
+    // chunk into a block would empty pendingChunks, leave sawNewline false,
+    // skip the first-frame check, and join the oversized first frame whole.
+    // Whether the pipe delivers exactly 1024 chunks is timing-dependent, but
+    // the oversized first frame (63.9 MiB of A's + 12288 more before the
+    // newline) exceeds FRAME_PARSE_CAP_BYTES no matter how it arrives.
+    const { runtime } = await setup({ maxWallMs: 60_000, addressSpaceMb: 2048 })
+    const result = await runtime.run({
+      program: [
+        'import os',
+        // 4 KiB writes are <= PIPE_BUF, so each os.write is atomic and the
+        // host sees one chunk per write; 16384 of them accumulate 64 MiB of
+        // newline-free bytes (16 fragment-count seals of 1024 chunks).
+        'chunk = b"A" * 4096',
+        'for _ in range(16384):',
+        '    os.write(3, chunk)',
+        // 12289 more A's push the first frame past 64 MiB; drain-loop so the
+        // write cannot truncate, then a newline and a small legitimate frame.
+        "data = b'A' * 12289 + b'\\n' + b'{\"type\":\"log\",\"text\":\"after-seal\"}\\n'",
+        'view = memoryview(data)',
+        'while view:',
+        '    view = view[os.write(3, view):]',
+        'return "done"',
+      ].join('\n'),
+      bindings: [],
+    })
+    expect(result.error?.kind).toBe('worker-exit')
+    expect(result.error?.message).toContain('protocol frame exceeded')
+  }, 120_000)
+
 })