Skip to content

Commit 4ff3c89

Browse files
authored
Merge branch 'main' into core-1
2 parents dd70a52 + 037b958 commit 4ff3c89

File tree

2 files changed

+185
-29
lines changed

2 files changed

+185
-29
lines changed

src/uu/mkdir/src/mkdir.rs

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore (ToDO) ugoa cmode
6+
// spell-checker:ignore (ToDO) ugoa cmode RAII
77

88
use clap::builder::ValueParser;
99
use clap::parser::ValuesRef;
1010
use clap::{Arg, ArgAction, ArgMatches, Command};
1111
use std::ffi::OsString;
1212
use std::path::{Path, PathBuf};
13-
#[cfg(not(windows))]
13+
#[cfg(all(unix, target_os = "linux"))]
1414
use uucore::error::FromIo;
1515
use uucore::error::{UResult, USimpleError};
1616
use uucore::translate;
@@ -191,7 +191,8 @@ pub fn mkdir(path: &Path, config: &Config) -> UResult<()> {
191191
create_dir(path, false, config)
192192
}
193193

194-
#[cfg(any(unix, target_os = "redox"))]
194+
/// Only needed on Linux to add ACL permission bits after directory creation.
195+
#[cfg(all(unix, target_os = "linux"))]
195196
fn chmod(path: &Path, mode: u32) -> UResult<()> {
196197
use std::fs::{Permissions, set_permissions};
197198
use std::os::unix::fs::PermissionsExt;
@@ -201,12 +202,6 @@ fn chmod(path: &Path, mode: u32) -> UResult<()> {
201202
)
202203
}
203204

204-
#[cfg(windows)]
205-
fn chmod(_path: &Path, _mode: u32) -> UResult<()> {
206-
// chmod on Windows only sets the readonly flag, which isn't even honored on directories
207-
Ok(())
208-
}
209-
210205
// Create a directory at the given path.
211206
// Uses iterative approach instead of recursion to avoid stack overflow with deep nesting.
212207
fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
@@ -250,13 +245,67 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
250245
create_single_dir(path, is_parent, config)
251246
}
252247

248+
/// RAII guard to restore umask on drop, ensuring cleanup even on panic.
249+
#[cfg(unix)]
250+
struct UmaskGuard(uucore::libc::mode_t);
251+
252+
#[cfg(unix)]
253+
impl UmaskGuard {
254+
/// Set umask to the given value and return a guard that restores the original on drop.
255+
fn set(new_mask: uucore::libc::mode_t) -> Self {
256+
let old_mask = unsafe { uucore::libc::umask(new_mask) };
257+
Self(old_mask)
258+
}
259+
}
260+
261+
#[cfg(unix)]
262+
impl Drop for UmaskGuard {
263+
fn drop(&mut self) {
264+
unsafe {
265+
uucore::libc::umask(self.0);
266+
}
267+
}
268+
}
269+
270+
/// Create a directory with the exact mode specified, bypassing umask.
271+
///
272+
/// GNU mkdir temporarily sets umask to 0 before calling mkdir(2), ensuring the
273+
/// directory is created atomically with the correct permissions. This avoids a
274+
/// race condition where the directory briefly exists with umask-based permissions.
275+
#[cfg(unix)]
276+
fn create_dir_with_mode(path: &Path, mode: u32) -> std::io::Result<()> {
277+
use std::os::unix::fs::DirBuilderExt;
278+
279+
// Temporarily set umask to 0 so the directory is created with the exact mode.
280+
// The guard restores the original umask on drop, even if we panic.
281+
let _guard = UmaskGuard::set(0);
282+
283+
std::fs::DirBuilder::new().mode(mode).create(path)
284+
}
285+
286+
#[cfg(not(unix))]
287+
fn create_dir_with_mode(path: &Path, _mode: u32) -> std::io::Result<()> {
288+
std::fs::create_dir(path)
289+
}
290+
253291
// Helper function to create a single directory with appropriate permissions
254292
// `is_parent` argument is not used on windows
255293
#[allow(unused_variables)]
256294
fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
257295
let path_exists = path.exists();
258296

