Skip to content

Commit 7a4c9ad

Browse files
authored
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.
1 parent e0c7afc commit 7a4c9ad

9 files changed

Lines changed: 163 additions & 40 deletions

File tree

AGENTS.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,18 @@ publish the binary; the `.deb` contains no Anthropic bytes.
2727
assignment.
2828
1. The compiled C launcher (`bin/claude`) execs the patched binary —
2929
`execv` **preserves `argv[0]`** (so Claude's embedded `grep`/`find`/`rg`
30-
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.
3742
1. `postinst` also merges two keys into `~/.claude/settings.json`:
3843
`env.LD_PRELOAD` (re-arms termux-exec for subprocess `/usr/bin/env` shebangs)
3944
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.
6065
| `package/payload/libexec/link-native.sh` | Idempotent native-path symlink (`~/.local/bin/claude` → launcher) reconcile, shared by `postinst` and the update command. |
6166
| `package/payload/libexec/patch-execpath.py` | The `CLAUDE_CODE_EXECPATH` patch. |
6267
| `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. |
6470
| `test/wrapper_test.c` | Unit tests for the launcher (greatest; recording exec stub). |
6571
| `test/compat.h` | Test-only `setenv`/`unsetenv` shims for Windows libc; force-included into the test build, never shipped. |
6672
| `vendor/greatest.h` | Vendored single-header test framework (greatest 1.5.0, ISC; from silentbicycle/greatest). Excluded from prek. |
6773
| `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. |
6975
| `scripts/build-deb.sh` | Stage + `dpkg-deb --build``artifacts/packages/`. |
7076
| `scripts/compile.sh` | Compile the wrapper + assemble the `.deb`, in a Termux env. |
7177
| `scripts/e2e.sh` | Install the prebuilt `.deb` + assert the fixes, in a Termux env. |
@@ -132,9 +138,12 @@ install) need the container, split across two tasks:
132138

