Skip to content

Commit e687490

Browse files
committed
Validate index rowid position and report corrupt records
1 parent 74cf4a0 commit e687490

3 files changed

Lines changed: 385 additions & 14 deletions

File tree

core/storage/btree.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5853,6 +5853,10 @@ pub enum IntegrityCheckError {
58535853
index_count: usize,
58545854
table_count: usize,
58555855
},
5856+
#[error("rowid not at end of record in index {index_name}")]
5857+
RowidNotAtEndOfRecord { index_name: String },
5858+
#[error("corrupt index record in index {index_name}: {error}")]
5859+
IndexRecordCorrupt { index_name: String, error: String },
58565860
#[error("NULL value in {table_name}.{column_name}")]
58575861
NotNullViolation {
58585862
table_name: String,

core/vdbe/execute.rs

Lines changed: 255 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ use crate::storage::database::DatabaseFile;
1313
use crate::storage::journal_mode;
1414
use crate::storage::page_cache::PageCache;
1515
use crate::storage::pager::{default_page1, CreateBTreeFlags, PageRef};
16-
use crate::storage::sqlite3_ondisk::{DatabaseHeader, PageSize, RawVersion};
16+
use crate::storage::sqlite3_ondisk::{read_varint, DatabaseHeader, PageSize, RawVersion};
1717
use crate::translate::collate::CollationSeq;
1818
use crate::translate::pragma::TURSO_CDC_VERSION_TABLE_NAME;
1919
use crate::types::{
2020
compare_immutable, compare_records_generic, AsValueRef, Extendable, IOCompletions, IOResult,
21-
ImmutableRecord, IndexInfo, SeekResult, Text,
21+
ImmutableRecord, IndexInfo, SeekResult, SerialType, SerialTypeKind, Text,
2222
};
2323
use crate::util::{
2424
normalize_ident, rewrite_check_expr_column_refs, rewrite_check_expr_table_refs,
@@ -9423,6 +9423,71 @@ fn format_integrity_check_result(errors: &[IntegrityCheckError]) -> String {
94239423
}
94249424
}
94259425

9426+
fn index_record_rowid_at_end(record: &ImmutableRecord, index_info: &IndexInfo) -> Result<bool> {
9427+
if !index_info.has_rowid {
9428+
return Ok(true);
9429+
}
9430+
9431+
let payload = record.get_payload();
9432+
if payload.is_empty() {
9433+
return Err(LimboError::Corrupt("Index record payload is empty".into()));
9434+
}
9435+
9436+
let (header_size, header_varint_len) = read_varint(payload)?;
9437+
let header_size = header_size as usize;
9438+
if header_size > payload.len() || header_varint_len > payload.len() {
9439+
return Err(LimboError::Corrupt(
9440+
"Index record header exceeds payload size".into(),
9441+
));
9442+
}
9443+
9444+
let mut header = &payload[header_varint_len..header_size];
9445+
let mut field_count = 0usize;
9446+
let mut data_size = 0usize;
9447+
let mut last_serial_type: Option<SerialType> = None;
9448+
9449+
while !header.is_empty() {
9450+
let (serial_type_raw, bytes_read) = read_varint(header)?;
9451+
header = &header[bytes_read..];
9452+
9453+
let serial_type = SerialType::try_from(serial_type_raw)?;
9454+
data_size = data_size
9455+
.checked_add(serial_type.size())
9456+
.ok_or_else(|| LimboError::Corrupt("Index record data size overflow".into()))?;
9457+
field_count += 1;
9458+
last_serial_type = Some(serial_type);
9459+
}
9460+
9461+
if field_count != index_info.num_cols {
9462+
return Err(LimboError::Corrupt(format!(
9463+
"Index record field count {} != expected {}",
9464+
field_count, index_info.num_cols
9465+
)));
9466+
}
9467+
9468+
let payload_data_len = payload.len().saturating_sub(header_size);
9469+
if data_size > payload_data_len {
9470+
return Err(LimboError::Corrupt(
9471+
"Index record data section too small".into(),
9472+
));
9473+
}
9474+
9475+
let last_serial_type = last_serial_type
9476+
.ok_or_else(|| LimboError::Corrupt("Index record has no serial types".into()))?;
9477+
9478+
Ok(matches!(
9479+
last_serial_type.kind(),
9480+
SerialTypeKind::I8
9481+
| SerialTypeKind::I16
9482+
| SerialTypeKind::I24
9483+
| SerialTypeKind::I32
9484+
| SerialTypeKind::I48
9485+
| SerialTypeKind::I64
9486+
| SerialTypeKind::ConstInt0
9487+
| SerialTypeKind::ConstInt1
9488+
))
9489+
}
9490+
94269491
pub enum OpIntegrityCheckState {
94279492
Start,
94289493
/// Phase 1: B-tree structural checks
@@ -9431,7 +9496,20 @@ pub enum OpIntegrityCheckState {
94319496
current_root_idx: usize,
94329497
state: IntegrityCheckState,
94339498
},
9434-
/// Phase 2: Index entry count validation (runs for both quick_check and integrity_check)
9499+
/// Phase 2: Index rowid position validation (skipped when quick=true)
9500+
/// Scans index entries and ensures the rowid is the last column in each record.
9501+
ValidatingIndexRowidPosition {
9502+
errors: Vec<IntegrityCheckError>,
9503+
/// Index into the tables list
9504+
current_table_idx: usize,
9505+
/// Index into the current table's indexes list
9506+
current_index_idx: usize,
9507+
/// Cursor used for index scanning (persisted across I/O yields)
9508+
cursor: Option<Box<BTreeCursor>>,
9509+
/// State of the current validation sub-phase
9510+
substate: IndexRowidValidationSubstate,
9511+
},
9512+
/// Phase 3: Index entry count validation (runs for both quick_check and integrity_check)
94359513
/// Counts rows in each table and entries in each index, then compares.
94369514
ValidatingIndexCounts {
94379515
errors: Vec<IntegrityCheckError>,
@@ -9448,7 +9526,7 @@ pub enum OpIntegrityCheckState {
94489526
/// Cursor used for counting (persisted across I/O yields)
94499527
cursor: Option<Box<BTreeCursor>>,
94509528
},
9451-
/// Phase 3: NOT NULL constraint validation (skipped when quick=true)
9529+
/// Phase 4: NOT NULL constraint validation (skipped when quick=true)
94529530
/// Scans table rows and checks that NOT NULL columns don't contain NULL values.
94539531
ValidatingNotNull {
94549532
errors: Vec<IntegrityCheckError>,
@@ -9459,7 +9537,7 @@ pub enum OpIntegrityCheckState {
94599537
/// State of the current validation sub-phase
94609538
substate: NotNullValidationSubstate,
94619539
},
9462-
/// Phase 4: UNIQUE constraint validation (skipped when quick=true)
9540+
/// Phase 5: UNIQUE constraint validation (skipped when quick=true)
94639541
/// Scans unique indexes and checks for duplicate keys.
94649542
ValidatingUnique {
94659543
errors: Vec<IntegrityCheckError>,
@@ -9475,7 +9553,7 @@ pub enum OpIntegrityCheckState {
94759553
/// State of the current validation sub-phase
94769554
substate: UniqueValidationSubstate,
94779555
},
9478-
/// Phase 5: Row-index consistency validation (skipped when quick=true)
9556+
/// Phase 6: Row-index consistency validation (skipped when quick=true)
94799557
/// For each table row, verifies it has a corresponding entry in each index.
94809558
ValidatingRowIndexConsistency {
94819559
errors: Vec<IntegrityCheckError>,
@@ -9507,6 +9585,21 @@ pub enum IndexCountValidationSubstate {
95079585
ComparingCounts,
95089586
}
95099587

9588+
/// Sub-states for index rowid position validation
9589+
#[derive(Debug, Clone)]
9590+
pub enum IndexRowidValidationSubstate {
9591+
/// Starting validation for a new table
9592+
StartTable,
9593+
/// Starting validation for a new index
9594+
StartIndex,
9595+
/// Rewinding to the beginning of the index
9596+
Rewinding,
9597+
/// Checking rowid position in the current index entry
9598+
CheckingEntry,
9599+
/// Advancing to the next entry
9600+
Advancing,
9601+
}
9602+
95109603
/// Sub-states for NOT NULL constraint validation
95119604
#[derive(Debug, Clone)]
95129605
pub enum NotNullValidationSubstate {
@@ -9693,7 +9786,25 @@ pub fn op_integrity_check(
96939786
}
96949787
}
96959788

9696-
// Phase 2: Index count validation (skip for quick_check per SQLite)
9789+
// Phase 2: Index rowid position validation (skip for quick_check per SQLite)
9790+
if !*quick
9791+
&& tables
9792+
.iter()
9793+
.any(|t| t.indexes.iter().any(|idx| idx.index_info.has_rowid))
9794+
{
9795+
let errors = std::mem::take(errors);
9796+
state.op_integrity_check_state =
9797+
OpIntegrityCheckState::ValidatingIndexRowidPosition {
9798+
errors,
9799+
current_table_idx: 0,
9800+
current_index_idx: 0,
9801+
cursor: None,
9802+
substate: IndexRowidValidationSubstate::StartTable,
9803+
};
9804+
return Ok(InsnFunctionStepResult::Step);
9805+
}
9806+
9807+
// Phase 3: Index count validation (skip for quick_check per SQLite)
96979808
if !*quick && tables.iter().any(|t| !t.indexes.is_empty()) {
96989809
// Transition to index count validation phase
96999810
let errors = std::mem::take(errors);
@@ -9709,7 +9820,7 @@ pub fn op_integrity_check(
97099820
return Ok(InsnFunctionStepResult::Step);
97109821
}
97119822

9712-
// Phase 3: NOT NULL constraint validation (skip for quick_check per SQLite)
9823+
// Phase 4: NOT NULL constraint validation (skip for quick_check per SQLite)
97139824
if !*quick && tables.iter().any(|t| !t.not_null_columns.is_empty()) {
97149825
// Transition to NOT NULL validation phase
97159826
let errors = std::mem::take(errors);
@@ -9722,7 +9833,7 @@ pub fn op_integrity_check(
97229833
return Ok(InsnFunctionStepResult::Step);
97239834
}
97249835

9725-
// Phase 4: UNIQUE constraint validation (skip if quick_check)
9836+
// Phase 5: UNIQUE constraint validation (skip if quick_check)
97269837
if !*quick
97279838
&& tables
97289839
.iter()
@@ -9747,6 +9858,136 @@ pub fn op_integrity_check(
97479858
state.pc += 1;
97489859
}
97499860
}
9861+
OpIntegrityCheckState::ValidatingIndexRowidPosition {
9862+
errors,
9863+
current_table_idx,
9864+
current_index_idx,
9865+
cursor,
9866+
substate,
9867+
} => {
9868+
loop {
9869+
match substate {
9870+
IndexRowidValidationSubstate::StartTable => {
9871+
// Skip tables without indexes that include rowid
9872+
while *current_table_idx < tables.len()
9873+
&& tables[*current_table_idx]
9874+
.indexes
9875+
.iter()
9876+
.all(|idx| !idx.index_info.has_rowid)
9877+
{
9878+
*current_table_idx += 1;
9879+
}
9880+
9881+
if *current_table_idx >= tables.len() {
9882+
// Done scanning rowid positions; transition to index count validation
9883+
let errors = std::mem::take(errors);
9884+
state.op_integrity_check_state =
9885+
OpIntegrityCheckState::ValidatingIndexCounts {
9886+
errors,
9887+
current_table_idx: 0,
9888+
current_index_idx: 0,
9889+
table_row_count: 0,
9890+
index_entry_count: 0,
9891+
substate: IndexCountValidationSubstate::StartTable,
9892+
cursor: None,
9893+
};
9894+
return Ok(InsnFunctionStepResult::Step);
9895+
}
9896+
9897+
*current_index_idx = 0;
9898+
*substate = IndexRowidValidationSubstate::StartIndex;
9899+
}
9900+
IndexRowidValidationSubstate::StartIndex => {
9901+
let table = &tables[*current_table_idx];
9902+
9903+
while *current_index_idx < table.indexes.len()
9904+
&& !table.indexes[*current_index_idx].index_info.has_rowid
9905+
{
9906+
*current_index_idx += 1;
9907+
}
9908+
9909+
if *current_index_idx >= table.indexes.len() {
9910+
// Done with this table, move to next
9911+
*current_table_idx += 1;
9912+
*substate = IndexRowidValidationSubstate::StartTable;
9913+
continue;
9914+
}
9915+
9916+
let index = &table.indexes[*current_index_idx];
9917+
*cursor = Some(Box::new(BTreeCursor::new(
9918+
pager.clone(),
9919+
index.root_page,
9920+
0,
9921+
)));
9922+
*substate = IndexRowidValidationSubstate::Rewinding;
9923+
}
9924+
IndexRowidValidationSubstate::Rewinding => {
9925+
let btree_cursor = cursor.as_mut().unwrap();
9926+
return_if_io!(btree_cursor.rewind());
9927+
if !btree_cursor.has_record() {
9928+
// Index is empty, move to next index
9929+
*cursor = None;
9930+
*current_index_idx += 1;
9931+
*substate = IndexRowidValidationSubstate::StartIndex;
9932+
continue;
9933+
}
9934+
*substate = IndexRowidValidationSubstate::CheckingEntry;
9935+
}
9936+
IndexRowidValidationSubstate::CheckingEntry => {
9937+
let btree_cursor = cursor.as_mut().unwrap();
9938+
let record = return_if_io!(btree_cursor.record())
9939+
.expect("record must exist when has_record() is true");
9940+
let index = &tables[*current_table_idx].indexes[*current_index_idx];
9941+
match index_record_rowid_at_end(record, &index.index_info) {
9942+
Ok(true) => {}
9943+
Ok(false) => {
9944+
errors.push(IntegrityCheckError::RowidNotAtEndOfRecord {
9945+
index_name: index.name.clone(),
9946+
});
9947+
}
9948+
Err(err) => {
9949+
errors.push(IntegrityCheckError::IndexRecordCorrupt {
9950+
index_name: index.name.clone(),
9951+
error: err.to_string(),
9952+
});
9953+
9954+
if errors.len() >= *max_errors {
9955+
let message = format_integrity_check_result(errors);
9956+
state.registers[*message_register] =
9957+
Register::Value(Value::build_text(message));
9958+
state.op_integrity_check_state = OpIntegrityCheckState::Start;
9959+
state.pc += 1;
9960+
return Ok(InsnFunctionStepResult::Step);
9961+
}
9962+
}
9963+
}
9964+
9965+
if errors.len() >= *max_errors {
9966+
let message = format_integrity_check_result(errors);
9967+
state.registers[*message_register] =
9968+
Register::Value(Value::build_text(message));
9969+
state.op_integrity_check_state = OpIntegrityCheckState::Start;
9970+
state.pc += 1;
9971+
return Ok(InsnFunctionStepResult::Step);
9972+
}
9973+
9974+
*substate = IndexRowidValidationSubstate::Advancing;
9975+
}
9976+
IndexRowidValidationSubstate::Advancing => {
9977+
let btree_cursor = cursor.as_mut().unwrap();
9978+
return_if_io!(btree_cursor.next());
9979+
if btree_cursor.has_record() {
9980+
*substate = IndexRowidValidationSubstate::CheckingEntry;
9981+
} else {
9982+
// Done with this index, move to next
9983+
*cursor = None;
9984+
*current_index_idx += 1;
9985+
*substate = IndexRowidValidationSubstate::StartIndex;
9986+
}
9987+
}
9988+
}
9989+
}
9990+
}
97509991
OpIntegrityCheckState::ValidatingIndexCounts {
97519992
errors,
97529993
current_table_idx,
@@ -9770,7 +10011,7 @@ pub fn op_integrity_check(
977010011
// Done with all tables, transition to NOT NULL validation, UNIQUE validation,
977110012
// row-index validation, or finish
977210013

9773-
// Phase 3: NOT NULL constraint validation (skip for quick_check)
10014+
// Phase 4: NOT NULL constraint validation (skip for quick_check)
977410015
if !*quick && tables.iter().any(|t| !t.not_null_columns.is_empty()) {
977510016
let errors = std::mem::take(errors);
977610017
state.op_integrity_check_state =
@@ -9783,7 +10024,7 @@ pub fn op_integrity_check(
978310024
return Ok(InsnFunctionStepResult::Step);
978410025
}
978510026

9786-
// Phase 4: UNIQUE constraint validation (skip for quick_check)
10027+
// Phase 5: UNIQUE constraint validation (skip for quick_check)
978710028
if !*quick
978810029
&& tables
978910030
.iter()
@@ -9802,7 +10043,7 @@ pub fn op_integrity_check(
980210043
return Ok(InsnFunctionStepResult::Step);
980310044
}
980410045

9805-
// Phase 5: Row-index consistency validation (skip if quick_check)
10046+
// Phase 6: Row-index consistency validation (skip if quick_check)
980610047
if !*quick && tables.iter().any(|t| !t.indexes.is_empty()) {
980710048
let errors = std::mem::take(errors);
980810049
state.op_integrity_check_state =
@@ -9917,7 +10158,7 @@ pub fn op_integrity_check(
991710158
if *current_table_idx >= tables.len() {
991810159
// Done with all tables, transition to UNIQUE validation, row-index validation, or finish
991910160

9920-
// Phase 4: UNIQUE constraint validation (skip for quick_check)
10161+
// Phase 5: UNIQUE constraint validation (skip for quick_check)
992110162
// Note: We can only be in ValidatingNotNull if quick=false, but check anyway for clarity
992210163
if !*quick
992310164
&& tables
@@ -9937,7 +10178,7 @@ pub fn op_integrity_check(
993710178
return Ok(InsnFunctionStepResult::Step);
993810179
}
993910180

9940-
// Phase 5: Row-index consistency validation (skip if quick_check)
10181+
// Phase 6: Row-index consistency validation (skip if quick_check)
994110182
if !*quick && tables.iter().any(|t| !t.indexes.is_empty()) {
994210183
let errors = std::mem::take(errors);
994310184
state.op_integrity_check_state =

0 commit comments

Comments
 (0)