@@ -529,3 +529,106 @@ fn extract_preserves_directory_permission() {
529529 let meta = fs:: symlink_metadata ( "extract_dir_perm/out/raw/images" ) . unwrap ( ) ;
530530 assert_eq ! ( meta. permissions( ) . mode( ) & 0o777 , 0o750 , ) ;
531531}
532+
533+ /// RAII guard for temporarily setting the process umask.
534+ /// Umask is process-global, so tests using this guard are protected by
535+ /// `#[serial(umask)]` to avoid racing with other tests that change it.
536+ #[ cfg( unix) ]
537+ struct UmaskGuard ( libc:: mode_t ) ;
538+
539+ #[ cfg( unix) ]
540+ impl UmaskGuard {
541+ fn set ( mask : u16 ) -> Self {
542+ // SAFETY: libc::umask is always safe to call with any mode_t value.
543+ // It atomically sets the new umask and returns the previous value.
544+ unsafe { Self ( libc:: umask ( mask as libc:: mode_t ) ) }
545+ }
546+ }
547+
548+ #[ cfg( unix) ]
549+ impl Drop for UmaskGuard {
550+ fn drop ( & mut self ) {
551+ // SAFETY: Restoring the original umask value that was returned by
552+ // the previous umask() call. This is always valid.
553+ unsafe {
554+ libc:: umask ( self . 0 ) ;
555+ }
556+ }
557+ }
558+
559+ /// Precondition: A GCM-encrypted archive contains a file with the setuid bit set (mode 0o4755),
560+ /// stored via `--keep-permission`.
561+ /// Action: Extract the archive without `--keep-permission`.
562+ /// Expectation: The extracted file has no special bits (setuid/setgid/sticky), and its
563+ /// permission bits are masked by the umask instead of the archived mode.
564+ #[ test]
565+ #[ cfg( unix) ]
566+ #[ serial_test:: serial( umask) ]
567+ fn extract_encrypted_gcm_without_keep_permission_drops_setuid ( ) {
568+ setup ( ) ;
569+ TestResources :: extract_in ( "raw/" , "extract_gcm_setuid/in/" ) . unwrap ( ) ;
570+
571+ set_permissions_or_skip ! ( "extract_gcm_setuid/in/raw/text.txt" , 0o4755 ) ;
572+
573+ cli:: Cli :: try_parse_from ( [
574+ "pna" ,
575+ "--quiet" ,
576+ "c" ,
577+ "-f" ,
578+ "extract_gcm_setuid/archive.pna" ,
579+ "--overwrite" ,
580+ "extract_gcm_setuid/in/" ,
581+ "--keep-permission" ,
582+ "--aes" ,
583+ "gcm" ,
584+ "--password" ,
585+ "password" ,
586+ ] )
587+ . unwrap ( )
588+ . execute ( )
589+ . unwrap ( ) ;
590+
591+ let mut found = false ;
592+ archive:: for_each_entry_with_password (
593+ "extract_gcm_setuid/archive.pna" ,
594+ Some ( "password" ) ,
595+ |entry| {
596+ if entry. header ( ) . path ( ) . as_str ( ) . ends_with ( "raw/text.txt" ) {
597+ found = true ;
598+ let mode = entry. metadata ( ) . permission_mode ( ) . unwrap ( ) ;
599+ assert_eq ! ( mode. get( ) & 0o7777 , 0o4755 ) ;
600+ }
601+ } ,
602+ )
603+ . unwrap ( ) ;
604+ assert ! ( found, "raw/text.txt entry not found in archive" ) ;
605+
606+ let _umask = UmaskGuard :: set ( 0o022 ) ;
607+ cli:: Cli :: try_parse_from ( [
608+ "pna" ,
609+ "--quiet" ,
610+ "x" ,
611+ "-f" ,
612+ "extract_gcm_setuid/archive.pna" ,
613+ "--overwrite" ,
614+ "--out-dir" ,
615+ "extract_gcm_setuid/out/" ,
616+ "--password" ,
617+ "password" ,
618+ "--strip-components" ,
619+ "2" ,
620+ ] )
621+ . unwrap ( )
622+ . execute ( )
623+ . unwrap ( ) ;
624+
625+ let extracted_mode = fs:: symlink_metadata ( "extract_gcm_setuid/out/raw/text.txt" )
626+ . unwrap ( )
627+ . permissions ( )
628+ . mode ( ) ;
629+ assert_eq ! (
630+ extracted_mode & 0o7777 ,
631+ 0o666 & !0o022 ,
632+ "without --keep-permission the archived mode (with setuid) must be replaced by the umask-masked default"
633+ ) ;
634+ }
0 commit comments