fix(deps): cap chromadb<1.5.4 — Rust bindings UAF on macOS 26 ARM64 - #1376
fix(deps): cap chromadb<1.5.4 — Rust bindings UAF on macOS 26 ARM64#1376MohamedAbdallah-14 wants to merge 1 commit into
Conversation
|
+1 confirming the bisect from another machine. Environment: macOS 26.4.1 ARM64 (Apple Silicon), Python 3.13.13, chromadb 1.5.7 (squarely in the broken range), populated palace at ~89k drawers. Symptom: 15+ Python SIGSEGV in Exposure shape on this machine: multiple Claude Code sessions running concurrently against the same palace. Each session's hooks spawn subprocesses that import mempalace and open their own chromadb client, so the on-disk palace gets attached by 2-3+ processes at once. I added per-palace flocks around The cap to |
|
Update 2026-05-06 Correction on "1.5.3 works": that was a single-shot The crash is intra-process. Dozens of threads in the same address space, one walker dereferences a freed node and falls into For my own setup I went the backend route: wrote a Migration: I'll open a separate PR once the search-side adapters in |
Mirrors upstream PR MemPalace#1376. ChromaDB 1.5.4 through at least 1.5.9 ship a Rust bindings UAF that SIGSEGVs in tokio-rt-worker threads when a populated palace is opened on macOS 26 ARM64. The upstream cap excludes the broken release line so installs do not pick it up automatically. PR MemPalace#1376 bisect confirms 1.5.3 works; this venv is now pinned to chromadb==1.5.3 locally and the published constraint allows any version in [1.5.0, 1.5.4).
PR MemPalace#1376 bisect on a different palace shape said 1.5.3 was the last working version. On this palace (~89k drawers, macOS 26.4.1 ARM64, Apple Silicon) chromadb 1.5.3 still SIGSEGVs in chromadb_rust_bindings on the first PersistentClient operation that loads HNSW. 1.5.0 verified clean: count() returns 89,049 and peek() returns valid drawer IDs without crashing. Tighten the pin to ==1.5.0 for now until a clean version newer than that is identified.
|
Corroborating evidence from a different platform — same library, same version range. Linux x86_64, chromadb 1.5.8 (squarely in your broken Stable shape:
Where my evidence differs from yours, and why I think it still supports the cap: the Linux faulting addresses cluster at
Either way: chromadb 1.5.8 has at least one Linux-tickling failure mode in the same .so, in the same threading layer, against the same workload pattern (palace open / collection touch). The Workload context: the daemon dispatches +1 on the cap. If a Linux-specific bisect would help broaden the evidence base, happy to do one — palace is big enough to reproduce reliably without instrumentation. Your Cross-ref to dmesg evidence on a 4-month-uptime production server, in case it helps future-you reading this thread: Open to filing a separate Linux-evidence issue if maintainers want the platforms tracked separately, but I read your PR's framing as broad enough ( |
chromadb 1.5.4..1.5.9 (latest) segfault in chromadb_rust_bindings.abi3.so on macOS 26 / ARM64 the moment a populated PersistentClient collection is opened. The crash is reproducible with a 5-line script and disables mempalace status / migrate / mine / MCP server lifecycle on that platform. Bisect on a 547k-record real palace (mempalace 3.3.4): | chromadb | result | |----------|-----------------| | 1.5.9 | SIGSEGV | | 1.5.8 | SIGSEGV | | 1.5.3 | works | | 1.5.0 | works | | 1.4.0 | works | | 1.3.0 | works | Regression boundary: 1.5.3 -> 1.5.4. Symptom: many tokio-rt-worker threads in a recursive enum walk (HNSW/segment iteration, by shape), one walker dereferences a poisoned pointer inside pthread_mutex_unlock of a node whose backing memory has been freed. Classic use-after-free under concurrent walkers + compaction/GC. Tracking: - MemPalace#1355 (mirror, OPEN) - chroma-core/chroma#6852 (upstream, OPEN) - MemPalace#1340, MemPalace#1274, MemPalace#1329 (related symptoms) This is a temporary cap. Lift once upstream chroma ships a fix and re-pin to >=<fixed-version>,<2. Lower bound bumped to >=1.5.0 (matches what mempalace was actually built and tested against in this 1.5.x line); maintainers may prefer >=1.0,<1.5.4 if older 1.x is also tested. PR MemPalace#426 is in flight on the lower-bound side. Tested locally on macOS 26.3.1 / Apple Silicon: chromadb 1.5.3 opens the existing palace cleanly (count=547,377, peek returns IDs); chromadb 1.5.8 (current install) crashes immediately on the same operation.
68a54b5 to
57ae771
Compare
|
Rebased on main (57ae771). Cross-platform evidence summary, since the thread now has three reproducers:
All in The Independently, #1386 has the structural fix: a |
Alternate vector backend implementing the full BaseBackend / BaseCollection contract using sqlite-vec's vec0 virtual table. Useful on platforms where chromadb_rust_bindings is unsafe — notably macOS 26 / ARM64, where the rust bindings have an intra-process UAF in the recursive segment walker (chroma-core/chroma#6852, MemPalace#1355, MemPalace#1376). Backend characteristics: - No Tokio runtime, no Rust extension, no recursive walker — the UAF cannot fire because the codepath does not exist. - Single sqlite_vec.db per palace; per-collection vec0 virtual table sized to the collection's dimension. - Chroma-style metadata filters compiled to SQL over json_extract(meta, …). Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $and, $or, plus bare-scalar equality. Where-document: $contains, $not_contains, $and, $or. Unknown operators raise UnsupportedFilterError per spec §1.4. - Implements add, upsert, query, get, delete, count, update (atomic override). update advertises supports_update via capabilities. Optional dep — opt in via pip install mempalace[sqlite-vec]. Registered through the existing mempalace.backends entry-point group, so selection goes through the standard registry. Migration: examples/migrate_chroma_to_sqlite_vec.py reads chroma.sqlite3 directly via stdlib sqlite3 (zero chromadb code involved, so the UAF cannot fire) and re-embeds via the existing get_embedding_function. Stock hnswlib cannot load chromadb's segment envelope, hence re-embed rather than vector copy. Resumable on drawer_id uniqueness — re-running picks up where it left off. Tested on a 664k-drawer palace with exact count parity to the source. Tests: 39 cases covering backend lifecycle, writes, queries, the where compiler (parametrized over every supported operator), where_document filters, get pagination, and registry-side selection. Skip cleanly when the sqlite-vec extra is not installed.
|
Thanks for surfacing the macOS-26 ARM64 use-after-free. We are keeping chromadb>=1.5.4 — the embeddinggemma/ChromaDB 1.5.x compatibility is now fixed in #1631, and backend stability is being addressed through the new pluggable-backends work rather than pinning Chroma down. Closing as superseded. If the UAF still reproduces on current develop, please open an issue with a repro; that crash report is genuinely useful. |
Summary
Cap
chromadb<1.5.4until upstream chroma fixes a use-after-free inchromadb_rust_bindings.abi3.sothat crashes mempalace on macOS 26 /Apple Silicon the moment a populated palace is opened.
Refs:
_segment_appears_healthyfalse-negative for partially-flushed segments #1274, Stop hook: 1.9 TB palace bloat + ChromaDB Rust bindings segfault despite #1231 fix #1329 (related symptoms —status/minesegfaults, MCP unrecoverable)Bisect
Tested against an existing 547,377-record palace (mempalace 3.3.4 on macOS 26.3.1 ARM64, Python 3.14.3, Apple M4 Pro):
Regression landed between 1.5.3 (2026-03-07) and 1.5.4. 1.5.9 (released 2026-05-04) is not a fix — same crash signature.
The current pin
chromadb>=1.5.4,<2forces every version in the broken range. This PR caps to<1.5.4so installations on macOS 26 don't pick up the broken release line.Crash diagnosis (without symbols)
Every crash on this machine has the same shape:
tokio-rt-workerthreads, 2sqlx-sqlite-worker-*, plus a pool of unnamed Rust workers all in the same deeply recursive call frames insidechromadb_rust_bindings.abi3.so(offsets+10160324/+10161424/+10162124/+10398848repeating — two alternating call sites and a key recursion frame; shape of an enum visitor on a tree with two recursive variants, almost certainly an HNSW/segment node walk).x16 = pthread_mutex_unlock, ESR isbyte read Translation fault, faulting address is a poisoned pointer (0x000001fede243d3a,0x00000200b0643d3a,0x000002077be43d3aacross runs — large, high-bit-set, not null).std::__1::mutex::lock(); one is inoperator newmid-allocation.That's a use-after-free of a node containing a mutex: concurrent walkers + a compaction/GC thread → one walker dereferences a node whose backing memory has been freed → unlocking the mutex faults.
Different fault-address shape than chroma#6852's reported null deref, but same library, same platform, same recursion pattern. Possibly two failure modes in the same code path, or the same bug with non-deterministic state.
12+ confirmed crashes on this machine in a 90-minute window today, all matching this signature, including ones with
parentProc: launchd(the live MCP server, not just CLI invocations).Workaround envelope
chromadb>=1.5.0,<1.5.4matches what mempalace 3.3.x was likely built and tested against in the 1.5.x line. If maintainers want a wider lower bound, #426 already proposes>=1.0,<2; combining its lower-bound relaxation with this upper-bound cap would give the broadest working envelope.Open to either bound; happy to amend.
When to lift
Once chroma ships a fix that covers macOS 26 / ARM64, re-pin to
>=<fixed-version>,<2.Test
Verified locally:
Same script in a fresh venv on the same machine, same palace data — chromadb 1.5.3 succeeds end to end; 1.5.8 (current install) and 1.5.9 (latest) both exit 139.