From 7e51697b0f8346e6b10e813070caea9c47ce929d Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 27 May 2026 13:29:48 -0700 Subject: [PATCH] Fixed rare buffer overflow with Encoder::snip() Encoder::snip() allocates 2 bytes for a Value, but the Value constructor actually initializes all 4 bytes. If the allocation is at the very end of a heap block it'll overwrite the end. In the case of CBSE-22711 the buffer is the Writer's internal buffer so the overwrite clobbered some following state of the Encoder, causing it to crash on the next use. Fixes CBL-8363 --- Fleece/Core/Array.hh | 2 +- Fleece/Core/Dict.hh | 2 +- Fleece/Core/Value.cc | 4 ++-- Fleece/Core/Value.hh | 11 +++++++++-- Tests/EncoderTests.cc | 24 ++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/Fleece/Core/Array.hh b/Fleece/Core/Array.hh index 8c252f72..5adc35c6 100644 --- a/Fleece/Core/Array.hh +++ b/Fleece/Core/Array.hh @@ -59,7 +59,7 @@ namespace fleece { namespace impl { inline iterator begin() const noexcept; - constexpr Array() :Value(internal::kArrayTag, 0, 0) { } + constexpr Array() :Value(internal::kArrayTag, 0, 0, 0, 0) { } protected: internal::HeapArray* heapArray() const; diff --git a/Fleece/Core/Dict.hh b/Fleece/Core/Dict.hh index e416e878..ff18e923 100644 --- a/Fleece/Core/Dict.hh +++ b/Fleece/Core/Dict.hh @@ -89,7 +89,7 @@ namespace fleece { namespace impl { const Value* get(const key_t&) const noexcept; - constexpr Dict() :Value(internal::kDictTag, 0, 0) { } + constexpr Dict() :Value(internal::kDictTag, 0, 0, 0, 0) { } protected: internal::HeapDict* heapDict() const noexcept FLPURE; diff --git a/Fleece/Core/Value.cc b/Fleece/Core/Value.cc index 2d3b5db5..ffe9ccf6 100644 --- a/Fleece/Core/Value.cc +++ b/Fleece/Core/Value.cc @@ -47,8 +47,8 @@ namespace fleece { namespace impl { class ConstValue : public Value { public: - constexpr ConstValue(internal::tags tag, int tiny, int byte1 = 0) - :Value(tag, tiny, byte1) { } + constexpr ConstValue(internal::tags tag, uint8_t tiny, uint8_t byte1 = 0) + :Value(tag, tiny, byte1, 0, 0) { } }; EVEN_ALIGNED static constexpr const ConstValue diff --git a/Fleece/Core/Value.hh b/Fleece/Core/Value.hh index 555d1aea..5f1aed8d 100644 --- a/Fleece/Core/Value.hh +++ b/Fleece/Core/Value.hh @@ -203,9 +203,16 @@ namespace fleece { namespace impl { void _release() const; protected: - constexpr Value(internal::tags tag, int tiny, int byte1 = 0) + // This constructor only writes to the first 2 bytes because this Value might be narrow. + Value(internal::tags tag, int tiny, int byte1 = 0) { + _byte[0] = uint8_t((tag<<4) | tiny); + _byte[1] = uint8_t(byte1); + } + + // This constructor writes to all 4 bytes, because `constexpr` requires full initialization. + constexpr Value(internal::tags tag, uint8_t tiny, uint8_t byte1, uint8_t byte2, uint8_t byte3) :_byte {(uint8_t)((tag<<4) | tiny), - (uint8_t)byte1} + byte1, byte2, byte3} { } static const Value* findRoot(slice) noexcept FLPURE; diff --git a/Tests/EncoderTests.cc b/Tests/EncoderTests.cc index 5c9cceca..0995a617 100644 --- a/Tests/EncoderTests.cc +++ b/Tests/EncoderTests.cc @@ -1212,4 +1212,28 @@ class EncoderTests { } } + TEST_CASE("Encoder snip overflow") { + // Test for CBSE-22711, wherein Encoder::snip() can write past the end of a buffer. + // The inline buffer is 256 bytes. We don't know exactly how many bytes + // beginArray/items/endArray consume, so sweep a window covering every + // possible alignment of reserveSpace(2) within the last 8 bytes of _initialBuf. + for (int extraInts = 0; extraInts < 280; ++extraInts) { + fleece::impl::Encoder enc; + enc.beginArray(); + for (int i = 0; i < extraInts; ++i) + enc.writeInt(i & 0x7f); // 1 or 2 bytes each, varies tag boundary + enc.endArray(); + + // Capture _items before snip; snip's placement-new must not modify it. + auto itemsBefore = *(void**)((char*)&enc + 0x178); // _items offset + REQUIRE_NOTHROW(enc.snip()); + auto itemsAfter = *(void**)((char*)&enc + 0x178); + + // Pre-fix: at some specific extraInts this will fail (low 2 bytes zeroed). + // Post-fix: itemsAfter == itemsBefore (or both null after finish/reset). + CAPTURE(extraInts); + REQUIRE(itemsBefore == itemsAfter); + } + } + } }