From df83b560831498b34c691854f3117b3978b67e87 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Wed, 10 Sep 2025 14:40:12 +0300 Subject: [PATCH] check freelist count in integrity check --- core/storage/btree.rs | 32 ++++++++++++++++++++++++++++---- core/vdbe/execute.rs | 11 +++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/core/storage/btree.rs b/core/storage/btree.rs index a89341974..e92156f0e 100644 --- a/core/storage/btree.rs +++ b/core/storage/btree.rs @@ -5487,6 +5487,13 @@ pub enum IntegrityCheckError { references: Vec, page_category: PageCategory, }, + #[error( + "Freelist count mismatch. actual_count={actual_count}, expected_count={expected_count}" + )] + FreelistCountMismatch { + actual_count: usize, + expected_count: usize, + }, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -5497,6 +5504,12 @@ pub(crate) enum PageCategory { FreePage, } +#[derive(Clone)] +pub struct CheckFreelist { + pub expected_count: usize, + pub actual_count: usize, +} + #[derive(Clone)] struct IntegrityCheckPageEntry { page_idx: usize, @@ -5509,6 +5522,7 @@ pub struct IntegrityCheckState { first_leaf_level: Option, page_reference: HashMap, page: Option, + pub freelist_count: CheckFreelist, } impl IntegrityCheckState { @@ -5518,9 +5532,17 @@ impl IntegrityCheckState { page_reference: HashMap::new(), first_leaf_level: None, page: None, + freelist_count: CheckFreelist { + expected_count: 0, + actual_count: 0, + }, } } + pub fn set_expected_freelist_count(&mut self, count: usize) { + self.freelist_count.expected_count = count; + } + pub fn start( &mut self, page_idx: usize, @@ -5554,10 +5576,7 @@ impl IntegrityCheckState { ) { let page_id = entry.page_idx as u64; let Some(previous) = self.page_reference.insert(page_id, referenced_by) else { - // do not traverse free pages as they have no meaingful structured content - if entry.page_category != PageCategory::FreePage { - self.page_stack.push(entry); - } + self.page_stack.push(entry); return; }; errors.push(IntegrityCheckError::PageReferencedMultipleTimes { @@ -5616,6 +5635,7 @@ pub fn integrity_check( let contents = page.get_contents(); if page_category == PageCategory::FreeListTrunk { + state.freelist_count.actual_count += 1; let next_freelist_trunk_page = contents.read_u32_no_offset(0); if next_freelist_trunk_page != 0 { state.push_page( @@ -5645,6 +5665,10 @@ pub fn integrity_check( } continue; } + if page_category == PageCategory::FreePage { + state.freelist_count.actual_count += 1; + continue; + } if page_category == PageCategory::Overflow { let next_overflow_page = contents.read_u32_no_offset(0); if next_overflow_page != 0 { diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 86ae514a4..d9fa306e2 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -7349,6 +7349,9 @@ pub fn op_integrity_check( let mut current_root_idx = 0; // check freelist pages first, if there are any for database if freelist_trunk_page > 0 { + let expected_freelist_count = + return_if_io!(pager.with_header(|header| header.freelist_pages.get())); + integrity_check_state.set_expected_freelist_count(expected_freelist_count as usize); integrity_check_state.start( freelist_trunk_page as usize, PageCategory::FreeListTrunk, @@ -7375,6 +7378,14 @@ pub fn op_integrity_check( *current_root_idx += 1; return Ok(InsnFunctionStepResult::Step); } else { + if integrity_check_state.freelist_count.actual_count + != integrity_check_state.freelist_count.expected_count + { + errors.push(IntegrityCheckError::FreelistCountMismatch { + actual_count: integrity_check_state.freelist_count.actual_count, + expected_count: integrity_check_state.freelist_count.expected_count, + }); + } let message = if errors.is_empty() { "ok".to_string() } else {