11//! Main-database rekey transition and durable intent publication.
22
3- use std:: collections:: BTreeMap ;
3+ use std:: collections:: { BTreeMap , BTreeSet } ;
44use std:: sync:: atomic:: Ordering ;
55
66use subtle:: ConstantTimeEq ;
@@ -11,9 +11,10 @@ use crate::catalog::codec::{Catalog, RekeyIntent, RekeyStage, RekeyStateRow};
1111use crate :: crypto:: kdf:: { derive_hk, derive_mk} ;
1212use crate :: crypto:: { CipherId , DerivedKey , MasterKey , SecretKey } ;
1313use crate :: errors:: PagedbError ;
14+ use crate :: pager:: PageKind ;
1415use crate :: pager:: header:: commit_header;
1516use crate :: pager:: structural_header:: MainDbHeaderFields ;
16- use crate :: vfs:: Vfs ;
17+ use crate :: vfs:: { OpenMode , Vfs , VfsFile } ;
1718
1819#[ cfg( test) ]
1920use super :: super :: core:: RekeyTestFault ;
@@ -27,6 +28,30 @@ use super::intent::{intent_proof, migrate_legacy, validate_intent_for_current_ci
2728/// retention runs.
2829const HISTORY_ROOT_BATCH : usize = 512 ;
2930
31+ async fn read_main_page_kind < F : VfsFile > ( file : & F , offset : u64 ) -> Result < Option < PageKind > > {
32+ let mut envelope = [ 0u8 ; 2 ] ;
33+ let mut filled = 0 ;
34+ while filled < envelope. len ( ) {
35+ let read = file
36+ . read_at ( offset + filled as u64 , & mut envelope[ filled..] )
37+ . await ?;
38+ if read == 0 {
39+ return Err ( PagedbError :: Io ( std:: io:: Error :: from (
40+ std:: io:: ErrorKind :: UnexpectedEof ,
41+ ) ) ) ;
42+ }
43+ filled += read;
44+ }
45+ if envelope == [ 0 , 0 ] {
46+ return Ok ( None ) ;
47+ }
48+ let kind = PageKind :: from_byte ( envelope[ 1 ] ) ?;
49+ if !kind. is_main_db ( ) {
50+ return Err ( PagedbError :: IllegalPageKind ) ;
51+ }
52+ Ok ( Some ( kind) )
53+ }
54+
3055impl < V : Vfs + Clone > Db < V > {
3156 /// Rekey the reachable main database and every catalog-linked immutable
3257 /// segment under `new_mk_epoch`.
@@ -219,23 +244,8 @@ impl<V: Vfs + Clone> Db<V> {
219244 self . rewrite_retained_history_roots ( state, & mut rewritten)
220245 . await ?;
221246 }
222- if state. free_list_root_page_id != 0 {
223- let ( _, chain_pages) = crate :: pager:: freelist:: read_chain (
224- & self . pager ,
225- self . realm_id ,
226- state. free_list_root_page_id ,
227- )
247+ self . rewrite_free_list_pages ( state. free_list_root_page_id )
228248 . await ?;
229- for page_id in chain_pages {
230- self . pager
231- . rewrite_page_under_current_epoch (
232- page_id,
233- self . realm_id ,
234- crate :: pager:: PageKind :: Free ,
235- )
236- . await ?;
237- }
238- }
239249 // Keep the catalog path carrying the intent source-readable until
240250 // target-header publication. This is the durable admission anchor
241251 // for an ordinary open that can verify only the stale A/B side.
@@ -248,6 +258,70 @@ impl<V: Vfs + Clone> Db<V> {
248258 Ok ( ( ) )
249259 }
250260
261+ /// Re-encrypt both the durable free-list chain and every reusable page it
262+ /// names. Rewriting only the chain would leave those pages sealed under
263+ /// the retired epoch, so a later allocation could not authenticate them.
264+ async fn rewrite_free_list_pages ( & self , root_page_id : u64 ) -> Result < ( ) > {
265+ if root_page_id == 0 {
266+ return Ok ( ( ) ) ;
267+ }
268+
269+ let ( entries, chain_pages) =
270+ crate :: pager:: freelist:: read_chain ( & self . pager , self . realm_id , root_page_id) . await ?;
271+ let mut reusable_pages = entries
272+ . into_iter ( )
273+ . map ( |( _, page_id) | page_id)
274+ . collect :: < BTreeSet < _ > > ( ) ;
275+
276+ for page_id in chain_pages {
277+ self . pager
278+ . rewrite_page_under_current_epoch (
279+ page_id,
280+ self . realm_id ,
281+ crate :: pager:: PageKind :: Free ,
282+ )
283+ . await ?;
284+ reusable_pages. remove ( & page_id) ;
285+ }
286+ if reusable_pages. is_empty ( ) {
287+ return Ok ( ( ) ) ;
288+ }
289+
290+ let file = self . vfs . open ( & self . main_db_path , OpenMode :: Read ) . await ?;
291+ for page_id in reusable_pages {
292+ let offset = page_id
293+ . checked_mul ( self . page_size as u64 )
294+ . ok_or_else ( || PagedbError :: arithmetic_overflow ( "free-list page offset" ) ) ?;
295+ let Some ( kind) = read_main_page_kind ( & file, offset) . await ? else {
296+ continue ;
297+ } ;
298+ self . pager
299+ . rewrite_page_under_current_epoch ( page_id, self . realm_id , kind)
300+ . await ?;
301+ }
302+ Ok ( ( ) )
303+ }
304+
305+ /// Re-encrypt pages superseded by copy-on-write catalog transitions after
306+ /// the target-authenticated header is durable. No live root discovers
307+ /// these residual pages, but physical integrity scans still authenticate
308+ /// every non-zero page and the source epoch is retired on success.
309+ async fn rewrite_rekey_residual_main_pages ( & self , state : & WriterState ) -> Result < ( ) > {
310+ let file = self . vfs . open ( & self . main_db_path , OpenMode :: Read ) . await ?;
311+ for page_id in 4 ..state. next_page_id {
312+ let offset = page_id
313+ . checked_mul ( self . page_size as u64 )
314+ . ok_or_else ( || PagedbError :: arithmetic_overflow ( "main-db page offset" ) ) ?;
315+ let Some ( kind) = read_main_page_kind ( & file, offset) . await ? else {
316+ continue ;
317+ } ;
318+ self . pager
319+ . rewrite_page_under_current_epoch ( page_id, self . realm_id , kind)
320+ . await ?;
321+ }
322+ Ok ( ( ) )
323+ }
324+
251325 /// Rewrite the commit-history index and every reader-visible root its
252326 /// retained rows still name.
253327 ///
@@ -353,6 +427,8 @@ impl<V: Vfs + Clone> Db<V> {
353427 target_header_key : & DerivedKey ,
354428 ) -> Result < ( ) > {
355429 if matches ! ( intent. stage, RekeyStage :: HeaderTargetPublished ) {
430+ self . rewrite_rekey_residual_main_pages ( state) . await ?;
431+ self . pager . flush_main ( self . realm_id ) . await ?;
356432 intent. stage = RekeyStage :: MainDone ;
357433 self . write_rekey_intent_locked (
358434 state,
0 commit comments