Skip to content

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

Open

Description

@kcramakrishna

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 --version
Segmentation 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

  1. Guard free(NULL) (and realloc(NULL, ·), __libc_free(NULL)) at the top of every allocator
    entry point
    , before any segment-table lookup. One compare-and-return fixes the crash.
  2. Stop exporting the libc allocator symbols as GLOBAL in .dynsym in the static release build
    (hidden/local visibility). That removes the interposition hazard permanently instead of relying on
    every path tolerating a foreign allocator.
  3. Add a release gate that runs <binary> --version (and daemon 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    editor/integrationEditor compatibility and CLI integrationstability/performanceServer crashes, OOM, hangs, high CPU/memoryux/behaviorDisplay bugs, docs, adoption UX

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions