[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

Status Fixed / completed
Reported on v2.1.139
Maintainer reply None cached
Activity 5 comments · opened Aug 25, 2026 · closed Aug 25, 2026

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 only
segmentation 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 own
free:

$ nm -S -D --defined-only /tmp/cc-2.1.243 | grep -w free
0000000001d10430 000000000000007e T free@@GLIBC_2.2.5

free spans 0x1d104300x1d104ae (size 0x7e). The fault at 0x1d10458 is
free + 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:

  1. The binary interposes free for the whole process.
  2. glibc's startup path calls __newlocale, which calls free on a pointer.
  3. That call routes into the bundled allocator rather than glibc's.
  4. The bundled allocator does not own the pointer; its metadata lookup yields

NULL and is dereferenced unchecked.

  1. SIGSEGV at si_addr=NULL, before main.

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 same
nm -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 calls
malloc 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.preload does not exist (access(...) = -1 ENOENT); no preloaded

allocator, no hardened_malloc.

  • ASLR is not the trigger: setarch -R still segfaults.
  • Locale is not the trigger: en_US.utf8 is 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 grep and 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, or
reallocarray 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.

View original on GitHub ↗

5 Comments

gosseljl · 6 days ago

2.1.243 segfaults at startup on glibc 2.44: interposed free() has no NULL guard

Version: 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.0
Severity: Total — the binary cannot start at all. claude --version segfaults.
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() calls free(NULL) — a mandatory no-op per C99 §7.20.3.2 — from a
path that runs during pre-main static initialization. The interposed free()
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

claude[489398]: segfault at 0 ip 0000000001d10458 sp 00007ffece627430 error 4 \
  in 2.1.243[1b0f458,1953000+3b33000] likely on CPU 6 (core 6, socket 0)

si_code: SEGV_MAPERR, fault address 0x0, error 4 = user-mode read of a
non-present page. Same ip across different PIDs and different CPU cores.

Backtrace

#0  0x0000000001d10458 free            (2.1.243 + 0x1b10458)
#1  0x00007fcb6a039d0b __newlocale     (libc.so.6 + 0x39d0b)
#2  0x0000000001abd282 n/a             (2.1.243 + 0x18bd282)
#3  0x00007fcb6a0b2cc4 n/a             (libc.so.6 + 0xb2cc4)
#4  0x00007fcb6a0b2df9 pthread_once    (libc.so.6 + 0xb2df9)
#5  0x0000000001abc2d2 n/a             (2.1.243 + 0x18bc2d2)
...
#16 0x00007fcb6a027da3 __libc_start_main (libc.so.6 + 0x27da3)
#17 0x0000000001953a2e n/a             (2.1.243 + 0x1753a2e)

Everything above #16 is pre-main static init, under nested pthread_once.

Registers at fault

rdi 0x0   rsi 0x0   rax 0x0   rcx 0x0

rdi is the incoming argument. The pointer being freed is NULL.

Root cause, both halves

glibc 2.44 __newlocale calls free(NULL)

0000000000039cd0 <__newlocale>:
  39ced:  movq   $0x0,0x10(%rsp)        ; char *newnames = NULL
  39cf6:  call   39070 <__newlocale_1.constprop.0>
  39cfb:  mov    0x10(%rsp),%rdi        ; rdi = newnames -- still NULL on this path
  39d05:  call   *0x204015(%rip)        ; free(NULL), INDIRECT through the GOT
  39d0b:  mov    0x8(%rsp),%rax         ; <- return address seen in frame #1

The indirect GOT call is what makes it interposable.

The interposed free() NULL-checks one instruction too late

free+0:   push %rbp; mov %rsp,%rbp
free+4:   mov  %rdi,%rsi                  ; ptr = NULL
free+7:   mov  0x3781982(%rip),%rax       ; page-map root (0x5491ec0, valid)
free+14:  mov  %rdi,%rcx
free+17:  shr  $0x1d,%rcx                 ; 0 >> 29 = 0
free+21:  mov  0x80(%rax,%rcx,8),%rax     ; submap[0] == NULL  (page map not yet built)
free+29:  mov  %esi,%ecx
free+31:  shr  $0xd,%ecx
free+34:  and  $0xfff8,%ecx
free+40:  mov  (%rax,%rcx,1),%rdi         ; *(0x0)  -> SIGSEGV
free+44:  test %rdi,%rdi                  ; the NULL check -- guards metadata, not the argument
free+47:  je   free+79

Verified 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

2.1.239   exports: (none)
2.1.240   exports: (none)
2.1.241   exports: (none)
2.1.243   exports: aligned_alloc calloc free malloc posix_memalign realloc

Binary size jumps 342,636,848 -> 377,568,472 bytes at the same boundary.

Ruled out

  • Locale. Reproduces identically under LANG=en_US.UTF-8, C.UTF-8, C,

LC_ALL=C, and with no locale variables set at all.

  • Hardware / memory. Deterministic, same faulting instruction, across

different CPU cores.

  • Bad download. Binary runs correctly once the page map is pre-warmed (below).
  • Environment. No LD_PRELOAD, no /etc/ld.so.preload, no GLIBC_TUNABLES,

no NODE_OPTIONS, no BUN_*.

Confirming experiment

Preloading a shared object whose constructor(101) performs one real
malloc/free — forcing the page map to initialize before the executable's own
init array runs — makes 2.1.243 start cleanly, 10/10:

volatile void *mi_sink;
__attribute__((constructor(101)))
static void mi_warm(void) {
    void *p = malloc(64);
    mi_sink = p;   /* escape; at -O2 gcc elides the pair as builtins */
    free(p);
}
/* gcc -O0 -fno-builtin -fPIC -shared -o mi_warm.so mi_warm.c */
$ LD_PRELOAD=./mi_warm.so ./2.1.243 --version
2.1.243 (Claude Code)

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() (and realloc), i.e. the
if (p == NULL) return; that mi_free normally carries, so it precedes the page
map 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, which
is legal C that the current build cannot survive.

Ricepies · 6 days ago

Same problem on archlinux

Jarred-Sumner · 6 days ago

Sorry about this. Working on a fix.

FangLabGames · 6 days ago

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.

Jarred-Sumner · 6 days ago

This issue should be fixed in Claude Code v2.1.245. Please re-run the install script:

curl -fsSL https://claude.ai/install.sh | bash

Sorry about this!