1313//! `next` pointer sits at byte 4 in the root and byte 0 in a chain page — any
1414//! chain walk must account for that offset.
1515
16+ use std:: collections:: BTreeSet ;
17+
1618use crate :: errors:: PagedbError ;
1719use crate :: pager:: Pager ;
1820use crate :: pager:: format:: data_page:: ENVELOPE_OVERHEAD ;
@@ -272,8 +274,12 @@ pub async fn release<V: Vfs>(
272274 if info. refcount <= 1 {
273275 // Collect entire chain.
274276 let mut freed = vec ! [ root_page_id] ;
277+ let mut seen = BTreeSet :: from ( [ root_page_id] ) ;
275278 let mut cur = info. next ;
276279 while cur != 0 {
280+ if !seen. insert ( cur) {
281+ return Err ( PagedbError :: overflow_chain_cycle ( root_page_id, cur) ) ;
282+ }
277283 let guard = pager
278284 . read_main_page ( cur, realm_id, PageKind :: Overflow )
279285 . await ?;
@@ -303,22 +309,46 @@ pub async fn read_chain<V: Vfs>(
303309 root_page_id : u64 ,
304310 total_len : u64 ,
305311) -> Result < Vec < u8 > > {
306- let mut out: Vec < u8 > = Vec :: with_capacity ( usize:: try_from ( total_len) . unwrap_or ( 0 ) ) ;
312+ let total_len = usize:: try_from ( total_len)
313+ . ok ( )
314+ . filter ( |len| isize:: try_from ( * len) . is_ok ( ) )
315+ . ok_or_else ( || PagedbError :: overflow_body_malformed ( "chain.total_length" ) ) ?;
316+ // Durable metadata must not choose an arbitrarily large allocation before
317+ // any chain page has been authenticated. Grow only as bytes are verified.
318+ let mut out: Vec < u8 > = Vec :: with_capacity ( total_len. min ( pager. page_size ( ) ) ) ;
307319
308320 let info = read_root_page ( pager, realm_id, root_page_id) . await ?;
321+ if info. root_data . len ( ) > total_len {
322+ return Err ( PagedbError :: overflow_body_malformed (
323+ "chain.assembled_length" ,
324+ ) ) ;
325+ }
309326 out. extend_from_slice ( & info. root_data ) ;
310327
328+ let mut seen = BTreeSet :: from ( [ root_page_id] ) ;
311329 let mut next = info. next ;
312330 while next != 0 {
331+ if !seen. insert ( next) {
332+ return Err ( PagedbError :: overflow_chain_cycle ( root_page_id, next) ) ;
333+ }
313334 let guard = pager
314335 . read_main_page ( next, realm_id, PageKind :: Overflow )
315336 . await ?;
316337 let body = guard. body ( ) ;
317338 let ( n, data) = decode_overflow ( & body) ?;
339+ if out
340+ . len ( )
341+ . checked_add ( data. len ( ) )
342+ . is_none_or ( |assembled| assembled > total_len)
343+ {
344+ return Err ( PagedbError :: overflow_body_malformed (
345+ "chain.assembled_length" ,
346+ ) ) ;
347+ }
318348 out. extend_from_slice ( data) ;
319349 next = n;
320350 }
321- if out. len ( ) as u64 != total_len {
351+ if out. len ( ) != total_len {
322352 return Err ( PagedbError :: overflow_body_malformed (
323353 "chain.assembled_length" ,
324354 ) ) ;
@@ -334,9 +364,13 @@ pub async fn collect_chain<V: Vfs>(
334364 root_page_id : u64 ,
335365) -> Result < Vec < u64 > > {
336366 let mut out = vec ! [ root_page_id] ;
367+ let mut seen = BTreeSet :: from ( [ root_page_id] ) ;
337368 let info = read_root_page ( pager, realm_id, root_page_id) . await ?;
338369 let mut next = info. next ;
339370 while next != 0 {
371+ if !seen. insert ( next) {
372+ return Err ( PagedbError :: overflow_chain_cycle ( root_page_id, next) ) ;
373+ }
340374 let guard = pager
341375 . read_main_page ( next, realm_id, PageKind :: Overflow )
342376 . await ?;
@@ -350,8 +384,38 @@ pub async fn collect_chain<V: Vfs>(
350384
351385#[ cfg( test) ]
352386mod tests {
387+ use std:: sync:: Arc ;
388+ use std:: time:: Duration ;
389+
390+ use crate :: crypto:: CipherId ;
391+ use crate :: crypto:: kdf:: derive_mk;
392+ use crate :: errors:: CorruptionDetail ;
393+ use crate :: pager:: PagerConfig ;
394+ use crate :: vfs:: memory:: MemVfs ;
395+
353396 use super :: * ;
354397
398+ const TEST_PAGE_SIZE : usize = 4096 ;
399+ const TEST_REALM : RealmId = RealmId :: new ( [ 0xA4 ; 16 ] ) ;
400+
401+ async fn test_pager ( ) -> Arc < Pager < MemVfs > > {
402+ let mk = derive_mk ( & [ 0xA5 ; 32 ] , & [ 0u8 ; 16 ] , 0 ) . unwrap ( ) ;
403+ let cfg = PagerConfig {
404+ page_size : TEST_PAGE_SIZE ,
405+ buffer_pool_pages : 16 ,
406+ segment_cache_pages : 16 ,
407+ cipher_id : CipherId :: Aes256Gcm ,
408+ mk_epoch : 0 ,
409+ main_db_file_id : [ 0xB4 ; 16 ] ,
410+ main_db_path : "/main.db" . into ( ) ,
411+ anchor_budget : 1_000_000 ,
412+ dek_lru_capacity : 16 ,
413+ observer_retry_count : 0 ,
414+ metrics_enabled : true ,
415+ } ;
416+ Arc :: new ( Pager :: open ( MemVfs :: new ( ) , mk, cfg) . await . unwrap ( ) )
417+ }
418+
355419 #[ test]
356420 fn round_trip_chain_page ( ) {
357421 let mut body = vec ! [ 0u8 ; 4096 - ENVELOPE_OVERHEAD ] ;
@@ -378,4 +442,111 @@ mod tests {
378442 // root: 4096 - 40 - 16 = 4040
379443 assert_eq ! ( overflow_root_capacity( 4096 ) , 4040 ) ;
380444 }
445+
446+ async fn cyclic_chain ( pager : & Pager < MemVfs > , root_page_id : u64 , chain_page_id : u64 ) {
447+ let mut root_body = vec ! [ 0u8 ; TEST_PAGE_SIZE - ENVELOPE_OVERHEAD ] ;
448+ encode_overflow_root ( & mut root_body, 1 , chain_page_id, b"" ) . unwrap ( ) ;
449+ pager
450+ . write_main_page ( root_page_id, TEST_REALM , PageKind :: OverflowRoot , & root_body)
451+ . await
452+ . unwrap ( ) ;
453+
454+ let mut chain_body = vec ! [ 0u8 ; TEST_PAGE_SIZE - ENVELOPE_OVERHEAD ] ;
455+ encode_overflow ( & mut chain_body, chain_page_id, b"" ) . unwrap ( ) ;
456+ pager
457+ . write_main_page ( chain_page_id, TEST_REALM , PageKind :: Overflow , & chain_body)
458+ . await
459+ . unwrap ( ) ;
460+ }
461+
462+ #[ tokio:: test( flavor = "current_thread" ) ]
463+ async fn read_chain_rejects_absurd_total_len_without_allocation_panic ( ) {
464+ let pager = test_pager ( ) . await ;
465+ let root_page_id = 42 ;
466+ let mut body = vec ! [ 0u8 ; TEST_PAGE_SIZE - ENVELOPE_OVERHEAD ] ;
467+ encode_overflow_root ( & mut body, 1 , 0 , b"small" ) . unwrap ( ) ;
468+ pager
469+ . write_main_page ( root_page_id, TEST_REALM , PageKind :: OverflowRoot , & body)
470+ . await
471+ . unwrap ( ) ;
472+
473+ let error = read_chain ( & pager, TEST_REALM , root_page_id, u64:: MAX )
474+ . await
475+ . unwrap_err ( ) ;
476+ assert ! ( matches!(
477+ error,
478+ PagedbError :: Corruption ( CorruptionDetail :: OverflowBodyMalformed { .. } )
479+ ) ) ;
480+ }
481+
482+ #[ tokio:: test( flavor = "current_thread" ) ]
483+ async fn read_chain_rejects_more_data_than_declared ( ) {
484+ let pager = test_pager ( ) . await ;
485+ let root_page_id = 43 ;
486+ let mut body = vec ! [ 0u8 ; TEST_PAGE_SIZE - ENVELOPE_OVERHEAD ] ;
487+ encode_overflow_root ( & mut body, 1 , 0 , b"too-long" ) . unwrap ( ) ;
488+ pager
489+ . write_main_page ( root_page_id, TEST_REALM , PageKind :: OverflowRoot , & body)
490+ . await
491+ . unwrap ( ) ;
492+
493+ let error = read_chain ( & pager, TEST_REALM , root_page_id, 1 )
494+ . await
495+ . unwrap_err ( ) ;
496+ assert ! ( matches!(
497+ error,
498+ PagedbError :: Corruption ( CorruptionDetail :: OverflowBodyMalformed { .. } )
499+ ) ) ;
500+ }
501+
502+ #[ tokio:: test( flavor = "current_thread" ) ]
503+ async fn read_chain_rejects_cycle_without_hanging ( ) {
504+ let pager = test_pager ( ) . await ;
505+ cyclic_chain ( & pager, 44 , 45 ) . await ;
506+ let error = tokio:: time:: timeout (
507+ Duration :: from_secs ( 1 ) ,
508+ read_chain ( & pager, TEST_REALM , 44 , 0 ) ,
509+ )
510+ . await
511+ . expect ( "cycle detection should return before the timeout" )
512+ . unwrap_err ( ) ;
513+ assert ! ( matches!(
514+ error,
515+ PagedbError :: Corruption ( CorruptionDetail :: OverflowChainCycle { .. } )
516+ ) ) ;
517+ }
518+
519+ #[ tokio:: test( flavor = "current_thread" ) ]
520+ async fn release_rejects_cycle_without_hanging ( ) {
521+ let pager = test_pager ( ) . await ;
522+ cyclic_chain ( & pager, 46 , 47 ) . await ;
523+ let result =
524+ tokio:: time:: timeout ( Duration :: from_secs ( 1 ) , release ( & pager, TEST_REALM , 46 , 48 ) )
525+ . await
526+ . expect ( "cycle detection should return before the timeout" ) ;
527+ let Err ( error) = result else {
528+ panic ! ( "overflow release cycles must not be accepted" ) ;
529+ } ;
530+ assert ! ( matches!(
531+ error,
532+ PagedbError :: Corruption ( CorruptionDetail :: OverflowChainCycle { .. } )
533+ ) ) ;
534+ }
535+
536+ #[ tokio:: test( flavor = "current_thread" ) ]
537+ async fn collect_chain_rejects_cycle_without_hanging ( ) {
538+ let pager = test_pager ( ) . await ;
539+ cyclic_chain ( & pager, 49 , 50 ) . await ;
540+ let error = tokio:: time:: timeout (
541+ Duration :: from_secs ( 1 ) ,
542+ collect_chain ( & pager, TEST_REALM , 49 ) ,
543+ )
544+ . await
545+ . expect ( "cycle detection should return before the timeout" )
546+ . expect_err ( "overflow collect cycles must not be accepted" ) ;
547+ assert ! ( matches!(
548+ error,
549+ PagedbError :: Corruption ( CorruptionDetail :: OverflowChainCycle { .. } )
550+ ) ) ;
551+ }
381552}
0 commit comments