133139
`mise run compile` produces the `.deb`, then `mise run test:e2e` installs it via
134140
`install.sh` and asserts the real fixes: `argv[0]=rg`/`bfs` dispatch to the
135-
embedded ripgrep/bfs, `LD_PRELOAD`-cleared startup, `TMPDIR` injection, the
136-
`settings.json` merge, the conditional/cached update paths, and the
137-
interpreter-repair `ensure`. The suite is built on the vendored shUnit2 (a
141+
embedded ripgrep/bfs, startup with `LD_PRELOAD` set (the launcher overwrites it
142+
with the uname shim), a runtime-init boot guard (`claude mcp list` spins up
143+
Bun's event loop — catching startup crashes the `--version` fast path can't,
144+
e.g. the Bun 1.4.0 `epoll_pwait2` segfault the uname shim prevents), `TMPDIR`
145+
injection, the `settings.json` merge, the conditional/cached update paths, and
146+
the interpreter-repair `ensure`. The suite is built on the vendored shUnit2 (a
138147
single sourced file; the install is its `oneTimeSetUp`, behaviors are
139148
definition-ordered `test_*` functions with the state-mutating ones last). On a
140149
dev host both tasks dispatch to `scripts/docker-run.sh <script>`, which pulls

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,13 @@ re-downloaded. Leave it empty (the default) to disable caching.
7777
(`patchelf`), and blanks the subprocess `CLAUDE_CODE_EXECPATH` assignment
7878
(`patch-execpath.py`) so embedded-tool re-execs route back through the
7979
launcher.
80-
1. The compiled launcher execs the patched binary with `LD_PRELOAD` cleared,
81-
preserving `argv[0]` so Claude's `grep`/`find`/`rg` dispatch works, and sets
82-
`TMPDIR`/`CLAUDE_CODE_TMPDIR` to the Termux prefix (only when unset) since
83-
Termux has no writable `/tmp`.
80+
1. The compiled launcher execs the patched binary, preserving `argv[0]` so
81+
Claude's `grep`/`find`/`rg` dispatch works. It overwrites `LD_PRELOAD` with a
82+
small `uname` shim: this evicts termux-exec's `libc.so` (which the glibc
83+
binary's loader can't load) and reports a `< 5.11` kernel so the bundled Bun
84+
skips an `epoll_pwait2` path that otherwise segfaults at startup on newer
85+
Android kernels (bun#32489). It also sets `TMPDIR`/`CLAUDE_CODE_TMPDIR` to the
86+
Termux prefix (only when unset) since Termux has no writable `/tmp`.
8487
1. The postinstall also symlinks the launcher onto Anthropic's native-install
8588
path (`~/.local/bin/claude`) so Claude's health check passes when it detects
8689
`installMethod: native`. The symlink targets the launcher (not the patched

mise-tasks/test/fast

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env bash
22
#MISE description="Unit-test the C launcher natively (fast; no Docker)"
33
# Keep Git Bash / MSYS from rewriting the leading-slash sentinel paths
4-
# (BINARY/TMPDIR_PATH below) into Windows paths on the way to zig (no-op
5-
# elsewhere).
4+
# (BINARY/TMPDIR_PATH/UNAME_SHIM below) into Windows paths on the way to zig
5+
# (no-op elsewhere).
66
#MISE env={ MSYS_NO_PATHCONV = "1" }
77
#
88
# Compiles src/claude-wrapper.c with zig and runs it directly — no Termux, no
@@ -18,6 +18,7 @@ set -euo pipefail
1818
# path/tmpdir the wrapper bakes in. They need not exist on disk: exec is faked.
1919
BINARY=/claude-code-termux-test/claude
2020
TMPDIR_PATH=/claude-code-termux-test/tmp
21+
UNAME_SHIM=/claude-code-termux-test/uname-spoof.so
2122

2223
out="$MISE_PROJECT_ROOT/artifacts/build"
2324
mkdir -p "$out"
@@ -31,6 +32,7 @@ case "${OSTYPE:-}" in msys* | cygwin* | win*) bin="$bin.exe" ;; esac
3132
# -include compat.h shims setenv/unsetenv on Windows.
3233
zig cc -O2 -Wall -Wextra -Werror \
3334
-DBINARY="\"$BINARY\"" -DTMPDIR_PATH="\"$TMPDIR_PATH\"" \
35+
-DUNAME_SHIM="\"$UNAME_SHIM\"" \
3436
-DCLAUDE_WRAPPER_NO_MAIN \
3537
-include "$MISE_PROJECT_ROOT/test/compat.h" \
3638
-I "$MISE_PROJECT_ROOT/vendor" \

scripts/build-deb.sh

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,17 @@ build="$root/artifacts/build"
3434
pkgdir="$root/artifacts/packages"
3535
stage="$build/${PKG}_${VERSION}_${ARCH}"
3636

37-
# The compiled launcher wrapper must be built first (build-wrapper.sh, inside
38-
# termux/termux-docker:aarch64) so the ELF matches the device.
37+
# The compiled launcher wrapper + uname shim must be built first
38+
# (build-wrapper.sh, inside termux/termux-docker:aarch64) so the ELF matches the
39+
# device.
3940
[ -x "$build/claude" ] || {
4041
echo "error: $build/claude not found — run build-wrapper.sh first." >&2
4142
exit 1
4243
}
44+
[ -f "$build/uname-spoof.so" ] || {
45+
echo "error: $build/uname-spoof.so not found — run build-wrapper.sh first." >&2
46+
exit 1
47+
}
4348

4449
# Reset only the staging tree, preserving the compiled wrapper at $build/claude.
4550
rm -rf "$stage"
@@ -53,10 +58,13 @@ printf '/%s/etc/%s.conf\n' "$PREFIX_REL" "$PKG" >"$stage/DEBIAN/conffiles"
5358

5459
# Payload, staged under the Termux prefix.
5560
install -d "$stage/$PREFIX_REL/bin"
61+
install -d "$stage/$PREFIX_REL/lib/$PKG"
5662
install -d "$stage/$PREFIX_REL/libexec/$PKG"
5763
install -d "$stage/$PREFIX_REL/etc"
5864
install -d "$stage/$PREFIX_REL/share/doc/$PKG"
5965
install -m 0755 "$build/claude" "$stage/$PREFIX_REL/bin/claude"
66+
# The wrapper LD_PRELOADs this; its baked-in UNAME_SHIM path must match.
67+
install -m 0644 "$build/uname-spoof.so" "$stage/$PREFIX_REL/lib/$PKG/uname-spoof.so"
6068
install -m 0755 "$root/package/payload/bin/claude-code-termux-update" "$stage/$PREFIX_REL/bin/claude-code-termux-update"
6169
install -m 0755 "$root/package/payload/libexec/bootstrap.sh" "$stage/$PREFIX_REL/libexec/$PKG/bootstrap.sh"
6270
install -m 0755 "$root/package/payload/libexec/link-native.sh" "$stage/$PREFIX_REL/libexec/$PKG/link-native.sh"

scripts/build-wrapper.sh

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,38 @@
11
#!/data/data/com.termux/files/usr/bin/bash
22
#
3-
# Compile the C launcher wrapper for aarch64 Termux. Must run in an
4-
# environment whose toolchain targets Termux's bionic — i.e. inside
3+
# Compile the C launcher wrapper (and its uname shim) for aarch64 Termux. Must
4+
# run in an environment whose toolchain targets Termux's bionic — i.e. inside
55
# `termux/termux-docker:aarch64` (CC=clang) — so the resulting ELF matches the
6-
# device. The compiled binary is written to build/claude and staged into the
7-
# .deb by build-deb.sh. (Termux-only, hence the absolute Termux shebang.)
6+
# device. The outputs are written to build/ and staged into the .deb by
7+
# build-deb.sh. (Termux-only, hence the absolute Termux shebang.)
88
#
99
set -euo pipefail
1010

1111
PREFIX=/data/data/com.termux/files/usr
1212
# The wrapper execs the `current` symlink that bootstrap.sh keeps pointing at
13-
# the patched binary, and sets TMPDIR to the Termux prefix tmp dir (Termux has
14-
# no writable /tmp). Both are baked in at compile time.
13+
# the patched binary, sets TMPDIR to the Termux prefix tmp dir (Termux has no
14+
# writable /tmp), and LD_PRELOADs the uname shim (see src/uname-shim.c). All
15+
# three are baked in at compile time. UNAME_SHIM must match the install path
16+
# build-deb.sh stages the .so to.
1517
BINARY="$PREFIX/opt/claude-code-termux/current"
1618
TMPDIR_PATH="$PREFIX/tmp"
19+
UNAME_SHIM="$PREFIX/lib/claude-code-termux/uname-spoof.so"
1720

1821
root=$(cd "$(dirname "$0")/.." && pwd)
1922
out="$root/artifacts/build"
2023
mkdir -p "$out"
2124

2225
: "${CC:=cc}"
26+
27+
# The uname shim is freestanding (-nostdlib -ffreestanding): no libc of its own,
28+
# so the glibc binary's ld.so loads it as an LD_PRELOAD regardless of the bionic
29+
# toolchain that built it, and its raw uname syscall never recurses into the
30+
# symbol it interposes.
31+
"$CC" -O2 -Wall -Wextra -Werror -shared -fPIC -nostdlib -ffreestanding \
32+
-fno-stack-protector -o "$out/uname-spoof.so" "$root/src/uname-shim.c"
33+
2334
"$CC" -O2 -Wall -Wextra -Werror -DBINARY="\"$BINARY\"" -DTMPDIR_PATH="\"$TMPDIR_PATH\"" \
35+
-DUNAME_SHIM="\"$UNAME_SHIM\"" \
2436
-o "$out/claude" "$root/src/claude-wrapper.c"
2537

2638
echo "$out/claude"

scripts/e2e.sh

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,12 @@ oneTimeSetUp() {
8989
# Probe whose BINARY is `env`, so the TMPDIR tests can read the environment the
9090
# wrapper hands to the exec'd process. It must be named `env`: the wrapper
9191
# preserves argv[0], and Termux's `env` is the coreutils multiplexer that
92-
# dispatches on argv[0]'s basename.
92+
# dispatches on argv[0]'s basename. UNAME_SHIM points at the installed shim
93+
# (already staged by the install above) — the wrapper LD_PRELOADs it, and
94+
# bionic's linker aborts on a missing preload.
9395
probe="$(mktemp -d)/env"
9496
clang -O2 -DBINARY="\"$PREFIX/bin/env\"" -DTMPDIR_PATH="\"/PROBE_TMPDIR\"" \
97+
-DUNAME_SHIM="\"$PREFIX/lib/claude-code-termux/uname-spoof.so\"" \
9598
-o "$probe" src/claude-wrapper.c >&2 || fatal "probe compile failed"
9699
}
97100

@@ -161,6 +164,22 @@ test_startup() {
161164
"'$PREFIX/bin/claude' --version >/dev/null 2>&1"
162165
}
163166

167+
# Runtime-init crash guard. `--version`/`--help` are fast paths that exit before
168+
# Bun spins up its event loop, so they stay green even when the bundled runtime
169+
# can't boot on this kernel — exactly how Claude 2.1.181's Bun 1.4.0 bump
170+
# (epoll_pwait2 with no ENOSYS fallback, bun#32489) slipped past CI as a
171+
# segfault-at-startup ("Bun has crashed") for every real session while
172+
# `claude --version` kept exiting 0. `mcp list` boots the full runtime (event
173+
# loop + HTTP thread) and exits cleanly with no servers configured — no network
174+
# or API auth — so a future runtime that can't start on Termux fails the build
175+
# instead of shipping. See anthropics/claude-code#50270.
176+
test_startup_boots_runtime() {
177+
local out
178+
out=$("$PREFIX/bin/claude" mcp list 2>&1)
179+
assertEquals 'mcp list boots the runtime and exits 0' 0 "$?"
180+
assertNotContains 'runtime boots without a Bun crash' "$out" 'Bun has crashed'
181+
}
182+
164183
# grep/find dispatch: Claude routes its embedded tools by argv[0]. The compiled
165184
# wrapper must preserve argv[0] through execv to reach ripgrep/bfs — the core
166185
# fix. A bash wrapper would arrive as argv[0]=bash and never dispatch.

src/claude-wrapper.c

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
* wrapper would arrive as argv[0]="…/bash". execv() preserves argv verbatim.
99
*
1010
* The wrapper is itself a bionic binary, so Termux's libtermux-exec loads into
11-
* it cleanly; it then clears LD_PRELOAD before exec'ing the glibc Claude binary
12-
* (whose ld.so would otherwise choke on termux-exec's unversioned libc.so).
11+
* it cleanly; it then overwrites LD_PRELOAD with the uname shim before exec'ing
12+
* the glibc Claude binary (see the LD_PRELOAD comment in claude_wrapper_run).
1313
*
14-
* BINARY (the absolute path to the patched Claude Code binary) and TMPDIR_PATH
15-
* (the Termux prefix tmp dir) are baked in at compile time via -DBINARY="…" and
16-
* -DTMPDIR_PATH="…".
14+
* BINARY (the absolute path to the patched Claude Code binary), TMPDIR_PATH (the
15+
* Termux prefix tmp dir), and UNAME_SHIM (the absolute path to the uname-spoof
16+
* .so) are baked in at compile time via -DBINARY="…", -DTMPDIR_PATH="…", and
17+
* -DUNAME_SHIM="…".
1718
*/
1819
#include <errno.h>
1920
#include <stdio.h>
@@ -29,6 +30,11 @@
2930
#error "TMPDIR_PATH must be defined at compile time (-DTMPDIR_PATH=\"/…/tmp\")"
3031
#endif
3132

33+
#ifndef UNAME_SHIM
34+
#error \
35+
"UNAME_SHIM must be defined at compile time (-DUNAME_SHIM=\"/…/uname-spoof.so\")"
36+
#endif
37+
3238
/*
3339
* The launch logic, with the exec call injected as a parameter so unit tests
3440
* can substitute a fake and observe the (path, argv) handoff — and the env it
@@ -61,7 +67,14 @@ int claude_wrapper_run(int argc, char **argv,
6167
second, settings-independent layer that travels with every launch.
6268
overwrite=0 so an explicit user value still wins. */
6369
(void)setenv("DISABLE_AUTOUPDATER", "1", 0);
64-
(void)unsetenv("LD_PRELOAD");
70+
/* Replace (not clear) LD_PRELOAD with the uname shim. termux-exec's
71+
unversioned libc.so text-script crashes the glibc binary's ld.so, so it
72+
must not survive into the exec — and overwriting the variable both evicts
73+
it and preloads our interposer, which reports a < 5.11 kernel so Bun avoids
74+
the epoll_pwait2 startup segfault (bun#32489; see src/uname-shim.c).
75+
overwrite=1: whatever was inherited (termux-exec, or a stale value) is
76+
intentionally displaced. */
77+
(void)setenv("LD_PRELOAD", UNAME_SHIM, 1);
6578
exec(BINARY, argv);
6679
fprintf(stderr, "claude wrapper: execv %s failed: %s\n", BINARY,
6780
strerror(errno));

src/uname-shim.c

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* uname-shim — LD_PRELOAD interposer that reports kernel release "5.10.0".
3+
*
4+
* Claude Code 2.1.181 bumped its bundled Bun to 1.4.0, whose HTTP event loop
5+
* calls epoll_pwait2 with no fallback and null-derefs at startup on every
6+
* kernel >= 5.11 (bun#32489 — "Segmentation fault at address 0x0"). Bun selects
7+
* that path from the kernel version it reads via glibc uname(), and uname() is
8+
* an undefined dynamic import in the Claude binary, so interposing it here makes
9+
* Bun see a < 5.11 kernel and take the working epoll_pwait path instead. 5.10 is
10+
* the highest pre-5.11 release (an LTS), so it keeps every other kernel feature.
11+
*
12+
* The launcher points LD_PRELOAD at this object (replacing termux-exec's, which
13+
* its glibc ld.so can't load anyway). It is built freestanding (-nostdlib): with
14+
* no libc dependency of its own it loads under glibc's ld.so regardless of the
15+
* toolchain that compiled it, and the raw uname syscall avoids recursing into
16+
* the very symbol it interposes. The fix is harmless once Anthropic ships a
17+
* fixed Bun (epoll_pwait works on 5.10 too), so it needs no later removal.
18+
*
19+
* aarch64 Termux only — __NR_uname and the svc calling convention are baked in.
20+
* See anthropics/claude-code#50270.
21+
*/
22+
23+
/* glibc struct utsname: six _UTSNAME_LENGTH (65) char fields. */
24+
#define UTS_LEN 65
25+
struct utsname {
26+
char sysname[UTS_LEN];
27+
char nodename[UTS_LEN];
28+
char release[UTS_LEN];
29+
char version[UTS_LEN];
30+
char machine[UTS_LEN];
31+
char domainname[UTS_LEN];
32+
};
33+
34+
static long raw_uname(struct utsname *buf) {
35+
register long x8 __asm__("x8") = 160; /* __NR_uname on aarch64 */
36+
register long x0 __asm__("x0") = (long)buf;
37+
__asm__ volatile("svc #0" : "+r"(x0) : "r"(x8) : "memory");
38+
return x0;
39+
}
40+
41+
int uname(struct utsname *buf) {
42+
/* The raw syscall returns 0 or a negative errno; normalize to the libc
43+
contract (0 / -1) so callers checking for -1 aren't fooled by a raw -errno.
44+
errno itself is left unset — failure (only EFAULT here) is unreachable with
45+
a valid buffer, and setting it would pull in the libc TLS we avoid. */
46+
if (raw_uname(buf) != 0) {
47+
return -1;
48+
}
49+
static const char release[] = "5.10.0";
50+
for (unsigned i = 0; i < sizeof(release); i++) {
51+
buf->release[i] = release[i];
52+
}
53+
return 0;
54+
}

test/wrapper_test.c

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
* wrapper is compiled with -DCLAUDE_WRAPPER_NO_MAIN so its main() doesn't
1010
* collide with greatest's here.
1111
*
12-
* BINARY and TMPDIR_PATH are the sentinel literals baked into the wrapper (see
13-
* scripts/test-wrapper.sh); they need not exist on disk because exec is faked.
12+
* BINARY, TMPDIR_PATH, and UNAME_SHIM are the sentinel literals baked into the
13+
* wrapper (see mise-tasks/test/fast); they need not exist on disk because exec
14+
* is faked.
1415
*/
1516
#define _GNU_SOURCE
1617
#include <stdio.h>
@@ -138,16 +139,18 @@ TEST preserves_existing_disable_autoupdater(void) {
138139

139140
/*
140141
* termux-exec is preloaded into every Termux shell; its text-script libc.so
141-
* crashes the glibc binary's ld.so, so the wrapper must clear LD_PRELOAD before
142-
* the handoff.
142+
* crashes the glibc binary's ld.so. The wrapper overwrites LD_PRELOAD with the
143+
* uname shim — evicting termux-exec AND preloading the interposer that keeps Bun
144+
* off the crashing epoll_pwait2 path (bun#32489). An inherited value is
145+
* displaced, not preserved.
143146
*/
144-
TEST clears_ld_preload(void) {
147+
TEST sets_ld_preload_to_uname_shim(void) {
145148
reset_fixture();
146149
setenv("LD_PRELOAD", "/usr/lib/libtermux-exec-ld-preload.so", 1);
147150
char *argv[] = {"claude", NULL};
148151
claude_wrapper_run(1, argv, recording_exec);
149152

150-
ASSERT(getenv("LD_PRELOAD") == NULL);
153+
ASSERT_STR_EQ(UNAME_SHIM, env_or_empty("LD_PRELOAD"));
151154
PASS();
152155
}
153156

@@ -181,7 +184,7 @@ int main(int argc, char **argv) {
181184
RUN_TEST(preserves_existing_tmpdirs);
182185
RUN_TEST(sets_disable_autoupdater_when_unset);
183186
RUN_TEST(preserves_existing_disable_autoupdater);
184-
RUN_TEST(clears_ld_preload);
187+
RUN_TEST(sets_ld_preload_to_uname_shim);
185188
RUN_TEST(exec_failure_returns_127);
186189
GREATEST_MAIN_END();
187190
}

0 commit comments

Comments
 (0)