Skip to content

Commit c3bf7c8

Browse files
committed
mkdir: acl and permission inheritance with -p
Workflow for permission setting and ACLs failed in several scenarios, most notable when passing -p. Parent directories in the mkdir call would not appropriately set ACLs and could end up with more open permissions. Generally, there was a misunderstanding that GNU coreutils was setting umask (0) and that was the default -- the real flow was using a shaped umask that takes current umask and ensures that the user has the ability to execute mkdir commands through the tree. The umask (0) call was part of a read setup for the equivalent of our UmaskGuard. New workflow focuses on safe defaults, shaped umask, and allowing the Kernel to do to apply ACLs. Adds a test specifically to guard against regression, ensuring a more restrictive ACL is respected with mkdir -p
1 parent df7ae35 commit c3bf7c8

File tree

2 files changed

+94
-49
lines changed

2 files changed

+94
-49
lines changed

src/uu/mkdir/src/mkdir.rs

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ use clap::{Arg, ArgAction, ArgMatches, Command};
1111
use std::ffi::OsString;
1212
use std::io::{Write, stdout};
1313
use std::path::{Path, PathBuf};
14-
#[cfg(all(unix, target_os = "linux"))]
15-
use uucore::error::FromIo;
1614
use uucore::error::{UResult, USimpleError};
1715
use uucore::translate;
1816

@@ -37,8 +35,8 @@ pub struct Config<'a> {
3735
/// Create parent directories as needed.
3836
pub recursive: bool,
3937

40-
/// File permissions (octal).
41-
pub mode: u32,
38+
/// File permissions (octal) if provided via -m
39+
pub mode: Option<u32>,
4240

