Skip to content

SIGABRT (invalid free in ts_stack_delete via cbm_destroy_thread_parser) on second index_repository in one server process — server dies silently #773

Description

@ericsonjulio1

Summary

The MCP server crashes with SIGABRT (malloc: pointer being freed was not allocated) on the second index_repository call within one server process, whenever both indexed directories contain roughly 30+ source files. The process dies silently — nothing on stderr — so the MCP client just sees MCP error -32000: Connection closed, and all codebase-memory tools are gone for the rest of the client session.

This makes any long-lived session that indexes two projects (or re-indexes one) kill the server. It likely explains "server randomly disconnected" reports, since the first index works and the crash only hits the next one.

Environment

  • codebase-memory-mcp v0.10.0, prebuilt release binary
  • macOS arm64 (Darwin 25.5.0, M-series), 8 GB RAM (mem.init budget_mb=4096 total_ram_mb=8192)
  • Client: Claude Code via stdio transport (also reproduces with no client at all — see repro)

Crash report (macOS .ips, faulting thread)

Five separate crashes across three days, all with this identical stack:

__pthread_kill
pthread_kill
abort
malloc_vreport
malloc_report
___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED
ts_stack_delete
ts_parser_delete
cbm_destroy_thread_parser
extract_worker
cbm_parallel_for
cbm_parallel_extract
cbm_pipeline_run
handle_index_repository
cbm_mcp_server_handle

So an extract_worker in the second pipeline run tears down its thread parser and ts_stack_delete frees a pointer the allocator doesn't own — looks like a per-thread parser (or its ts_stack) surviving from the first run and being destroyed again, or being freed against the wrong allocator/arena after the first run's cleanup.

Deterministic repro (no client needed, ~30 s)

#!/bin/bash
# Generates two unrelated dirs of tiny .py files, then indexes both in ONE server process.
# First index succeeds; the second crashes the server (5/5 runs on my machine).
BASE=$(mktemp -d)
mkdir -p "$BASE/dirA" "$BASE/dirB"
for i in $(seq 1 30); do
  printf 'class Handler%s:\n    def run(self, x):\n        return self.helper(x) + %s\n    def helper(self, x):\n        for i in range(10):\n            x += i\n        return x\n\ndef main_%s():\n    return Handler%s().run(%s)\n' \
    $i $i $i $i $i > "$BASE/dirA/mod$i.py"
done
for i in $(seq 1 60); do
  printf 'def util_%s(y):\n    return y * %s\n' $i $i > "$BASE/dirB/util$i.py"
done

{
  printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"repro","version":"1.0"}}}'
  printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}'
  sleep 0.3
  printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"index_repository","arguments":{"repo_path":"%s/dirA","mode":"full"}}}\n' "$BASE"
  sleep 4
  printf '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"index_repository","arguments":{"repo_path":"%s/dirB","mode":"full"}}}\n' "$BASE"
  sleep 15
} | codebase-memory-mcp

Expected: two "status":"indexed" results.
Actual: response for id 2 only; the process aborts (Abort trap: 6) ~1–2 s after the id 3 call, before any response. macOS writes a crash report to ~/Library/Logs/DiagnosticReports/codebase-memory-mcp-*.ips with the stack above.

What I ruled out (all with the same binary, fresh process each)

Scenario Result
Single index of either dir (any size, incl. a 236-file mixed-language repo) ✅ OK
2nd index in a new process (DBs from run 1 on disk) ✅ OK
Tiny pair: 1-file dir then 2-file dir ✅ OK (too small to trigger)
Tiny first index (2 files) → large second (236 files) ✅ OK
~30-file dir → ~60-file dir, same process 💥 SIGABRT
Same at larger sizes, nested or unrelated dirs, pure Python or mixed languages 💥 SIGABRT

So it needs both runs big enough (my guess: enough files that cbm_parallel_for spins up multiple/reused worker threads), and on-disk DB state is irrelevant — it's purely in-process state carried from the first extract into the second.

Happy to attach the full .ips crash reports or test a patched binary.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions