This is an automated email from the ASF dual-hosted git repository.
airborne12 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 70cdb58e940 [fix](be) Reject overflowing lengths and indices when
decoding SNII integers (#69726)
70cdb58e940 is described below
commit 70cdb58e9403d6a46ffead0571ea27d272b76dc5
Author: Jack <[email protected]>
AuthorDate: Thu Oct 29 09:05:38 2026 +0800
[fix](be) Reject overflowing lengths and indices when decoding SNII
integers (#69726)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Three decode primitives of the SNII index format take
numbers read from the file and do arithmetic on them without checking
for overflow.
- The FRQ prelude added the untrusted `sbdir_len` to the header length
before comparing it with the buffer, and summed the super-block lengths
into the window region length unchecked. A crafted length wraps the sum
to a small value, so the bounds check passes while a Slice of a huge
length is cut from a buffer that holds no such bytes, and the block
decoder then reads past the end of it. `memory_required` already
compared `sbdir_len` with the remaining bytes before calling
`verify_covered_crc`, but `open` did not. The overflow check now lives
in `verify_covered_crc`, which both call, the super-block sum uses the
file's own `checked_add_u64`, and the final region check compares
against the room left instead of adding to the start offset.
- `decode_varint64` accepted a tenth byte with payload above bit 63. The
extra bits were shifted out, and the encoding decoded to a silently
truncated value. The check sits on the terminating byte, so the loop
over the continuation bytes is unchanged.
- `pfor_decode` and `pfor_skip` added each exception index delta to a
32-bit running index and tested the sum afterwards, so a delta could
wrap the index back into the block and be accepted. The running index is
now 64 bits wide: the index is below the block size before every add and
a delta is at most 2^32 - 1, so the sum cannot wrap.
Nothing here is reachable from data the writers produce. The checks make
a damaged or crafted index file fail with a corruption status instead of
being decoded into a wrong value or an out-of-bounds read.
---
.../storage/index/inverted/snii/encoding/pfor.cpp | 6 +-
.../index/inverted/snii/encoding/varint.cpp | 7 +++
.../index/inverted/snii/format/frq_prelude.cpp | 17 ++++--
.../index/inverted/snii/encoding/pfor_test.cpp | 48 ++++++++++++++++
.../index/inverted/snii/encoding/varint_test.cpp | 23 ++++++++
.../index/inverted/snii/snii_frq_prelude_test.cpp | 65 ++++++++++++++++++++++
6 files changed, 160 insertions(+), 6 deletions(-)
diff --git a/be/src/storage/index/inverted/snii/encoding/pfor.cpp
b/be/src/storage/index/inverted/snii/encoding/pfor.cpp
index 30e6dc95b8a..f8ad4f15959 100644
--- a/be/src/storage/index/inverted/snii/encoding/pfor.cpp
+++ b/be/src/storage/index/inverted/snii/encoding/pfor.cpp
@@ -419,7 +419,9 @@ Status pfor_decode(ByteSource* src, size_t n, uint32_t*
out) {
uint32_t n_exc;
RETURN_IF_ERROR(src->get_varint32(&n_exc));
RETURN_IF_ERROR(bitunpack(src, n, w, out));
- uint32_t idx = 0;
+ // 64 bits wide: a delta from the file is at most 2^32 - 1 and idx is
below n before every add, so the
+ // sum cannot wrap back into [0, n) the way a 32-bit one can.
+ uint64_t idx = 0;
for (uint32_t i = 0; i < n_exc; ++i) {
uint32_t d, val;
RETURN_IF_ERROR(src->get_varint32(&d));
@@ -442,7 +444,7 @@ Status pfor_skip(ByteSource* src, size_t n) {
const size_t packed = (static_cast<size_t>(w) * n + 7) / 8;
Slice unused;
RETURN_IF_ERROR(src->get_bytes(packed, &unused));
- uint32_t idx = 0;
+ uint64_t idx = 0; // wide for the same reason as in pfor_decode
for (uint32_t i = 0; i < n_exc; ++i) {
uint32_t d = 0;
uint32_t val = 0;
diff --git a/be/src/storage/index/inverted/snii/encoding/varint.cpp
b/be/src/storage/index/inverted/snii/encoding/varint.cpp
index 183b24e7b96..6a760082f42 100644
--- a/be/src/storage/index/inverted/snii/encoding/varint.cpp
+++ b/be/src/storage/index/inverted/snii/encoding/varint.cpp
@@ -49,6 +49,13 @@ Status decode_varint64(const uint8_t* p, const uint8_t* end,
uint64_t* v, const
uint8_t b = *p++;
result |= static_cast<uint64_t>(b & 0x7F) << shift;
if ((b & 0x80) == 0) {
+ // The tenth byte only has room for bit 63. A higher payload bit
was shifted out of the
+ // result above, and the encoding would decode to a silently
truncated value. Checked
+ // here, on the last byte, so the loop over the continuation bytes
stays as it was.
+ if (shift == 63 && b > 1) {
+ return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED,
false>(
+ "varint64 overflow");
+ }
*v = result;
*next = p;
return Status::OK();
diff --git a/be/src/storage/index/inverted/snii/format/frq_prelude.cpp
b/be/src/storage/index/inverted/snii/format/frq_prelude.cpp
index 2393c83a80e..7950262fe64 100644
--- a/be/src/storage/index/inverted/snii/format/frq_prelude.cpp
+++ b/be/src/storage/index/inverted/snii/format/frq_prelude.cpp
@@ -213,11 +213,15 @@ struct Header {
// Verifies the trailing crc covers [start of buffer .. end of
super_block_dir].
// covered_len = header bytes (up to and including sbdir_len) + sbdir_len.
Status verify_covered_crc(Slice prelude, size_t header_end, uint64_t
sbdir_len) {
- const size_t covered = header_end + static_cast<size_t>(sbdir_len);
- if (covered + sizeof(uint32_t) > prelude.size()) {
+ // sbdir_len is untrusted: a plain sum could wrap to a small offset that
points back into the header.
+ uint64_t covered_len = 0;
+ RETURN_IF_ERROR(checked_add_u64(header_end, sbdir_len, "frq_prelude:
sbdir_len overflow",
+ &covered_len));
+ if (covered_len > prelude.size() || prelude.size() - covered_len <
sizeof(uint32_t)) {
return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED, false>(
"frq_prelude: buffer too short for crc region");
}
+ const size_t covered = static_cast<size_t>(covered_len);
uint32_t stored = 0;
ByteSource crc_src(prelude.subslice(covered, sizeof(uint32_t)));
RETURN_IF_ERROR(crc_src.get_fixed32(&stored));
@@ -287,7 +291,9 @@ Status decode_super_block_dir(Slice dir, const Header& h,
std::vector<SbDirRow>*
return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED,
false>(
"frq_prelude: super-block dir inconsistent");
}
- expect_off += r.block_len;
+ RETURN_IF_ERROR(checked_add_u64(expect_off, r.block_len,
+ "frq_prelude: super-block region
offset overflow",
+ &expect_off));
prev_last = r.last_docid;
rows->push_back(r);
}
@@ -512,8 +518,11 @@ Status FrqPreludeReader::open(Slice prelude,
FrqPreludeReader* out) {
uint64_t window_region_len = 0;
RETURN_IF_ERROR(decode_super_block_dir(dir, h, &rows, &window_region_len));
+ // verify_covered_crc already proved the crc fits, so region_start <=
prelude.size() and the
+ // subtraction cannot underflow. window_region_len is a sum of untrusted
lengths: compare it to
+ // the room left instead of adding it to region_start.
const size_t region_start = header_end + static_cast<size_t>(h.sbdir_len)
+ sizeof(uint32_t);
- if (region_start + static_cast<size_t>(window_region_len) >
prelude.size()) {
+ if (window_region_len > prelude.size() - region_start) {
return Status::Error<ErrorCode::INVERTED_INDEX_FILE_CORRUPTED, false>(
"frq_prelude: window region past buffer");
}
diff --git a/be/test/storage/index/inverted/snii/encoding/pfor_test.cpp
b/be/test/storage/index/inverted/snii/encoding/pfor_test.cpp
index a26fe4e7209..4227e68bad6 100644
--- a/be/test/storage/index/inverted/snii/encoding/pfor_test.cpp
+++ b/be/test/storage/index/inverted/snii/encoding/pfor_test.cpp
@@ -102,6 +102,54 @@ TEST(SniiPfor, ExceptionsAtBoundaries) {
roundtrip(v);
}
+namespace {
+
+// A hand-built block: bit width 0 (no packed bytes) followed by (index delta,
value) exceptions.
+ByteSink exceptions_only(const std::vector<std::pair<uint32_t, uint32_t>>&
exceptions) {
+ ByteSink sink;
+ sink.put_u8(0);
+ sink.put_varint32(static_cast<uint32_t>(exceptions.size()));
+ for (const auto& [delta, value] : exceptions) {
+ sink.put_varint32(delta);
+ sink.put_varint32(value);
+ }
+ return sink;
+}
+
+void expect_corrupted(const ByteSink& sink, size_t n) {
+ std::vector<uint32_t> out(n);
+ ByteSource decode_src(sink.view());
+ EXPECT_TRUE(pfor_decode(&decode_src, n, out.data())
+
.is<doris::ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>());
+ ByteSource skip_src(sink.view());
+ EXPECT_TRUE(pfor_skip(&skip_src,
n).is<doris::ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>());
+}
+
+} // namespace
+
+// The second delta wraps the 32-bit exception index from 5 back to 3, which
is inside the block.
+TEST(SniiPfor, ExceptionIndexDeltaWrapIsCorruption) {
+ expect_corrupted(exceptions_only({{5, 7}, {0xFFFFFFFEU, 9}}), 8);
+}
+
+TEST(SniiPfor, ExceptionIndexPastBlockIsCorruption) {
+ expect_corrupted(exceptions_only({{8, 7}}), 8);
+ expect_corrupted(exceptions_only({{3, 7}, {5, 9}}), 8);
+ expect_corrupted(exceptions_only({{0, 7}}), 0);
+}
+
+// The first exception may sit at index 0, and the last at n - 1.
+TEST(SniiPfor, ExceptionIndexEdgesAreAccepted) {
+ const ByteSink sink = exceptions_only({{0, 7}, {7, 9}});
+ std::vector<uint32_t> out(8);
+ ByteSource decode_src(sink.view());
+ ASSERT_TRUE(pfor_decode(&decode_src, 8, out.data()).ok());
+ EXPECT_EQ(out[0], 7U);
+ EXPECT_EQ(out[7], 9U);
+ ByteSource skip_src(sink.view());
+ EXPECT_TRUE(pfor_skip(&skip_src, 8).ok());
+}
+
// Compare the optimized encoder with the original byte layout across varied
// inputs. The operation counter checks that each value's width is evaluated
once.
namespace {
diff --git a/be/test/storage/index/inverted/snii/encoding/varint_test.cpp
b/be/test/storage/index/inverted/snii/encoding/varint_test.cpp
index 0639aebccef..3e36dcc905e 100644
--- a/be/test/storage/index/inverted/snii/encoding/varint_test.cpp
+++ b/be/test/storage/index/inverted/snii/encoding/varint_test.cpp
@@ -56,6 +56,29 @@ TEST(SniiVarint, TruncatedFails) {
EXPECT_FALSE(decode_varint32(buf, buf + 1, &out, &next).ok());
}
+TEST(SniiVarint, TenthByteOverflowFails) {
+ // Nine continuation bytes carry bits 0..62. The tenth byte can only add
bit 63, so a larger
+ // payload would be shifted out of the result and decode to a silently
truncated value.
+ uint8_t buf[10];
+ for (int i = 0; i < 9; ++i) {
+ buf[i] = 0xFF;
+ }
+ uint64_t out = 0;
+ const uint8_t* next = nullptr;
+
+ buf[9] = 0x01; // 2^64 - 1, the largest valid encoding
+ ASSERT_TRUE(decode_varint64(buf, buf + 10, &out, &next).ok());
+ EXPECT_EQ(out, 0xFFFFFFFFFFFFFFFFULL);
+ EXPECT_EQ(next, buf + 10);
+
+ for (int tenth : {0x02, 0x40, 0x7F}) {
+ buf[9] = static_cast<uint8_t>(tenth);
+ EXPECT_FALSE(decode_varint64(buf, buf + 10, &out, &next).ok()) <<
tenth;
+ }
+ buf[9] = 0x81; // a continuation past the tenth byte
+ EXPECT_FALSE(decode_varint64(buf, buf + 10, &out, &next).ok());
+}
+
TEST(SniiVarint, Overflow32Fails) {
// encode a value > 2^32-1, decoding with decode_varint32 should fail
uint8_t buf[10];
diff --git a/be/test/storage/index/inverted/snii/snii_frq_prelude_test.cpp
b/be/test/storage/index/inverted/snii/snii_frq_prelude_test.cpp
index 6c4512878b4..f238a5ff9f2 100644
--- a/be/test/storage/index/inverted/snii/snii_frq_prelude_test.cpp
+++ b/be/test/storage/index/inverted/snii/snii_frq_prelude_test.cpp
@@ -36,6 +36,7 @@
#include "common/status.h"
#include "storage/index/inverted/snii/common/slice.h"
#include "storage/index/inverted/snii/encoding/byte_sink.h"
+#include "storage/index/inverted/snii/encoding/crc32c.h"
#include "storage/index/inverted/snii/encoding/varint.h"
#include "storage/index/inverted/snii/format/dict_entry.h"
#include "storage/index/inverted/snii/format/frq_prelude.h"
@@ -379,6 +380,70 @@ TEST(SniiFrqPreludeTest,
FP08DdDiskLenSumOverflowIsCorruption) {
EXPECT_TRUE(s.is<doris::ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>()) <<
s.to_string();
}
+namespace {
+
+// Assembles a prelude by hand: header, super_block_dir rows (last_docid
delta, block_off, block_len) and
+// the crc32c over both, with no window region after it. The crc is correct,
so only the lengths are bad.
+ByteSink hand_built_prelude(uint64_t n, uint64_t group_size, uint64_t n_super,
+ const std::vector<std::vector<uint64_t>>&
dir_rows) {
+ ByteSink dir;
+ for (const auto& row : dir_rows) {
+ for (uint64_t field : row) {
+ dir.put_varint64(field);
+ }
+ }
+ ByteSink prelude;
+ prelude.put_u8(0); // flags: no prx
+ prelude.put_varint64(n);
+ prelude.put_varint64(group_size);
+ prelude.put_varint64(n_super);
+ prelude.put_varint64(dir.size());
+ prelude.put_bytes(dir.view());
+ prelude.put_fixed32(doris::snii::crc32c(prelude.view()));
+ return prelude;
+}
+
+void expect_open_corruption(const ByteSink& prelude, const std::string&
message) {
+ FrqPreludeReader reader;
+ const Status s = FrqPreludeReader::open(prelude.view(), &reader);
+ EXPECT_TRUE(s.is<doris::ErrorCode::INVERTED_INDEX_FILE_CORRUPTED>()) <<
s.to_string();
+ EXPECT_NE(s.to_string().find(message), std::string::npos) << s.to_string();
+}
+
+} // namespace
+
+// FP-10: sbdir_len is read from the file. A value near 2^64 must be rejected
as an overflow instead of
+// wrapping header_end + sbdir_len back to a small offset inside the header.
+TEST(SniiFrqPreludeTest, FP10SbdirLenWrapIsCorruption) {
+ ByteSink prelude;
+ prelude.put_u8(0);
+ prelude.put_varint64(1);
+ prelude.put_varint64(kPreludeGroupSize);
+ prelude.put_varint64(1);
+ prelude.put_varint64(std::numeric_limits<uint64_t>::max() - 2);
+ for (int i = 0; i < 16; ++i) {
+ prelude.put_u8(0); // room, so that a wrapped offset would still look
like it is in range
+ }
+ expect_open_corruption(prelude, "sbdir_len overflow");
+}
+
+// FP-11: one super-block whose block_len is 2^64 - 1. With a correct crc and
a consistent directory,
+// region_start + block_len used to wrap to a small number, and a window
region of that huge length was
+// cut from a buffer that holds no bytes for it.
+TEST(SniiFrqPreludeTest, FP11HugeWindowRegionIsCorruption) {
+ expect_open_corruption(hand_built_prelude(1, kPreludeGroupSize, 1,
+ {{1, 0,
std::numeric_limits<uint64_t>::max()}}),
+ "window region past buffer");
+}
+
+// FP-12: two super-blocks whose lengths add up to 2^64 must not wrap the
running region offset to 0.
+TEST(SniiFrqPreludeTest, FP12SuperBlockRegionOffsetWrapIsCorruption) {
+ expect_open_corruption(hand_built_prelude(2, 1, 2,
+ {{1, 0,
std::numeric_limits<uint64_t>::max()},
+ {1,
std::numeric_limits<uint64_t>::max(), 1}}),
+ "super-block region offset overflow");
+}
+
// FP-09 (E2E): a high-df windowed term ("failed", 9000 docs) queried through
the
// real reader returns the full docid set (new path == old semantics), the
docid
// path fetches exactly the (now-smaller) prelude range once, and the on-disk
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]