Skip to content

safe-bash: heredoc-chain exec loop → fatal uncatchable OOM under default limits on a 128 MB isolate #614

Description

@kamilio

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:917parseShellUnit builds a fresh Parser per unit
  • packages/safe-bash/src/shell/parser.ts:118-131Lexer 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions