fix(code-runtime-python): reject an oversized maxLogBytes at load instead of metering log capture at runtime

The child log ledger encodes an admitted entry to UTF-8 once to charge its
serialized cost, so a maxLogBytes approaching addressSpaceMb lets a legitimate
near-budget log entry breach RLIMIT_AS and die as worker-exit instead of
truncating. Two runtime fixes were tried and both traded one resource bound for
another: an exact serialized-cost check is either a full encode (the allocation
being avoided) or a per-character Python loop that burns the CPU budget (a 10 MB
write hits SIGXCPU under cpuSeconds:1). The breach is a property of the
maxLogBytes/addressSpaceMb pair, not any write, so reject the incompatible pair
at load — maxLogBytes must stay within one eighth of the addressSpaceMb byte
count — and revert _LogStream to its original character-count buffering, which
is memory-safe once the budget fits the address space. The check runs on every
platform since the incompatibility is a config-value property, not a runtime one.

Replace the child-flood regression tests (which asserted the reverted runtime
behavior) with a load-rejection test. The host-side accrueStrayCost UTF-8
per-lead validation and its tests are unaffected. Update the note and zh pair.
This commit is contained in:
Chinesezjc
2026-08-31 14:22:37 +08:00
committed by Tianyi Cui
parent c24e1e991b
commit 2df88b5bbe
6 changed files with 75 additions and 164 deletions
@@ -216,6 +216,18 @@ const FRAME_ENVELOPE_BYTES = 64
*/
const CLOSE_REAP_MARGIN_MS = 2_000
/**
* The largest fraction of `addressSpaceMb` that `maxLogBytes` may claim, enforced
* at load. The child's log ledger encodes an admitted entry to UTF-8 once to
* charge its serialized cost, so a near-budget entry transiently needs the entry
* plus its encode copy — roughly twice `maxLogBytes` — on top of the interpreter
* baseline, all under `RLIMIT_AS`. One eighth leaves an 8x margin over the raw
* budget, comfortably past that transient at any admissible cap, so a legitimate
* near-budget log entry truncates instead of breaching the address space. A fixed
* safety invariant tying two configs together, not a deployment knob.
*/
const LOG_CAPTURE_ADDRESS_SPACE_HEADROOM_FRACTION = 1 / 8
/**
* Interval between process-group liveness probes while settlement waits for an
* escalated SIGKILL to empty the group (see the `killing` branch in
@@ -676,6 +688,29 @@ export class PythonCodeRuntime extends CodeRuntime {
throw new Error(`dsh-code-runtime-python: config.${key} must not exceed ${limit} (a payload that large cannot cross the ${FRAME_CEILING_BYTES}-byte fd-3 frame ceiling, so the run would fail as worker-exit rather than output-limit), got ${String(this.config[key])}`)
}
}
// The child's log ledger admits an entry up to `maxLogBytes` and, to charge
// its serialized cost, encodes it to UTF-8 once — a transient allocation of
// up to `maxLogBytes` more bytes (and a control-char-dense entry escapes up
// to sixfold on the wire, though the encode itself is the raw copy). That
// copy happens under `RLIMIT_AS`, so a `maxLogBytes` that approaches
// `addressSpaceMb` makes a legitimate near-budget log entry breach the
// address space and die as `worker-exit` instead of truncating. Rather than
// meter every child write against the address space at runtime — which trades
// the memory bound for a per-character CPU cost on the hot path — reject the
// incompatible pair at load: require `maxLogBytes` to leave the child room
// for the interpreter baseline plus the entry and its encode copy. The bound
// is `LOG_CAPTURE_ADDRESS_SPACE_HEADROOM_FRACTION` of the address space, well
// clear of the ~2x-plus-baseline the push path needs at the default 64 KiB
// cap. Checked on every platform, not just where `RLIMIT_AS` is enforced: the
// incompatibility is a property of the two config values, and the child OOMs
// on a Linux deployment regardless of the host that assembled the config, so
// a uniform load-time rejection is the fail-loud contract (Darwin skips only
// the runtime `setrlimit`, not this static check).
const addressSpaceBytes = this.config.addressSpaceMb * 1024 * 1024
const logCaptureCeiling = Math.floor(addressSpaceBytes * LOG_CAPTURE_ADDRESS_SPACE_HEADROOM_FRACTION)
if (this.config.maxLogBytes > logCaptureCeiling) {
throw new Error(`dsh-code-runtime-python: config.maxLogBytes must not exceed ${logCaptureCeiling} (${LOG_CAPTURE_ADDRESS_SPACE_HEADROOM_FRACTION} of the ${addressSpaceBytes}-byte addressSpaceMb, leaving the child room to encode a near-budget log entry without breaching RLIMIT_AS), got ${String(this.config.maxLogBytes)}`)
}
ctx.effect(() => () => this.teardown(), 'python code-runtime teardown')
}