Skip to content

Commit 034051f

Browse files
authored
Merge 'fix: do not do unchecked indexing to non-static page offsets #4736' from Pedro Muniz
Replace panicking unchecked buffer indexing with bounds-checked access throughout the btree read path. Corrupt cell pointers or varint data now return LimboError::Corrupt instead of causing index-out-of-bounds panics. Changes: - pager.rs: Add bounds checks in cell_table_interior_read_rowid, cell_table_leaf_read_rowid, cell_index_read_payload_ptr, and _cell_get_raw_region_faster (now returns Result) - sqlite3_ondisk.rs: Add bounds checks in read_btree_cell for all page types; make read_payload return Result with cell_len < 4 check - btree.rs: Update callers of _cell_get_raw_region_faster for Result - integrity_check.rs: Add tests for corrupt cell pointers on leaf and interior pages, and for SELECT on corrupt pages; update existing cell overflow test to assert no panic Closes #4736 Generate by Turso Agent Closes #5467
2 parents 1bf68d4 + de68a8d commit 034051f

File tree

5 files changed

+395
-57
lines changed

5 files changed

+395
-57
lines changed

core/error.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,35 @@ macro_rules! bail_corrupt_error {
180180
};
181181
}
182182

183+
/// Bounds-checked buffer slicing that returns `LimboError::Corrupt` on out-of-bounds.
184+
///
185+
/// Accepts any range expression: `buf, pos..`, `buf, start..end`, etc.
186+
#[macro_export]
187+
macro_rules! slice_in_bounds_or_corrupt {
188+
($buf:expr, $range:expr) => {
189+
$buf.get($range).ok_or_else(|| {
190+
$crate::error::cold_return($crate::error::LimboError::Corrupt(format!(
191+
"range {:?} out of bounds for buffer size {}",
192+
$range,
193+
$buf.len()
194+
)))
195+
})?
196+
};
197+
}
198+
199+
/// Asserts a condition or bails with `LimboError::Corrupt`.
200+
///
201+
/// Usage:
202+
/// `assert_or_bail_corrupt!(condition, "message {}", arg)`
203+
#[macro_export]
204+
macro_rules! assert_or_bail_corrupt {
205+
($cond:expr, $($arg:tt)*) => {
206+
if !($cond) {
207+
$crate::bail_corrupt_error!($($arg)*);
208+
}
209+
};
210+
}
211+
183212
#[macro_export]
184213
macro_rules! bail_constraint_error {
185214
($($arg:tt)*) => {

core/storage/btree.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2957,7 +2957,7 @@ impl BTreeCursor {
29572957
parent_max_local,
29582958
parent_min_local,
29592959
parent_page_type,
2960-
);
2960+
)?;
29612961
let buf = parent_contents.as_ptr();
29622962
&buf[cell_start..cell_start + cell_len]
29632963
};
@@ -3046,7 +3046,7 @@ impl BTreeCursor {
30463046
max_local,
30473047
min_local,
30483048
page_type,
3049-
);
3049+
)?;
30503050
let buf = old_page_contents.as_ptr();
30513051
let cell_buf = &mut buf[cell_start..cell_start + cell_len];
30523052
// TODO(pere): make this reference and not copy
@@ -7691,7 +7691,7 @@ fn defragment_page(page: &PageContent, usable_space: usize, max_frag_bytes: isiz
76917691
max_local,
76927692
min_local,
76937693
page_type,
7694-
);
7694+
)?;
76957695

76967696
if pc > last_offset {
76977697
is_physically_sorted = false;

core/storage/pager.rs

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,10 @@ impl PageInner {
397397
let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES);
398398
let cell_pointer = self.read_u16(cell_pointer) as usize;
399399
const LEFT_CHILD_PAGE_SIZE_BYTES: usize = 4;
400-
let (rowid, _) = read_varint(&buf[cell_pointer + LEFT_CHILD_PAGE_SIZE_BYTES..])?;
400+
let (rowid, _) = read_varint(crate::slice_in_bounds_or_corrupt!(
401+
buf,
402+
cell_pointer + LEFT_CHILD_PAGE_SIZE_BYTES..
403+
))?;
401404
Ok(rowid as i64)
402405
}
403406

