@@ -38,6 +38,7 @@ use crate::config::Validated;
3838use crate :: copy:: CopyBuffer ;
3939use crate :: copy:: copy_with_buffer;
4040use crate :: error:: QuotaResource ;
41+ use crate :: security:: permissions:: SanitizedMode ;
4142use crate :: security:: quota:: QuotaPermit ;
4243use crate :: types:: DestDir ;
4344use crate :: types:: SafePath ;
@@ -542,14 +543,16 @@ pub fn check_extension_allowed(
542543/// - Strip sticky bit (0o1000) if required by security policy
543544/// - Ensure world-writable permissions are only set if allowed
544545///
545- /// Mode sanitization MUST be performed by the caller (typically in the
546- /// validation layer via `SecurityConfig::sanitize_mode()`). This function
547- /// does NOT perform any sanitization and will apply the mode value directly.
546+ /// The [`SanitizedMode`] parameter type enforces mode sanitization at
547+ /// compile time: only
548+ /// [`sanitize_permissions`](crate::security::sanitize_permissions)
549+ /// can construct one, so a raw, unsanitized mode read from an archive header
550+ /// cannot reach this function by mistake.
548551///
549552/// # Arguments
550553///
551554/// * `path` - Path where file should be created
552- /// * `mode` - Optional Unix file mode (must be pre-sanitized by caller)
555+ /// * `mode` - Optional pre-sanitized Unix file mode
553556/// * `create_new` - If `true`, fail with `AlreadyExists` instead of truncating
554557/// an existing file at `path`
555558///
@@ -563,7 +566,7 @@ pub fn check_extension_allowed(
563566#[ cfg( unix) ]
564567pub fn create_file_with_mode (
565568 path : & Path ,
566- mode : Option < u32 > ,
569+ mode : Option < SanitizedMode > ,
567570 create_new : bool ,
568571) -> std:: io:: Result < File > {
569572 use std:: fs:: OpenOptions ;
@@ -585,7 +588,7 @@ pub fn create_file_with_mode(
585588
586589 if let Some ( m) = mode {
587590 // Apply sanitized mode during open (already stripped setuid/setgid)
588- opts. mode ( m) ;
591+ opts. mode ( m. as_u32 ( ) ) ;
589592 }
590593
591594 let file = opts. open ( path) ?;
@@ -598,7 +601,7 @@ pub fn create_file_with_mode(
598601 // TOCTOU window between this open() and the permission change (issue
599602 // #460).
600603 if let Some ( m) = mode {
601- file. set_permissions ( Permissions :: from_mode ( m) ) ?;
604+ file. set_permissions ( Permissions :: from_mode ( m. as_u32 ( ) ) ) ?;
602605 }
603606
604607 Ok ( file)
@@ -625,7 +628,7 @@ pub fn create_file_with_mode(
625628#[ cfg( not( unix) ) ]
626629pub fn create_file_with_mode (
627630 path : & Path ,
628- _mode : Option < u32 > ,
631+ _mode : Option < SanitizedMode > ,
629632 create_new : bool ,
630633) -> std:: io:: Result < File > {
631634 if create_new {
@@ -720,7 +723,7 @@ pub fn create_file_with_mode(
720723pub fn extract_file_with_permit < R : Read > (
721724 reader : & mut R ,
722725 safe_path : & SafePath ,
723- mode : Option < u32 > ,
726+ mode : Option < SanitizedMode > ,
724727 _permit : QuotaPermit ,
725728 dest : & DestDir ,
726729 report : & mut ExtractionReport ,
@@ -1079,12 +1082,22 @@ mod tests {
10791082 use crate :: NoopProgress ;
10801083 use crate :: SecurityConfig ;
10811084 use crate :: copy:: CopyBuffer ;
1085+ use crate :: security:: permissions:: sanitize_permissions;
10821086 use crate :: security:: quota:: QuotaTracker ;
10831087 use std:: assert_matches;
10841088 use std:: io:: Cursor ;
10851089 use std:: path:: PathBuf ;
10861090 use tempfile:: TempDir ;
10871091
1092+ /// Builds a [`SanitizedMode`] for tests that don't otherwise need a
1093+ /// `SecurityConfig` in scope. None of the modes used across these tests
1094+ /// carry setuid/setgid/world-writable bits, so sanitizing with the
1095+ /// default config never changes the value.
1096+ fn sanitized ( mode : u32 ) -> SanitizedMode {
1097+ let config = SecurityConfig :: default ( ) . validate ( ) . expect ( "valid config" ) ;
1098+ sanitize_permissions ( mode, & config)
1099+ }
1100+
10881101 #[ test]
10891102 fn test_extract_file_with_permit_integer_overflow_check ( ) {
10901103 let temp = TempDir :: new ( ) . expect ( "failed to create temp dir" ) ;
@@ -1111,7 +1124,7 @@ mod tests {
11111124 let result = extract_file_with_permit (
11121125 & mut reader,
11131126 & safe_path,
1114- Some ( 0o644 ) ,
1127+ Some ( sanitized ( 0o644 ) ) ,
11151128 permit,
11161129 & dest,
11171130 & mut report,
@@ -1175,7 +1188,7 @@ mod tests {
11751188 let result = extract_file_with_permit (
11761189 & mut reader,
11771190 & safe_path,
1178- Some ( 0o644 ) ,
1191+ Some ( sanitized ( 0o644 ) ) ,
11791192 permit,
11801193 & dest,
11811194 & mut report,
@@ -1224,7 +1237,7 @@ mod tests {
12241237 let result = extract_file_with_permit (
12251238 & mut reader,
12261239 & safe_path,
1227- Some ( 0o644 ) ,
1240+ Some ( sanitized ( 0o644 ) ) ,
12281241 permit,
12291242 & dest,
12301243 & mut report,
@@ -1270,7 +1283,7 @@ mod tests {
12701283 let result = extract_file_with_permit (
12711284 & mut reader,
12721285 & safe_path,
1273- Some ( 0o644 ) ,
1286+ Some ( sanitized ( 0o644 ) ) ,
12741287 permit,
12751288 & dest,
12761289 & mut report,
@@ -1327,7 +1340,7 @@ mod tests {
13271340 let result = extract_file_with_permit (
13281341 & mut reader,
13291342 & safe_path,
1330- Some ( 0o644 ) ,
1343+ Some ( sanitized ( 0o644 ) ) ,
13311344 permit,
13321345 & dest,
13331346 & mut report,
@@ -1557,8 +1570,8 @@ mod tests {
15571570 let file_path = temp. path ( ) . join ( "test_0o644.txt" ) ;
15581571
15591572 // Create file with mode 0o644
1560- let file =
1561- create_file_with_mode ( & file_path , Some ( 0o644 ) , false ) . expect ( "should create file" ) ;
1573+ let file = create_file_with_mode ( & file_path , Some ( sanitized ( 0o644 ) ) , false )
1574+ . expect ( "should create file" ) ;
15621575 drop ( file) ;
15631576
15641577 // Verify file exists
@@ -1586,8 +1599,8 @@ mod tests {
15861599 let file_path = temp. path ( ) . join ( "test_0o755.txt" ) ;
15871600
15881601 // Create file with mode 0o755
1589- let file =
1590- create_file_with_mode ( & file_path , Some ( 0o755 ) , false ) . expect ( "should create file" ) ;
1602+ let file = create_file_with_mode ( & file_path , Some ( sanitized ( 0o755 ) ) , false )
1603+ . expect ( "should create file" ) ;
15911604 drop ( file) ;
15921605
15931606 // Verify file exists
@@ -1615,8 +1628,8 @@ mod tests {
16151628 let file_path = temp. path ( ) . join ( "test_0o600.txt" ) ;
16161629
16171630 // Create file with mode 0o600
1618- let file =
1619- create_file_with_mode ( & file_path , Some ( 0o600 ) , false ) . expect ( "should create file" ) ;
1631+ let file = create_file_with_mode ( & file_path , Some ( sanitized ( 0o600 ) ) , false )
1632+ . expect ( "should create file" ) ;
16201633 drop ( file) ;
16211634
16221635 // Verify file exists
@@ -1703,7 +1716,7 @@ mod tests {
17031716 let config = SecurityConfig :: default ( ) . validate ( ) . expect ( "valid config" ) ;
17041717
17051718 // Mode 0o777 in archive, sanitized to 0o775 (world-writable stripped)
1706- let sanitized_mode = 0o775u32 ;
1719+ let sanitized_mode = sanitize_permissions ( 0o777 , & config ) ;
17071720 let permit = QuotaTracker :: new ( )
17081721 . reserve ( 0 , & config)
17091722 . expect ( "reservation should succeed" ) ;
@@ -1763,7 +1776,7 @@ mod tests {
17631776 // process-global but safe to mutate here. Restored unconditionally.
17641777 let previous_umask = unsafe { libc:: umask ( 0o077 ) } ;
17651778
1766- let result = create_file_with_mode ( & file_path, Some ( 0o755 ) , false ) ;
1779+ let result = create_file_with_mode ( & file_path, Some ( sanitized ( 0o755 ) ) , false ) ;
17671780
17681781 // Restore previous umask unconditionally before any assert.
17691782 unsafe { libc:: umask ( previous_umask) } ;
0 commit comments