Skip to content

[BUG] v2.1.117 embedded ugrep wrapper amplifies regex backtracking from grep-process-OOM into V8-heap-OOM (8 GB ceiling) — host freezes on WSL2 #54394

Description

@dowdys

Summary

The v2.1.117 change ("Native builds on macOS and Linux: the Glob and Grep tools are replaced by embedded bfs and ugrep available through the Bash tool") routes every grep shell invocation through claude.exe (via exec -a ugrep in the shell-snapshot wrapper). Each grep therefore runs inside a fresh claude.exe process carrying its full V8 heap (~200 MB idle, 8 GB ceiling).

When a regex with catastrophic-backtracking shape runs against a long line (multi-MB minified HTML, a long line in a session jsonl, etc.), V8 holds the match buffer and RSS climbs to 8–15 GB before either the V8 ceiling fires or the host runs out of RAM. Pre-2.1.117, the same bad regex against the same input would run in system GNU grep (a small C process, ~30 MB peak), be slow, and the user would Ctrl+C. Post-2.1.117, same shape, same input, the host freezes on swap thrash until manual reboot.

This has caused 3 host freezes in 3 days on my WSL2 box.

Reproduction

Minimum failing case (works against any multi-MB single-line HTML body):

# Get a representative single-line minified HTML body (anything 1+ MB will do)
curl -sk -o /tmp/big.html https://www.meetscoresonline.com/  # or any single-line minified site

# Run a regex with multiple unbounded quantifiers — the shape that catastrophically backtracks
grep -oE '"a":"[^"]*"|"b":"[^"]*"|"c":"[^"]*"|"d":"[^"]*"|"e":"[^"]*"|"f":"[^"]*"|"g":"[^"]*"' /tmp/big.html | head -20

Watch top / ps. The grep process (which is actually claude.exe-as-ugrep under the wrapper) climbs to 8+ GB RSS within seconds. On a small box, swap fills, host freezes.

In production, this happens via Claude (the model) writing patterns like:

curl -sk "$URL" 2>&1 | grep -oE '"EventName":"[^"]*"|"MeetCity":"[^"]*"|"MeetState":"[^"]*"|"HostClub":"[^"]*"|"StatusText":"[^"]*"|"meetfromdate":"[^"]*"|"meettodate":"[^"]*"|2026 AAU MV[^<\"]*' | head -20

— a perfectly reasonable-looking JSON-extraction pattern. The model has no way to know that the multiple [^"]* alternations turn pathological under ugrep on a long line, because in every prior environment (including pre-2.1.117 Claude Code), the same pattern would just be slow, not lethal.

Expected vs Actual

Expected: A regex with bad shape is slow but bounded — at worst, the user Ctrl+Cs and the grep process dies with no host impact.

Actual: The grep process is claude.exe with V8 heap. Heap balloons to 8+ GB. On hosts with <16 GB RAM, swap saturates and the host becomes unresponsive. On hosts with more RAM, the V8 ceiling at ~8.2 GB triggers an internal OOM. Either way: significantly worse outcome than pre-wrapper grep.

Multiple existing issues report related "memory leak" / OOM symptoms — #4953, #11155, #25926, #27421, #30470 among others. I believe a meaningful fraction of these are this same root cause: V8 heap holding a tool result that wouldn't have been a problem in C-process grep.

Environment

  • Claude Code 2.1.119 → 2.1.121 (issue first reproduced 2026-04-26 on 2.1.119; reproduced again on 2.1.121 today)
  • WSL2, Linux 6.6.87.2-microsoft-standard-WSL2, systemd 255, cgroup v2
  • Ubuntu 24.04
  • 19 GB RAM, 8 GB swap
  • Node v22.20.0
  • Bash (zsh has the same issue per the snapshot's parallel branch)

Root Cause

The shell snapshot at ~/.claude/shell-snapshots/snapshot-bash-*.sh defines (verbatim):

function grep {
  local _cc_bin=\"\${CLAUDE_CODE_EXECPATH:-}\"
  [[ -x \$_cc_bin ]] || _cc_bin=/home/goduk/.local/bin/claude
  if [[ ! -x \$_cc_bin ]]; then command grep \"\$@\"; return; fi
  if [[ -n \$ZSH_VERSION ]]; then
    ARGV0=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 \"\$@\"
  elif ...
  else
    ( exec -a ugrep \"\$_cc_bin\" -G --ignore-files --hidden -I --exclude-dir=.git --exclude-dir=.svn ... \"\$@\" )
  fi
}

The intent — gitignore-aware, binary-skipping search via embedded ugrep — is good for performance. The unintended consequence is that V8's process memory profile is now the per-grep memory profile.

Suggested fixes (in increasing order of effort)

  1. Run the embedded matcher under a memory rlimit / setrlimit guard before invoking the search loop. Cap RSS at, say, 1 GB for matcher invocations. ugrep itself needs nowhere near that for any legitimate search; the ceiling exists only to bound pathological backtracking.

  2. Switch the embedded matcher to a non-backtracking engine (RE2, Hyperscan, or Rust's regex crate). ripgrep already does this — it's the default for the Bash tool's rg shim per the changelog. Aligning ugrep on RE2 (or replacing it with rg for the grep shim too) would eliminate the catastrophic-backtracking class entirely while preserving feature parity.

  3. Document the failure mode in the changelog and recommend a user-side cgroup wrapper (others have shipped one — see zenn.dev/tjst_t/articles/260219-claude-code-cgroup-memory-limit). Short-term mitigation while a real fix lands.

Related issues

Workaround I'm shipping locally

While waiting for an upstream fix, I'm wrapping every claude.exe invocation in a systemd-run --user --scope cgroup with MemoryMax=2G for matcher invocations and MemoryMax=5G for sessions, plus a PreToolUse hook that detects multi-quantifier regex shapes and refuses to run them. I'll happily share details if useful for the upstream fix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:bashbugSomething isn't workinghas reproHas detailed reproduction stepsperf:memoryplatform:wslIssue specifically occurs on WSLstaleIssue is inactive

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions