From 0beefac57db484eaaeaa76c0cd1be09019d05f75 Mon Sep 17 00:00:00 2001 From: jianminzhao <76990468+jianminzhao@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:53:40 -0700 Subject: [PATCH] CBL-8813: Bump fleece submodule, add DeDuplicateEncoder regression test Bumps vendor/fleece to pick up the HeapDict::_count fix (see the paired fleece PR for the root-cause writeup): a remove()-then-set() of a key that exists in a MutableDict's immutable _source left count() one too low, corrupting HeapDict::kvArray()'s cache-array sizing -- an out-of-bounds write that only fails cleanly in debug builds (HeapArray::setting()'s bounds check), silently corrupting the heap in shipping builds. Root cause of the crashes reported in CBSE-23608. Adds a regression test exercising the actual code path that crashed in the field: DeDuplicateEncoder (used only by VectorRecord::encodeBodyAndExtra) encoding a MutableDict in this remove-then-set state. Before the fix this doesn't fail cleanly -- it crashes the process, since HeapDict::kvArray() is reached through Dict::iterator's public constructor, itself the C-linkage function FLDictIterator_Begin(), and a C++ exception can never safely cross an extern "C" boundary. So the test asserts the correct post-fix behavior (successful encode, correct roundtrip) rather than trying to catch the crash, and is only meaningful -- and only safe to run in CI -- with the fleece fix in place. Full CppTests suite run clean: 575 cases (572 baseline + 3 new: this one plus the two in the paired fleece PR), 35 pre-existing/unrelated failures (missing test-fixture files), zero new regressions. --- LiteCore/tests/VectorRecordTest.cc | 40 ++++++++++++++++++++++++++++++ vendor/fleece | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/LiteCore/tests/VectorRecordTest.cc b/LiteCore/tests/VectorRecordTest.cc index 03826fede6..85183bdfd5 100644 --- a/LiteCore/tests/VectorRecordTest.cc +++ b/LiteCore/tests/VectorRecordTest.cc @@ -19,6 +19,7 @@ #include "c4.hh" #include "HybridClock.hh" #include "VectorRecord.hh" +#include "DeDuplicateEncoder.hh" #include "VersionVector.hh" #include "fleece/Mutable.hh" #include @@ -275,3 +276,42 @@ TEST_CASE_METHOD(DataFileTestFixture, "VectorRecord legacy revIDs", "[VectorReco cerr << "Storage:\n" << doc.dumpStorage(); } } + +TEST_CASE("DeDuplicateEncoder remove-then-reset undercounted dict (CBL-8812)", "[VectorRecord]") { + // Same underlying Fleece bug as MutableTests.cc's "MutableDict remove then re-set a source + // key" (Fleece submodule), but exercised through the actual code path that crashed in the + // field, not just Fleece's own public count()/iterator: DeDuplicateEncoder's Dict handling + // constructs a Dict::iterator, which for a mutable Dict is backed by HeapDict::kvArray() -- + // the function that sizes its cache array from the (buggy, too-low) count(), then overruns + // it once the real iteration yields more entries than that. This is the one call site that + // actually reaches that path in this codebase (VectorRecord::encodeBodyAndExtra). + // + // NOTE: before HeapDict::setting() is fixed, this test does not fail cleanly -- it crashes + // the whole process. HeapDict::kvArray() is reached through Dict::iterator's public + // constructor, which is implemented as the C-linkage function FLDictIterator_Begin(); a C++ + // exception can never cross an extern "C" boundary; it hits std::terminate() instead, + // uncatchable by any caller, debug build or not. So this test is only safe to run -- and + // only meaningful as a regression check -- once that fix is in place. + Doc doc = Doc::fromJSON("{\"a\":1,\"b\":2,\"c\":3}"_sl); // keeps the parsed data alive + Dict source = doc.asDict(); + REQUIRE(source.count() == 3); + + MutableDict dict = source.mutableCopy(); + REQUIRE(dict.count() == 3); + + dict.remove("b"_sl); + dict.set("b"_sl, 20); + REQUIRE(dict.count() == 3); + + Encoder enc; + DeDuplicateEncoder ddenc(enc); + ddenc.writeValue(dict, 1); + alloc_slice encoded = enc.finish(); + + Doc decoded = Doc(encoded); + Dict roundTrip = decoded.asDict(); + REQUIRE(roundTrip.count() == 3); + CHECK(roundTrip.get("a"_sl).asInt() == 1); + CHECK(roundTrip.get("b"_sl).asInt() == 20); + CHECK(roundTrip.get("c"_sl).asInt() == 3); +} diff --git a/vendor/fleece b/vendor/fleece index c85566da78..ca6e2f075b 160000 --- a/vendor/fleece +++ b/vendor/fleece @@ -1 +1 @@ -Subproject commit c85566da78083dd6231e4294cc040aa8876dfef9 +Subproject commit ca6e2f075b507bf6b89168dc0051166247d47edf