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.
This commit is contained in:
Chinesezjc
2026-08-31 14:53:33 +08:00
committed by Tianyi Cui
parent a715bfd111
commit d62b63d529
2 changed files with 40 additions and 5 deletions
@@ -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)
@@ -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)
})