Bundled ugrep balloons to 9–14 GB RSS compiling a bounded-interval BRE (plain grep is transparently routed to it)

Status Fixed / completed
Reported on v2.1.220
Maintainer reply None cached
Activity 8 comments · opened Aug 2, 2026 · closed Aug 20, 2026

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 the claude binary as ugrep, so ordinary grep calls 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.

View original on GitHub ↗

3 Comments

developerinlondon · 28 days ago

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's exceeds complexity limits guard 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 limits error 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.

developerinlondon · 28 days ago

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.

ErikEremenko · 26 days ago

Corroborating on aarch64, still present in 2.1.221.

Same shape as your repro — bundled ugrep under -G -o with 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=memory on the kernel cmdline, and this box also runs vm.overcommit_memory=1. With no memory cgroup controller, systemd-oomd and Docker --memory limits are silently inert, and reclaim always "succeeds" by evicting page cache — so no allocation ever fails and the kernel OOM killer never fires. Zero oom-kill events across every occurrence.

The consequence is worse than an OOM kill:

  • the runaway is never terminated (7.0 GB, still alive at 775 s)
  • page cache collapses (3.1 GB → 24 MB), swap fills to 99%, and the box enters refault thrash at ~182 MB/s with 45+ processes in D-state
  • load reaches 50–90 and a watchdog resets the machine

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 -v observation 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.

Showing cached comments. Read the full discussion on GitHub ↗