@@ -28,10 +28,22 @@ impl<V: Vfs> BTree<V> {
2828
2929 /// Rekey this tree while sharing traversal state with other live roots.
3030 ///
31- /// Retained snapshots commonly share most of their pages. A caller walking
32- /// several roots supplies one page-kind map so each physical page is
33- /// authenticated and rewritten exactly once without hiding a cross-kind
34- /// alias.
31+ /// Copy-on-write snapshots share most of their physical pages, so a caller
32+ /// rewriting several roots — the live tree plus every root a retained
33+ /// commit-history row still names — passes one map and each page is
34+ /// authenticated and re-sealed exactly once. Without it the work is
35+ /// `O(roots × pages)` of AEAD rather than `O(unique pages)`.
36+ ///
37+ /// The map holds each page's *kind*, not just its id, because the two
38+ /// failure modes are not the same. A page already walked as a node and
39+ /// referenced again as a node is an ordinary snapshot share and is skipped;
40+ /// the same id presented under a different role means one of the two
41+ /// references survived the page being freed and reused, and is reported as
42+ /// [`CorruptionDetail::PageKindAliased`](crate::errors::CorruptionDetail::PageKindAliased).
43+ /// A plain id set could not tell them apart.
44+ ///
45+ /// Returns the number of pages this call rewrote — pages an earlier walk
46+ /// already covered are not counted twice.
3547 pub ( crate ) async fn rekey_walk_unique (
3648 & self ,
3749 visited : & mut BTreeMap < u64 , PageKind > ,
@@ -48,79 +60,108 @@ impl<V: Vfs> BTree<V> {
4860 page_id,
4961 ) ) ;
5062 }
51- if let Some ( kind ) = visited. get ( & page_id) {
52- match kind {
63+ if let Some ( & walked_as ) = visited. get ( & page_id) {
64+ match walked_as {
5365 PageKind :: BTreeLeaf | PageKind :: BTreeInternal => continue ,
54- _ => return Err ( PagedbError :: IllegalPageKind ) ,
66+ other => {
67+ // Named for the role, not a kind: which of the two node
68+ // kinds this page is has not been read yet, and cannot
69+ // be — the alias is decided before any read.
70+ return Err ( PagedbError :: page_kind_aliased (
71+ page_id,
72+ other. name ( ) ,
73+ "btree_node" ,
74+ ) ) ;
75+ }
5576 }
5677 }
57- // Determine kind by reading the node under its own header kind byte.
58- let ( is_leaf, page_kind, body_bytes) = {
59- let ( g, page_kind) = self . pager . read_main_node ( page_id, self . realm_id ) . await ?;
60- let body = g. body ( ) ;
61- let header = read_header ( & body) ?;
62- let is_leaf = header. kind == NodeKind :: Leaf ;
63- ( is_leaf, page_kind, body. to_vec ( ) )
78+ // `read_node_guard` is the only accessor that proves the
79+ // authenticated envelope kind and the encrypted body header agree.
80+ // This walk both records a page's kind and re-seals the page under
81+ // it, so taking those two from different sources would let a
82+ // mis-routed page be laundered into a freshly authenticated one and
83+ // would leave `visited` describing a kind that is no longer on disk.
84+ let ( guard, node_kind) = self . read_node_guard ( page_id) . await ?;
85+ let page_kind = match node_kind {
86+ NodeKind :: Leaf => PageKind :: BTreeLeaf ,
87+ NodeKind :: Internal => PageKind :: BTreeInternal ,
6488 } ;
6589 visited. insert ( page_id, page_kind) ;
6690
67- if is_leaf {
68- // Collect overflow chains referenced by this leaf.
69- let leaf = Leaf :: decode ( & body_bytes) ?;
70- for ( _k, v) in & leaf. records {
71- if let LeafValue :: Overflow {
72- root_page_id : ov_root,
73- ..
74- } = v
75- {
91+ match node_kind {
92+ NodeKind :: Leaf => {
93+ let leaf = Leaf :: decode ( guard. body_ref ( ) ) ?;
94+ let overflow_roots: Vec < u64 > = leaf
95+ . records
96+ . iter ( )
97+ . filter_map ( |( _, value) | match value {
98+ LeafValue :: Overflow { root_page_id, .. } => Some ( * root_page_id) ,
99+ LeafValue :: Inline ( _) => None ,
100+ } )
101+ . collect ( ) ;
102+ drop ( guard) ;
103+
104+ for overflow_root in overflow_roots {
76105 count += self
77- . rekey_overflow_unique ( page_id, * ov_root , visited)
106+ . rekey_overflow_unique ( page_id, overflow_root , visited)
78107 . await ?;
79108 }
80109 }
81- // Rewrite the leaf page.
82- self . pager
83- . rewrite_page_under_current_epoch ( page_id, self . realm_id , PageKind :: BTreeLeaf )
84- . await ?;
85- count += 1 ;
86- } else {
87- // Internal node: push children onto stack.
88- let internal = internal:: Internal :: decode ( & body_bytes) ?;
89- if internal. leftmost_child != 0 {
90- stack. push ( ( page_id, internal. leftmost_child ) ) ;
91- }
92- for entry in & internal. entries {
93- if entry. right_child != 0 {
94- stack. push ( ( page_id, entry. right_child ) ) ;
110+ NodeKind :: Internal => {
111+ let node = internal:: Internal :: decode ( guard. body_ref ( ) ) ?;
112+ drop ( guard) ;
113+
114+ // A zero child id is an absent slot, not a pointer.
115+ if node. leftmost_child != 0 {
116+ stack. push ( ( page_id, node. leftmost_child ) ) ;
117+ }
118+ for entry in & node. entries {
119+ if entry. right_child != 0 {
120+ stack. push ( ( page_id, entry. right_child ) ) ;
121+ }
95122 }
96123 }
97- // Rewrite the internal page.
98- self . pager
99- . rewrite_page_under_current_epoch (
100- page_id,
101- self . realm_id ,
102- PageKind :: BTreeInternal ,
103- )
104- . await ?;
105- count += 1 ;
106124 }
125+
126+ self . pager
127+ . rewrite_page_under_current_epoch ( page_id, self . realm_id , page_kind)
128+ . await ?;
129+ count += 1 ;
107130 }
108131 Ok ( count)
109132 }
110133
134+ /// Rewrite the overflow chain rooted at `root` — referenced by leaf
135+ /// `leaf_page_id` — returning the number of pages this call rewrote.
136+ ///
137+ /// Overflow roots are reference-counted, so a chain reached a second time
138+ /// through a different leaf (in this tree or in another snapshot's tree)
139+ /// arrives at the *same* root and is skipped whole. A chain *page* reached
140+ /// twice therefore cannot be a legitimate share: either the chain loops, or
141+ /// two distinct roots claim one page. Both mean the chain has no honest
142+ /// terminator, which is why neither is treated as a stopping condition.
111143 async fn rekey_overflow_unique (
112144 & self ,
113145 leaf_page_id : u64 ,
114146 root : u64 ,
115147 visited : & mut BTreeMap < u64 , PageKind > ,
116148 ) -> Result < u64 > {
149+ // Unlike an internal child slot or a chain terminator, zero is not a
150+ // valid overflow root: an `Overflow` leaf value always owns at least
151+ // its root page.
117152 if is_reserved ( root) {
118153 return Err ( PagedbError :: reserved_page_referenced ( leaf_page_id, root) ) ;
119154 }
120- if let Some ( kind) = visited. get ( & root) {
121- return match kind {
155+ if let Some ( & walked_as) = visited. get ( & root) {
156+ return match walked_as {
157+ // Already rewritten with its whole chain, by a leaf holding the
158+ // other reference to this refcounted value.
122159 PageKind :: OverflowRoot => Ok ( 0 ) ,
123- _ => Err ( PagedbError :: IllegalPageKind ) ,
160+ other => Err ( PagedbError :: page_kind_aliased (
161+ root,
162+ other. name ( ) ,
163+ PageKind :: OverflowRoot . name ( ) ,
164+ ) ) ,
124165 } ;
125166 }
126167 visited. insert ( root, PageKind :: OverflowRoot ) ;
@@ -135,10 +176,14 @@ impl<V: Vfs> BTree<V> {
135176 if is_reserved ( next) {
136177 return Err ( PagedbError :: reserved_page_referenced ( root, next) ) ;
137178 }
138- if let Some ( kind ) = visited. get ( & next) {
139- return match kind {
179+ if let Some ( & walked_as ) = visited. get ( & next) {
180+ return match walked_as {
140181 PageKind :: Overflow => Err ( PagedbError :: overflow_chain_cycle ( root, next) ) ,
141- _ => Err ( PagedbError :: IllegalPageKind ) ,
182+ other => Err ( PagedbError :: page_kind_aliased (
183+ next,
184+ other. name ( ) ,
185+ PageKind :: Overflow . name ( ) ,
186+ ) ) ,
142187 } ;
143188 }
144189 visited. insert ( next, PageKind :: Overflow ) ;
@@ -419,3 +464,189 @@ impl<V: Vfs> BTree<V> {
419464 None
420465 }
421466}
467+
468+ #[ cfg( test) ]
469+ mod tests {
470+ use std:: collections:: BTreeMap ;
471+
472+ use crate :: errors:: CorruptionDetail ;
473+ use crate :: pager:: format:: page_kind:: PageKind ;
474+ use crate :: vfs:: memory:: MemVfs ;
475+ use crate :: { Db , PagedbError , RealmId } ;
476+
477+ use super :: super :: core:: BTree ;
478+
479+ const PAGE : usize = 4096 ;
480+ const KEK : [ u8 ; 32 ] = [ 0x7B ; 32 ] ;
481+ const REALM : RealmId = RealmId :: new ( [ 0x7C ; 16 ] ) ;
482+
483+ /// A tree deep enough to hold internal nodes, plus one overflow value so
484+ /// the walk covers both the node and the chain path.
485+ async fn populated_db ( ) -> Db < MemVfs > {
486+ let db = Db :: open_internal ( MemVfs :: new ( ) , KEK , PAGE , REALM )
487+ . await
488+ . unwrap ( ) ;
489+ let mut txn = db. begin_write ( ) . await . unwrap ( ) ;
490+ for index in 0u16 ..256 {
491+ txn. put ( format ! ( "key-{index:04}" ) . as_bytes ( ) , & index. to_le_bytes ( ) )
492+ . await
493+ . unwrap ( ) ;
494+ }
495+ txn. put ( b"overflowing" , & vec ! [ 0x33 ; PAGE * 3 ] )
496+ . await
497+ . unwrap ( ) ;
498+ txn. commit ( ) . await . unwrap ( ) ;
499+ db
500+ }
501+
502+ async fn data_tree ( db : & Db < MemVfs > ) -> BTree < MemVfs > {
503+ let state = db. writer . lock ( ) . await ;
504+ BTree :: open (
505+ db. pager . clone ( ) ,
506+ REALM ,
507+ state. root_page_id ,
508+ state. next_page_id ,
509+ db. page_size ,
510+ )
511+ }
512+
513+ /// The whole point of the shared map: snapshots overlap almost entirely, so
514+ /// a second root covering already-walked pages must cost nothing. Without
515+ /// it, rekey re-authenticates and re-seals every shared page once per
516+ /// retained root.
517+ #[ tokio:: test( flavor = "current_thread" ) ]
518+ async fn a_second_walk_over_shared_pages_rewrites_nothing ( ) {
519+ let db = populated_db ( ) . await ;
520+ let tree = data_tree ( & db) . await ;
521+
522+ let mut shared = BTreeMap :: new ( ) ;
523+ let first = tree. rekey_walk_unique ( & mut shared) . await . unwrap ( ) ;
524+ assert ! (
525+ first > 1 ,
526+ "fixture must span more than a single page, walked {first}"
527+ ) ;
528+ let recorded = shared. len ( ) ;
529+
530+ let second = tree. rekey_walk_unique ( & mut shared) . await . unwrap ( ) ;
531+ assert_eq ! (
532+ second, 0 ,
533+ "every page was already covered by the first walk"
534+ ) ;
535+ assert_eq ! (
536+ shared. len( ) ,
537+ recorded,
538+ "a repeat walk must not discover new pages"
539+ ) ;
540+ }
541+
542+ /// Each walk on its own map does the full work — the dedup above is a
543+ /// property of the shared map, not of the tree being walked twice.
544+ #[ tokio:: test( flavor = "current_thread" ) ]
545+ async fn an_unshared_walk_covers_every_page_again ( ) {
546+ let db = populated_db ( ) . await ;
547+ let tree = data_tree ( & db) . await ;
548+
549+ let first = tree. rekey_walk_unique ( & mut BTreeMap :: new ( ) ) . await . unwrap ( ) ;
550+ let second = tree. rekey_walk ( ) . await . unwrap ( ) ;
551+ assert_eq ! ( first, second) ;
552+ }
553+
554+ /// The map records kinds, not just ids, so that a page reached under two
555+ /// different roles is a reported alias rather than a silent skip. Here a
556+ /// real overflow root — already walked as `OverflowRoot` — is presented as
557+ /// a B+ tree root, which is what a freed-and-reused page looks like from
558+ /// the second reference.
559+ #[ tokio:: test( flavor = "current_thread" ) ]
560+ async fn a_page_reached_under_two_kinds_is_reported_not_skipped ( ) {
561+ let db = populated_db ( ) . await ;
562+ let tree = data_tree ( & db) . await ;
563+
564+ let mut shared = BTreeMap :: new ( ) ;
565+ tree. rekey_walk_unique ( & mut shared) . await . unwrap ( ) ;
566+ let ( & overflow_root, _) = shared
567+ . iter ( )
568+ . find ( |( _, kind) | * * kind == PageKind :: OverflowRoot )
569+ . expect ( "the fixture stores one overflow value" ) ;
570+
571+ let aliased = BTree :: open (
572+ db. pager . clone ( ) ,
573+ REALM ,
574+ overflow_root,
575+ overflow_root + 1 ,
576+ db. page_size ,
577+ ) ;
578+ let error = aliased. rekey_walk_unique ( & mut shared) . await . unwrap_err ( ) ;
579+ assert ! (
580+ matches!(
581+ error,
582+ PagedbError :: Corruption ( CorruptionDetail :: PageKindAliased {
583+ page_id,
584+ walked_as: "overflow_root" ,
585+ referenced_as: "btree_node" ,
586+ } ) if page_id == overflow_root
587+ ) ,
588+ "expected an alias naming the page and both roles, got {error:?}"
589+ ) ;
590+ }
591+
592+ /// Rekey re-seals each page under the kind it walked it as. Taking that
593+ /// kind from the body header while the envelope says otherwise would launder
594+ /// a mis-routed page into a freshly authenticated one, so the walk must go
595+ /// through the accessor that proves the two agree.
596+ #[ tokio:: test( flavor = "current_thread" ) ]
597+ async fn a_page_whose_envelope_contradicts_its_body_is_never_re_sealed ( ) {
598+ let db = populated_db ( ) . await ;
599+ let ( leaf_page_id, forged) = {
600+ let state = db. writer . lock ( ) . await ;
601+ let tree = BTree :: open (
602+ db. pager . clone ( ) ,
603+ REALM ,
604+ state. root_page_id ,
605+ state. next_page_id ,
606+ db. page_size ,
607+ ) ;
608+ let mut reachable = std:: collections:: BTreeSet :: new ( ) ;
609+ tree. collect_all_page_ids ( & mut reachable) . await . unwrap ( ) ;
610+ let mut leaf = None ;
611+ for & page_id in & reachable {
612+ if let Ok ( ( _, PageKind :: BTreeLeaf ) ) = db. pager . read_main_node ( page_id, REALM ) . await
613+ {
614+ leaf = Some ( page_id) ;
615+ break ;
616+ }
617+ }
618+ ( leaf. expect ( "the fixture has leaves" ) , state. next_page_id )
619+ } ;
620+
621+ // Copy a live leaf's bytes under the internal-node envelope kind: the
622+ // body stays structurally valid, so the only defect is the routing.
623+ let guard = db
624+ . pager
625+ . read_main_node ( leaf_page_id, REALM )
626+ . await
627+ . unwrap ( )
628+ . 0 ;
629+ let body = guard. body_ref ( ) . to_vec ( ) ;
630+ drop ( guard) ;
631+ db. pager
632+ . write_main_page ( forged, REALM , PageKind :: BTreeInternal , & body)
633+ . await
634+ . unwrap ( ) ;
635+ db. pager . flush_main ( REALM ) . await . unwrap ( ) ;
636+ db. pager . reset_main_pages ( ) ;
637+
638+ let tree = BTree :: open ( db. pager . clone ( ) , REALM , forged, forged + 1 , db. page_size ) ;
639+ let error = tree. rekey_walk ( ) . await . unwrap_err ( ) ;
640+ assert ! (
641+ matches!(
642+ error,
643+ PagedbError :: Corruption ( CorruptionDetail :: NodeKindMismatch {
644+ page_id: Some ( page_id) ,
645+ expected: "internal" ,
646+ found: "leaf" ,
647+ } ) if page_id == forged
648+ ) ,
649+ "expected the kind disagreement to stop the walk, got {error:?}"
650+ ) ;
651+ }
652+ }
0 commit comments