grep shim (embedded ugrep 7.5.0) allocates unbounded memory on double-bounded-repeat BRE patterns — can OOM-kill the host
Bug report: grep shim (embedded ugrep) allocates unbounded memory on bounded-repeat BRE patterns — can OOM-kill the entire host
Summary
Claude Code's shell snapshot replaces grep with a function that re-execs the claude binary as an embedded ugrep 7.5.0 (exec -a ugrep "$CLAUDE_BIN" -G --ignore-files --hidden -I ...). On a ~1 MB input, a basic-regex pattern with bounded repeats on both sides of a literal (.\{0,120\}LITERAL.\{0,300\}) makes the embedded ugrep allocate memory without bound: observed 14.5 GB RSS in ~90 seconds (≈2.5 GB per 15 s) until manually killed. Under ulimit -v 2G it segfaults instead of failing gracefully.
Real-world impact: research subagents routinely download JS-heavy pages (webpack bundles, minified single-line files) and grep them. On a 15 GiB host this caused three kernel OOM incidents in one night (2026-07-10), twice killing the tmux server and the whole Claude Code session. The kernel OOM log names the victim by the versioned binary (Killed process ... (2.1.206)), which initially masked the root cause.
Environment
- Claude Code: 2.1.206 (native binary install,
~/.local/bin/claude) - Embedded grep:
grep --version(through the shim) →ugrep 7.5.0 x86_64-pc-linux-gnu +sse2; -P:pcre2jit; -z:zlib,bzip2,zstd,brotli,7z,tar/pax/cpio/zip - OS: Ubuntu 26.04 LTS, kernel 7.0.0-22-generic, x86_64, 8 cores / 15 GiB RAM + 31 GiB swap
- System grep (unaffected): GNU grep 3.12
Deterministic reproduction
Run inside a Claude Code Bash session (so that grep resolves to the snapshot shim function, not /usr/bin/grep):
# 1. Generate a ~1 MB single-line "minified bundle"
python3 -c 'open("synth.js","w").write("{\"a\":\"b\",\"c\":123,\"d\":\"eee\"," * 40000 + "\"Pricing\":\"/pricing\"}")'
# 2. Cap memory so the repro is safe, then grep with bounded repeats on both sides of a literal
( ulimit -v 2097152; grep -o '.\{0,120\}"Pricing".\{0,300\}' synth.js )
# → Segmentation fault (embedded ugrep crashes on allocation failure)
# 3. Without the ulimit the process instead grows ~2.5 GB / 15 s until the host OOMs
# (observed live: 14.5 GB RSS / 93% of RAM before manual kill -9)
Reproduced 6/6 times on the synthetic file (with -o, without -o, and with -c).
Narrowing
| Pattern (BRE, -G) | Result |
|---|---|
| .\{0,120\}"Pricing".\{0,300\} (bounds on both sides) | segfault under 2 G ulimit / unbounded growth without |
| .\{0,120\}"Pricing" (leading bound only) | OK, instant |
| "Pricing".\{0,300\} (trailing bound only) | OK, instant |
| "Pricing" (literal) | OK, instant |
| Same double-bounded pattern via GNU grep 3.12 | OK, < 1 s, low memory |
Additional observations:
-ois not required;grep -cwith the same pattern also blows up.- On a real-world 1.1 MB webpack bundle (single line) the behavior is nondeterministic: the exact same command sometimes completes instantly and sometimes balloons — the synthetic file above reproduces deterministically.
- A line-split (normalized) version of the real bundle also segfaulted under the ulimit, so extreme line length is a contributing factor but not strictly required.
- The shim function is only defined in interactive snapshot shells; nested
bash -c '...'silently falls back to GNU grep, which makes the failure look intermittent from an agent's perspective.
Why this is worse for agents than for humans
Autonomous subagents frequently: scrape a pricing/docs page → save the minified JS bundle → grep it with wide context windows exactly like .\{0,120\}"keyword".\{0,300\}. Nothing in the tool output warns that grep is not GNU grep. One such call can take down every session on the machine (kernel OOM killed the tmux pane scope; the tmux server exited with it).
Expected behavior
Any of:
- Embedded ugrep enforces a memory ceiling / fails gracefully with an error message instead of unbounded allocation and segfault;
- The shim falls back to
command grepwhen the embedded engine fails (it already falls back when the binary is missing); - At minimum, the pathological double-bounded-repeat case is fixed (likely an upstream ugrep 7.5.0 issue worth forwarding to Genivia/ugrep — GNU grep handles the same pattern fine).
Workarounds we use meanwhile
command grep ...for anything scraped/minified (bypasses the shim, deterministic);- pre-normalizing bundles:
sed -e 's/[;{}]/&\n/g' -e 's/,"/,\n"/g' file | fold -w 2000 > file.norm; - a watchdog that kills any
ugrep-argv process above 3 GiB RSS.
Kernel log excerpts (2026-07-10, UTC)
00:53:53 systemd[5982]: tmux-spawn-….scope: Failed with result 'oom-kill'. (first session death)
01:29:19 kernel: Out of memory: Killed process 3685903 (2.1.206) total-vm:17037916kB, anon-rss:3295124kB ...
01:29:20 systemd[5982]: tmux-spawn-….scope: Consumed ... 14.6G memory peak, 31.8G memory swap peak. (second session death)
01:45 live capture before third OOM: PID 3697348 `ugrep -G ... -o .\{0,120\}"Pricing".\{0,300\} main.js` at 14.5 GB RSS / 93% MEM