Skip to content

fix: prevent aarch64 startup crash and cross-file doc chunk id collisions - #19

Open
anvanster wants to merge 5 commits into
mainfrom
fix/issues-15-16
Open

fix: prevent aarch64 startup crash and cross-file doc chunk id collisions#19
anvanster wants to merge 5 commits into
mainfrom
fix/issues-15-16

Conversation

@anvanster

Copy link
Copy Markdown
Member

Intent

The developer asked the agent to triage two newly filed GitHub issues against the CodeGraph repo and then fix them: issue #16, where markdown doc chunk IDs were minted from a per-file counter so indexing a second markdown file silently overwrote the first file's chunks in RocksDB and the vector store, and issue #15, an aarch64 Linux SIGSEGV at startup. They explicitly required that #15 be reproduced on their arm64 Ubuntu OrbStack VM rather than diagnosed from theory, and after seeing the reproduction and root cause (the __libc_single_threaded glibc shim declared as an immutable static landing in read-only memory and being dynamically exported on aarch64), they authorized applying that fix and committing the #16 fix. They then asked to bump the version, commit, and push, with pushes going through the no-mistakes gate rather than directly to origin, and with CHANGELOG.md left untouched per their standing rule against hand-editing generated files. Earlier in the same session they had also insisted the already-published npm 0.20.0 must not be republished or renumbered, which framed this as a follow-on 0.20.1 patch release carrying the two issue fixes.

What Changed

  • Moved the __libc_single_threaded glibc 2.31 shim into a new codegraph-server::glibc_compat module that backs the symbol with a writable UnsafeCell<u8> (SingleThreaded::ZERO) instead of an immutable static u8 in .rodata. Both the binary target (main.rs) and the cfg(test) definition in lib.rs now share that storage, so glibc's startup write to the flag no longer faults - previously every invocation on aarch64 Linux, including --version, died with SIGSEGV before main() because the executable's read-only definition took precedence over libc's via .dynsym (issue codegraph-server SIGSEGVs at startup on Linux/aarch64 (before main, even --version) #15).
  • Doc chunk ids are now namespaced by source file: chunk_id() prefixes the per-file counter with a pinned 64-bit FNV-1a hash of the source path (doc-{hash:016x}-{counter:04}), replacing the bare doc-{counter:04} that made every document reuse the same global RocksDB/chunk_cache/HNSW keys and silently overwrite earlier files' chunks (issue codegraph_index_markdown` overwrites previously indexed documents in v0.19.1 #16). FNV-1a is used deliberately over DefaultHasher, whose output is not stable across Rust releases, since the hash is baked into persisted keys.
  • Added unit tests for cross-source id uniqueness, per-source id stability, large-vs-small document counter overlap, and the pinned FNV-1a vectors, plus a doc_multi_source integration test exercising multi-file indexing through DocStore. Bumped the workspace, mcp-package, VS Code, and JetBrains version strings to 0.20.1 and synced the VSIX install example in both READMEs.

Risk Assessment

✅ Low: The follow-up commit resolves the previously reported sibling defect at the right boundary - both the binary and the cfg(test) lib definition now share the single SingleThreaded(UnsafeCell&lt;u8&gt;) newtype, whose non-Freeze type is what forces writable .data placement, and the two definitions remain mutually exclusive per build target so no target sees zero or two of them; the rest of the branch (source-namespaced doc chunk ids, consistent 0.20.1 pins) was verified clean in the prior pass.

Testing

Ran the docs unit tests in codegraph-memory (all 14 pass, including the four new id-collision/stability tests), then wrote and ran a new end-to-end integration test that indexes two markdown files through the real RocksDB-backed DocStore and reopens it - it fails on the base commit's docs.rs with 7 of 10 chunks silently evicted and passes on the branch with both documents intact and searchable. For the aarch64 startup crash I built the pre-fix and post-fix shim variants inside an aarch64 Linux glibc container and captured ELF section placement plus actual process exit status: read-only .rodata shim SIGSEGVs before main(), the writable SingleThreaded shim reaches main(); the same comparison was repeated for the cfg(test) shim form. No UI surface is involved in this change, so the reviewer-visible evidence is CLI transcripts rather than screenshots. Transient repro binaries and the pulled container image were removed; the only working-tree change left is the new test file.

Evidence: Issue #15 - aarch64 Linux before/after reproduction (SIGSEGV vs clean start)

=== aarch64 Linux reproduction of issue #15 (codegraph-server SIGSEGV at startup) === host: Linux 6.19.13-orbstack-gbd1dc07b8cf4 aarch64 libc: ldd (Debian GLIBC 2.41-12+deb13u3) 2.41 rustc: rustc 1.97.1 (8bab26f4f 2026-07-14) --- BEFORE fix - pub static __libc_single_threaded: u8 = 0 --- symbol : 70: 000000000003aac9 1 OBJECT GLOBAL DEFAULT 14 __libc_single_threaded section: .rodata (flags: A) exported in .dynsym: 1 entry run : KILLED, exit=139 (SIGSEGV) - never reached main() --- AFTER fix - glibc_compat::SingleThreaded (UnsafeCell<u8>) --- symbol : 70: 00000000000609b9 1 OBJECT GLOBAL DEFAULT 27 __libc_single_threaded section: .bss (flags: WA) exported in .dynsym: 1 entry run : codegraph-server 0.20.1 (main() reached) [exit 0]

=== aarch64 Linux reproduction of issue #15 (codegraph-server SIGSEGV at startup) ===
host: Linux 6.19.13-orbstack-gbd1dc07b8cf4 aarch64
libc: ldd (Debian GLIBC 2.41-12+deb13u3) 2.41
rustc: rustc 1.97.1 (8bab26f4f 2026-07-14)

--- BEFORE fix - pub static __libc_single_threaded: u8 = 0 ---
    symbol : 70: 000000000003aac9     1 OBJECT  GLOBAL DEFAULT   14 __libc_single_threaded
    section: .rodata (flags: A)
    exported in .dynsym: 1 entry
    run    : KILLED, exit=139 (SIGSEGV) - never reached main()

--- AFTER fix  - glibc_compat::SingleThreaded (UnsafeCell<u8>) ---
    symbol : 70: 00000000000609b9     1 OBJECT  GLOBAL DEFAULT   27 __libc_single_threaded
    section: .bss (flags: WA)
    exported in .dynsym: 1 entry
    run    : codegraph-server 0.20.1 (main() reached)  [exit 0]
Evidence: Issue #16 - second markdown file silently evicts the first (pre-fix docs.rs)

running 1 test indexed architecture.md -> 10 chunks indexed onboarding.md -> 3 chunks list_doc_sources -> 2 source(s) chunks still stored for architecture.md -> 7 thread 'indexing_a_second_source_does_not_evict_the_first' panicked at crates/codegraph-memory/tests/doc_multi_source.rs:107:9: assertion left == right failed: indexing the second file must not drop chunks from the first left: 7 right: 10 test indexing_a_second_source_does_not_evict_the_first ... FAILED

   Compiling codegraph-memory v0.20.1 (/Users/anvanster/.no-mistakes/worktrees/b8216ba13cd2/01KZJF58SQZ239MEF2NKEABMCF/crates/codegraph-memory)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running tests/doc_multi_source.rs (target/debug/deps/doc_multi_source-0401732d95570382)

running 1 test
indexed architecture.md -> 10 chunks
indexed onboarding.md   -> 3 chunks
list_doc_sources        -> 2 source(s)
chunks still stored for architecture.md -> 7

thread 'indexing_a_second_source_does_not_evict_the_first' (47457249) panicked at crates/codegraph-memory/tests/doc_multi_source.rs:107:9:
assertion `left == right` failed: indexing the second file must not drop chunks from the first
  left: 7
 right: 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test indexing_a_second_source_does_not_evict_the_first ... FAILED

failures:

failures:
    indexing_a_second_source_does_not_evict_the_first

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.95s

error: test failed, to rerun pass `-p codegraph-memory --test doc_multi_source`
Evidence: Issue #16 - both documents intact, searchable, and persisted after fix

running 1 test indexed architecture.md -> 10 chunks indexed onboarding.md -> 3 chunks list_doc_sources -> 2 source(s) chunks still stored for architecture.md -> 10 search_docs hit -> architecture.md § Subsystem 7 (0.56) search_docs hit -> architecture.md § Subsystem 8 (0.53) search_docs hit -> architecture.md § Subsystem 9 (0.53) after reopen -> 2 source(s), architecture.md 10 chunks, onboarding.md 3 chunks test indexing_a_second_source_does_not_evict_the_first ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Compiling codegraph-memory v0.20.1 (/Users/anvanster/.no-mistakes/worktrees/b8216ba13cd2/01KZJF58SQZ239MEF2NKEABMCF/crates/codegraph-memory)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.53s
     Running tests/doc_multi_source.rs (target/debug/deps/doc_multi_source-0401732d95570382)

running 1 test
indexed architecture.md -> 10 chunks
indexed onboarding.md   -> 3 chunks
list_doc_sources        -> 2 source(s)
chunks still stored for architecture.md -> 10
search_docs hit         -> architecture.md § Subsystem 7 (0.56)
search_docs hit         -> architecture.md § Subsystem 8 (0.53)
search_docs hit         -> architecture.md § Subsystem 9 (0.53)
after reopen            -> 2 source(s), architecture.md 10 chunks, onboarding.md 3 chunks
test indexing_a_second_source_does_not_evict_the_first ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.96s
Evidence: Issue #15 - cfg(test) shim from lib.rs, aarch64 Linux (commit 6cbdde6)

# post-fix (glibc_compat::SingleThreaded) running 1 test test tests::test_executable_starts_with_the_shim_linked_in ... ok test result: ok. 1 passed; 0 failed # pre-fix (plain pub static __libc_single_threaded: u8 = 0) Segmentation fault exit=139


running 1 test
test tests::test_executable_starts_with_the_shim_linked_in ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Segmentation fault
exit=139

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ crates/codegraph-server/src/lib.rs:23 - The cfg(test) copy of the glibc shim is still #[no_mangle] pub static __libc_single_threaded: u8 = 0, i.e. an immutable static that lands in .rodata - the exact defect just fixed in main.rs. The test executable links ONNX Runtime and exports the symbol the same way the binary does, so on aarch64 Linux the dynamic linker binds glibc's startup write of the flag to this read-only byte and the process dies with SIGSEGV before any test function runs. x86_64 CI (.github/workflows/codegraph-pr.yml runs on ubuntu-latest) never exercises it, so cargo test -p codegraph-server --lib on the arm64 Ubuntu VM used to reproduce issue codegraph-server SIGSEGVs at startup on Linux/aarch64 (before main, even --version) #15 still crashes at startup. Fix at the same boundary as main.rs: give this definition writable storage (share the SingleThreaded(UnsafeCell&lt;u8&gt;) newtype rather than a second hand-rolled copy, so the invariant lives in one place for both the bin and the test target).

🔧 Fix: share writable glibc_single_threaded shim with test target
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • cargo test -p codegraph-memory --lib docs:: - 14 passed, covering the new chunk-id uniqueness, stability and pinned-FNV-1a tests
  • cargo test -p codegraph-memory --test doc_multi_source -- --nocapture - new end-to-end DocStore test (RocksDB persistence, semantic search, store reopen)
  • Same test re-run with git show 284c8e5:crates/codegraph-memory/src/docs.rs swapped in - reproduces issue #16 data loss (7 of 10 chunks survive), then restored HEAD docs.rs
  • aarch64 Linux repro in docker run --platform linux/arm64 rust:1-slim-trixie - built main_before.rs (pre-fix pub static __libc_single_threaded: u8 = 0) and main_after.rs (branch glibc_compat::SingleThreaded), inspected with readelf -sW/readelf -SW/readelf --dyn-syms, and ran both binaries
  • rustc --test variant of the cfg(all(target_os = &#34;linux&#34;, test)) shim from lib.rs, run on aarch64 Linux in both pre-fix and post-fix form
  • git diff 284c8e5..HEAD over Cargo.toml, mcp-package/package.json, mcp-package/server.json, mcp-package/bin/fetch-engine.js, vscode/package.json, jetbrains/gradle.properties - version strings all move 0.20.0 -> 0.20.1
⚠️ **Document** - 1 warning
  • ⚠️ scripts/publish-release-assets.sh:76 - The release version is hand-copied into the VSIX install example in both README.md and vscode/README.md, but publish-release-assets.sh only pin-checks the ENGINE_VERSION constants and package/server.json. That is why both READMEs still said 0.20.0 after this bump (I fixed them). Follow-up worth doing outside this change: either extend the release script's pin check to the README install examples, or make the example version-agnostic (e.g. codegraph-<version>.vsix / a marketplace-only instruction) so it cannot go stale again.
⚠️ **Lint** - 3 infos
  • ℹ️ mcp-package/package-lock.json:3 - Pre-existing version drift this bump makes more visible: mcp-package/package-lock.json still records 0.17.1 and vscode/package-lock.json still records 0.15.0 for their own packages, while package.json is now 0.20.1. Not fixed here - lockfiles must be regenerated by npm rather than hand-edited, and doing so is outside a documentation/lint pass.
  • ℹ️ vscode/package.json:1797 - The lint script runs eslint src --ext ts, but vscode/ has no eslint config of any kind, so the script fails immediately under ESLint 9+ ("couldn't find an eslint.config.js"). Pre-existing and unrelated to this change (no TypeScript changed), and adding a flat config is a real configuration decision rather than a mechanical fix, so I left it.
  • ℹ️ crates/codegraph-memory/src/docs.rs:74 - The workspace is not cargo fmt --all --check clean, in files untouched by this change (e.g. crates/codegraph-c/src/visitor.rs, several pre-existing regions of crates/codegraph-memory/src/docs.rs). No rustfmt.toml and no CI fmt gate exist, so formatting is evidently not enforced; reformatting would add large unrelated churn to this patch, so I limited fmt review to the changed hunks (which are conforming).
✅ **Push** - passed

✅ No issues found.

anvanster and others added 5 commits August 8, 2026 22:06
Indexing a second markdown file silently destroyed chunks from the first.
parse_markdown reset its counter per call, so every document minted
doc-0001, doc-0002, ... while that id is simultaneously the RocksDB key
(doc:{id} and docvec:{id}), the chunk_cache key and the HNSW point id -
a single global namespace. The second file wrote straight over the first,
which then vanished from codegraph_list_doc_sources and
codegraph_search_docs while indexing still reported status: success.

The loss was partial and size-dependent, which is why it read as
intermittent: a 3-chunk file took only the first 3 chunks of a 10-chunk
file, and the 7 survivors made the next remove_source look like it had
done its job. Re-indexing the larger file afterwards then wiped the
smaller one entirely.

Ids are now doc-{fnv1a(source_file):016x}-{counter:04}. FNV-1a rather
than DefaultHasher because the value is baked into a persisted key, and
DefaultHasher's output is explicitly not guaranteed stable across Rust
releases - a toolchain upgrade would silently orphan every chunk already
on disk. The reference vectors are pinned by a test for the same reason.

Old and new ids have different shapes, so they coexist safely and no
migration is needed. An index written before this change keeps whatever
survived until its sources are re-indexed.

Reproduced and verified end to end with the reporter's exact steps
against a real engine: before, indexing two files left list_doc_sources
reporting 1 source; after, it reports 2, re-indexing the first no longer
wipes the second, and both markers are findable via search_docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rVbt7rENTwXkdHt3Bpgb5
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
Two engine crashes/data-loss bugs are fixed since 0.20.0, so the engine
needs a release of its own for clients to fetch:

  #15  SIGSEGV at startup on Linux/aarch64, every invocation
  #16  indexing a markdown file destroyed the previous file's chunks

Moves every pin together, which is the only safe way to move any of them:
Cargo.toml (and the workspace members in Cargo.lock), the two ENGINE_VERSION
pins clients fetch by (mcp-package/bin/fetch-engine.js, shared with the VS
Code client, and CodeGraphServerResolver.ENGINE_VERSION for JetBrains), the
npm package version, both version fields in server.json - the server entry
and the npm package it resolves to - the VSIX version, and pluginVersion.

publish-release-assets.sh refuses to publish while any client pin disagrees
with Cargo.toml, so a partial bump would have failed there rather than in
the field; keeping them in one commit keeps that check meaningful.

Note the ordering this creates: the clients now ask for release assets
tagged v0.20.1, which do not exist yet. Binaries have to be built and
scripts/publish-release-assets.sh run before this version is published to
npm, or installs will fetch nothing. package-npm.sh now refuses to package
until those assets are live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rVbt7rENTwXkdHt3Bpgb5
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

🔍 CodeGraph PR Review

15 files changed (+349/−27, 12 functions) · Risk: 🟢 low

Blast radius

4 direct callers affected (2 breaking) across crates/codegraph-memory/src

⚠️ Test gaps (2 functions, 0 coverage)

  • chunk_id (crates/codegraph-memory/src/docs.rs) — signature_changed
  • collect_leaf_chunks (crates/codegraph-memory/src/docs.rs) — body_changed

Suggested reviewers

Andrey Vasilevsky (83 lines), anvanster (10 lines)

Suggested commit: feat(src): <describe the change> · 3 tests cover the changes
🤖 Generated by CodeGraph

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant