[BUG] 2.1.243 linux-x64 exports malloc/free and interposes glibc's allocator, segfaulting on startup in __newlocale; 2.1.205 does not export them and works
What's wrong?
The linux-x64 binary for 2.1.243 segfaults immediately on startup. --version
crashes 100% of the time. The fault occurs before Bun's crash handler is
installed, so there is no Bun panic banner — the shell reports onlysegmentation fault (core dumped).
The core dump shows the fault reached through a pthread_once one-time init in
glibc, via __newlocale:
Signal: 11 (SEGV) si_code: SEGV_MAPERR
Stack trace of thread 118392:
#0 0x0000000001d10458 n/a (/tmp/cc-2.1.243 + 0x1b10458)
#1 0x00007f0e1943530a __newlocale (libc.so.6 + 0x3530a)
#2 0x0000000001abd282 n/a (/tmp/cc-2.1.243 + 0x18bd282)
#3 0x00007f0e1949d879 pthread_once (libc.so.6 + 0x9d879)
#4 0x0000000001abc2d2 n/a (/tmp/cc-2.1.243 + 0x18bc2d2)
ELF object binary architecture: AMD x86-64
This is a different signature from the JSC CachedTypes bytecode-decode crashes
reported elsewhere.
Older builds on the same machine work correctly:
| Version | Result |
|---|---|
| 2.1.139 | works |
| 2.1.205 | works |
| 2.1.243 | SIGSEGV, exit 139 |
Root cause
The binary exports the allocator symbols, interposing glibc's allocator
process-wide:
$ nm -D --defined-only /tmp/cc-2.1.243 | grep -wE 'malloc|calloc|realloc|free'
0000000001d102d0 T malloc@@GLIBC_2.2.5
0000000001d10320 T calloc@@GLIBC_2.2.5
0000000001d103c0 T realloc@@GLIBC_2.2.5
0000000001d10430 T free@@GLIBC_2.2.5
The crashing frame #0 is at 0x1d10458, which falls inside the binary's ownfree:
$ nm -S -D --defined-only /tmp/cc-2.1.243 | grep -w free
0000000001d10430 000000000000007e T free@@GLIBC_2.2.5
free spans 0x1d10430–0x1d104ae (size 0x7e). The fault at 0x1d10458 isfree + 0x28, 40 bytes in — about a third of the way through the function,
consistent with an early metadata lookup on the incoming pointer. Load bias is
0x200000, matching coredumpctl's module-relative +0x1b10458. Frame #1 is
glibc's __newlocale.
Sequence:
- The binary interposes
freefor the whole process. - glibc's startup path calls
__newlocale, which callsfreeon a pointer. - That call routes into the bundled allocator rather than glibc's.
- The bundled allocator does not own the pointer; its metadata lookup yields
NULL and is dereferenced unchecked.
- SIGSEGV at
si_addr=NULL, beforemain.
This is the standard interposed-allocator failure mode: freeing a pointer that
was allocated by a different allocator. Something in early glibc init allocates
through a path the bundled allocator never observed, and __newlocale frees it.
Note this reasoning does not depend on the glibc version. Only Claude Code
versions were varied in testing; whether 2.1.243 also crashes on older glibc is
untested. It may not be Arch-specific.
2.1.205 exports none of the allocator symbols. The samenm -D --defined-only query against 2.1.205, widened to include reallocarray,strdup, and strndup, returns no output. (It does export 575 symbols overall
— see the counts below — just none of these.)
| | 2.1.205 (works) | 2.1.243 (crashes) |
|---|---|---|
| Exported malloc/calloc/realloc/free | none | all four |
| Exported reallocarray | none | yes |
In 2.1.205 the bundled allocator stays private, so glibc's startup allocations
and frees are handled by a single consistent allocator. In 2.1.243 the allocator
is exported with default visibility and interposes process-wide, creating the
mismatch above.
Both builds export a dynamic symbol table, so --export-dynamic is not the
difference:
2.1.205 exported dynamic symbols: 575
2.1.243 exported dynamic symbols: 706
2.1.243 exports 131 more symbols than 2.1.205, with the allocator block among
them. That is consistent with a bundled allocator being built with its
override/interpose mode enabled rather than kept private — mimalloc, for
instance, has a build option that switches exactly this behaviour. It also
correlates with the 257 MB to 378 MB size increase.
Locale cause ruled out. en_US.utf8 is generated and present on this system,
and forcing LC_ALL=C LANG=C still segfaults identically:
$ LC_ALL=C LANG=C /tmp/cc-2.1.243 --version
zsh: segmentation fault (core dumped)
$ locale -a
C
C.utf8
en_US.utf8
POSIX
strace: fault occurs during dynamic-linker startup, before main
No syscall fails. Every mmap succeeds. The process dies immediately after the
runtime seeds its allocator, before the binary opens any of its own files:
mprotect(0x7f3e9b015000, 16384, PROT_READ) = 0
mprotect(0x7f3e9adfe000, 4096, PROT_READ) = 0
mprotect(0x7f3e9b108000, 4096, PROT_READ) = 0
mprotect(0x7f3e9b10d000, 4096, PROT_READ) = 0
mprotect(0x7f3e9b112000, 4096, PROT_READ) = 0
mprotect(0x7f3e9b185000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=16384*1024, ...}) = 0
getrandom("...", 8, GRND_NONBLOCK) = 8
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} ---
+++ killed by SIGSEGV (core dumped) +++
si_addr=NULL is a null-pointer dereference, not address-space exhaustion.
This rules out overcommit limits, vm.max_map_count, ASLR entropy /vm.mmap_rnd_bits, transparent hugepages, and any failed large reservation —
no syscall in the trace returns an error.
Build comparison: 2.1.205 (works) vs 2.1.243 (crashes)
Same shared-library dependency set, different build:
| | 2.1.205 | 2.1.243 |
|-----------------------|---------|---------|
| Binary size | 257 MB | 378 MB |
| Shared libs | identical | identical |
ldd for both:
linux-vdso.so.1
librt.so.1 => /usr/lib/librt.so.1
libc.so.6 => /usr/lib/libc.so.6
/lib64/ld-linux-x86-64.so.2
libpthread.so.0 => /usr/lib/libpthread.so.0
libdl.so.2 => /usr/lib/libdl.so.2
libm.so.6 => /usr/lib/libm.so.6
Note librt.so.1, libpthread.so.0, and libdl.so.2 have been empty
compatibility stubs since glibc 2.34 — true of both builds, so not a
differentiator.
The 131 symbols added in 2.1.243 fall into two groups.
Allocator override set (the full interpose surface):
malloc calloc realloc free
aligned_alloc _aligned_malloc posix_memalign __posix_memalign memalign
valloc pvalloc cfree vfree
strdup strndup
malloc_usable_size malloc_size malloc_good_size
reallocarr reallocarray reallocf
__libc_malloc __libc_calloc __libc_realloc __libc_free __libc_cfree
__libc_memalign __libc_valloc __libc_pvalloc
stdout@GLIBC_2.2.5 stderr@GLIBC_2.2.5
The combination of reallocarr, reallocf, malloc_good_size,_aligned_malloc, and vfree matches a mimalloc-style override set — the
allocator's interpose-everything mode, not incidental exports.
Node-API / V8 / libuv ABI:
node_api_create_external_sharedarraybuffer
node_api_create_object_with_properties
node_api_create_sharedarraybuffer
node_api_is_sharedarraybuffer
node_api_set_prototype
napi_internal_threadsafe_function_env_teardown
uv_tty_reset_mode
_ZN2v811CpuProfiler... (and ~40 further mangled v8:: symbols)
The second group is almost certainly the intent: exporting the V8/Node-API ABI
is what allows native .node addons to resolve symbols against the host
process. The allocator override set appears to have been exported as a side
effect of enabling it — consistent with a feature addition rather than a
toolchain accident.
(reallocarray in this set is tagged GLIBC_2.26 in objdump -T output, but
as DF .text with nonzero size — exported, not required. It is not a dependency
on a newer glibc.)
Why the interposition is fatal rather than merely unusual: glibc callsmalloc internally through aliases that resolve within libc.so and never
traverse the dynamic symbol table, so those allocations come from glibc's
allocator regardless of interposition. __newlocale's call to free, however,
does go through the PLT and lands in the bundled allocator. The pointer is
allocated by glibc and freed by the bundled allocator, which does not own it.
Exporting __libc_malloc and __libc_free cannot close this gap, because
libc-internal calls to them are already bound at link time.
Also note stdout and stderr are exported as data symbols, which risks
copy-relocation problems independently of this bug.
Ruled out
/etc/ld.so.preloaddoes not exist (access(...) = -1 ENOENT); no preloaded
allocator, no hardened_malloc.
- ASLR is not the trigger:
setarch -Rstill segfaults. - Locale is not the trigger:
en_US.utf8is generated and present, and
LC_ALL=C LANG=C still segfaults.
- No failed syscall: overcommit limits,
vm.max_map_count,vm.mmap_rnd_bits,
and transparent hugepages are all irrelevant here.
- Not a corrupted download. The binary's SHA-256 matches the published
manifest exactly, so the crash is in the shipped artifact:
```
$ sha256sum /tmp/cc-2.1.243
4b0dafeedd0b469c41988e200036fd773e7553ba960349c9f02a82c6d1f2ba27
$ curl -fsSL .../2.1.243/manifest.json | jq -r '.platforms["linux-x64"].checksum'
4b0dafeedd0b469c41988e200036fd773e7553ba960349c9f02a82c6d1f2ba27
```
Unverified
Stated for completeness rather than asserted as fact:
- Not tested: 2.1.243 on a different distro or glibc version, a stock (non-zen)
kernel, or a second machine.
- First-bad version between 2.1.205 and 2.1.243 not bisected.
Environment
- OS: Arch Linux (rolling)
- Kernel: 7.1.9-zen1-2-zen
- glibc: 2.44
- CPU: 12th Gen Intel Core i7-12700 (Alder Lake, x86-64)
- Terminal: Ghostty, zsh
- Locale:
LANG=en_US.UTF-8, generated and present (crash is locale-independent) - Binary size: 378 MB
- Install method: direct download from
downloads.claude.ai, because the
install script cannot complete (see companion issue)
- Note: this system uses ripgrep aliased over
grepand a Rust coreutils
replacement for ls. Not believed relevant — the fault is inside the shipped
binary — but noted for completeness.
Steps to reproduce
VER=$(curl -fsSL https://downloads.claude.ai/claude-code-releases/latest)
curl -fsSL -o /tmp/cc-$VER "https://downloads.claude.ai/claude-code-releases/$VER/linux-x64/claude"
chmod +x /tmp/cc-$VER
/tmp/cc-$VER --version # segfaults, no panic banner
Expected behaviour
The linux-x64 build should not export malloc, calloc, realloc, free, orreallocarray from the executable. Interposing glibc's allocator process-wide
means any pointer allocated by glibc or ld.so before the bundled allocator is
ready will be freed by the wrong allocator.
Suggested fix: keep exporting the Node-API and V8 symbols if native addon
support is the goal, but exclude the allocator override set from the export
list. A linker version script listing only the node_api_*, napi_*, uv_*,
and v8:: symbols would achieve this, as would disabling the bundled
allocator's override build option. The allocator can still be used internally;
it must not override glibc's.
5 Comments
2.1.243 segfaults at startup on glibc 2.44: interposed
free()has no NULL guardVersion: 2.1.243 (
GIT_SHA 8565f923a3ec61dc4c61bf7bfd995521c702c9fc,BUILD_TIME 2026-08-24T21:05:22Z)Platform: Linux x86-64, CachyOS, glibc
2.44+r24+g16be1518495f-1, kernel 7.2.0Severity: Total — the binary cannot start at all.
claude --versionsegfaults.Reproducibility: 100% (10/10 attempts)
Last good version: 2.1.241
Summary
2.1.243 is the first build to statically interpose the allocator. glibc 2.44's
__newlocale()callsfree(NULL)— a mandatory no-op per C99 §7.20.3.2 — from apath that runs during pre-
mainstatic initialization. The interposedfree()does not NULL-check its argument before indexing mimalloc's page map, and at that
point in startup the page map is still unpopulated, so the lookup dereferences
0x0.Kernel record
si_code: SEGV_MAPERR, fault address0x0,error 4= user-mode read of anon-present page. Same
ipacross different PIDs and different CPU cores.Backtrace
Everything above
#16is pre-mainstatic init, under nestedpthread_once.Registers at fault
rdiis the incoming argument. The pointer being freed is NULL.Root cause, both halves
glibc 2.44
__newlocalecallsfree(NULL)The indirect GOT call is what makes it interposable.
The interposed
free()NULL-checks one instruction too lateVerified in a live debugger at the moment of the fault: the page-map root pointer
is valid, and every level-1 entry is still NULL — mimalloc had not yet served
a single allocation.
Regression window
Binary size jumps 342,636,848 -> 377,568,472 bytes at the same boundary.
Ruled out
LANG=en_US.UTF-8,C.UTF-8,C,LC_ALL=C, and with no locale variables set at all.different CPU cores.
LD_PRELOAD, no/etc/ld.so.preload, noGLIBC_TUNABLES,no
NODE_OPTIONS, noBUN_*.Confirming experiment
Preloading a shared object whose
constructor(101)performs one realmalloc/free— forcing the page map to initialize before the executable's owninit array runs — makes 2.1.243 start cleanly, 10/10:
This isolates the bug to page-map initialization ordering, not to the locale
subsystem or to the host environment.
Suggested fix
Add the standard early-out to the interposed
free()(andrealloc), i.e. theif (p == NULL) return;thatmi_freenormally carries, so it precedes the pagemap lookup. Alternatively, ensure the page map is initialized from a constructor
that is ordered before any other static initializer, or point unpopulated
level-1 entries at the shared empty submap at image load time rather than on
first allocation.
The NULL guard is the safer of the two: it also protects every other
free(NULL)that any linked library may perform before mimalloc is warm, whichis legal C that the current build cannot survive.
Same problem on archlinux
Sorry about this. Working on a fix.
Same here:
Environment
Claude Code: 2.1.243 native Linux x64 build
OS: Arch Linux
Kernel: 7.1.6-arch1-1
glibc: 2.44
Architecture: x86_64
With 2.1.243, even the following command crashes immediately:
claude --version
Output:
Segmentation fault (core dumped)
The systemd core dump shows:
#0 0x0000000001d10458 free (2.1.243 + 0x1b10458)
https://github.com/anthropics/claude-code/pull/1 0x00007f7fb323530a __newlocale (libc.so.6 + 0x3530a)
I then rolled back only the Claude Code binary:
ln -sf ~/.local/share/claude/versions/2.1.241 ~/.local/bin/claude
After that:
claude --version
returns normally:
2.1.241 (Claude Code)
No other system or configuration changes were made.
This issue should be fixed in Claude Code v2.1.245. Please re-run the install script:
Sorry about this!