Skip to content

Commit 7057a8b

Browse files
committed
✅ Add regression test for default-off metadata on encrypted entries
Extracting a GCM-encrypted entry without --keep-permission must not restore the archived mode, including the setuid bit.
1 parent ef6ccb7 commit 7057a8b

1 file changed

Lines changed: 103 additions & 0 deletions

File tree

cli/tests/cli/extract/option_keep_permission.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)