fix(code-runtime-python): bound stray capture by serialized cost, chunk-scan, and flush on destroy

The line-aggregating stray capture from the previous round regressed three
ways the review caught. Rewrite it on the fd-3 reader's raw-Buffer-chunk
shape: accumulate chunks with a byte counter and split on the raw 0x0a byte,
so a large newline-free write no longer re-copies the residual and re-scans
from index 0 per chunk (both O(N^2)). Meter each admitted entry by serialized
cost through a new jsonStringCostUpTo that walks to the cap and stops, so a
near-budget control-char-dense line never allocates the sixfold-inflated
JSON.stringify result the old ledger did (the critical: ~1.6 GiB transient
under a large maxLogBytes). Flush the residual explicitly in the closeDeadline
handler before it destroys the streams, so a setsid escapee's path (which
fires no end) does not drop a leader's final newline-free diagnostic.

Harden the sync-spawn leak assertion to a set difference against a pre-run
snapshot, immune to a parallel worker's concurrent tmpdir create/delete.

Decline the round-2 request to enforce the fd-3 ceiling per-frame: the counter
check must precede Buffer.concat to prevent ~2x memory doubling (two
regression tests assert this), and the batch-edge false reject it would fix is
reachable only at a maxLogBytes/maxValueBytes configured within one pipe read
of the 256 MiB ceiling, far past the defaults. Documented at the check and in
the note Alternatives.

Add flood, NUL-flood, short-escape, and closeDeadline-flush regression tests
(restoring per-file 100% coverage); update the Agent Note and zh pair.
This commit is contained in:
Chinesezjc
2026-08-31 14:21:57 +08:00
committed by Tianyi Cui
parent c8bf75cbe4
commit 8093d22164
6 changed files with 195 additions and 66 deletions
@@ -729,6 +729,40 @@ describe('PythonCodeRuntime — programs and bindings', () => {
expect(result.logs.join('').length).toBeLessThan(4096)
})
it('bounds a control-char-dense native residual by serialized cost, not raw length', async () => {
// A newline-free NUL flood passes the cheap `length + 3` lower bound at a
// raw length well under the budget, but each NUL serializes to `` (6
// bytes), so the true JSON cost is ~6x. The ledger must charge that
// serialized cost — and `jsonStringCostUpTo` must measure it WITHOUT
// allocating the escaped copy, so a near-budget line under a large
// maxLogBytes cannot momentarily allocate a multi-gigabyte `JSON.stringify`
// result. Under a small budget the residual is truncated once the serialized
// cost crosses it.
const { runtime } = await setup({ maxLogBytes: 4096 })
const result = await runtime.run({
program: ['import os', 'os.write(1, b"\\x00" * 4000)', 'return None'].join('\n'),
bindings: [],
})
expect(result.error).toBeUndefined()
expect(result.logs.at(-1)).toBe(logTruncationMarker(4096))
})
it('charges the exact serialized cost of short-escape and quote/backslash characters', async () => {
// Exercises every branch of jsonStringCostUpTo's per-character cost: a tab
// and other C0 controls with short JSON forms (\t etc., 2 bytes), a quote
// and backslash (2 bytes each), a `\uXXXX` control (6 bytes), a multibyte
// BMP character (raw UTF-8 width), and plain ASCII. Under a budget large
// enough to admit it, the line survives verbatim — proving the cost walker
// does not over- or under-charge and the string round-trips unescaped.
const { runtime } = await setup({ maxLogBytes: 4096 })
const result = await runtime.run({
program: ['import os', String.raw`os.write(1, "\ta\"b\\c\x01é\n".encode("utf-8"))`, 'return None'].join('\n'),
bindings: [],
})
expect(result.error).toBeUndefined()
expect(result.logs).toEqual(['\ta"b\\c\x01é'])
})
it('fails a completion dict with a non-string key as invalid-output (no key coercion)', async () => {
// json.dumps would coerce {1: "a", "1": "b"} to a single "1" key, silently
// dropping data. The shape validator rejects it before encoding.
@@ -2043,6 +2077,28 @@ describe('PythonCodeRuntime — budgets, termination, disposal', () => {
expect(elapsed).toBeLessThan(4_000)
}, 8000)
it('flushes a newline-free diagnostic when the closeDeadline forces settlement', async () => {
// A leader that writes an unterminated diagnostic via `os.write(1, ...)` and
// then exits, leaving a setsid orphan holding the pipes open, settles through
// the closeDeadline destroy() path — which fires no `end`. The residual must
// be flushed before destroy() drops it, or the diagnostic is lost from
// `logs`. The value is decided by the done frame; the diagnostic must survive.
const { runtime } = await setup({ graceMs: 100 })
const result = await runtime.run({
program: [
'import os, subprocess, sys',
'os.write(1, b"leader-diagnostic-no-newline")',
'subprocess.Popen([sys.executable, "-c", "import time; time.sleep(5)"],',
' start_new_session=True)',
'return "escaped"',
].join('\n'),
bindings: [],
})
expect(result.error).toBeUndefined()
expect(result.value).toBe('escaped')
expect(result.logs).toContain('leader-diagnostic-no-newline')
}, 8000)
it('reaps a same-group child that ignores SIGTERM and releases the pipes before close', async () => {
// The same-group counterpart to the setsid-orphan case above. A descendant
// left in the child's OWN process group (no setsid, so `kill(-pid)` reaches