259-
match std::fs::create_dir(path) {
297+
// Calculate the mode to use for directory creation
298+
#[cfg(unix)]
299+
let create_mode = if is_parent {
300+
// For parent directories with -p, use umask-derived mode with u+wx
301+
(!mode::get_umask() & 0o777) | 0o300
302+
} else {
303+
config.mode
304+
};
305+
#[cfg(not(unix))]
306+
let create_mode = config.mode;
307+
308+
match create_dir_with_mode(path, create_mode) {
260309
Ok(()) => {
261310
if config.verbose {
262311
println!(
@@ -265,30 +314,17 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<(
265314
);
266315
}
267316

317+
// On Linux, we may need to add ACL permission bits via chmod.
318+
// On other Unix systems, the directory was already created with the correct mode.
268319
#[cfg(all(unix, target_os = "linux"))]
269-
let new_mode = if path_exists {
270-
config.mode
271-
} else {
320+
if !path_exists {
272321
// TODO: Make this macos and freebsd compatible by creating a function to get permission bits from
273322
// acl in extended attributes
274323
let acl_perm_bits = uucore::fsxattr::get_acl_perm_bits_from_xattr(path);
275-
276-
if is_parent {
277-
(!mode::get_umask() & 0o777) | 0o300 | acl_perm_bits
278-
} else {
279-
config.mode | acl_perm_bits
324+
if acl_perm_bits != 0 {
325+
chmod(path, create_mode | acl_perm_bits)?;
280326
}
281-
};
282-
#[cfg(all(unix, not(target_os = "linux")))]
283-
let new_mode = if is_parent {
284-
(!mode::get_umask() & 0o777) | 0o300
285-
} else {
286-
config.mode
287-
};
288-
#[cfg(windows)]
289-
let new_mode = config.mode;
290-
291-
chmod(path, new_mode)?;
327+
}
292328

293329
// Apply SELinux context if requested
294330
#[cfg(feature = "selinux")]

tests/by-util/test_mkdir.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,126 @@ fn test_mkdir_environment_expansion() {
788788
}
789789
}
790790

791+
/// Test that mkdir -m creates directories with the exact requested mode,
792+
/// bypassing umask. This verifies the fix for issue #10022.
793+
///
794+
/// Previously, mkdir would create the directory with umask-based permissions
795+
/// and then chmod afterward, leaving a brief window with wrong permissions.
796+
/// Now it temporarily sets umask to 0 and creates with the exact mode.
797+
#[cfg(not(windows))]
798+
#[test]
799+
fn test_mkdir_mode_ignores_umask() {
800+
// Test that -m 0700 with restrictive umask still creates 0700
801+
{
802+
let (at, mut ucmd) = at_and_ucmd!();
803+
let restrictive_umask: mode_t = 0o077; // Would normally block group/other
804+
805+
ucmd.arg("-m")
806+
.arg("0700")
807+
.arg("test_700")
808+
.umask(restrictive_umask)
809+
.succeeds();
810+
811+
let perms = at.metadata("test_700").permissions().mode() as mode_t;
812+
assert_eq!(perms, 0o40700, "Expected 0700, got {:o}", perms & 0o777);
813+
}
814+
815+
// Test that -m 0777 is honored even with umask 022
816+
// This is the key test: without the fix, 0777 & ~022 = 0755
817+
{
818+
let (at, mut ucmd) = at_and_ucmd!();
819+
let common_umask: mode_t = 0o022;
820+
821+
ucmd.arg("-m")
822+
.arg("0777")
823+
.arg("test_777")
824+
.umask(common_umask)
825+
.succeeds();
826+
827+
let perms = at.metadata("test_777").permissions().mode() as mode_t;
828+
assert_eq!(
829+
perms,
830+
0o40777,
831+
"Expected 0777 (umask should be ignored with -m), got {:o}",
832+
perms & 0o777
833+
);
834+
}
835+
836+
// Test that -m 0755 with umask 077 still creates 0755
837+
{
838+
let (at, mut ucmd) = at_and_ucmd!();
839+
let very_restrictive_umask: mode_t = 0o077;
840+
841+
ucmd.arg("-m")
842+
.arg("0755")
843+
.arg("test_755")
844+
.umask(very_restrictive_umask)
845+
.succeeds();
846+
847+
let perms = at.metadata("test_755").permissions().mode() as mode_t;
848+
assert_eq!(perms, 0o40755, "Expected 0755, got {:o}", perms & 0o777);
849+
}
850+
851+
// Test symbolic mode also ignores umask
852+
{
853+
let (at, mut ucmd) = at_and_ucmd!();
854+
let umask: mode_t = 0o022;
855+
856+
ucmd.arg("-m")
857+
.arg("a=rwx")
858+
.arg("test_symbolic")
859+
.umask(umask)
860+
.succeeds();
861+
862+
let perms = at.metadata("test_symbolic").permissions().mode() as mode_t;
863+
assert_eq!(perms, 0o40777, "Expected 0777, got {:o}", perms & 0o777);
864+
}
865+
}
866+
867+
/// Test that mkdir -p -m applies mode correctly:
868+
/// - Parent directories use umask-derived permissions (with u+wx)
869+
/// - Final directory uses the exact requested mode (ignoring umask)
870+
#[cfg(not(windows))]
871+
#[test]
872+
fn test_mkdir_parent_mode_with_explicit_mode() {
873+
let (at, mut ucmd) = at_and_ucmd!();
874+
let umask: mode_t = 0o022;
875+
876+
ucmd.arg("-p")
877+
.arg("-m")
878+
.arg("0700")
879+
.arg("parent/child/target")
880+
.umask(umask)
881+
.succeeds();
882+
883+
// Parent directories created by -p use umask-derived mode with u+wx
884+
let parent_perms = at.metadata("parent").permissions().mode() as mode_t;
885+
let expected_parent = ((!umask & 0o777) | 0o300) + 0o40000;
886+
assert_eq!(
887+
parent_perms,
888+
expected_parent,
889+
"Parent should have umask-derived mode, got {:o}",
890+
parent_perms & 0o777
891+
);
892+
893+
let child_perms = at.metadata("parent/child").permissions().mode() as mode_t;
894+
assert_eq!(
895+
child_perms,
896+
expected_parent,
897+
"Intermediate dir should have umask-derived mode, got {:o}",
898+
child_perms & 0o777
899+
);
900+
901+
// Final directory should have exactly the requested mode
902+
let target_perms = at.metadata("parent/child/target").permissions().mode() as mode_t;
903+
assert_eq!(
904+
target_perms,
905+
0o40700,
906+
"Target should have exact requested mode 0700, got {:o}",
907+
target_perms & 0o777
908+
);
909+
}
910+
791911
#[test]
792912
fn test_mkdir_concurrent_creation() {
793913
// Test concurrent mkdir -p operations: 10 iterations, 8 threads, 40 levels nesting

0 commit comments

Comments
 (0)