@@ -411,14 +414,12 @@ impl PageInner {
411414
let cell_pointer_array_start = self.header_size();
412415
let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES);
413416
let cell_pointer = self.read_u16(cell_pointer) as usize;
414-
// Validate cell pointer is within page bounds (need 4 bytes for u32)
415-
if cell_pointer + 4 > buf.len() {
416-
return Err(crate::LimboError::Corrupt(format!(
417-
"cell pointer {} out of bounds for page size {}",
418-
cell_pointer,
419-
buf.len()
420-
)));
421-
}
417+
crate::assert_or_bail_corrupt!(
418+
cell_pointer + 4 <= buf.len(),
419+
"cell pointer {} out of bounds for page size {}",
420+
cell_pointer,
421+
buf.len()
422+
);
422423
Ok(u32::from_be_bytes([
423424
buf[cell_pointer],
424425
buf[cell_pointer + 1],
@@ -435,9 +436,9 @@ impl PageInner {
435436
let cell_pointer = cell_pointer_array_start + (idx * CELL_PTR_SIZE_BYTES);
436437
let cell_pointer = self.read_u16(cell_pointer) as usize;
437438
let mut pos = cell_pointer;
438-
let (_, nr) = read_varint(&buf[pos..])?;
439+
let (_, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, pos..))?;
439440
pos += nr;
440-
let (rowid, _) = read_varint(&buf[pos..])?;
441+
let (rowid, _) = read_varint(crate::slice_in_bounds_or_corrupt!(buf, pos..))?;
441442
Ok(rowid as i64)
442443
}
443444

@@ -461,11 +462,13 @@ impl PageInner {
461462
let page_type = self.page_type()?;
462463
let (payload_size, varint_len, header_skip) = match page_type {
463464
PageType::IndexInterior => {
464-
let (size, len) = read_varint(&buf[cell_offset + 4..])?;
465+
let (size, len) =
466+
read_varint(crate::slice_in_bounds_or_corrupt!(buf, cell_offset + 4..))?;
465467
(size, len, 4usize)
466468
}
467469
PageType::IndexLeaf => {
468-
let (size, len) = read_varint(&buf[cell_offset..])?;
470+
let (size, len) =
471+
read_varint(crate::slice_in_bounds_or_corrupt!(buf, cell_offset..))?;
469472
(size, len, 0usize)
470473
}
471474
_ => unreachable!("cell_index_read_payload_ptr called on non-index page"),
@@ -484,25 +487,43 @@ impl PageInner {
484487

485488
let (payload_slice, first_overflow) = if overflows {
486489
let overflow_ptr_offset = payload_start + local_size - 4;
490+
crate::assert_or_bail_corrupt!(
491+
overflow_ptr_offset + 4 <= buf.len(),
492+
"overflow pointer offset {} out of bounds for page size {}",
493+
overflow_ptr_offset,
494+
buf.len()
495+
);
487496
let first_overflow_page = u32::from_be_bytes([
488497
buf[overflow_ptr_offset],
489498
buf[overflow_ptr_offset + 1],
490499
buf[overflow_ptr_offset + 2],
491500
buf[overflow_ptr_offset + 3],
492501
]);
502+
let payload_end = payload_start + local_size - 4;
503+
crate::assert_or_bail_corrupt!(
504+
payload_start < payload_end && payload_end <= buf.len(),
505+
"payload range {}..{} out of bounds for page size {}",
506+
payload_start,
507+
payload_end,
508+
buf.len()
509+
);
493510
// SAFETY: valid as long as page is alive
494511
let slice = unsafe {
495-
std::mem::transmute::<&[u8], &'static [u8]>(
496-
&buf[payload_start..payload_start + local_size - 4],
497-
)
512+
std::mem::transmute::<&[u8], &'static [u8]>(&buf[payload_start..payload_end])
498513
};
499514
(slice, Some(first_overflow_page))
500515
} else {
516+
let payload_end = payload_start + payload_size as usize;
517+
crate::assert_or_bail_corrupt!(
518+
payload_end <= buf.len(),
519+
"payload range {}..{} out of bounds for page size {}",
520+
payload_start,
521+
payload_end,
522+
buf.len()
523+
);
501524
// SAFETY: valid as long as page is alive
502525
let slice = unsafe {
503-
std::mem::transmute::<&[u8], &'static [u8]>(
504-
&buf[payload_start..payload_start + payload_size as usize],
505-
)
526+
std::mem::transmute::<&[u8], &'static [u8]>(&buf[payload_start..payload_end])
506527
};
507528
(slice, None)
508529
};
@@ -540,14 +561,14 @@ impl PageInner {
540561
let max_local = payload_overflow_threshold_max(page_type, usable_size);
541562
let min_local = payload_overflow_threshold_min(page_type, usable_size);
542563
let cell_count = self.cell_count();
543-
Ok(self._cell_get_raw_region_faster(
564+
self._cell_get_raw_region_faster(
544565
idx,
545566
usable_size,
546567
cell_count,
547568
max_local,
548569
min_local,
549570
page_type,
550-
))
571+
)
551572
}
552573

553574
#[inline]
@@ -559,13 +580,14 @@ impl PageInner {
559580
max_local: usize,
560581
min_local: usize,
561582
page_type: PageType,
562-
) -> (usize, usize) {
583+
) -> crate::Result<(usize, usize)> {
563584
let buf = self.as_ptr();
564585
turso_assert_less_than!(idx, cell_count);
565586
let start = self.cell_get_raw_start_offset(idx);
566587
let len = match page_type {
567588
PageType::IndexInterior => {
568-
let (len_payload, n_payload) = read_varint(&buf[start + 4..]).unwrap();
589+
let (len_payload, n_payload) =
590+
read_varint(crate::slice_in_bounds_or_corrupt!(buf, start + 4..))?;
569591
let (overflows, to_read) = sqlite3_ondisk::payload_overflows(
570592
len_payload as usize,
571593
max_local,
@@ -579,11 +601,13 @@ impl PageInner {
579601
}
580602
}
581603
PageType::TableInterior => {
582-
let (_, n_rowid) = read_varint(&buf[start + 4..]).unwrap();
604+
let (_, n_rowid) =
605+
read_varint(crate::slice_in_bounds_or_corrupt!(buf, start + 4..))?;
583606
4 + n_rowid
584607
}
585608
PageType::IndexLeaf => {
586-
let (len_payload, n_payload) = read_varint(&buf[start..]).unwrap();
609+
let (len_payload, n_payload) =
610+
read_varint(crate::slice_in_bounds_or_corrupt!(buf, start..))?;
587611
let (overflows, to_read) = sqlite3_ondisk::payload_overflows(
588612
len_payload as usize,
589613
max_local,
@@ -601,8 +625,10 @@ impl PageInner {
601625
}
602626
}
603627
PageType::TableLeaf => {
604-
let (len_payload, n_payload) = read_varint(&buf[start..]).unwrap();
605-
let (_, n_rowid) = read_varint(&buf[start + n_payload..]).unwrap();
628+
let (len_payload, n_payload) =
629+
read_varint(crate::slice_in_bounds_or_corrupt!(buf, start..))?;
630+
let (_, n_rowid) =
631+
read_varint(crate::slice_in_bounds_or_corrupt!(buf, start + n_payload..))?;
606632
let (overflows, to_read) = sqlite3_ondisk::payload_overflows(
607633
len_payload as usize,
608634
max_local,
@@ -620,7 +646,14 @@ impl PageInner {
620646
}
621647
}
622648
};
623-
(start, len)
649+
crate::assert_or_bail_corrupt!(
650+
start + len <= buf.len(),
651+
"cell region {}..{} out of bounds for page size {}",
652+
start,
653+
start + len,
654+
buf.len()
655+
);
656+
Ok((start, len))
624657
}
625658

626659
pub fn is_leaf(&self) -> bool {

core/storage/sqlite3_ondisk.rs

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -805,18 +805,31 @@ pub fn read_btree_cell(
805805
match page_type {
806806
PageType::IndexInterior => {
807807
let mut pos = pos;
808+
crate::assert_or_bail_corrupt!(
809+
pos + 4 <= page.len(),
810+
"cell offset {} out of bounds for page size {}",
811+
pos,
812+
page.len()
813+
);
808814
let left_child_page =
809815
u32::from_be_bytes([page[pos], page[pos + 1], page[pos + 2], page[pos + 3]]);
810816
pos += 4;
811-
let (payload_size, nr) = read_varint(&page[pos..])?;
817+
let (payload_size, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?;
812818
pos += nr;
813819

814820
let (overflows, to_read) =
815821
payload_overflows(payload_size as usize, max_local, min_local, usable_size);
816822
let to_read = if overflows { to_read } else { page.len() - pos };
817823

824+
crate::assert_or_bail_corrupt!(
825+
pos + to_read <= page.len(),
826+
"payload range {}..{} out of bounds for page size {}",
827+
pos,
828+
pos + to_read,
829+
page.len()
830+
);
818831
let (payload, first_overflow_page) =
819-
read_payload(&page[pos..pos + to_read], payload_size as usize);
832+
read_payload(&page[pos..pos + to_read], payload_size as usize)?;
820833
Ok(BTreeCell::IndexInteriorCell(IndexInteriorCell {
821834
left_child_page,
822835
payload,
@@ -826,26 +839,39 @@ pub fn read_btree_cell(
826839
}
827840
PageType::TableInterior => {
828841
let mut pos = pos;
842+
crate::assert_or_bail_corrupt!(
843+
pos + 4 <= page.len(),
844+
"cell offset {} out of bounds for page size {}",
845+
pos,
846+
page.len()
847+
);
829848
let left_child_page =
830849
u32::from_be_bytes([page[pos], page[pos + 1], page[pos + 2], page[pos + 3]]);
831850
pos += 4;
832-
let (rowid, _) = read_varint(&page[pos..])?;
851+
let (rowid, _) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?;
833852
Ok(BTreeCell::TableInteriorCell(TableInteriorCell {
834853
left_child_page,
835854
rowid: rowid as i64,
836855
}))
837856
}
838857
PageType::IndexLeaf => {
839858
let mut pos = pos;
840-
let (payload_size, nr) = read_varint(&page[pos..])?;
859+
let (payload_size, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?;
841860
pos += nr;
842861

843862
let (overflows, to_read) =
844863
payload_overflows(payload_size as usize, max_local, min_local, usable_size);
845864
let to_read = if overflows { to_read } else { page.len() - pos };
846865

866+
crate::assert_or_bail_corrupt!(
867+
pos + to_read <= page.len(),
868+
"payload range {}..{} out of bounds for page size {}",
869+
pos,
870+
pos + to_read,
871+
page.len()
872+
);
847873
let (payload, first_overflow_page) =
848-
read_payload(&page[pos..pos + to_read], payload_size as usize);
874+
read_payload(&page[pos..pos + to_read], payload_size as usize)?;
849875
Ok(BTreeCell::IndexLeafCell(IndexLeafCell {
850876
payload,
851877
first_overflow_page,
@@ -854,17 +880,24 @@ pub fn read_btree_cell(
854880
}
855881
PageType::TableLeaf => {
856882
let mut pos = pos;
857-
let (payload_size, nr) = read_varint(&page[pos..])?;
883+
let (payload_size, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?;
858884
pos += nr;
859-
let (rowid, nr) = read_varint(&page[pos..])?;
885+
let (rowid, nr) = read_varint(crate::slice_in_bounds_or_corrupt!(page, pos..))?;
860886
pos += nr;
861887

862888
let (overflows, to_read) =
863889
payload_overflows(payload_size as usize, max_local, min_local, usable_size);
864890
let to_read = if overflows { to_read } else { page.len() - pos };
865891

892+
crate::assert_or_bail_corrupt!(
893+
pos + to_read <= page.len(),
894+
"payload range {}..{} out of bounds for page size {}",
895+
pos,
896+
pos + to_read,
897+
page.len()
898+
);
866899
let (payload, first_overflow_page) =
867-
read_payload(&page[pos..pos + to_read], payload_size as usize);
900+
read_payload(&page[pos..pos + to_read], payload_size as usize)?;
868901
Ok(BTreeCell::TableLeafCell(TableLeafCell {
869902
rowid: rowid as i64,
870903
payload,
@@ -878,21 +911,30 @@ pub fn read_btree_cell(
878911
/// read_payload takes in the unread bytearray with the payload size
879912
/// and returns the payload on the page, and optionally the first overflow page number.
880913
#[allow(clippy::readonly_write_lock)]
881-
fn read_payload(unread: &'static [u8], payload_size: usize) -> (&'static [u8], Option<u32>) {
914+
fn read_payload(
915+
unread: &'static [u8],
916+
payload_size: usize,
917+
) -> Result<(&'static [u8], Option<u32>)> {
882918
let cell_len = unread.len();
883919
// We will let overflow be constructed back if needed or requested.
884920
if payload_size <= cell_len {
885921
// fit within 1 page
886-
(&unread[..payload_size], None)
922+
Ok((&unread[..payload_size], None))
887923
} else {
888924
// overflow
925+
if cell_len < 4 {
926+
bail_corrupt_error!(
927+
"overflow cell too small: {} bytes, need at least 4",
928+
cell_len
929+
);
930+
}
889931
let first_overflow_page = u32::from_be_bytes([
890932
unread[cell_len - 4],
891933
unread[cell_len - 3],
892934
unread[cell_len - 2],
893935
unread[cell_len - 1],
894936
]);
895-
(&unread[..cell_len - 4], Some(first_overflow_page))
937+
Ok((&unread[..cell_len - 4], Some(first_overflow_page)))
896938
}
897939
}
898940

0 commit comments

Comments
 (0)