fix: prevent aarch64 startup crash and cross-file doc chunk id collisions - #19
Open
anvanster wants to merge 5 commits into
Open
fix: prevent aarch64 startup crash and cross-file doc chunk id collisions#19anvanster wants to merge 5 commits into
anvanster wants to merge 5 commits into
Conversation
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
🔍 CodeGraph PR Review15 files changed (+349/−27, 12 functions) · Risk: 🟢 low Blast radius4 direct callers affected (2 breaking) across
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_threadedglibc 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
__libc_single_threadedglibc 2.31 shim into a newcodegraph-server::glibc_compatmodule that backs the symbol with a writableUnsafeCell<u8>(SingleThreaded::ZERO) instead of an immutablestatic u8in.rodata. Both the binary target (main.rs) and thecfg(test)definition inlib.rsnow 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 beforemain()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).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 baredoc-{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 overDefaultHasher, whose output is not stable across Rust releases, since the hash is baked into persisted keys.doc_multi_sourceintegration 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 singleSingleThreaded(UnsafeCell<u8>)newtype, whose non-Freezetype is what forces writable.dataplacement, 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]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: assertionleft == rightfailed: 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 ... FAILEDEvidence: 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 outEvidence: 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 (plainpub static __libc_single_threaded: u8 = 0) Segmentation fault exit=139Pipeline
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- Thecfg(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.ymlruns on ubuntu-latest) never exercises it, socargo test -p codegraph-server --libon 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 theSingleThreaded(UnsafeCell<u8>)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 testscargo 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 withgit show 284c8e5:crates/codegraph-memory/src/docs.rsswapped in - reproduces issue #16 data loss (7 of 10 chunks survive), then restored HEAD docs.rsaarch64 Linux repro indocker run --platform linux/arm64 rust:1-slim-trixie- builtmain_before.rs(pre-fixpub static __libc_single_threaded: u8 = 0) andmain_after.rs(branchglibc_compat::SingleThreaded), inspected withreadelf -sW/readelf -SW/readelf --dyn-syms, and ran both binariesrustc --testvariant of thecfg(all(target_os = "linux", test))shim from lib.rs, run on aarch64 Linux in both pre-fix and post-fix formgit diff 284c8e5..HEADover 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.1scripts/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.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- Thelintscript runseslint 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 notcargo fmt --all --checkclean, 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.