Skip to content

fix(deps): cap chromadb<1.5.4 — Rust bindings UAF on macOS 26 ARM64 - #1376

Closed
MohamedAbdallah-14 wants to merge 1 commit into
MemPalace:developfrom
MohamedAbdallah-14:fix/chromadb-pin-macos26-uaf
Closed

fix(deps): cap chromadb<1.5.4 — Rust bindings UAF on macOS 26 ARM64#1376
MohamedAbdallah-14 wants to merge 1 commit into
MemPalace:developfrom
MohamedAbdallah-14:fix/chromadb-pin-macos26-uaf

Conversation

@MohamedAbdallah-14

Copy link
Copy Markdown

Summary

Cap chromadb<1.5.4 until upstream chroma fixes a use-after-free in
chromadb_rust_bindings.abi3.so that crashes mempalace on macOS 26 /
Apple Silicon the moment a populated palace is opened.

Refs:

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):

chromadb result
1.5.9 (latest) SIGSEGV (exit 139)
1.5.8 SIGSEGV
1.5.3 works (count=547,377, peek returns IDs)
1.5.0 works
1.4.0 works
1.3.0 works

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,<2 forces every version in the broken range. This PR caps to <1.5.4 so 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:

  • 13 tokio-rt-worker threads, 2 sqlx-sqlite-worker-*, plus a pool of unnamed Rust workers all in the same deeply recursive call frames inside chromadb_rust_bindings.abi3.so (offsets +10160324 / +10161424 / +10162124 / +10398848 repeating — 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).
  • The crashing thread is in the unwind path right after the recursion returns. x16 = pthread_mutex_unlock, ESR is byte read Translation fault, faulting address is a poisoned pointer (0x000001fede243d3a, 0x00000200b0643d3a, 0x000002077be43d3a across runs — large, high-bit-set, not null).
  • Many sibling threads are blocked on std::__1::mutex::lock(); one is in operator new mid-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.4 matches 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:

import chromadb
client = chromadb.PersistentClient(path="~/.mempalace/palace")
col = client.get_collection("mempalace_drawers")
print(col.count())   # crashes on 1.5.4..1.5.9, prints 547377 on 1.5.3
print(col.peek(limit=2)["ids"])

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.

@igorls igorls added bug Something isn't working storage dependencies Pull requests that update a dependency file labels May 6, 2026
@hkf57

hkf57 commented May 6, 2026

Copy link
Copy Markdown

+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 chromadb_rust_bindings.abi3.so in a single working day, all matching the signature you describe - deep recursive frames in the rust bindings, surrounding tokio-rt-worker pool, KERN_INVALID_ADDRESS / EXC_BAD_ACCESS faults. Process lifetimes range from 20 seconds to a few minutes before the unwind crash. Happens at any phase: open, search, write.

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 PersistentClient open, a singleton flock to refuse a second concurrent MCP server, and a probe so hook subprocesses bow out when an MCP holder is live (hkf57/mempalace feat/chroma-flock-gate). Those reduced frequency materially but did not eliminate crashes; the UAF still fires under single-process load on a populated palace.

The cap to <1.5.4 is the right fix. Happy to test 1.5.3 against the same workload and report back if it would help unblock review.

@MohamedAbdallah-14

Copy link
Copy Markdown
Author

Update 2026-05-06

Correction on "1.5.3 works": that was a single-shot count + peek probe and didn't exercise the bug. Re-tested against real ingest load and 1.5.3 crashes too — same UAF, same recursive-walker frames. The <1.5.4 cap still helps (1.5.4+ regressed something further), but the bug is older than the cap.

The crash is intra-process. Dozens of threads in the same address space, one walker dereferences a freed node and falls into pthread_mutex_unlock on a poisoned pointer. Reproduces with no other mempalace process on the machine. TOKIO_WORKER_THREADS=1 / RAYON_NUM_THREADS=1 don't help — chromadb's Rust extension initializes its own Tokio runtime; those env vars don't reach it.

For my own setup I went the backend route: wrote a sqlite_vec backend at mempalace.backends.sqlite_vec.SqliteVecBackend using sqlite-vec's vec0 virtual table. Implements the full BaseCollection / BaseBackend surface — add/upsert/query/get/delete/count/update — with chroma-style metadata filters compiled to SQL over json_extract(meta, ...). Selected via MEMPALACE_BACKEND=sqlite_vec, registered through the existing mempalace.backends entry-point group.

Migration: chroma.sqlite3 holds docs + metadata in plain SQLite; chromadb wraps hnswlib's index with its own segment envelope, so stock hnswlib can't load it. Re-embedding via get_embedding_function() — ~100/s on M4 Pro, ~90 min for a 540k palace.

I'll open a separate PR once the search-side adapters in searcher.py / mcp_server.py are done and the daemon has a week of stable use behind it. This cap is still the right immediate fix — independent of any backend swap.

hkf57 added a commit to hkf57/mempalace that referenced this pull request May 6, 2026
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).
hkf57 added a commit to hkf57/mempalace that referenced this pull request May 6, 2026
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.
@jphein

