Skip to content

Commit 31fa920

Browse files
authored
Merge pull request redpanda-data#26051 from andrwng/storage-index-overflow-truncate
storage: fix index state truncate overflow
2 parents 7c8a215 + e882d67 commit 31fa920

File tree

2 files changed

+48
-6
lines changed

2 files changed

+48
-6
lines changed

src/v/storage/index_state.cc

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -772,12 +772,21 @@ bool index_state::truncate(
772772
if (new_max_offset < base_offset) {
773773
return needs_persistence;
774774
}
775-
const uint32_t i = new_max_offset() - base_offset();
776-
auto res = index.offset_lower_bound(i);
777-
size_t remove_back_elems = index.size() - res.value_or(index.size());
778-
if (remove_back_elems > 0) {
779-
needs_persistence = true;
780-
pop_back(remove_back_elems);
775+
static constexpr int64_t u32_max = std::numeric_limits<uint32_t>::max();
776+
int64_t delta = new_max_offset() - base_offset();
777+
// NOTE: we expect that deltas above u32_max would have not been
778+
// added to the index, unless by an older buggy version of Redpanda.
779+
// With this in mind, either there are no entries above u32_max, or
780+
// offset_lower_bound() isn't going to work correctly anyway, so we just
781+
// skip removal and rely on queries to detect the overflow.
782+
if (delta <= u32_max) {
783+
auto i = static_cast<uint32_t>(delta);
784+
auto res = index.offset_lower_bound(i);
785+
size_t remove_back_elems = index.size() - res.value_or(index.size());
786+
if (remove_back_elems > 0) {
787+
needs_persistence = true;
788+
pop_back(remove_back_elems);
789+
}
781790
}
782791
if (new_max_offset < max_offset) {
783792
needs_persistence = true;

src/v/storage/tests/index_state_test.cc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,3 +410,36 @@ TEST(IndexState, NonDataTimestampsWithOverflow) {
410410
true,
411411
0);
412412
}
413+
414+
BOOST_AUTO_TEST_CASE(index_overflow_truncate) {
415+
storage::index_state state;
416+
417+
// Previous versions of Redpanda can have an index with offsets spanning
418+
// greater than uint32 offset space. In these cases, the index is not
419+
// reliable and truncation shouldn't attempt to truncate entries based on
420+
// an overflown lookup. But, queries should fallback to returning the
421+
// beginning of the index.
422+
const model::timestamp dummy_ts;
423+
const storage::offset_delta_time should_offset{false};
424+
storage::offset_time_index time_idx{dummy_ts, should_offset};
425+
constexpr long uint32_max = std::numeric_limits<uint32_t>::max();
426+
state.add_entry(0, time_idx, 1);
427+
state.add_entry(100, time_idx, 2);
428+
state.add_entry(static_cast<uint32_t>(uint32_max + 1), time_idx, 3);
429+
state.add_entry(static_cast<uint32_t>(uint32_max + 10), time_idx, 4);
430+
state.max_offset = model::offset{uint32_max + 10};
431+
BOOST_CHECK_EQUAL(4, state.size());
432+
433+
// The truncation shouldn't remove index entries, given the overflow.
434+
auto needs_flush = state.truncate(
435+
model::offset(uint32_max + 1), model::timestamp::now());
436+
BOOST_CHECK(needs_flush);
437+
BOOST_CHECK_EQUAL(4, state.size());
438+
BOOST_CHECK_EQUAL(state.max_offset, model::offset{uint32_max + 1});
439+
440+
// Queries for the offset should start from the beginning of the segment.
441+
auto res = state.find_nearest(model::offset(uint32_max + 1));
442+
BOOST_REQUIRE(res.has_value());
443+
BOOST_CHECK_EQUAL(res->offset, model::offset{0});
444+
BOOST_CHECK_EQUAL(res->filepos, 1);
445+
}

0 commit comments

Comments
 (0)