diff --git a/packages/code-runtime/code-runtime-python/src/index.ts b/packages/code-runtime/code-runtime-python/src/index.ts index 2672a39d42..85e1320fdf 100644 --- a/packages/code-runtime/code-runtime-python/src/index.ts +++ b/packages/code-runtime/code-runtime-python/src/index.ts @@ -191,19 +191,6 @@ function materializePyScripts(): string { return join(dir, 'bootstrap.py') } -/** - * The fd-3 receive cap for one unframed line: a pure host-memory-safety - * bound, NOT an output budget. Binding `call` frames legitimately carry large - * arguments (the seam puts no byte cap on binding traffic), so the cap - * must sit far above any plausible frame while still stopping a hostile - * newline-free flood from growing the host accumulator without bound — the - * child's RLIMIT_AS bounds the child, not the host string. 64 MiB mirrors - * the order of the worker backend's default outer-output cap while keeping - * decode amplification (see FRAME_PARSE_CAP_BYTES) a bounded factor of the - * wire bytes; completion values have their own `maxValueBytes` check at the - * `done` handler, deliberately decoupled from this. Not a config knob because - * it is an internal framing invariant, not a deployment choice. - */ /** * A frame's RAW length is capped before JSON.parse: the 64 MiB fd-3 frame * parse cap bounds the bytes, not the decoded structure, and a compact wide @@ -1322,17 +1309,16 @@ export class PythonCodeRuntime extends CodeRuntime { // pipe read of the 64 MiB cap, orders of magnitude past the 32/64 KiB // defaults. // - // The cap used HERE is FRAME_PARSE_CAP_BYTES, not the old 256 MiB - // wire ceiling, and ONLY when the held bytes are still a single unframed - // line (this chunk carries no newline, and earlier newline-bearing - // chunks were joined immediately): a frame between 64 MiB and the - // ceiling would otherwise be fully `Buffer.concat`-ed (a second copy - // of its bytes) and only then dropped in the line loop — the - // peak-memory doubling this pre-concat check exists to prevent. - // Dropping the oversized unframed buffer before the join keeps the - // peak at one copy of the wire bytes. When this chunk DOES carry a - // newline the buffer holds several frames, so the FIRST-FRAME check - // below (not this counter, which charges them all) decides. + // The cap is enforced ONLY when the held bytes are still a single + // unframed line (this chunk carries no newline, and earlier + // newline-bearing chunks were joined immediately): a frame past the cap + // would otherwise be fully `Buffer.concat`-ed (a second copy of its + // bytes) and only then dropped in the line loop — the peak-memory + // doubling this pre-concat check exists to prevent. Dropping the + // oversized unframed buffer before the join keeps the peak at one copy + // of the wire bytes. When this chunk DOES carry a newline the buffer + // holds several frames, so the FIRST-FRAME check below (not this + // counter, which charges them all) decides. if (pendingBytes > FRAME_PARSE_CAP_BYTES && !chunk.includes(0x0a)) { pendingChunks = [] sealedBlocks = [] @@ -1376,8 +1362,8 @@ export class PythonCodeRuntime extends CodeRuntime { // serve here — it charges the whole buffer, which legitimately // holds several frames each within the cap. A first frame past the // cap is dropped before the join (one copy of its wire bytes); - // later frames in the same buffer are handled by the per-line check - // in the loop below. + // later frames in the same buffer are handled line by line in the + // loop below. let firstFrameLen = 0 let sawNewline = false // Sealed blocks hold newline-free prefixes only (see the sealing 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 4b0bbfe08d..b12171ce11 100644 --- a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts @@ -143,10 +143,12 @@ describe('PythonCodeRuntime — seam descriptors and misuse', () => { it('skips relative PATH entries when resolving a basename pythonBin', async () => { // resolvePythonBin must return an absolute path: a RELATIVE PATH entry - // ('.' here) would otherwise resolve the basename against the host CWD — - // spawn() then tries './python3' from the test process's directory, where - // no interpreter exists, surfacing an ENOENT worker-exit instead of a - // normal run. The relative entry is skipped and the absolute entry used. + // ('.' here) would otherwise resolve the basename against the host CWD. + // This run's CWD holds no executable named python3, so both the relative + // skip and the accessSync-miss fall through to the absolute entry — the + // case pins the contract (absolute candidate wins over a relative PATH + // prefix), not a worker-exit distinction, which would need an executable + // named python3 in the test CWD. const cp = await import('node:child_process') const nodePath = await import('node:path') const pythonDir = nodePath.dirname(cp.execFileSync('which', ['python3'], { encoding: 'utf8' }).trim()) @@ -4925,12 +4927,13 @@ describe('PythonCodeRuntime — hostile peer', () => { 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. + // reaches the join and its first-frame check; sealing it into a block + // would empty pendingChunks, leave sawNewline false, and skip that check. // 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. + // the oversized first frame (63.9 MiB of A's + 12289 more before the + // newline) exceeds FRAME_PARSE_CAP_BYTES no matter how it arrives — the + // case pins the worker-exit settlement, not a pre/post copy-count + // distinction (both orders reject an over-cap frame). const { runtime } = await setup({ maxWallMs: 60_000, addressSpaceMb: 2048 }) const result = await runtime.run({ program: [