@@ -201,6 +201,27 @@ impl Catalog {
201201 Ok ( k)
202202 }
203203
204+ /// Validate and return the name suffix of a segment-row key. This is used
205+ /// before recovery derives a diagnostic name from authenticated catalog
206+ /// bytes, so malformed rows cannot cause a slice panic or a repair action.
207+ pub fn validate_segment_key < ' a > ( key : & ' a [ u8 ] , meta : & SegmentMeta ) -> Result < & ' a [ u8 ] > {
208+ const SEGMENT_KEY_PREFIX_LEN : usize = 1 + 16 ;
209+ if key. first ( ) . copied ( ) != Some ( CatalogRowKind :: Segment as u8 ) {
210+ return Err ( PagedbError :: catalog_row_invalid ( "segment.key.kind" ) ) ;
211+ }
212+ if key. len ( ) < SEGMENT_KEY_PREFIX_LEN {
213+ return Err ( PagedbError :: catalog_row_invalid ( "segment.key.length" ) ) ;
214+ }
215+ let name = & key[ SEGMENT_KEY_PREFIX_LEN ..] ;
216+ if name. len ( ) > MAX_SEGMENT_NAME_LEN {
217+ return Err ( PagedbError :: catalog_row_invalid ( "segment.key.name_length" ) ) ;
218+ }
219+ if key[ 1 ..SEGMENT_KEY_PREFIX_LEN ] != meta. realm_id . 0 [ ..] {
220+ return Err ( PagedbError :: catalog_row_invalid ( "segment.key.realm_id" ) ) ;
221+ }
222+ Ok ( name)
223+ }
224+
204225 /// Rekey-state row key: `[0x03]` (singleton, no suffix).
205226 #[ must_use]
206227 pub fn rekey_state_key ( ) -> Vec < u8 > {
@@ -489,12 +510,25 @@ impl Catalog {
489510 b. copy_from_slice ( & bytes[ 33 ..49 ] ) ;
490511 b
491512 } ;
492- let linked_commit = if bytes[ 49 ] == 1 {
493- let mut b = [ 0u8 ; 8 ] ;
494- b. copy_from_slice ( & bytes[ 50 ..58 ] ) ;
495- Some ( CommitId ( u64:: from_le_bytes ( b) ) )
496- } else {
497- None
513+ let linked_commit = match bytes[ 49 ] {
514+ 0 => {
515+ if bytes[ 50 ..58 ] . iter ( ) . any ( |byte| * byte != 0 ) {
516+ return Err ( PagedbError :: catalog_row_invalid (
517+ "segment_meta.linked_commit" ,
518+ ) ) ;
519+ }
520+ None
521+ }
522+ 1 => {
523+ let mut b = [ 0u8 ; 8 ] ;
524+ b. copy_from_slice ( & bytes[ 50 ..58 ] ) ;
525+ Some ( CommitId ( u64:: from_le_bytes ( b) ) )
526+ }
527+ _ => {
528+ return Err ( PagedbError :: catalog_row_invalid (
529+ "segment_meta.linked_commit" ,
530+ ) ) ;
531+ }
498532 } ;
499533 let mut buf = [ 0u8 ; 8 ] ;
500534 buf. copy_from_slice ( & bytes[ 58 ..66 ] ) ;
@@ -718,6 +752,74 @@ mod tests {
718752 assert ! ( matches!( err, PagedbError :: Corruption { .. } ) ) ;
719753 }
720754
755+ #[ test]
756+ fn segment_meta_rejects_invalid_linked_commit_discriminator_and_unused_bytes ( ) {
757+ let meta = SegmentMeta {
758+ segment_id : [ 9 ; 16 ] ,
759+ segment_kind : SegmentKind :: Unspecified ,
760+ realm_id : RealmId ( [ 0 ; 16 ] ) ,
761+ parent_file_id : [ 0 ; 16 ] ,
762+ linked_commit : None ,
763+ page_count : 2 ,
764+ total_bytes : 8192 ,
765+ final_counter : 0 ,
766+ mk_epoch : 0 ,
767+ cipher_id : 1 ,
768+ format_version : 1 ,
769+ evictable : Evictable :: Authoritative ,
770+ } ;
771+ let mut encoded = Catalog :: encode_segment_meta ( & meta) ;
772+ encoded[ 49 ] = 2 ;
773+ assert ! ( matches!(
774+ Catalog :: decode_segment_meta( & encoded) ,
775+ Err ( PagedbError :: Corruption (
776+ crate :: errors:: CorruptionDetail :: CatalogRowInvalid {
777+ field: "segment_meta.linked_commit"
778+ }
779+ ) )
780+ ) ) ;
781+
782+ let mut encoded = Catalog :: encode_segment_meta ( & meta) ;
783+ encoded[ 50 ] = 1 ;
784+ assert ! ( matches!(
785+ Catalog :: decode_segment_meta( & encoded) ,
786+ Err ( PagedbError :: Corruption (
787+ crate :: errors:: CorruptionDetail :: CatalogRowInvalid {
788+ field: "segment_meta.linked_commit"
789+ }
790+ ) )
791+ ) ) ;
792+ }
793+
794+ #[ test]
795+ fn segment_key_validation_rejects_malformed_routing_bytes ( ) {
796+ let meta = SegmentMeta {
797+ segment_id : [ 1 ; 16 ] ,
798+ segment_kind : SegmentKind :: Unspecified ,
799+ realm_id : RealmId ( [ 2 ; 16 ] ) ,
800+ parent_file_id : [ 3 ; 16 ] ,
801+ linked_commit : None ,
802+ page_count : 2 ,
803+ total_bytes : 8192 ,
804+ final_counter : 0 ,
805+ mk_epoch : 0 ,
806+ cipher_id : 1 ,
807+ format_version : 1 ,
808+ evictable : Evictable :: Authoritative ,
809+ } ;
810+ assert ! ( Catalog :: validate_segment_key( & [ ] , & meta) . is_err( ) ) ;
811+ assert ! ( Catalog :: validate_segment_key( & [ CatalogRowKind :: Quota as u8 ; 17 ] , & meta) . is_err( ) ) ;
812+ assert ! (
813+ Catalog :: validate_segment_key( & [ CatalogRowKind :: Segment as u8 ; 16 ] , & meta) . is_err( )
814+ ) ;
815+ let wrong_realm = Catalog :: segment_key ( RealmId ( [ 4 ; 16 ] ) , b"name" ) . unwrap ( ) ;
816+ assert ! ( Catalog :: validate_segment_key( & wrong_realm, & meta) . is_err( ) ) ;
817+ let mut long_name = vec ! [ CatalogRowKind :: Segment as u8 ] ;
818+ long_name. extend_from_slice ( & meta. realm_id . 0 ) ;
819+ long_name. extend_from_slice ( & vec ! [ b'n' ; MAX_SEGMENT_NAME_LEN + 1 ] ) ;
820+ assert ! ( Catalog :: validate_segment_key( & long_name, & meta) . is_err( ) ) ;
821+ }
822+
721823 #[ test]
722824 fn segment_meta_unlinked_round_trip ( ) {
723825 let m = SegmentMeta {
0 commit comments