Skip to content

Commit 1132b82

Browse files
anvansterclaude
andcommitted
fix(server): make the glibc single-threaded shim writable (#15)
codegraph-server segfaulted at startup on Linux/aarch64 - every invocation, including --version and --help, before main() ever ran. The shim defined __libc_single_threaded as an immutable static, which lands in .rodata, and its comment claimed "on newer glibc the real symbol shadows this at runtime". That is the opposite of how ELF resolves it: a definition in the executable takes precedence over the one in libc. On aarch64 the symbol is also emitted into .dynsym, so glibc bound its own startup write of the flag to our read-only byte and took SIGSEGV. x86_64 escaped it only because the symbol is not dynamically exported there, so glibc kept using its own copy - which is why this looked environment- specific and why the shipped x86_64 binaries were fine. glibc owns the value: it sets the flag at startup and clears it on thread creation. We only supply storage, wrapped in an UnsafeCell so it lands in writable memory, and never read it. On a glibc too old to maintain it the byte stays 0, the conservative "not single threaded" answer, so the SLES 15 SP4 / glibc 2.31 case the shim exists for still links and runs. Verified on aarch64 Ubuntu 24.04 (glibc 2.39), built from these sources: before, --version and --help both exited 139 and the symbol was `R`; after, the symbol is `B` and --version, --help and --info all exit 0. codegraph-pro-server carries the same shim and needs the same change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017rVbt7rENTwXkdHt3Bpgb5
1 parent 77edac8 commit 1132b82

1 file changed

Lines changed: 31 additions & 4 deletions

File tree

crates/codegraph-server/src/main.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,38 @@ use std::sync::atomic::{AtomicUsize, Ordering};
1414
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
1515

1616
// glibc 2.31 compat: __libc_single_threaded was added in glibc 2.32 but ONNX
17-
// Runtime references it. Provide a fallback for SLES 15 SP4 and similar.
18-
// On newer glibc the real symbol shadows this at runtime.
17+
// Runtime references it, so a build for SLES 15 SP4 and similar needs a
18+
// definition to link against.
19+
//
20+
// The storage must be WRITABLE. This was previously `pub static ... : u8 = 0`,
21+
// which lands in .rodata, and the comment claimed "on newer glibc the real
22+
// symbol shadows this at runtime" - the opposite of how ELF resolves it. A
23+
// definition in the executable takes precedence over the one in libc, and on
24+
// aarch64 this symbol is also emitted into .dynsym, so glibc bound its own
25+
// startup write of the flag to our read-only byte and took SIGSEGV before
26+
// main(): every invocation died, including `--version` and `--help`
27+
// (issue #15). x86_64 escaped only because the symbol is not dynamically
28+
// exported there, so glibc kept using its own copy.
29+
//
30+
// glibc owns the value: it sets the flag at startup and clears it when a
31+
// thread is created. We only supply the storage, and never read it. On a glibc
32+
// too old to maintain it, the byte stays 0, which is the conservative
33+
// "not single threaded" answer.
1934
#[cfg(target_os = "linux")]
20-
#[no_mangle]
21-
pub static __libc_single_threaded: u8 = 0;
35+
mod glibc_compat {
36+
use std::cell::UnsafeCell;
37+
38+
#[repr(transparent)]
39+
pub struct SingleThreaded(UnsafeCell<u8>);
40+
41+
// SAFETY: glibc is the only writer, from its own startup and
42+
// thread-creation paths, and this process never reads the byte. The
43+
// UnsafeCell is what places it in writable memory rather than .rodata.
44+
unsafe impl Sync for SingleThreaded {}
45+
46+
#[no_mangle]
47+
pub static __libc_single_threaded: SingleThreaded = SingleThreaded(UnsafeCell::new(0));
48+
}
2249

2350
#[derive(Parser)]
2451
#[command(name = "codegraph-server")]

0 commit comments

Comments
 (0)