anthropics / anthropics/claude-code
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
- 主要言語
- Python
- スター
- 145k
- フォーク
- 23.1k
- PR マージ指標
- PR 指標を取得中
説明
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`:
```asm
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:
```gdb
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.
コントリビューションガイド
このリポジトリのコントリビューションガイドは索引されていません
調査の方向性
Start by reproducing the startup failure for the 2.1.243 binary on glibc 2.44 and compare it with glibc 2.43, then inspect the interposed free entry and the __newlocale call path described here. Review the related issues for the proposed NULL guard and symbol-export alternatives; done means the released CLI starts and exits normally on glibc 2.44 without the segfault.
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- c, linux
- 領域
- cli, operating-systems
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 活発
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 35/100