Bundled ugrep exhausts all RAM+swap on a two-sided bounded-quantifier regex; injected `grep` shell function routes every grep into it

Status Fixed / completed
Reported on v2.1.212
Maintainer reply None cached
Activity 1 comment · opened Jul 21, 2026 · closed Aug 20, 2026

Summary

Claude Code's shell snapshot installs a grep shell function that shadows
/usr/bin/grep and re-execs the Claude Code native binary as ugrep. That
bundled ugrep allocates without bound — segfaulting at any ulimit, or
consuming all system RAM and swap when unbounded — while compiling a regex
that contains two or more wide bounded quantifiers (e.g. .{0,120}foo.{0,120}).

Because the failure happens at regex-compile time, it is independent of input
size, flags, and recursion: it reproduces on a 17-byte file. Because the
shell function is injected transparently, a user who types an ordinary grep
command has no indication they are not running GNU grep — which handles the same
pattern instantly and correctly.

Impact observed: two such invocations reached 13.6 GB and 11.5 GB RSS,
consumed 25 of 31 GB RAM plus all 32 GB of swap, and rendered an entire Linux
desktop unusable for 40+ minutes (memory-pressure stall 84%, load average
220). The machine never recovered on its own — the kernel OOM killer did not fire,
because swap absorbed the leak into a thrashing livelock rather than an OOM event.

Environment

| | |
|---|---|
| OS | Ubuntu 24.04.4 LTS (Noble), kernel 6.8.0-124-generic, x86_64 |
| Hardware | 8 vCPU / 31 GiB RAM / 32 GiB swapfile (swappiness 60) |
| Claude Code | VS Code extension anthropic.claude-code-2.1.212-linux-x64 (also present: 2.1.216) |
| Offending binary | ~/.vscode/extensions/anthropic.claude-code-<ver>-linux-x64/resources/native-binary/claude, invoked as ugrep |
| Shell | bash 5.2 |

Root cause

~/.claude/shell-snapshots/snapshot-bash-*.sh defines:

grep () {
    # ... passthrough for a few flags ...
    local _cc_bin="${CLAUDE_CODE_EXECPATH:-}"
    [[ -x $_cc_bin ]] || _cc_bin="$HOME/.local/bin/claude"   # (path genericized)
    ( exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I \
        --exclude-dir=.git --exclude-dir=.svn --exclude-dir=.hg \
        --exclude-dir=.bzr --exclude-dir=.jj --exclude-dir=.sl "$@" )
}

So grep ... in any Claude Code Bash shell is really the bundled ugrep.

The bundled ugrep appears to expand a bounded repetition {0,N} into ~N DFA
states. A single such quantifier is linear and harmless; two of them multiply
(N×N) and detonate.
.* is a single loop state and never expands, which is why
the unbounded form is safe and the bounded form is not — the opposite of the
usual intuition about regex danger.

Minimal reproduction

⚠️ Run only with the ulimit/timeout guards below. Without them this will consume all RAM and swap and can livelock the machine.
cd "$(mktemp -d)"
printf 'hello xyzzy world\n' > tiny.txt          # 18 bytes
CC=~/.vscode/extensions/anthropic.claude-code-2.1.212-linux-x64/resources/native-binary/claude

run() {  # $1 = regex
  ( ulimit -v 2000000                            # hard 2 GB address-space cap
    timeout 15 env -i PATH=/usr/bin:/bin bash -c \
      "exec -a ugrep $CC -G --ignore-files --hidden -I -E \"\$1\" tiny.txt" _ "$1"
  ) >/dev/null 2>&1
  local e=$?
  printf '  %-26s exit=%-4s %s\n' "$1" "$e" "$([ $e -ge 128 ] && echo 'BLOWUP' || echo ok)"
}

run '.{0,120}xyzzy.{0,120}'   # BLOWUP  (exit 139, SIGSEGV at the 2 GB cap)
run '.{0,60}xyzzy.{0,60}'     # BLOWUP
run '.{0,40}0040.{0,40}'      # BLOWUP
run '.{0,40}xyzzy.{0,40}'     # ok      (borderline; literal-dependent)
run '.{0,30}xyzzy.{0,30}'     # ok
run '.{0,120}xyzzy'           # ok      single side, however wide
run 'xyzzy.{0,120}'           # ok      single side, however wide
run '.*xyzzy.*'               # ok      unbounded star never expands

# Control — real GNU grep handles every one of the above instantly:
/usr/bin/grep -oE '.{0,500}xyzzy.{0,500}' tiny.txt   # -> "hello xyzzy world", exit 0

Measured results

| Pattern | Bundled ugrep | /usr/bin/grep |
|---|---|---|
| .{0,120}xyzzy.{0,120} | 💥 SIGSEGV at 2 GB | ✅ instant |
| .{0,60}xyzzy.{0,60} | 💥 SIGSEGV at 2 GB | ✅ |
| .{0,40}0040.{0,40} | 💥 SIGSEGV at 2 GB | ✅ |
| .{0,40}xyzzy.{0,40} | ✅ (borderline) | ✅ |
| .{0,120}xyzzy (one side) | ✅ | ✅ |
| xyzzy.{0,120} (one side) | ✅ | ✅ |
| .*xyzzy.* | ✅ | ✅ |
| .{0,500}xyzzy.{0,500} | 💥 | ✅ |

Flags make no difference. The same pattern still blows up with no flags at
all, with -o -m 10, and with -c — confirming the allocation precedes any
matching or output.

Why this is worse than an ordinary regex-performance footgun

  1. Invisible substitution. The user types grep; a different engine with

different performance characteristics runs. Nothing in the output indicates
this. type grep is the only tell.

  1. It fails as a memory bomb, not an error. GNU grep, PCRE, and RE2 all

handle these patterns fine. There is no diagnostic, no bounded failure, and no
--max-memory-style guard — just unbounded growth until the machine dies.

  1. Inverted intuition. Practitioners are trained to fear unbounded .*. Here

.* is safe and bounded {0,N} is fatal, so the natural "make it safer by
bounding it" instinct actively causes the crash.

  1. Output caps don't help. -m, -c, and the Grep tool's head_limit all

look like mitigations and none of them are.

  1. No OOM rescue on a swap-backed host. With a large swapfile the kernel

absorbs the growth instead of OOM-killing, converting a would-be crash into a
multi-hour system-wide livelock.

Suggested fixes (in rough priority order)

  1. Cap memory in the bundled ugrep. Fail the query with a clear error rather

than allocating without bound. This alone downgrades the incident from
"machine lost" to "one command failed".

  1. Fix or bound the DFA expansion for repeated bounded quantifiers (cap the

expansion, fall back to an NFA/lazy-DFA path, or reject with a diagnostic).

  1. Make the substitution visible / opt-out. Users should be able to discover

that grep is not GNU grep, and to disable the shell-function injection.

  1. Fall back to the system binary when the bundled engine cannot handle a

pattern within its budget.

Local mitigations applied (for reference — not a substitute for a fix)

  • Always invoke /usr/bin/grep by absolute path in Bash, bypassing the function.
  • A PreToolUse hook that blocks command shapes carrying two or more wide bounded

quantifiers before they execute.

  • earlyoom installed, so a future runaway is killed in seconds rather than

livelocking the host.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