Repository navigation
v0.12.0/v0.12.1 Linux release binaries SIGSEGV at startup on glibc 2.44 — bundled free() dereferences its segment table before init #2763
Description
Summary
Both v0.12.0 and v0.12.1 Linux amd64 release assets die with SIGSEGV before printing anything,
on a host with current glibc (2.44). The v0.11.0 asset of the same shape runs normally. The process
never reaches main(): glibc's __newlocale calls free(NULL) during C++ locale static
initialization, that call is interposed to the executable's own bundled free(), and that free()
dereferences its segment table pointer unconditionally — which is still NULL at that point.
This affects the AUR -bin recipes too (they repack these assets), so Arch/Manjaro users upgrading
get a binary that cannot even answer --version.
Environment
| OS | Manjaro Linux, kernel 7.1.13-2-MANJARO x86_64 |
| glibc | 2.44+r24+g16be1518495f-1 |
| libstdc++ / gcc-libs | 16.2.1+r23+gd564253eb6c8-1 |
| CPU | Intel i9-9900K (no AVX-512, so a CPU-feature cause is ruled out) |
| Assets | codebase-memory-mcp-linux-amd64.tar.gz, codebase-memory-mcp-ui-linux-amd64.tar.gz, v0.12.0 and v0.12.1 |
Reproduction
curl -fsSLO https://github.com/DeusData/codebase-memory-mcp/releases/download/v0.12.1/codebase-memory-mcp-ui-linux-amd64.tar.gz
sha512sum codebase-memory-mcp-ui-linux-amd64.tar.gz
# 54c9c05929c033e7e2635073397fe7167947399ab228fa3cfe4ca8acbd6b85d21e3ce383ebd34b65c0a46b7a5561fd01ca637e6d6dae85dad29b18de1990ab5b
tar -xf codebase-memory-mcp-ui-linux-amd64.tar.gz
./codebase-memory-mcp --versionSegmentation fault (core dumped) # exit 139, no output
Same for --help, same for the v0.12.0 plain asset, same with env -i, same with LC_ALL=C LANG=C,
and same for a locally built AUR package from the same bytes — so this is not packaging, not the
environment, and not locale data. The v0.11.0 asset on the same host prints codebase-memory-mcp 0.11.0
and exits 0.
Crash
Signal: 11 (SEGV) si_code: SEGV_MAPERR
#0 0x...5c952a5 free () <- INSIDE the executable
#1 0x7ffff763530a __newlocale () from /usr/lib/libc.so.6
#2 ... std::locale::facet::_S_create_c_locale (...) from /usr/lib/libstdc++.so.6
#3 ... (libc.so.6)
#4 ... pthread_once () from /usr/lib/libc.so.6
#5 ... std::locale::facet::_S_get_c_locale ()
#6 ... std::ctype<char>::ctype (...) from /usr/lib/libstdc++.so.6
#7 ... std::locale::_Impl::_Impl (size_t)
#8 ... std::locale::locale ()
#9 ... std::ios_base::Init::Init ()
#10 ... (libstdc++.so.6)
#11 ... (ld-linux-x86-64.so.2) <- ELF init_array, before main
rip = 0x...5c952a5 rdi = 0x0 rsi = 0x0
rdi = 0 means the interposed free(NULL) is what kills it.
Mechanism
The release binary statically links its own libc and exports the allocator entry points in its
global dynamic symbol table:
readelf --dyn-syms ./codebase-memory-mcp | grep -E '__libc_(free|malloc|calloc|realloc|memalign)'
372: 0000000000741290 173 FUNC GLOBAL DEFAULT 15 __libc_free
385: 00000000007469c0 115 FUNC GLOBAL DEFAULT 15 __libc_memalign
398: 0000000000744ef0 32 FUNC GLOBAL DEFAULT 15 __libc_realloc
414: 0000000000744770 51 FUNC GLOBAL DEFAULT 15 __libc_calloc
432: 00000000007442c0 25 FUNC GLOBAL DEFAULT 15 __libc_malloc
(and free itself at 0x741290). The executable is first in the global lookup scope, so when system
libstdc++.so.6 / libc.so.6 resolve free/__libc_free, they get the bundled copy.
glibc 2.44 frees a NULL temporary inside __newlocale. That call lands in the bundled allocator
before the allocator has any state:
0000000000741290 <free>:
741290: f3 0f 1e fa endbr64
741294: 48 8b 05 e5 ad 51 13 mov 0x1351ade5(%rip),%rax # 13c5c080 <- segment table ptr, still NULL
74129b: 48 89 fa mov %rdi,%rdx
74129e: 48 89 fe mov %rdi,%rsi
7412a1: 48 c1 ea 1d shr $0x1d,%rdx # segment index = ptr >> 29
7412a5: 48 8b 14 d0 mov (%rax,%rdx,8),%rdx # <-- fault: load from NULL+0
In v0.11.0 the same exported symbol (free @ 0x4fccb0) loads no global pointer before it checks the
argument, so the identical free(NULL) from __newlocale is harmless — which is why only 0.12.x
breaks and why it looks environment-specific: it needs a system glibc that calls free(NULL) there
(2.44 does). Upstream CI on an older glibc would stay green.
Suggested fix
- Guard
free(NULL)(andrealloc(NULL, ·),__libc_free(NULL)) at the top of every allocator
entry point, before any segment-table lookup. One compare-and-return fixes the crash. - Stop exporting the libc allocator symbols as
GLOBALin.dynsymin the static release build
(hidden/local visibility). That removes the interposition hazard permanently instead of relying on
every path tolerating a foreign allocator. - Add a release gate that runs
<binary> --version(anddaemon start) in a container on the
newest glibc, not just on the build host's, so a startup regression cannot ship.
Impact / workaround for users
Every process dies before main(), so any MCP client (VS Code, Claude Code, Hermes, Codex, …) fails to
start the server, and --version, --help and install are all unusable. Until a fixed release:
stay on v0.11.0 (works), and pin it in the package manager.
Activity
fix(agents): Use codebase-memory-mcp from nixpkgs