@@ -14,14 +14,15 @@ use crate::btree::leaf::{Leaf, LeafValue};
1414use crate :: btree:: overflow;
1515use crate :: catalog:: codec:: { Catalog , SegmentMeta } ;
1616use crate :: crypto:: aad:: { Aad , AadFields , MAIN_DB_SEGMENT_ID } ;
17+ use crate :: errors:: PagedbError ;
1718use crate :: pager:: format:: data_page:: extract_page_header_ids;
1819use crate :: pager:: format:: page_kind:: PageKind ;
19- use crate :: pager:: page_space:: { FIRST_ALLOCATABLE_PAGE_ID , is_reserved} ;
20+ use crate :: pager:: page_space:: { FIRST_ALLOCATABLE_PAGE_ID , is_reserved, page_offset } ;
2021use crate :: pager:: { PageGuard , Pager } ;
2122use crate :: segment:: authenticated_metadata:: authenticate_segment_metadata;
2223use crate :: txn:: db:: Db ;
2324use crate :: vfs:: types:: OpenMode ;
24- use crate :: vfs:: { Vfs , VfsFile } ;
25+ use crate :: vfs:: { Vfs , VfsFile , read_exact_at } ;
2526
2627/// A single page-level issue found during deep walk.
2728#[ non_exhaustive]
@@ -260,7 +261,7 @@ pub async fn run_deep_walk<V: Vfs + Clone>(db: &Db<V>) -> Result<DeepWalkReport>
260261
261262 let vfs: & V = & db. vfs ;
262263 let main_file_res = vfs. open ( main_db_path, OpenMode :: Read ) . await ;
263- let main_file = match main_file_res {
264+ let mut main_file = match main_file_res {
264265 Ok ( f) => f,
265266 Err ( e) => {
266267 report. page_issues . push ( PageIssue {
@@ -275,24 +276,25 @@ pub async fn run_deep_walk<V: Vfs + Clone>(db: &Db<V>) -> Result<DeepWalkReport>
275276 // (HK-MAC, cleartext) and are already verified by `Db::open`. Skip them.
276277 // Page 2 and 3 are reserved (apply-journal). Walk from page 4.
277278 for page_id in 4 ..next_page_id {
278- let offset = page_id * page_size as u64 ;
279- let mut buf = vec ! [ 0u8 ; page_size] ;
280- match main_file. read_at ( offset, & mut buf) . await {
281- Ok ( n) if n < page_size => {
282- // Short read at the tail — the file may be smaller than expected.
283- // Report but continue.
279+ let offset = match page_offset ( page_id, page_size, "deep-walk main page offset" ) {
280+ Ok ( offset) => offset,
281+ Err ( error) => {
284282 report. page_issues . push ( PageIssue {
285283 page_id,
286- description : format ! ( "short read: expected {page_size} bytes, got {n }" ) ,
284+ description : format ! ( "{error }" ) ,
287285 } ) ;
288286 report. pages_examined += 1 ;
289287 continue ;
290288 }
291- Ok ( _) => { }
292- Err ( e) => {
289+ } ;
290+ let mut buf = vec ! [ 0u8 ; page_size] ;
291+ match read_exact_at ( & mut main_file, offset, & mut buf) . await {
292+ Ok ( ( ) ) => { }
293+ Err ( error) => {
294+ let description = describe_page_read_failure ( & mut main_file, offset, error) . await ;
293295 report. page_issues . push ( PageIssue {
294296 page_id,
295- description : format ! ( "read error: {e}" ) ,
297+ description,
296298 } ) ;
297299 report. pages_examined += 1 ;
298300 continue ;
@@ -435,7 +437,7 @@ async fn check_segment<V: Vfs + Clone>(
435437 let page_size = pager. page_size ( ) ;
436438
437439 // Check file exists.
438- let Ok ( file) = vfs. open ( & live, OpenMode :: Read ) . await else {
440+ let Ok ( mut file) = vfs. open ( & live, OpenMode :: Read ) . await else {
439441 report. drift_issues . push ( DriftIssue {
440442 segment_id : meta. segment_id ,
441443 description : "segment file missing from seg/" . to_string ( ) ,
@@ -464,7 +466,16 @@ async fn check_segment<V: Vfs + Clone>(
464466 // We don't have a metadata API, but we can check via read: try reading one
465467 // byte past the expected end. If it succeeds (on some VFS) we skip the
466468 // check; if we read exactly `page_count * page_size` bytes we're consistent.
467- let expected_size = meta. page_count * page_size as u64 ;
469+ let expected_size = match page_offset ( meta. page_count , page_size, "segment expected size" ) {
470+ Ok ( offset) => offset,
471+ Err ( error) => {
472+ report. segment_issues . push ( SegmentIssue {
473+ segment_id : meta. segment_id ,
474+ description : format ! ( "{error}" ) ,
475+ } ) ;
476+ return ;
477+ }
478+ } ;
468479 let mut probe = vec ! [ 0u8 ; 1 ] ;
469480 let over_read = file. read_at ( expected_size, & mut probe) . await ;
470481 match over_read {
@@ -483,24 +494,24 @@ async fn check_segment<V: Vfs + Clone>(
483494 // Walk data pages (1 .. page_count - 1, skipping header=0 and footer=last).
484495 let last_data = footer_page_id;
485496 for page_id in 1 ..last_data {
486- let offset = page_id * page_size as u64 ;
487- let mut buf = vec ! [ 0u8 ; page_size] ;
488- let read_res = file. read_at ( offset, & mut buf) . await ;
489- match read_res {
490- Ok ( n) if n < page_size => {
497+ let offset = match page_offset ( page_id, page_size, "segment data page offset" ) {
498+ Ok ( offset) => offset,
499+ Err ( error) => {
491500 report. segment_issues . push ( SegmentIssue {
492501 segment_id : meta. segment_id ,
493- description : format ! (
494- "short read at page {page_id}: expected {page_size} bytes, got {n}"
495- ) ,
502+ description : format ! ( "page {page_id}: {error}" ) ,
496503 } ) ;
497504 continue ;
498505 }
499- Ok ( _) => { }
500- Err ( e) => {
506+ } ;
507+ let mut buf = vec ! [ 0u8 ; page_size] ;
508+ match read_exact_at ( & mut file, offset, & mut buf) . await {
509+ Ok ( ( ) ) => { }
510+ Err ( error) => {
511+ let description = describe_page_read_failure ( & mut file, offset, error) . await ;
501512 report. segment_issues . push ( SegmentIssue {
502513 segment_id : meta. segment_id ,
503- description : format ! ( "read error at page {page_id}: {e }" ) ,
514+ description : format ! ( "page {page_id}: {description }" ) ,
504515 } ) ;
505516 continue ;
506517 }
@@ -553,6 +564,34 @@ async fn check_segment<V: Vfs + Clone>(
553564 }
554565}
555566
567+ /// Turn a failed full-page read into a description that keeps the byte counts.
568+ ///
569+ /// `read_exact_at` completes a legal short read and only gives up once the
570+ /// backend stops making progress, so the partial count it consumed never
571+ /// reaches the caller — a truncated file arrives here as a bare
572+ /// `UnexpectedEof`. On a diagnostic surface those numbers are the product: an
573+ /// operator needs to know the file is short and by how much, not merely that a
574+ /// read ended. Any other error is already self-describing and passes through.
575+ async fn describe_page_read_failure < F : VfsFile > (
576+ file : & mut F ,
577+ offset : u64 ,
578+ error : PagedbError ,
579+ ) -> String {
580+ let is_eof = matches ! (
581+ & error,
582+ PagedbError :: Io ( io) if io. kind( ) == std:: io:: ErrorKind :: UnexpectedEof
583+ ) ;
584+ if !is_eof {
585+ return format ! ( "read error: {error}" ) ;
586+ }
587+ match file. len ( ) . await {
588+ Ok ( len) => format ! ( "truncated: page starts at offset {offset}, file is {len} bytes" ) ,
589+ Err ( len_error) => {
590+ format ! ( "read error: {error} (file length unavailable: {len_error})" )
591+ }
592+ }
593+ }
594+
556595/// Collect the set of all page IDs reachable from the main B+ tree root,
557596/// the catalog root, the commit-history root, and the free-list root.
558597/// Pages 0..=3 (reserved) are always considered reachable.
@@ -877,10 +916,10 @@ async fn diagnose_overflow_chain<V: Vfs + Clone>(
877916#[ cfg( test) ]
878917mod tests {
879918 use super :: * ;
880- use crate :: OpenOptions ;
881919 use crate :: btree:: node:: body_capacity;
882920 use crate :: pager:: format:: data_page:: ENVELOPE_OVERHEAD ;
883921 use crate :: vfs:: memory:: MemVfs ;
922+ use crate :: { OpenOptions , SegmentKind , SegmentPageKind } ;
884923
885924 const PAGE : usize = 4096 ;
886925 const REALM : crate :: RealmId = crate :: RealmId :: new ( [ 0xD3 ; 16 ] ) ;
@@ -1074,6 +1113,72 @@ mod tests {
10741113 ) ;
10751114 }
10761115
1116+ /// A catalog `page_count` large enough to overflow a byte offset must be
1117+ /// rejected as a structured issue, never reach the arithmetic that would
1118+ /// wrap it, and never abort the walk. Authenticated metadata validation is
1119+ /// what stops it, ahead of any footer index or loop bound; the checked
1120+ /// `page_offset` behind it is the second line, covered directly in
1121+ /// `pager::page_space`.
1122+ #[ tokio:: test( flavor = "current_thread" ) ]
1123+ async fn deep_walk_rejects_impossible_catalog_page_count ( ) {
1124+ let db = open_db ( ) . await ;
1125+ let mut segment = db
1126+ . create_segment ( REALM , SegmentKind :: Unspecified )
1127+ . await
1128+ . unwrap ( ) ;
1129+ segment
1130+ . append_page ( SegmentPageKind :: Data , b"deep-walk" )
1131+ . await
1132+ . unwrap ( ) ;
1133+ let mut meta = segment. seal ( ) . await . unwrap ( ) ;
1134+ {
1135+ let mut txn = db. begin_write ( ) . await . unwrap ( ) ;
1136+ txn. link_segment ( "overflow" , & meta) . await . unwrap ( ) ;
1137+ txn. commit ( ) . await . unwrap ( ) ;
1138+ }
1139+
1140+ meta. page_count = ( u64:: MAX / PAGE as u64 ) + 2 ;
1141+ let ( catalog_root, next_page_id) = {
1142+ let state = db. writer . lock ( ) . await ;
1143+ ( state. catalog_root_page_id , state. next_page_id )
1144+ } ;
1145+ let mut tree = BTree :: open (
1146+ db. pager . clone ( ) ,
1147+ db. realm_id ,
1148+ catalog_root,
1149+ next_page_id,
1150+ db. page_size ,
1151+ ) ;
1152+ let key = Catalog :: segment_key ( REALM , b"overflow" ) . unwrap ( ) ;
1153+ tree. put ( & key, & Catalog :: encode_segment_meta ( & meta) )
1154+ . await
1155+ . unwrap ( ) ;
1156+ tree. flush ( ) . await . unwrap ( ) ;
1157+ {
1158+ let mut state = db. writer . lock ( ) . await ;
1159+ state. catalog_root_page_id = tree. root_page_id ( ) ;
1160+ state. next_page_id = state. next_page_id . max ( tree. next_page_id ( ) ) ;
1161+ }
1162+
1163+ let report = run_deep_walk ( & db) . await . unwrap ( ) ;
1164+ assert ! (
1165+ report. segment_issues. iter( ) . any( |issue| {
1166+ issue. segment_id == meta. segment_id
1167+ && issue
1168+ . description
1169+ . contains( "authenticated segment metadata invalid" )
1170+ } ) ,
1171+ "impossible segment geometry must become a structured issue, got {report:?}"
1172+ ) ;
1173+ assert ! (
1174+ report. segment_issues. iter( ) . all( |issue| {
1175+ !issue. description. contains( "segment expected size" )
1176+ && !issue. description. contains( "segment data page offset" )
1177+ } ) ,
1178+ "validation must reject the record before any offset arithmetic runs: {report:?}"
1179+ ) ;
1180+ }
1181+
10771182 async fn sole_overflow_root ( db : & Db < MemVfs > , leaf_page_id : u64 ) -> u64 {
10781183 let ( guard, _) = db. pager . read_main_node ( leaf_page_id, REALM ) . await . unwrap ( ) ;
10791184 let leaf = Leaf :: decode ( guard. body_ref ( ) ) . unwrap ( ) ;
0 commit comments