2.1.242+ segfault is glibc-2.44-only: bisected 2.35→2.44, glibc 2.44's __newlocale refactor added an unconditional free(NULL); simulating a NULL guard boots 2.1.243 cleanly

Status Open
Reported on v2.1.237
Maintainer reply None cached
Activity 0 comments · opened Aug 25, 2026

Follow-up with new data on the 2.1.242+ startup segfault (#89334, #89369, #89389, #89399, #89414 and ~15 duplicates). I am not re-reporting the crash — I bisected the glibc side of it, and tested whether a NULL guard is a sufficient fix.

TL;DR

  1. The crash is glibc-2.44-only. Bisected across 7 glibc versions with the same binary on the same kernel: 2.35 / 2.36 / 2.39 / 2.41 / 2.42 / 2.43 all boot fine; only 2.44 segfaults. So this is not Arch-specific per se (answers the explicit "untested" note in #89389) and not kernel-related (contradicts #89414).
  2. The trigger is a new unconditional free(NULL) introduced by a __newlocale refactor in glibc 2.44. Disassembly below.
  3. Simulating a NULL guard in the interposed free makes 2.1.243 start completely normally. This confirms #89334's root cause and disproves the cross-allocator-free hypothesis in #89389 — a single NULL check appears to be a sufficient fix.

---

1. glibc bisection — same binary, same kernel

Identical 2.1.243 binary bind-mounted read-only into containers, all sharing the same host kernel 7.1.9 (so the kernel variable is held constant):

| Distro image | glibc | /cc --version | rc |
|---|---|---|---|
| ubuntu:22.04 | 2.35 | 2.1.243 (Claude Code) | 0 |
| debian:bookworm-slim | 2.36 | 2.1.243 (Claude Code) | 0 |
| ubuntu:24.04 | 2.39 | 2.1.243 (Claude Code) | 0 |
| debian:trixie-slim | 2.41 | 2.1.243 (Claude Code) | 0 |
| ubuntu:25.10 | 2.42 | 2.1.243 (Claude Code) | 0 |
| fedora:43 | 2.42 | 2.1.243 (Claude Code) | 0 |
| debian:sid-slim | 2.43 | 2.1.243 (Claude Code) | 0 |
| archlinux:base | 2.44 | (no output) | 139 (SIGSEGV) |

$ docker run --rm -v ~/.local/share/claude/versions/2.1.243:/cc:ro debian:sid-slim \
    sh -c 'ldd --version|head -1; /cc --version; echo rc=$?'
ldd (Debian GLIBC 2.43-4) 2.43
2.1.243 (Claude Code)
rc=0

$ docker run --rm -v ~/.local/share/claude/versions/2.1.243:/cc:ro archlinux:base \
    sh -c 'ldd --version|head -1; /cc --version; echo rc=$?'
ldd (GNU libc) 2.44
rc=139

The boundary is exactly 2.43 → 2.44. Two consequences:

  • #89414's "kernel 7.1.x" attribution is wrong. Every row above ran on kernel 7.1.9-arch1-2. Swapping only glibc flips the outcome.
  • Reports clustering on Arch/CachyOS are just a rolling-release artifact — they shipped glibc 2.44 first. Every distro will hit this as 2.44 lands (Fedora 44, Ubuntu 26.04, Debian sid once it moves past 2.43).

2. Why 2.44 specifically: __newlocale gained an unconditional free(NULL)

glibc 2.44 refactored __newlocale into a 0x55-byte shell delegating to __newlocale_1.constprop.0:

00000000000352d0 <__newlocale>:            ; glibc 2.44
   352ef: movq   $0x0,-0x10(%rbp)          ; local ptr = NULL
   352f7: call   347c0 <__newlocale_1.constprop.0>
   352fc: mov    -0x10(%rbp),%rdi          ; read it back — still NULL on the common path
   35304: call   *0x1e3a1e(%rip)           ; <-- UNCONDITIONAL indirect call to free, via GOT
   3530a: mov    -0x18(%rbp),%rax          ; <-- return address seen as frame #1 in every report

0x3530a is precisely the newlocale+0x3a return address in the backtraces of #89334/#89369/#89389/#89399. The local pointer is initialised to NULL, __newlocale_1 leaves it untouched on the common path, and 2.44 then frees it unconditionally, relying on free(NULL) being a no-op (C17 7.22.3.3p2).

For contrast, glibc 2.43's __newlocale is a single 0xad6-byte function whose only free is a free@plt on a conditional cleanup path (paired with its 2 malloc calls) — it never reaches free(NULL) during startup:

$ nm -S -D --defined-only libc-2.43 | grep -w __newlocale
0000000000037400 0000000000000ad6 T __newlocale@@GLIBC_2.2.5     # 2.43: monolithic, free@plt (conditional)
$ nm -S -D --defined-only libc-2.44 | grep -w __newlocale
00000000000352d0 0000000000000055 T __newlocale@@GLIBC_2.2.5     # 2.44: shell + unconditional free

Note the 2.44 call is an indirect call through the GOT, i.e. exactly the path that the executable's exported free@@GLIBC_2.2.5 interposes.

3. A NULL guard is sufficient — verified

The open question left by the existing reports is whether patching the NULL case is enough, or whether the interposed allocator will immediately hit a genuine cross-allocator free afterwards (#89389 hypothesises "the bundled allocator does not own the pointer"). I tested it.

Simulated a NULL guard by trapping free at its entry in gdb and returning early whenever rdi == 0, leaving everything else untouched:

break *0x1d10430          # free entry in 2.1.243
commands
silent
if $rdi == 0
  set $rip = *(unsigned long *)$rsp    # skip the body, return to caller
  set $rsp = $rsp + 8
  set $nullfrees = $nullfrees + 1
end
continue
end
set $nullfrees = 0
run --version
printf "free(NULL) intercepted: %d\n", $nullfrees

Result:

2.1.243 (Claude Code)
[Inferior 1 (process 446466) exited normally]

===> free(NULL) intercepted: 5

Startup completes cleanly, version prints, process exits normally, with exactly 5 free(NULL) calls intercepted and zero other faults. So:

  • #89334's root cause (missing NULL check) is confirmed.
  • #89389's cross-allocator hypothesis is not what's happening — every pointer reaching the interposed free on the startup path is either NULL or genuinely owned by the bundled allocator.
  • Adding a NULL guard to the interposed free looks sufficient to unblock startup. Un-exporting the allocator symbols (the other obvious fix) would also work and is arguably the safer one, but is not required to stop the crash.

4. Environment

  • Host: Arch Linux, kernel 7.1.9-arch1-2, x86-64, glibc 2.44
  • Affected: 2.1.242, 2.1.243 (native installer builds). 2.1.237 / 2.1.238 / 2.1.241 on the same machine are fine (consistent with #89334's 2.1.239/2.1.240/2.1.241 matrix)
  • 2.1.243: BuildID c39577b41056cc1abf1ea7ed72a8b4c1ba407c42, 377568472 bytes
  • 2.1.241: BuildID d11007948a6167b426d967c295579e51ded8d0b6, 342636848 bytes
  • dmesg is identical on every run: segfault at 0 ip 0000000001d10458 ... error 4 in 2.1.243[1b0f458,1953000+3b33000]

Non-workarounds (all still rc=139, in case they save someone a detour): LC_ALL=C, LC_ALL=C.UTF-8, LANG=C, GLIBC_TUNABLES=glibc.malloc.tcache_count=0. LD_PRELOAD cannot help either — the executable's own definitions win the global symbol lookup scope regardless.

The only user-side workaround is pinning an older version, which #89391 notes install.sh makes difficult.

View original on GitHub ↗