Found in safe-bash deep pentest (v3). Verified with executed PoCs against current src/ on Node 22.
Severity: High — threat (d): fatal uncatchable OOM on a 128 MB isolate under default limits
Summary
The v2-C1 parse OOM is fixed for single-parse shapes (ParseBudget now fires), but the exec-loop variant is still open: Shell.#execute parses each unit with a fresh Parser, and every Lexer constructor scans all newlines of the whole source into newlineOffsets. A many-unit script (heredoc-chain shape) at ~1/3 of maxSourceBytes causes ~2.5 GB of large-array churn plus ~12–14 KB transient heap per executed command, killing a 128 MB isolate before maxCommands engages. cloudflareWorkerLimits mitigates; defaults do not.
Location
packages/safe-bash/src/shell/shell.ts:236,280 — per-unit parseShellUnit loop
packages/safe-bash/src/shell/parser.ts:917 — parseShellUnit builds a fresh Parser per unit
packages/safe-bash/src/shell/parser.ts:118-131 — Lexer constructor scans every newline of the whole source per unit
PoC (executed)
// 340 KB source — one third of the 1 MiB maxSourceBytes default
const src = "cat <<E\n".repeat(34000) + "E\n".repeat(34000);
await shell.exec(src); // default limits, node --max-old-space-size=128
Measured:
- N=34000 (340 KB) →
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory (uncatchable). Also fatal at N=16000 (160 KB).
- At a 512 MB cap, N=16000 peaks at 159 MB heap before
ShellLimitError(maxCommands) at 4.3 s. Plain E\n×16000 peaks 142 MB and survives the 128 MB cap at 127 MB — the heredoc shape dies; differentiator is per-unit newlineOffsets churn (32000-element array × ~10k units).
- Standalone
parseShell of the same source is clean (maxParseUnits at 49 MB/61 ms) — the blow-up is exec-loop-specific.
- With
cloudflareWorkerLimits: N=16000 → maxCommands at 547 ms/70 MB peak; N=34000 → maxSourceBytes in 4 ms. Mitigated.
Caveat: fatal threshold depends on V8 GC heuristics at the 128 MB cap; workerd GC behavior may differ (same caveat as v2).
Suggested fix
Reuse one Parser/Lexer across exec units (incremental lexing) or build newlineOffsets lazily/bounded; charge the newline scan to ParseBudget; consider lowering the default maxCommands or the per-command transient footprint for small-heap deployments.
Status: v2-C1 (heredoc-chain variant) STILL OPEN, transformed
Found in safe-bash deep pentest (v3). Verified with executed PoCs against current
src/on Node 22.Severity: High — threat (d): fatal uncatchable OOM on a 128 MB isolate under default limits
Summary
The v2-C1 parse OOM is fixed for single-parse shapes (
ParseBudgetnow fires), but the exec-loop variant is still open:Shell.#executeparses each unit with a freshParser, and everyLexerconstructor scans all newlines of the whole source intonewlineOffsets. A many-unit script (heredoc-chain shape) at ~1/3 ofmaxSourceBytescauses ~2.5 GB of large-array churn plus ~12–14 KB transient heap per executed command, killing a 128 MB isolate beforemaxCommandsengages.cloudflareWorkerLimitsmitigates; defaults do not.Location
packages/safe-bash/src/shell/shell.ts:236,280— per-unitparseShellUnitlooppackages/safe-bash/src/shell/parser.ts:917—parseShellUnitbuilds a freshParserper unitpackages/safe-bash/src/shell/parser.ts:118-131—Lexerconstructor scans every newline of the whole source per unitPoC (executed)
Measured:
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory(uncatchable). Also fatal at N=16000 (160 KB).ShellLimitError(maxCommands)at 4.3 s. PlainE\n×16000 peaks 142 MB and survives the 128 MB cap at 127 MB — the heredoc shape dies; differentiator is per-unitnewlineOffsetschurn (32000-element array × ~10k units).parseShellof the same source is clean (maxParseUnitsat 49 MB/61 ms) — the blow-up is exec-loop-specific.cloudflareWorkerLimits: N=16000 →maxCommandsat 547 ms/70 MB peak; N=34000 →maxSourceBytesin 4 ms. Mitigated.Caveat: fatal threshold depends on V8 GC heuristics at the 128 MB cap; workerd GC behavior may differ (same caveat as v2).
Suggested fix
Reuse one Parser/Lexer across exec units (incremental lexing) or build
newlineOffsetslazily/bounded; charge the newline scan toParseBudget; consider lowering the defaultmaxCommandsor the per-command transient footprint for small-heap deployments.Status: v2-C1 (heredoc-chain variant) STILL OPEN, transformed