Bundled search helper allocates without bound on a BRE pattern with a literal | and -o (v2.1.233)

Status Fixed / completed
Reported on v2.1.233
Maintainer reply None cached
Activity 1 comment · opened Aug 19, 2026 · closed Aug 19, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

The bundled claude.exe search helper (invoked as ugrep) allocates memory without bound when given a BRE pattern containing a literal | together with -o, on inputs as small as ~110 bytes. Growth is linear at roughly 200–270 MB/s with no plateau — measured straight through 5 GB before being OOM-killed by a deliberately applied cgroup cap. Virtual size extends in exact 1,024.1 MB steps (1,347.5 → 2,371.5 → 3,395.7 → 4,419.8 → 5,444.0 MB), which looks like a 1 GiB arena being extended repeatedly rather than a bounded buffer being resized. The allocation is not proportional to input size.

This is reachable from an ordinary grep command: the shell snapshot sourced into Bash tool sessions defines grep as a shell function that exec -a ugreps the bundled claude.exe with -G and a fixed flag set prepended (type grep in such a shell reports grep is a function; passthrough to the system binary fires only for a narrow flag set). So a user typing a normal grep can trigger multi-gigabyte allocation without ever intending to invoke the bundled binary.

Real-world impact observed twice on one host, days apart: on a host where several Claude Code sessions share one memory cgroup, a single occurrence starved every co-tenant process for ~46 minutes. The cgroup sat at its MemoryHigh watermark with oom_kill=0 throughout — the kernel throttled reclaim rather than killing anything, so every session stalled while all conventional health checks reported them healthy. Both incidents reached an identical 7,492.2 MB virtual size, suggesting growth continues until something external stops it.

What Should Happen?

The helper should handle the pattern in milliseconds at a few MB, as the system grep does: /usr/bin/grep -o on the identical pattern and input completes in 0.00s at ~3 MB (exiting 1, correctly, since | is a literal character in BRE, not alternation). Failing that, a pathological pattern should fail or degrade with a clear error rather than allocating without limit — e.g. by bounding the match-buffer allocation in the -o BRE path, or by applying an address-space limit (RLIMIT_AS) when the binary self-invokes as the grep replacement, so the failure stays local to one command instead of stalling the host.

Error Messages/Logs

There is no error output from the helper itself — it allocates silently until killed. When run under a memory-capped systemd unit, the kill is visible only in the journal:


systemd[...]: run-....service: A process of this unit has been killed by the OOM killer.
systemd[...]: run-....service: Main process exited, code=killed, status=9/KILL
systemd[...]: run-....service: Failed with result 'oom-kill'.


Sampled growth (1-second cadence from /proc, 5 GB cap): RSS climbing ~200–270 MB/s, reaching 5,014 MB at ~23s before the kill. In the uncapped real-world incidents there was no log line at all — the shared cgroup throttled with `oom_kill=0`, which is what made it hard to diagnose.

Steps to Reproduce

Tested on Claude Code 2.1.233, Ubuntu 24.04.4 LTS x86_64, glibc 2.39. Run step 3 under a memory cap — uncapped it will consume all available RAM.

  1. Locate the bundled helper (installed with @anthropic-ai/claude-code; ~324 MB binary):
   CC_BIN="$(npm root -g)/@anthropic-ai/claude-code/bin/claude.exe"
  1. Control — the system grep on the identical pattern and input completes instantly (~3 MB, exit 1):
   /usr/bin/grep -o '.\{0,30\}|.\{0,30\}' <<< 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore ma'
  1. The bundled helper on the same pattern and input, with the argv the shell-snapshot grep function produces, inside a 1 GB cap:
   systemd-run --user --property=MemoryMax=1G --property=MemorySwapMax=0 \
     --property=RuntimeMaxSec=60 --collect --wait --pipe \
     /bin/bash -c "exec -a ugrep $CC_BIN -G --ignore-files --hidden -I --exclude-dir=.git -o '.\{0,30\}|.\{0,30\}' <<< 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore ma'"
  1. Observed: OOM-killed at the 1 GB cap in ~4 seconds (status=9/KILL in the journal). With a 5 GB cap it reaches 5,014 MB in ~23 seconds and is killed there — no plateau. Any short subject reproduces it (both lorem-ipsum and unrelated real text tested); the trigger is the pattern class (-G with -o and a literal | in the pattern), not the data.
  1. To see how an ordinary user reaches this path: in a Claude Code Bash tool session, type grepgrep is a function, whose body is ( exec -a ugrep "$CLAUDE_CODE_EXECPATH" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@" ) — so a plain grep -o '.\{0,30\}|.\{0,30\}' <<< "..." typed in a session runs the bundled helper with exactly the argv in step 3.

Claude Model

None

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

2.1.233 (Claude Code)

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

Other

Additional Information

Reproduces headless in a plain bash shell; terminal-independent. Two production occurrences on the same host (days apart) both reached an identical 7,492.2 MB VSZ — two more 1 GiB arena steps beyond where the capped test was killed. Full 1-second RSS/VSZ sampling data from capped-cgroup runs available on request, and happy to test candidate fixes on this platform. I have not characterised which other BRE constructs trigger the same path — a literal | with -o is simply the case observed in the wild.

View original on GitHub ↗

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