jphein commented May 6, 2026

Copy link
Copy Markdown
Collaborator

Corroborating evidence from a different platform — same library, same version range.

Linux x86_64, chromadb 1.5.8 (squarely in your broken 1.5.4..1.5.9 range), 4-core Intel server, mempalace 3.3.4, palace size 160K drawers. 24 chromadb segfaults today in a 2.5-hour window (02:47 → 05:13 PDT), all matching the same signature:

tokio-rt-worker[<pid>]: segfault at <addr> ip <ip> sp <sp> error 4
  in chromadb_rust_bindings.abi3.so[32d1877,<base>+21a8000]

Stable shape:

  • All in tokio-rt-worker[…] threads (same as your macOS reproducer's "13 tokio-rt-worker threads" finding)
  • All in chromadb_rust_bindings.abi3.so
  • All at the same instruction pointer offset 0x8d2877 within the library, run after run, PID after PID. Fixed point.
  • error 4 on Linux = user-mode read fault on non-present page

Where my evidence differs from yours, and why I think it still supports the cap: the Linux faulting addresses cluster at 0x0 (null read, ~80%) and 0x44, not the poisoned-pointer shape you saw on macOS (0x000001fede243d3a, etc.). Two ways to read this:

  • Same UAF, different downstream effect. macOS hits a freed-but-still-allocated mutex-bearing node (poisoned by malloc's tag); Linux's allocator zeroes freed pages so the same dereference lands on 0x0/0x44 instead. Same bug, allocator-dependent crash address.
  • Different bug, same hot spot. The IP collisions on 0x8d2877 across 24 distinct PIDs say the failure point is a single instruction in the library, but doesn't disambiguate between "one bug" and "multiple bugs at the same recursive call site."

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 <1.5.4 cap should prevent both signatures simultaneously — they're symptoms of code that didn't exist before 1.5.4.

Workload context: the daemon dispatches mempalace mine as a subprocess on each Stop-hook fire (Claude Code session end). Each subprocess opens the palace, ingests the transcript, then exits. Each segfault corresponds to one subprocess; the data is persisted before the SEGV (the daemon serves search against it correctly afterward), so the immediate impact is dmesg pollution and zero exit code visibility, not data loss. But it's noise that masks any real segfaults that might happen, and on a corpus that grows the failure rate naturally grows with it.

+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 <1.5.4 shape is also correct for me from a workload-compatibility standpoint: mempalace 3.3.x was tested against the 1.5.0-1.5.3 line per the regression window.

Cross-ref to dmesg evidence on a 4-month-uptime production server, in case it helps future-you reading this thread:

May 06 04:23:09 disks kernel: tokio-rt-worker[664338]: segfault at 0 ip 0x8d2877 sp 0x... error 4
                                                       in chromadb_rust_bindings.abi3.so
May 06 04:24:29 disks kernel: tokio-rt-worker[664771]: segfault at 44 ip 0x8d2877 ...
May 06 04:28:36 disks kernel: tokio-rt-worker[666771]: segfault at 0  ip 0x8d2877 ...
[...and 21 more in the same 2.5-hour window]

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) that the same cap addresses both. Let me know.

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.
@MohamedAbdallah-14
MohamedAbdallah-14 force-pushed the fix/chromadb-pin-macos26-uaf branch from 68a54b5 to 57ae771 Compare May 11, 2026 16:15
@MohamedAbdallah-14

Copy link
Copy Markdown
Author

Rebased on main (57ae771).

Cross-platform evidence summary, since the thread now has three reproducers:

  • macOS 26 ARM64, chromadb 1.5.7, ~89k drawers (@hkf57)
  • macOS 26 ARM64, chromadb 1.5.4+, ~540k drawers (mine)
  • Linux x86_64, chromadb 1.5.8, ~160k drawers (@jphein)

All in chromadb_rust_bindings.abi3.so, all in tokio-rt-worker threads, all on the palace-open / collection-touch path. On Linux the faulting IP collides at 0x8d2877 across 24 distinct PIDs in a 2.5h window — a single call site in the .so, not allocator-tag drift. macOS sees poisoned pointers (malloc-tagged frees), Linux sees 0 / 0x44 reads (zeroed freed pages); same instruction, allocator-dependent crash address.

The <1.5.4 cap is still the right immediate fix.

Independently, #1386 has the structural fix: a sqlite_vec backend at mempalace.backends.sqlite_vec.SqliteVecBackend that bypasses chromadb_rust_bindings entirely (MEMPALACE_BACKEND=sqlite_vec). Complementary, not competing — this PR protects users who cannot migrate today; #1386 gives a clean exit for those who can.

MohamedAbdallah-14 added a commit to MohamedAbdallah-14/mempalace that referenced this pull request May 11, 2026
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.
@igorls
igorls changed the base branch from main to develop May 17, 2026 19:35
@igorls

igorls commented Jun 6, 2026

Copy link
Copy Markdown
Member

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.

@igorls igorls closed this Jun 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dependencies Pull requests that update a dependency file storage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants