44
55use std:: collections:: BTreeMap ;
66use std:: sync:: Arc ;
7- use std:: sync:: atomic:: { AtomicU64 , Ordering as AtomOrd } ;
7+ use std:: sync:: atomic:: { AtomicBool , AtomicU64 , Ordering as AtomOrd } ;
88
99use bytes:: Bytes ;
1010use tokio:: sync:: Mutex as AsyncMutex ;
11- use tracing;
1211
1312use crate :: crypto:: aad:: { AadFields , MAIN_DB_SEGMENT_ID } ;
1413use crate :: crypto:: key_manager:: DekLru ;
@@ -41,7 +40,7 @@ pub struct PagerConfig {
4140 pub anchor_budget : u64 ,
4241 pub dek_lru_capacity : usize ,
4342 /// Number of AEAD-verification retries on a cache miss before surfacing
44- /// a `ChecksumFailure`. Set to > 0 only in Observer mode to absorb torn
43+ /// a `ChecksumFailure`. Set to > 0 only in ` Observer` mode to absorb torn
4544 /// reads; all other modes keep this at 0 so that AEAD failures remain
4645 /// hard corruption signals.
4746 pub observer_retry_count : u32 ,
@@ -162,6 +161,7 @@ pub struct Pager<V: Vfs> {
162161 /// in the cache. Reads use per-page epoch routing; writes always use this
163162 /// atomic to pick the DEK.
164163 active_epoch : AtomicU64 ,
164+ read_only : AtomicBool ,
165165 files : AsyncMutex < BTreeMap < FileKey , Arc < AsyncMutex < V :: File > > > > ,
166166 dek_lru : parking_lot:: Mutex < DekLru > ,
167167 main_nonce : parking_lot:: Mutex < MainDbNonceGen > ,
@@ -172,7 +172,7 @@ pub struct Pager<V: Vfs> {
172172 journal_nonces : parking_lot:: Mutex < BTreeMap < [ u8 ; 16 ] , SegmentNonceGen > > ,
173173 pub ( crate ) inner : Arc < PagerInner > ,
174174 /// Retries on AEAD failure before surfacing `ChecksumFailure`. Non-zero
175- /// only in Observer mode to absorb torn reads from a concurrent writer.
175+ /// only in ` Observer` mode to absorb torn reads from a concurrent writer.
176176 observer_retry_count : u32 ,
177177}
178178
@@ -236,13 +236,28 @@ impl<V: Vfs> Pager<V> {
236236 files : AsyncMutex :: new ( BTreeMap :: new ( ) ) ,
237237 inner,
238238 active_epoch : AtomicU64 :: new ( initial_epoch) ,
239+ read_only : AtomicBool :: new ( false ) ,
239240 mk : parking_lot:: RwLock :: new ( mk) ,
240241 observer_retry_count,
241242 vfs,
242243 cfg,
243244 } )
244245 }
245246
247+ /// Restrict all lazily-opened persistent files to read access and reject
248+ /// pager flushes. This only changes in-memory state.
249+ pub ( crate ) fn set_read_only ( & self ) {
250+ self . read_only . store ( true , AtomOrd :: SeqCst ) ;
251+ }
252+
253+ /// Enable persistent writes after a frozen read-only handle transitions
254+ /// to Follower mode. Cached read-only file handles are discarded so later
255+ /// pager operations reopen them with read/write access.
256+ pub ( crate ) async fn enable_write_access ( & self ) {
257+ self . read_only . store ( false , AtomOrd :: SeqCst ) ;
258+ self . files . lock ( ) . await . clear ( ) ;
259+ }
260+
246261 /// Atomically advance the active epoch used for flush (write) operations.
247262 /// Also installs a new master key. Called by `Db::rekey_db` immediately
248263 /// before the final page flush so all dirty pages are re-sealed under the
@@ -602,14 +617,22 @@ impl<V: Vfs> Pager<V> {
602617 // mk_epoch before constructing AAD and selecting the DEK.
603618 self . inner . record_miss ( file) ;
604619 let page_size = self . cfg . page_size ;
620+ let page_size_u64 =
621+ u64:: try_from ( page_size) . map_err ( |_| PagedbError :: arithmetic_overflow ( "page size" ) ) ?;
622+ let page_offset = page_id
623+ . checked_mul ( page_size_u64)
624+ . ok_or_else ( || PagedbError :: arithmetic_overflow ( "page read offset" ) ) ?;
605625 let file_handle = self . open_file_handle ( file) . await ?;
606626
607627 // Observer-mode retry loop: on AEAD failure retry up to
608628 // `observer_retry_count` times (10 ms backoff) to absorb torn reads
609629 // from a concurrent writer. In non-observer mode (retry_count == 0)
610630 // the loop body executes exactly once and any AEAD failure is a hard
611631 // corruption signal.
612- let max_attempts = self . observer_retry_count + 1 ;
632+ let max_attempts = self
633+ . observer_retry_count
634+ . checked_add ( 1 )
635+ . ok_or_else ( || PagedbError :: arithmetic_overflow ( "observer retry attempts" ) ) ?;
613636 let mut last_err: Option < PagedbError > = None ;
614637 for attempt in 0 ..max_attempts {
615638 if attempt > 0 {
@@ -618,7 +641,7 @@ impl<V: Vfs> Pager<V> {
618641 let mut buf = vec ! [ 0u8 ; page_size] ;
619642 {
620643 let f = file_handle. lock ( ) . await ;
621- let n = f. read_at ( page_id * page_size as u64 , & mut buf) . await ?;
644+ let n = f. read_at ( page_offset , & mut buf) . await ?;
622645 if n < page_size {
623646 for b in & mut buf[ n..] {
624647 * b = 0 ;
@@ -690,12 +713,17 @@ impl<V: Vfs> Pager<V> {
690713 segment_id : [ u8 ; 16 ] ,
691714 dest_path : Option < & str > ,
692715 ) -> Result < ( ) > {
716+ if self . read_only . load ( AtomOrd :: SeqCst ) {
717+ return Err ( PagedbError :: ReadOnly ) ;
718+ }
693719 let dirty_ids = self . inner . cache_for_key ( file) . lock ( ) . dirty_for_file ( file) ;
694720 if dirty_ids. is_empty ( ) {
695721 return Ok ( ( ) ) ;
696722 }
697723
698724 let page_size = self . cfg . page_size ;
725+ let page_size_u64 =
726+ u64:: try_from ( page_size) . map_err ( |_| PagedbError :: arithmetic_overflow ( "page size" ) ) ?;
699727 let flush_epoch = self . active_epoch . load ( AtomOrd :: SeqCst ) ;
700728
701729 // Serial gather: snapshot each dirty page's plaintext + kind under the
@@ -723,6 +751,15 @@ impl<V: Vfs> Pager<V> {
723751 prepared. push ( ( * pid, kind, wire) ) ;
724752 }
725753
754+ // Validate every physical offset before consuming any nonce.
755+ let offsets: Vec < u64 > = prepared
756+ . iter ( )
757+ . map ( |( pid, _, _) | {
758+ pid. checked_mul ( page_size_u64)
759+ . ok_or_else ( || PagedbError :: arithmetic_overflow ( "page write offset" ) )
760+ } )
761+ . collect :: < Result < _ > > ( ) ?;
762+
726763 // Pre-allocate a nonce per page (counter increments — single-threaded
727764 // by design; cheap).
728765 let mut nonces: Vec < Nonce > = Vec :: with_capacity ( prepared. len ( ) ) ;
@@ -773,11 +810,8 @@ impl<V: Vfs> Pager<V> {
773810
774811 // Issue physical-id-order vectored writes.
775812 let mut reqs: Vec < WriteReq < ' _ > > = Vec :: with_capacity ( prepared. len ( ) ) ;
776- for ( pid, _kind, wire) in & prepared {
777- reqs. push ( WriteReq {
778- offset : * pid * page_size as u64 ,
779- buf : wire,
780- } ) ;
813+ for ( ( _, _kind, wire) , offset) in prepared. iter ( ) . zip ( offsets) {
814+ reqs. push ( WriteReq { offset, buf : wire } ) ;
781815 }
782816 if let Some ( path) = dest_path {
783817 // Alternate destination (compaction's compacted copy): open it
@@ -801,21 +835,33 @@ impl<V: Vfs> Pager<V> {
801835 }
802836
803837 async fn open_file_handle ( & self , file : FileKey ) -> Result < Arc < AsyncMutex < V :: File > > > {
804- let mut files = self . files . lock ( ) . await ;
805- if let Some ( h) = files. get ( & file) {
806- return Ok ( h. clone ( ) ) ;
838+ let cached = {
839+ let files = self . files . lock ( ) . await ;
840+ files. get ( & file) . cloned ( )
841+ } ;
842+ if let Some ( handle) = cached {
843+ return Ok ( handle) ;
807844 }
845+
808846 let path = match file {
809847 FileKey :: Main => self . cfg . main_db_path . clone ( ) ,
810848 FileKey :: Segment ( id) => format ! ( "seg/{}" , crate :: hex:: to_hex_lower( & id) ) ,
811849 FileKey :: ApplyJournal ( id) => {
812850 format ! ( "applyjournal/{}" , crate :: hex:: to_hex_lower( & id) )
813851 }
814852 } ;
815- let f = self . vfs . open ( & path, OpenMode :: CreateOrOpen ) . await ?;
816- let arc = Arc :: new ( AsyncMutex :: new ( f) ) ;
817- files. insert ( file, arc. clone ( ) ) ;
818- Ok ( arc)
853+ let mode = if self . read_only . load ( AtomOrd :: SeqCst ) {
854+ OpenMode :: Read
855+ } else {
856+ OpenMode :: CreateOrOpen
857+ } ;
858+ let opened = Arc :: new ( AsyncMutex :: new ( self . vfs . open ( & path, mode) . await ?) ) ;
859+ let mut files = self . files . lock ( ) . await ;
860+ if let Some ( handle) = files. get ( & file) {
861+ return Ok ( handle. clone ( ) ) ;
862+ }
863+ files. insert ( file, opened. clone ( ) ) ;
864+ Ok ( opened)
819865 }
820866
821867 fn next_nonce_for_flush ( & self , file : FileKey ) -> Result < Nonce > {
0 commit comments