Bundled ugrep balloons to 9–14 GB RSS compiling a bounded-interval BRE (plain grep is transparently routed to it)
Environment
- Claude Code 2.1.220, Linux x86_64
- Bundled
ugrep 7.5.0 x86_64-pc-linux-gnu +sse2; -P:pcre2jit - Claude Code's shell integration installs a
grep()function that re-execs theclaudebinary asugrep, so ordinarygrepcalls made by agent Bash tool calls run this bundled ugrep.
Repro
Zero input needed — the blowup is in pattern compilation:
bash -c 'exec -a ugrep ~/.local/bin/claude -G -o "\"content\":\"[^\"]*coder[^\"]\{0,300\}" /dev/null'
RSS climbs past 9.3 GB within 60 s (still growing when killed). Against a real 11 MB file (longest line 28 KB) it reached 14.3 GB max RSS in ~95 s.
GNU grep, same pattern, same 11 MB file: 8 MB RSS, <10 ms.
| Tool | Input | Max RSS | Time |
|---|---|---|---|
| bundled ugrep -G -o | /dev/null | >9.3 GB | killed at 60 s |
| bundled ugrep -G -o | 11 MB jsonl | 14.3 GB | ~95 s |
| GNU grep -o | 11 MB jsonl | 8 MB | <10 ms |
The trigger appears to be the bounded interval \{0,300\} after a negated class under -G — presumably DFA/state-set construction exploding on the counted repetition.
Curiously, under ulimit -v 2097152 the same invocation exits 0 almost immediately, so an internal allocation-failure path already avoids the explosion — a default cap on pattern-compile memory would likely be a small fix.
Impact
An agent session issued an innocuous-looking grep -o '"content":"[^"]*coder[^"]\{0,300\}' session.jsonl via the Bash tool; the shell integration routed it to the bundled ugrep, which grew to 11 GB and pushed a 125 GB host into memory-pressure thrash (swap exhausted, sibling Claude Code sessions frozen in D-state reclaim for ~15 minutes until the process was killed).
Since agents generate grep patterns freely, a pattern-compile memory/complexity guard (or falling back to system grep for interval-heavy BREs) would prevent a single tool call from taking out the host.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Root-caused and fixed upstream. The blowup is in ugrep's DFA construction: a counted repetition overlapping a preceding repeat (
[^"]*x[^"]\{0,300\}) legitimately explodes the DFA, but ugrep'sexceeds complexity limitsguard only counts states, and each state carries ~2.5 KB of iteration-tagged positions — so the guard fires only after ~14 GB / 95 s. Reproduced on ugrep 7.8.3 (latest).Filed Genivia/ugrep#555 with measurements; fix submitted as Genivia/ugrep#556 — a position-count cap feeding the existing error path, which turns this case into a clean
exceeds complexity limitserror in 0.59 s at 155 MB. Once merged upstream, bumping the bundled ugrep picks it up. Until then, a session-level cgroup memory cap on the CLI is an effective host-side mitigation.Cross-referencing prior sightings of the same underlying defect that were filed as platform-specific symptoms: #64133 (macOS, 8 GB+ on bounded repetition over minified input) and #54394 (WSL2, embedded ugrep wrapper OOM freezing the host). Root cause and upstream fix are in this issue: Genivia/ugrep#555 / PR Genivia/ugrep#556.
Corroborating on aarch64, still present in 2.1.221.
Same shape as your repro — bundled ugrep under
-G -owith a\{0,300\}bounded interval — reached 7.0 GB RSS and was still growing 775 s later on an 8 GB Raspberry Pi 5.(My sampler truncated argv at 200 chars, so I can't show the full pattern. The visible portion is
-G … -o .\{0,300\}worklist.and it was almost certainly followed by a second interval, consistent with #67021.)Why I'm adding a data point rather than another duplicate: on this host there was no OOM kill at all.
Raspberry Pi OS ships
cgroup_disable=memoryon the kernel cmdline, and this box also runsvm.overcommit_memory=1. With no memory cgroup controller,systemd-oomdand Docker--memorylimits are silently inert, and reclaim always "succeeds" by evicting page cache — so no allocation ever fails and the kernel OOM killer never fires. Zerooom-killevents across every occurrence.The consequence is worse than an OOM kill:
Because it dies by watchdog reset there is no shutdown sequence, pstore is empty, and nothing is logged — it presents as spontaneous hardware resets. It took several weeks and a purpose-built per-process RSS sampler to attribute it to ugrep at all, because the crash destroys its own evidence.
So "the OOM killer will contain it" doesn't hold universally. I think that strengthens the case for the pattern-compile memory cap you suggest — on a host like this it's the only layer that works.
Worth noting your
ulimit -vobservation held here too: an address-space cap makes ugrep's allocation-failure path exit cleanly rather than explode, so it's an effective interim mitigation for anyone hitting this.Environment: Claude Code 2.1.221 (npm install, Node v22.22.2), Raspberry Pi 5, aarch64, 8 GB RAM + 2 GB swap, Debian 12 (bookworm), kernel 6.12 series.