@@ -604,6 +604,7 @@ impl<V: Vfs> Pager<V> {
604604 /// AAD is constructed from on-disk header bytes, not from `Pager.active_epoch`
605605 /// or the configured cipher. This makes mixed-epoch and mixed-cipher coexistence
606606 /// work correctly without global invariants.
607+ #[ allow( clippy:: too_many_lines) ]
607608 async fn read_page (
608609 & self ,
609610 file : FileKey ,
@@ -619,6 +620,14 @@ impl<V: Vfs> Pager<V> {
619620 if page. realm_id_bytes != Some ( realm_id. 0 ) {
620621 return Err ( PagedbError :: ChecksumFailure ) ;
621622 }
623+ // Enforce the same kind binding a cold read's AAD enforces.
624+ // Without this, a stale pointer reading a recycled page under
625+ // the wrong kind succeeds while the page is warm and only
626+ // starts failing after eviction — hiding structural damage
627+ // until long after the write that caused it.
628+ if page. kind_byte != 0 && page. kind_byte != expected_kind. as_byte ( ) {
629+ return Err ( PagedbError :: ChecksumFailure ) ;
630+ }
622631 self . inner . record_hit ( file) ;
623632 cache. pin ( ( file, page_id) ) ;
624633 return Ok ( PageGuard {
@@ -720,9 +729,17 @@ impl<V: Vfs> Pager<V> {
720729 Err ( e) => return Err ( e) ,
721730 }
722731 }
732+ tracing:: error!(
733+ page_id,
734+ ?file,
735+ expected_kind = ?expected_kind,
736+ realm = ?realm_id. 0 ,
737+ "page AEAD/MAC verification failed on read"
738+ ) ;
723739 Err ( last_err. unwrap_or ( PagedbError :: ChecksumFailure ) )
724740 }
725741
742+ #[ allow( clippy:: too_many_lines) ]
726743 async fn flush_file (
727744 & self ,
728745 file : FileKey ,
@@ -744,8 +761,11 @@ impl<V: Vfs> Pager<V> {
744761 let flush_epoch = self . active_epoch . load ( AtomOrd :: SeqCst ) ;
745762
746763 // Serial gather: snapshot each dirty page's plaintext + kind under the
747- // cache lock. Cheap memcpy; no AEAD work happens here.
764+ // cache lock. Cheap memcpy; no AEAD work happens here. The gathered
765+ // `Arc<Page>` is retained per pid so the dirty-clear below can detect
766+ // pages replaced by a concurrent writer during the (slow) seal+write.
748767 let mut prepared: Vec < ( u64 , PageKind , Vec < u8 > ) > = Vec :: with_capacity ( dirty_ids. len ( ) ) ;
768+ let mut gathered: Vec < ( u64 , Arc < Page > ) > = Vec :: with_capacity ( dirty_ids. len ( ) ) ;
749769 for pid in & dirty_ids {
750770 let page = self
751771 . inner
@@ -755,6 +775,7 @@ impl<V: Vfs> Pager<V> {
755775 . ok_or_else ( || {
756776 PagedbError :: Io ( std:: io:: Error :: other ( "dirty page missing from cache" ) )
757777 } ) ?;
778+ gathered. push ( ( * pid, page. clone ( ) ) ) ;
758779 let kind = if page. kind_byte != 0 {
759780 PageKind :: from_byte ( page. kind_byte ) ?
760781 } else {
@@ -765,6 +786,7 @@ impl<V: Vfs> Pager<V> {
765786 let mut wire = vec ! [ 0u8 ; page_size] ;
766787 let plain = body ( & page. bytes ) ;
767788 wire[ HEADER_LEN ..HEADER_LEN + plain. len ( ) ] . copy_from_slice ( plain) ;
789+ tracing:: trace!( page_id = * pid, ?kind, ?file, "flush: writing page" ) ;
768790 prepared. push ( ( * pid, kind, wire) ) ;
769791 }
770792
@@ -843,10 +865,25 @@ impl<V: Vfs> Pager<V> {
843865 f. sync ( ) . await ?;
844866 }
845867
846- // Clear dirty flags.
868+ // Clear dirty flags — but ONLY for pages still holding the exact
869+ // `Arc<Page>` we gathered. A concurrent writer replaces the Arc on
870+ // every write; unconditionally clearing here would wipe its dirty
871+ // flag while its content never reached disk (lost update → stale
872+ // page on next cold read → AEAD/kind mismatch).
873+ // A replaced page keeps its flag and flushes on the next cycle.
847874 let mut cache = self . inner . cache_for_key ( file) . lock ( ) ;
848- for pid in dirty_ids {
849- cache. clear_dirty ( ( file, pid) ) ;
875+ for ( pid, snapshot) in gathered {
876+ match cache. get ( ( file, pid) ) {
877+ Some ( current) if Arc :: ptr_eq ( & current, & snapshot) => {
878+ cache. clear_dirty ( ( file, pid) ) ;
879+ }
880+ _ => {
881+ tracing:: debug!(
882+ page_id = pid,
883+ "page re-dirtied during flush; keeping dirty for next cycle"
884+ ) ;
885+ }
886+ }
850887 }
851888 Ok ( ( ) )
852889 }
0 commit comments