You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix Bun 1.4.0 startup segfault via uname shim (#21)
Claude Code 2.1.181 bumped its bundled Bun to 1.4.0, whose HTTP event
loop calls epoll_pwait2 with no fallback and null-derefs at startup on
every kernel >= 5.11 (bun#32489 — "Segmentation fault at address 0x0").
Newer releases crashed on launch while CI stayed green, because the e2e
suite only ran `claude --version` — a fast path that exits before the
event loop starts.
Bun selects that path from the kernel version it reads via glibc
uname(). Ship a freestanding LD_PRELOAD interposer (src/uname-shim.c)
that reports kernel 5.10.0, so Bun takes the working epoll_pwait path.
uname() is an undefined dynamic import in the Claude binary, so the
interposer is consulted; building it -nostdlib keeps it loadable under
glibc's ld.so and its raw syscall avoids recursing into the symbol it
overrides. The launcher now overwrites LD_PRELOAD with the shim (it
already had to displace termux-exec's libc.so) instead of clearing it.
Reporting 5.10 stays correct once Anthropic ships a fixed Bun, so the
shim needs no later removal.
Also add an e2e runtime-init boot guard (`claude mcp list` spins up the
event loop) so a future startup-breaking runtime fails the build instead
of slipping past --version.
dispatch works), it **clears `LD_PRELOAD`** (so the glibc binary's `ld.so`
31
-
doesn't choke on termux-exec's text-script `libc.so`), it **sets
32
-
`TMPDIR`/`CLAUDE_CODE_TMPDIR`** to the Termux prefix when unset (Termux has no
33
-
writable `/tmp`; see `src/claude-wrapper.c` for the env-vs-hardcoded-`/tmp`
34
-
analysis and the MCP-browser-bridge limitation, `claude-code#15637`), and it
35
-
**sets `DISABLE_AUTOUPDATER`** when unset, a second settings-independent
36
-
layer behind `autoUpdates: false` (next item) against the self-updater.
30
+
dispatch works), it **overwrites `LD_PRELOAD` with the uname shim**
31
+
(`lib/claude-code-termux/uname-spoof.so`): this both evicts termux-exec's
32
+
text-script `libc.so` (which the glibc binary's `ld.so` can't load) and
33
+
preloads a freestanding `uname()` interposer that reports a `< 5.11` kernel,
34
+
so Bun skips the `epoll_pwait2` path that segfaults at startup on newer
35
+
kernels (bun#32489 — see `src/uname-shim.c` and `claude-code#50270`). It also
36
+
**sets `TMPDIR`/`CLAUDE_CODE_TMPDIR`** to the Termux prefix when unset (Termux
37
+
has no writable `/tmp`; see `src/claude-wrapper.c` for the
38
+
env-vs-hardcoded-`/tmp` analysis and the MCP-browser-bridge limitation,
39
+
`claude-code#15637`), and it **sets `DISABLE_AUTOUPDATER`** when unset, a
40
+
second settings-independent layer behind `autoUpdates: false` (next item)
41
+
against the self-updater.
37
42
1.`postinst` also merges two keys into `~/.claude/settings.json`:
38
43
`env.LD_PRELOAD` (re-arms termux-exec for subprocess `/usr/bin/env` shebangs)
39
44
and `autoUpdates: false` (so the in-session updater can't clobber the patched
@@ -60,12 +65,13 @@ publish the binary; the `.deb` contains no Anthropic bytes.
60
65
|`package/payload/libexec/link-native.sh`| Idempotent native-path symlink (`~/.local/bin/claude` → launcher) reconcile, shared by `postinst` and the update command. |
61
66
|`package/payload/libexec/patch-execpath.py`| The `CLAUDE_CODE_EXECPATH` patch. |
62
67
|`package/payload/etc/claude-code-termux.conf`|`CLAUDE_CODE_VERSION` pin + `CLAUDE_CODE_CACHE_DIR` (both empty by default). |
63
-
|`src/claude-wrapper.c`| The C launcher (`-DBINARY=` baked in at compile). `claude_wrapper_run()` takes its exec function as a parameter — a seam the unit tests fake. |
68
+
|`src/claude-wrapper.c`| The C launcher (`-DBINARY=`/`-DTMPDIR_PATH=`/`-DUNAME_SHIM=` baked in at compile). `claude_wrapper_run()` takes its exec function as a parameter — a seam the unit tests fake. |
69
+
|`src/uname-shim.c`| Freestanding `LD_PRELOAD``uname()` interposer reporting kernel `5.10.0`, so Bun avoids the `epoll_pwait2` startup segfault (bun#32489). Built by `build-wrapper.sh`, shipped to `lib/claude-code-termux/uname-spoof.so`, preloaded by the launcher. |
64
70
|`test/wrapper_test.c`| Unit tests for the launcher (greatest; recording exec stub). |
65
71
|`test/compat.h`| Test-only `setenv`/`unsetenv` shims for Windows libc; force-included into the test build, never shipped. |
66
72
|`vendor/greatest.h`| Vendored single-header test framework (greatest 1.5.0, ISC; from silentbicycle/greatest). Excluded from prek. |
67
73
|`vendor/shunit2`| Vendored single-file shell test framework (shUnit2 2.1.9pre, Apache-2.0; pinned to kward/shunit2 master @ `f39734a` — no tagged release since 2.1.8, and master carries the `egrep`→`grep -E` fix we'd otherwise patch). Drives `scripts/e2e.sh`. Carries one local patch (grep `PATCHED FOR TERMUX`; re-apply on bump): `#! /bin/sh` stub scripts → resolve `sh` via PATH (submitted upstream as kward/shunit2#189). Excluded from prek. |
68
-
|`scripts/build-wrapper.sh`| Compile the wrapper with Termux's clang. |
74
+
|`scripts/build-wrapper.sh`| Compile the wrapper + uname shim with Termux's clang. |
0 commit comments