4341
/// Print message for each created directory.
4442
pub verbose: bool,
@@ -55,18 +53,18 @@ pub struct Config<'a> {
5553
clippy::unnecessary_wraps,
5654
reason = "fn sig must match on all platforms"
5755
)]
58-
fn get_mode(_matches: &ArgMatches) -> Result<u32, String> {
59-
Ok(DEFAULT_PERM)
56+
fn get_mode(_matches: &ArgMatches) -> Result<Option<u32>, String> {
57+
Ok(None)
6058
}
6159

6260
#[cfg(not(windows))]
63-
fn get_mode(matches: &ArgMatches) -> Result<u32, String> {
61+
fn get_mode(matches: &ArgMatches) -> Result<Option<u32>, String> {
6462
// Not tested on Windows
6563
if let Some(m) = matches.get_one::<String>(options::MODE) {
66-
mode::parse_chmod(DEFAULT_PERM, m, true, mode::get_umask())
64+
mode::parse_chmod(DEFAULT_PERM, m, true, mode::get_umask()).map(Some)
6765
} else {
68-
// If no mode argument is specified return the mode derived from umask
69-
Ok(!mode::get_umask() & DEFAULT_PERM)
66+
// If no mode argument, let the kernel apply umask and ACLs naturally.
67+
Ok(None)
7068
}
7169
}
7270

@@ -196,17 +194,6 @@ pub fn mkdir(path: &Path, config: &Config) -> UResult<()> {
196194
create_dir(path, false, config)
197195
}
198196

199-
/// Only needed on Linux to add ACL permission bits after directory creation.
200-
#[cfg(all(unix, target_os = "linux"))]
201-
fn chmod(path: &Path, mode: u32) -> UResult<()> {
202-
use std::fs::{Permissions, set_permissions};
203-
use std::os::unix::fs::PermissionsExt;
204-
let mode = Permissions::from_mode(mode);
205-
set_permissions(path, mode).map_err_context(
206-
|| translate!("mkdir-error-cannot-set-permissions", "path" => path.quote()),
207-
)
208-
}
209-
210197
// Create a directory at the given path.
211198
// Uses iterative approach instead of recursion to avoid stack overflow with deep nesting.
212199
fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
@@ -272,43 +259,59 @@ impl Drop for UmaskGuard {
272259

273260
/// Create a directory with the exact mode specified, bypassing umask.
274261
///
275-
/// GNU mkdir temporarily sets umask to 0 before calling mkdir(2), ensuring the
262+
/// GNU mkdir temporarily sets umask to shaped mask before calling mkdir(2), ensuring the
276263
/// directory is created atomically with the correct permissions. This avoids a
277264
/// race condition where the directory briefly exists with umask-based permissions.
278265
#[cfg(unix)]
279-
fn create_dir_with_mode(path: &Path, mode: u32) -> std::io::Result<()> {
266+
fn create_dir_with_mode(
267+
path: &Path,
268+
mode: u32,
269+
shaped_umask: rustix::fs::Mode,
270+
) -> std::io::Result<()> {
280271
use std::os::unix::fs::DirBuilderExt;
281272

282-
// Temporarily set umask to 0 so the directory is created with the exact mode.
283-
// The guard restores the original umask on drop, even if we panic.
284-
let _guard = UmaskGuard::set(rustix::fs::Mode::empty());
273+
let _guard = UmaskGuard::set(shaped_umask);
285274

286275
std::fs::DirBuilder::new().mode(mode).create(path)
287276
}
288277

289278
#[cfg(not(unix))]
290-
fn create_dir_with_mode(path: &Path, _mode: u32) -> std::io::Result<()> {
279+
fn create_dir_with_mode(path: &Path, _mode: u32, _shaped_umask: u32) -> std::io::Result<()> {
291280
std::fs::create_dir(path)
292281
}
293282

294283
// Helper function to create a single directory with appropriate permissions
295284
// `is_parent` argument is not used on windows
296285
#[allow(unused_variables)]
297286
fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
298-
let path_exists = path.exists();
299-
300-
// Calculate the mode to use for directory creation
301287
#[cfg(unix)]
302-
let create_mode = if is_parent {
303-
// For parent directories with -p, use umask-derived mode with u+wx
304-
(!mode::get_umask() & 0o777) | 0o300
305-
} else {
306-
config.mode
288+
let (mkdir_mode, shaped_umask) = {
289+
let umask = mode::get_umask();
290+
let umask_bits = rustix::fs::Mode::from_bits_truncate(umask);
291+
if is_parent {
292+
// Parent directories are never affected by -m (matches GNU behavior).
293+
// We pass 0o777 as the mode and shape the umask so it cannot block
294+
// owner write or execute (u+wx), ensuring the owner can traverse and
295+
// write into the parent to create children. All other umask bits are
296+
// preserved so the kernel applies them — and any default ACL on the
297+
// grandparent — through the normal mkdir(2) path.
298+
(
299+
DEFAULT_PERM,
300+
umask_bits & !rustix::fs::Mode::from_bits_truncate(0o300),
301+
)
302+
} else {
303+
match config.mode {
304+
// Explicit -m: shape umask so it cannot block explicitly requested bits.
305+
Some(m) => (m, umask_bits & !rustix::fs::Mode::from_bits_truncate(m)),
306+
// No -m: leave umask fully intact; kernel applies umask + ACL naturally.
307+
None => (DEFAULT_PERM, umask_bits),
308+
}
309+
}
307310
};
308311
#[cfg(not(unix))]
309-
let create_mode = config.mode;
312+
let (mkdir_mode, shaped_umask) = (config.mode.unwrap_or(DEFAULT_PERM), 0u32);
310313

311-
match create_dir_with_mode(path, create_mode) {
314+
match create_dir_with_mode(path, mkdir_mode, shaped_umask) {
312315
Ok(()) => {
313316
if config.verbose {
314317
writeln!(
@@ -318,18 +321,6 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<(
318321
)?;
319322
}
320323

321-
// On Linux, we may need to add ACL permission bits via chmod.
322-
// On other Unix systems, the directory was already created with the correct mode.
323-
#[cfg(all(unix, target_os = "linux"))]
324-
if !path_exists {
325-
// TODO: Make this macos and freebsd compatible by creating a function to get permission bits from
326-
// acl in extended attributes
327-
let acl_perm_bits = uucore::fsxattr::get_acl_perm_bits_from_xattr(path);
328-
if acl_perm_bits != 0 {
329-
chmod(path, create_mode | acl_perm_bits)?;
330-
}
331-
}
332-
333324
// Apply SELinux context if requested
334325
#[cfg(feature = "selinux")]
335326
if config.set_security_context && uucore::selinux::is_selinux_enabled() {

tests/by-util/test_mkdir.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,60 @@ fn test_mkdir_acl() {
360360
assert_eq!(perms, 16893);
361361
}
362362

363+
#[test]
364+
#[cfg(target_os = "linux")]
365+
fn test_mkdir_acl_inheritance_with_restrictive_mask() {
366+
use rustc_hash::FxHashMap;
367+
use std::ffi::OsString;
368+
369+
let (at, mut ucmd) = at_and_ucmd!();
370+
371+
at.mkdir("parent");
372+
373+
let mut map: FxHashMap<OsString, Vec<u8>> = FxHashMap::default();
374+
375+
// Default ACL with mask::r-x (0o5) — more restrictive than a umask of 0o022 would allow.
376+
// With umask 0o022, group bits would be r-x already, but the mask enforces this
377+
// regardless of what umask would permit. With umask 0o000, without ACL mask the
378+
// child would get rwx for group, but mask caps it to r-x.
379+
//
380+
// Encoding: header(0x0002) + entries:
381+
// ACL_USER_OBJ (0x0001) perm=7 (rwx)
382+
// ACL_GROUP_OBJ (0x0004) perm=7 (rwx) — would be rwx without mask
383+
// ACL_MASK (0x0010) perm=5 (r-x) — restricts group effective to r-x
384+
// ACL_OTHER (0x0020) perm=0 (---)
385+
let xattr_val: Vec<u8> = vec![
386+
2, 0, 0, 0, // header
387+
1, 0, 7, 0, 255, 255, 255, 255, // ACL_USER_OBJ rwx
388+
4, 0, 7, 0, 255, 255, 255, 255, // ACL_GROUP_OBJ rwx (masked to r-x)
389+
16, 0, 5, 0, 255, 255, 255, 255, // ACL_MASK r-x
390+
32, 0, 0, 0, 255, 255, 255, 255, // ACL_OTHER ---
391+
];
392+
393+
map.insert(OsString::from("system.posix_acl_default"), xattr_val);
394+
uucore::fsxattr::apply_xattrs(at.plus("parent"), map).unwrap();
395+
396+
// umask 0o000 — without correct ACL inheritance, group would get rwx (7)
397+
// With correct inheritance the mask restricts group to r-x (5)
398+
ucmd.arg("-p").arg("parent/child").umask(0o000).succeeds();
399+
400+
let perms = at.metadata("parent/child").permissions().mode();
401+
// Expected: user=rwx(7), group=r-x(5 from mask), other=---(0)
402+
// mode bits: 0o750 = 0o40750 with directory bit
403+
assert_eq!(
404+
perms & 0o777,
405+
0o750,
406+
"Expected group bits capped to r-x by ACL mask, got {:o}",
407+
perms & 0o777
408+
);
409+
410+
// Verify the child itself has an ACL (indicated by presence of xattr)
411+
assert!(
412+
uucore::fsxattr::has_acl(at.plus("parent/child")),
413+
"Child directory should have inherited ACL entries"
414+
);
415+
}
416+
363417
#[test]
364418
fn test_mkdir_trailing_dot() {
365419
new_ucmd!().arg("-p").arg("-v").arg("test_dir").succeeds();

0 commit comments

Comments
 (0)