33use crate :: Result ;
44use crate :: errors:: PagedbError ;
55use crate :: pager:: format:: page_kind:: PageKind ;
6+ use crate :: pager:: page_space:: is_reserved;
67use crate :: vfs:: Vfs ;
78
89use crate :: btree:: leaf:: { Leaf , LeafValue } ;
@@ -106,21 +107,39 @@ impl<V: Vfs> BTree<V> {
106107 /// Collect all page IDs reachable from this tree's root (internal nodes,
107108 /// leaves, overflow chains) into `out`. Used by the deep-walk integrity
108109 /// checker to identify orphan pages.
109- #[ allow( clippy:: too_many_lines) ]
110+ ///
111+ /// Authoritative, not best-effort: `Ok(())` means every reachable node and
112+ /// overflow page authenticated as its own kind, decoded structurally, and
113+ /// pointed only at allocatable pages. Anything else is a corruption error,
114+ /// because a partial set that its caller cannot distinguish from a complete
115+ /// one turns every live page into a false orphan report — and, worse, reads
116+ /// as a clean bill of health.
110117 pub async fn collect_all_page_ids (
111118 & self ,
112119 out : & mut std:: collections:: BTreeSet < u64 > ,
113120 ) -> Result < ( ) > {
114121 if self . root_page_id == 0 {
115122 return Ok ( ( ) ) ;
116123 }
117- let mut stack: Vec < u64 > = vec ! [ self . root_page_id] ;
124+ // Track traversal separately from `out`. `out` is the caller's
125+ // accumulator across every tree in the database and arrives pre-seeded
126+ // with the reserved pages, so doubling it as the visited set would let
127+ // whatever the caller happened to put in it silently truncate this
128+ // walk — the walk's own progress must not depend on that.
129+ let mut visited = std:: collections:: BTreeSet :: new ( ) ;
130+ let mut stack: Vec < ( u64 , u64 ) > = vec ! [ ( 0 , self . root_page_id) ] ;
118131
119- while let Some ( page_id) = stack. pop ( ) {
120- if !out. insert ( page_id) {
121- // Already visited.
132+ while let Some ( ( parent_page_id, page_id) ) = stack. pop ( ) {
133+ if is_reserved ( page_id) {
134+ return Err ( PagedbError :: reserved_page_referenced (
135+ parent_page_id,
136+ page_id,
137+ ) ) ;
138+ }
139+ if !visited. insert ( page_id) {
122140 continue ;
123141 }
142+ out. insert ( page_id) ;
124143
125144 let ( guard, kind) = self . read_node_guard ( page_id) . await ?;
126145 match kind {
@@ -137,19 +156,21 @@ impl<V: Vfs> BTree<V> {
137156 drop ( guard) ;
138157
139158 for overflow_root in overflow_roots {
140- self . collect_overflow_chain ( overflow_root, out) . await ?;
159+ self . collect_overflow_chain ( page_id, overflow_root, & mut visited, out)
160+ . await ?;
141161 }
142162 }
143163 NodeKind :: Internal => {
144164 let internal = internal:: Internal :: decode ( guard. body_ref ( ) ) ?;
145165 drop ( guard) ;
146166
167+ // A zero child id is an absent slot, not a pointer.
147168 if internal. leftmost_child != 0 {
148- stack. push ( internal. leftmost_child ) ;
169+ stack. push ( ( page_id , internal. leftmost_child ) ) ;
149170 }
150171 for entry in & internal. entries {
151172 if entry. right_child != 0 {
152- stack. push ( entry. right_child ) ;
173+ stack. push ( ( page_id , entry. right_child ) ) ;
153174 }
154175 }
155176 }
@@ -158,37 +179,55 @@ impl<V: Vfs> BTree<V> {
158179 Ok ( ( ) )
159180 }
160181
161- /// Walk an overflow chain starting at `root` and insert all page IDs into
162- /// `out`. This is authoritative for reachability collection, so malformed
163- /// pages or missing links propagate as corruption instead of being omitted.
182+ /// Walk the overflow chain rooted at `root` — referenced by leaf
183+ /// `leaf_page_id` — and insert every page ID into `out`.
184+ ///
185+ /// Authoritative on the same terms as [`Self::collect_all_page_ids`]. A
186+ /// repeated page is reported as a cycle rather than treated as the end of
187+ /// the chain: the two are indistinguishable to a caller that just stops
188+ /// walking, and only one of them is a healthy tree.
189+ ///
190+ /// `visited` is the traversal's tree-wide page set. Overflow pages and node
191+ /// pages draw from the same id space, so one set both deduplicates a chain
192+ /// shared by several refcounting leaves and catches a chain that loops back
193+ /// into the node graph.
164194 async fn collect_overflow_chain (
165195 & self ,
196+ leaf_page_id : u64 ,
166197 root : u64 ,
198+ visited : & mut std:: collections:: BTreeSet < u64 > ,
167199 out : & mut std:: collections:: BTreeSet < u64 > ,
168200 ) -> Result < ( ) > {
169- if root == 0 || !out. insert ( root) {
201+ // Unlike an internal child slot or a chain terminator, zero is not a
202+ // valid overflow root: an `Overflow` leaf value always owns at least
203+ // its root page.
204+ if is_reserved ( root) {
205+ return Err ( PagedbError :: reserved_page_referenced ( leaf_page_id, root) ) ;
206+ }
207+ if !visited. insert ( root) {
208+ // Already walked — a value whose chain is shared by refcount.
170209 return Ok ( ( ) ) ;
171210 }
211+ out. insert ( root) ;
172212
173- let mut seen = std:: collections:: BTreeSet :: new ( ) ;
174- seen. insert ( root) ;
175213 let info = overflow:: read_root_page ( & self . pager , self . realm_id , root) . await ?;
176214 let mut chain_id = info. next ;
177215
178216 while chain_id != 0 {
179- if !seen. insert ( chain_id) {
180- return Err ( PagedbError :: corruption (
181- crate :: errors:: CorruptionDetail :: HeaderUnverifiable ,
182- ) ) ;
217+ if is_reserved ( chain_id) {
218+ return Err ( PagedbError :: reserved_page_referenced ( root, chain_id) ) ;
219+ }
220+ if !visited. insert ( chain_id) {
221+ return Err ( PagedbError :: overflow_chain_cycle ( root, chain_id) ) ;
183222 }
223+ out. insert ( chain_id) ;
184224
185225 let guard = self
186226 . pager
187227 . read_main_page ( chain_id, self . realm_id , PageKind :: Overflow )
188228 . await ?;
189229 let body = guard. body ( ) ;
190230 let ( next, _) = overflow:: decode_overflow ( & body) ?;
191- out. insert ( chain_id) ;
192231 chain_id = next;
193232 }
194233
@@ -208,7 +247,7 @@ impl<V: Vfs> BTree<V> {
208247 let mut stack: Vec < ( u64 , u64 ) > = vec ! [ ( 0 , self . root_page_id) ] ;
209248 let mut seen = std:: collections:: BTreeSet :: new ( ) ;
210249 while let Some ( ( parent, page_id) ) = stack. pop ( ) {
211- if page_id < 4 {
250+ if is_reserved ( page_id) {
212251 return Some ( format ! (
213252 "internal {parent} -> RESERVED child page {page_id}"
214253 ) ) ;
@@ -234,56 +273,12 @@ impl<V: Vfs> BTree<V> {
234273 return Some ( format ! ( "leaf {page_id} (parent {parent}) decode failed" ) ) ;
235274 } ;
236275 for ( k, v) in & leaf. records {
237- if let LeafValue :: Overflow { root_page_id, .. } = v {
238- // Walk the FULL overflow chain: every page and every
239- // next-pointer must be a real page (>= 4). A reserved or
240- // unreadable link means a chain page was freed/reused
241- // while still linked (use-after-free).
242- let mut chain = * root_page_id;
243- let mut first = true ;
244- let mut chain_seen = std:: collections:: BTreeSet :: new ( ) ;
245- while chain != 0 {
246- if chain < 4 {
247- return Some ( format ! (
248- "leaf {page_id} (parent {parent}) key {:02x?} overflow chain \
249- -> RESERVED page {chain} (use-after-free)",
250- & k[ ..k. len( ) . min( 8 ) ]
251- ) ) ;
252- }
253- if !chain_seen. insert ( chain) {
254- return Some ( format ! (
255- "leaf {page_id} (parent {parent}) overflow chain CYCLE at {chain}"
256- ) ) ;
257- }
258- // root (`OverflowRoot`): next after refcount[4];
259- // chain page (`Overflow`): next at byte 0.
260- let ( kind, next_off, what) = if first {
261- ( PageKind :: OverflowRoot , 4usize , "root" )
262- } else {
263- ( PageKind :: Overflow , 0usize , "chain page" )
264- } ;
265- first = false ;
266- let cg = match self
267- . pager
268- . read_main_page ( chain, self . realm_id , kind)
269- . await
270- {
271- Ok ( cg) => cg,
272- Err ( e) => {
273- return Some ( format ! (
274- "leaf {page_id} (parent {parent}) overflow {what} {chain} \
275- UNREADABLE ({e:?}) — freed/reused"
276- ) ) ;
277- }
278- } ;
279- let cbody = cg. body ( ) ;
280- if cbody. len ( ) < next_off + 8 {
281- break ;
282- }
283- let mut b = [ 0u8 ; 8 ] ;
284- b. copy_from_slice ( & cbody[ next_off..next_off + 8 ] ) ;
285- chain = u64:: from_le_bytes ( b) ;
286- }
276+ if let LeafValue :: Overflow { root_page_id, .. } = v
277+ && let Some ( desc) = self
278+ . find_dangling_in_overflow ( page_id, parent, k, * root_page_id)
279+ . await
280+ {
281+ return Some ( desc) ;
287282 }
288283 }
289284 } else {
@@ -305,4 +300,69 @@ impl<V: Vfs> BTree<V> {
305300 }
306301 None
307302 }
303+
304+ /// Walk the full overflow chain rooted at `root` — held by key `key` in
305+ /// leaf `page_id` — and describe the first anomaly, if any.
306+ ///
307+ /// Every page and every next-pointer must be allocatable. A reserved or
308+ /// unreadable link means a chain page was freed and reused while still
309+ /// linked. Zero terminates a chain but can never *be* one: an `Overflow`
310+ /// value owns at least its root page.
311+ async fn find_dangling_in_overflow (
312+ & self ,
313+ page_id : u64 ,
314+ parent : u64 ,
315+ key : & [ u8 ] ,
316+ root : u64 ,
317+ ) -> Option < String > {
318+ let key_prefix = & key[ ..key. len ( ) . min ( 8 ) ] ;
319+ if root == 0 {
320+ return Some ( format ! (
321+ "leaf {page_id} (parent {parent}) key {key_prefix:02x?} overflow value has no \
322+ root page"
323+ ) ) ;
324+ }
325+
326+ let mut chain = root;
327+ let mut first = true ;
328+ let mut chain_seen = std:: collections:: BTreeSet :: new ( ) ;
329+ while chain != 0 {
330+ if is_reserved ( chain) {
331+ return Some ( format ! (
332+ "leaf {page_id} (parent {parent}) key {key_prefix:02x?} overflow chain -> \
333+ RESERVED page {chain} (use-after-free)"
334+ ) ) ;
335+ }
336+ if !chain_seen. insert ( chain) {
337+ return Some ( format ! (
338+ "leaf {page_id} (parent {parent}) overflow chain CYCLE at {chain}"
339+ ) ) ;
340+ }
341+ // root (`OverflowRoot`): next after refcount[4];
342+ // chain page (`Overflow`): next at byte 0.
343+ let ( kind, next_off, what) = if first {
344+ ( PageKind :: OverflowRoot , 4usize , "root" )
345+ } else {
346+ ( PageKind :: Overflow , 0usize , "chain page" )
347+ } ;
348+ first = false ;
349+ let guard = match self . pager . read_main_page ( chain, self . realm_id , kind) . await {
350+ Ok ( guard) => guard,
351+ Err ( e) => {
352+ return Some ( format ! (
353+ "leaf {page_id} (parent {parent}) overflow {what} {chain} UNREADABLE \
354+ ({e:?}) — freed/reused"
355+ ) ) ;
356+ }
357+ } ;
358+ let body = guard. body ( ) ;
359+ if body. len ( ) < next_off + 8 {
360+ break ;
361+ }
362+ let mut next = [ 0u8 ; 8 ] ;
363+ next. copy_from_slice ( & body[ next_off..next_off + 8 ] ) ;
364+ chain = u64:: from_le_bytes ( next) ;
365+ }
366+ None
367+ }
308368}
0 commit comments