Where: packages/safe-bash/src/shell/runtime.ts ~:4583-4620 — parameterPattern tries every prefix/suffix length and calls matchTokens (src/shell/pattern.ts:80+) per candidate; for a *-containing pattern each attempt is itself linear, so the removal is O(n²) at 2 µs/step. The work ledger is sized limit*4+1024 = 64 M units (:4586), far more than 30 s can process, so maxCpuMs/maxWallClockMs is the binding limit. Interruptible (abort honored at the deadline).
PoC (~60 bytes, default limits):
x=$(printf "%05000d" 0); echo ${x#*1} | wc -c
Variants: ${x##*1}, ${x%1*}, ${x/#*1/}, ${x//*1/}.
Measured: 1 KB → 691 ms; 3 KB → 9 958 ms (${x##*1} 7 026 ms, ${x/%*1/} 10 808 ms; re-verified independently: 9 887 ms); 5 KB → maxWallClockMs at 30 003 ms; 10 KB … 1 MB → maxCpuMs/maxWallClockMs at ~30 s, result never produced. Control: case $x in *1) on the same 3 KB value → 3–18 ms.
Impact: (d) — a 60-byte script pins the thread for the full wall clock per exec, loopable across execs. Same class as closed #623/#634 (under-priced work ledger), different mechanism.
Fix: for *-anchored removals compute the match once (single backtracking scan with an end-position search for longest/shortest) instead of trying every split; or charge length per candidate against a ledger sized to real CPU (≈1–2 M units), not 4 × maxExpansionBytes.
Found in security audit v3 (2026-09-07).
Where:
packages/safe-bash/src/shell/runtime.ts~:4583-4620—parameterPatterntries every prefix/suffix length and callsmatchTokens(src/shell/pattern.ts:80+) per candidate; for a*-containing pattern each attempt is itself linear, so the removal is O(n²) at2 µs/step. The work ledger is sizedlimit*4+1024= 64 M units (:4586), far more than 30 s can process, somaxCpuMs/maxWallClockMsis the binding limit. Interruptible (abort honored at the deadline).PoC (~60 bytes, default limits):
Variants:
${x##*1},${x%1*},${x/#*1/},${x//*1/}.Measured: 1 KB → 691 ms; 3 KB → 9 958 ms (
${x##*1}7 026 ms,${x/%*1/}10 808 ms; re-verified independently: 9 887 ms); 5 KB →maxWallClockMsat 30 003 ms; 10 KB … 1 MB →maxCpuMs/maxWallClockMsat ~30 s, result never produced. Control:case $x in *1)on the same 3 KB value → 3–18 ms.Impact: (d) — a 60-byte script pins the thread for the full wall clock per exec, loopable across execs. Same class as closed #623/#634 (under-priced work ledger), different mechanism.
Fix: for
*-anchored removals compute the match once (single backtracking scan with an end-position search for longest/shortest) instead of trying every split; or chargelengthper candidate against a ledger sized to real CPU (≈1–2 M units), not4 × maxExpansionBytes.Found in security audit v3 (2026-09-07).