Skip to content

Commit 037b958

Browse files
authored
mkdir: create directories atomically with correct permissions (#10036)
* mkdir: create directories atomically with correct permissions Fix #10022: mkdir -m MODE was creating directories with umask-based permissions first, then calling chmod afterward. This left a brief window where the directory existed with wrong permissions. Now we match GNU mkdir behavior by temporarily setting umask to 0 before the mkdir syscall, passing the exact requested mode to the kernel, then restoring the original umask. The directory is created atomically with the correct permissions. Before (two syscalls, race condition): mkdir("dir", 0777) -> created with 0755 (umask applied) chmod("dir", 0700) -> fixed afterward After (single syscall, atomic): umask(0) mkdir("dir", 0700) -> created with exact mode umask(original) Also added tests to verify -m MODE bypasses umask correctly. * mkdir: add UmaskGuard for panic-safe umask restoration Add RAII guard to ensure umask is restored even on panic. Encapsulate unsafe umask calls in UmaskGuard::set() for a safe interface. * mkdir: fix clippy and spelling warnings - Add RAII to spell-checker ignore list - Narrow chmod cfg to Linux only (only used for ACL bits) * mkdir: fix unused import on non-Linux FromIo is only used in chmod, which is now Linux-only.
1 parent d72d6f7 commit 037b958

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)