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

未关闭
#89,420 0 条评论 4 个 reaction 已指派 0 人 在 GitHub 查看
area:packaging duplicate has repro platform:linux regression
主要语言
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

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。