Where: packages/safe-bash/src/commands/streams.ts:325-360 — tee opens every operand up front (targets.add(await openFileOutput(...)) in the operand loop) with no operand-count cap and no file-descriptor budget. Each open target retains stream/sink state (~14 KB heap per target measured) for the lifetime of the command. Only the per-command argv cap (64 KB) and maxExpansionFields (10 000) indirectly bound the count — and 64 KB of argv buys ~6 000–9 000 targets (/d/fNNNN).
PoC (Node 22, --max-old-space-size=128 as Workers-isolate proxy):
seq 1 9000 | xargs printf "/d/f%s\n" > /list; mkdir /d
printf "x" > /payload
tee $(cat /list) < /payload > /sink # 1-byte payload!
→ fatal V8 OOM at 128 MB heap (uncatchable). Calibration: 4 000 targets → exit 0, +49 MB heap; 6 000 → OOM. Note the output-byte budget never engages: the payload is 1 byte per file (9 KB total); the memory cost is purely per-open-target structural overhead. At default Node heap, 9 000 targets × 128 KiB payload = +252 MB transient.
Impact: (d) — a ~110-byte script kills a Workers isolate (taking co-resident tenants' in-flight work with it) with all byte budgets green; also an FD-exhaustion vector on real-fs backends. Same uncovered class as v2 H4 (pipeline stages) but on the tee write path; v1 #11 noted "uncapped tee operand count (2 000 targets, 25 ms)" without the memory-amplification measurement.
Fix: cap tee operands (e.g. maxTeeTargets ≈ 64) like the v2 maxPipelineStages fix; alternatively stream targets in bounded batches (open → write → close per window) and charge each open target to a descriptor/arena ledger.
Where:
packages/safe-bash/src/commands/streams.ts:325-360—teeopens every operand up front (targets.add(await openFileOutput(...))in the operand loop) with no operand-count cap and no file-descriptor budget. Each open target retains stream/sink state (~14 KB heap per target measured) for the lifetime of the command. Only the per-command argv cap (64 KB) andmaxExpansionFields(10 000) indirectly bound the count — and 64 KB of argv buys ~6 000–9 000 targets (/d/fNNNN).PoC (Node 22,
--max-old-space-size=128as Workers-isolate proxy):→ fatal V8 OOM at 128 MB heap (uncatchable). Calibration: 4 000 targets → exit 0, +49 MB heap; 6 000 → OOM. Note the output-byte budget never engages: the payload is 1 byte per file (9 KB total); the memory cost is purely per-open-target structural overhead. At default Node heap, 9 000 targets × 128 KiB payload = +252 MB transient.
Impact: (d) — a ~110-byte script kills a Workers isolate (taking co-resident tenants' in-flight work with it) with all byte budgets green; also an FD-exhaustion vector on real-fs backends. Same uncovered class as v2 H4 (pipeline stages) but on the tee write path; v1 #11 noted "uncapped
teeoperand count (2 000 targets, 25 ms)" without the memory-amplification measurement.Fix: cap tee operands (e.g.
maxTeeTargets≈ 64) like the v2maxPipelineStagesfix; alternatively stream targets in bounded batches (open → write → close per window) and charge each open target to a descriptor/arena ledger.