From 650c4c9b0ec95fedd899e66bba6a9ed486080c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Savi=C4=87?= Date: Mon, 10 Aug 2026 12:57:50 +0000 Subject: [PATCH 1/2] Bounds-check per-entry key read in compact_tuple_sketch::deserialize The entry-reading loop reads each entry as an 8-byte key followed by a Summary-width summary. The summary read is passed the remaining capacity and bounds-checks itself, but the key read is an unchecked fixed-size copy_from_mem that relies solely on the pre-loop keys-only reservation, which assumes every entry's summary occupies the width Summary serializes to. When that assumption does not hold (a truncated buffer, or a buffer whose serialized summary width differs from the Summary it is deserialized as) the read cursor advances past the data. num_entries is read from the preamble and is unaffected, so the loop runs the full count and a later key read walks off the end of the buffer: silent on a normal build, a heap-buffer-overflow under AddressSanitizer. Add an ensure_minimum_memory check before the key read, mirroring the remaining-capacity check the summary read already performs and the up-front size validation the compact theta parser does. A malformed buffer now throws std::out_of_range instead of reading out of bounds. Adds a tuple_sketch_test case that deserializes a float-summary sketch as a double-summary sketch and asserts std::out_of_range. Co-authored-by: SavicStefan <50296686+SavicStefan@users.noreply.github.com> --- tuple/include/tuple_sketch_impl.hpp | 1 + tuple/test/tuple_sketch_test.cpp | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/tuple/include/tuple_sketch_impl.hpp b/tuple/include/tuple_sketch_impl.hpp index b3f2d0bd..3a9d2b75 100644 --- a/tuple/include/tuple_sketch_impl.hpp +++ b/tuple/include/tuple_sketch_impl.hpp @@ -583,6 +583,7 @@ compact_tuple_sketch compact_tuple_sketch::deserialize(const void* b std::unique_ptr summary(alloc.allocate(1), deleter_of_summaries(1, false, allocator)); for (size_t i = 0; i < num_entries; ++i) { uint64_t key; + ensure_minimum_memory(base + size - ptr, sizeof(uint64_t)); ptr += copy_from_mem(ptr, key); ptr += sd.deserialize(ptr, base + size - ptr, summary.get(), 1); entries.emplace_back(key, std::move(*summary)); diff --git a/tuple/test/tuple_sketch_test.cpp b/tuple/test/tuple_sketch_test.cpp index 6e44e284..5c28d417 100644 --- a/tuple/test/tuple_sketch_test.cpp +++ b/tuple/test/tuple_sketch_test.cpp @@ -371,4 +371,19 @@ TEST_CASE("filter", "[tuple_sketch]") { } } +TEST_CASE("tuple sketch: deserialize with mismatched summary width", "[tuple_sketch]") { + // A compact sketch serialized with a narrower summary (float, 4 bytes) and then + // deserialized as a wider summary (double, 8 bytes). The per-entry stride the reader + // assumes (8-byte key + 8-byte summary) is larger than the entries actually occupy + // (8-byte key + 4-byte summary), so the read cursor advances past the end of the buffer. + // num_entries is read from the preamble and is unaffected, so the entry loop still runs + // the full count and the per-entry key read walks off the end. This must throw rather + // than read out of bounds (a heap-buffer-overflow under AddressSanitizer). + auto update_sketch = update_tuple_sketch::builder().build(); + for (int i = 0; i < 100; ++i) update_sketch.update(i, 1.0f); + auto bytes = update_sketch.compact().serialize(); + REQUIRE_THROWS_AS(compact_tuple_sketch::deserialize(bytes.data(), bytes.size()), + std::out_of_range); +} + } /* namespace datasketches */ From 944aed14595535f15e37cf17f9e91c1a8bb080e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Savi=C4=87?= Date: Mon, 10 Aug 2026 14:58:20 +0000 Subject: [PATCH 2/2] Fixed test name, to make more sense --- tuple/test/tuple_sketch_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tuple/test/tuple_sketch_test.cpp b/tuple/test/tuple_sketch_test.cpp index 5c28d417..30c415f9 100644 --- a/tuple/test/tuple_sketch_test.cpp +++ b/tuple/test/tuple_sketch_test.cpp @@ -371,7 +371,7 @@ TEST_CASE("filter", "[tuple_sketch]") { } } -TEST_CASE("tuple sketch: deserialize with mismatched summary width", "[tuple_sketch]") { +TEST_CASE("tuple sketch: deserialize bounds-checks each entry key", "[tuple_sketch]") { // A compact sketch serialized with a narrower summary (float, 4 bytes) and then // deserialized as a wider summary (double, 8 bytes). The per-entry stride the reader // assumes (8-byte key + 8-byte summary) is larger than the entries actually occupy