Skip to content

CBL-8813: Fix HeapDict::_count desync on remove()-then-set() of a sou… - #311

Merged
jianminzhao merged 1 commit into
release/4.1from
cbl-8813-fleece
Sep 1, 2026
Merged

CBL-8813: Fix HeapDict::_count desync on remove()-then-set() of a sou…#311
jianminzhao merged 1 commit into
release/4.1from
cbl-8813-fleece

Conversation

@jianminzhao

Copy link
Copy Markdown
Contributor

…rce key

HeapDict::setting() couldn't distinguish two different reasons a slot can be empty for a key that exists in _source: a slot just created to shadow that key for the first time (already counted, no increment needed), versus a tombstone left behind by an earlier remove() or removeAll() (which already decremented _count, so re-setting it must increment). Both look identical -- empty slot, key present in _source -- so the increment was always skipped, leaving _count one too low after remove()-then-set() (or removeAll()-then-set()) on a source key.

That undercount corrupts kvArray(): it sizes its cache array from the (wrong, too-low) count(), then the correct iterator overruns it, an out-of-bounds write that HeapArray::setting()'s bounds check only catches in debug builds (#if DEBUG) -- in shipping builds it silently corrupts the heap. This is the root cause of the crashes reported in CBSE-23608 (VectorRecord::setRemoteRevision performs exactly this remove-then-set pattern on load-then-save of a synced document).

Fix: track whether the slot pre-existed the call (a tombstone) versus was just created (a fresh shadow of a source key), and increment count() in the former case.

Adds two regression tests (remove() and removeAll() variants) that fail with the old count() and pass with the fix; verified against the full Fleece test suite with no new failures.

…rce key

HeapDict::setting() couldn't distinguish two different reasons a slot
can be empty for a key that exists in _source: a slot just created to
shadow that key for the first time (already counted, no increment
needed), versus a tombstone left behind by an earlier remove() or
removeAll() (which already decremented _count, so re-setting it must
increment). Both look identical -- empty slot, key present in _source
-- so the increment was always skipped, leaving _count one too low
after remove()-then-set() (or removeAll()-then-set()) on a source key.

That undercount corrupts kvArray(): it sizes its cache array from the
(wrong, too-low) count(), then the correct iterator overruns it, an
out-of-bounds write that HeapArray::setting()'s bounds check only
catches in debug builds (#if DEBUG) -- in shipping builds it silently
corrupts the heap. This is the root cause of the crashes reported in
CBSE-23608 (VectorRecord::setRemoteRevision performs exactly this
remove-then-set pattern on load-then-save of a synced document).

Fix: track whether the slot pre-existed the call (a tombstone) versus
was just created (a fresh shadow of a source key), and increment
count() in the former case.

Adds two regression tests (remove() and removeAll() variants) that
fail with the old count() and pass with the fix; verified against the
full Fleece test suite with no new failures.
@jianminzhao
jianminzhao merged commit ca6e2f0 into release/4.1 Sep 1, 2026
3 checks passed
@jianminzhao
jianminzhao deleted the cbl-8813-fleece branch September 1, 2026 22:46
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.

2 participants