Skip to content

safe-bash: child command dispatch (env/timeout/xargs) allocates ~726 KB per call — 300 × env true = fatal OOM at 128 MB isolate #632

Description

@kamilio

Where: packages/safe-bash/src/shell/runtime.ts — the child-dispatch path used by env <cmd>, timeout <cmd>, and xargs (invokeChild at :1218, shebangTarget/shebangStage at :3028+, Runtime construction at :1255). Each child dispatch allocates ~726 KB transient (measured, --expose-gc diff: 300 × env true → peak +242 MB, +4 KB/iter retained after GC). Plain builtins cost ~7 KB/iter; function calls ~30 KB/iter — the invoke path is 25–100× heavier.

PoC (Node 22, --max-old-space-size=128 Workers-isolate proxy):

for i in $(seq 1 300); do env true; done

fatal V8 OOM at 128 MB heap (uncatchable). Calibration: 150 iters → +43 MB, exit 0; 200 iters → +91 MB; 300 iters → OOM. Same via for i in $(seq 1 300); do timeout 5 true; done and via seq 1 5000 | xargs -n 1 true (+1.4 GB at default heap, OOMs even --max-old-space-size=4096 at ~5 000 dispatches). All budgets green: 300 commands is 3% of maxCommands, zero output, zero expansion bytes. GC cannot keep up because the allocation rate (~726 KB/iter, allocations held live across the child await) outruns the scavenger on a constrained heap.

Impact: (d) — a ~45-byte script kills a Workers isolate (destroying co-resident tenants' in-flight work) in < 1 s of wall clock. env/timeout/xargs are default-registered commands, and xargs is the canonical fan-out tool, so any tenant script that processes a few hundred items via xargs -n 1 or a shell loop around timeout is a reliable isolate killer. On Node it multiplies RSS ~300× vs. the work performed.

Fix: profile the per-dispatch allocation (prime suspects: AbortSignal.any + cancellation owner/boundary graph, InvocationScope chains, Runtime field initialization, per-child env object cloning at :3067 { ...context.env, ...options.env, PWD } — with a large env this alone is O(env) per dispatch). Then: reuse a pooled child-Runtime template, avoid re-cloning env when unchanged (share the parent record), and/or charge each dispatch a flat cost against a budget (e.g. maxCommands priced in KB) so the loop hits a catchable ShellLimitError before the heap does.

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

    Labels

    bugSomething is not working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions