Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/uu/mkdir/src/mkdir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,18 @@ fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<(
}

Err(_) if path.is_dir() => {
// Directory already exists - check if this is a logical directory creation
// (i.e., not just a parent reference like "test_dir/..")
// Directory already exists. Only treat this as success when we
// are creating parent directories (is_parent) or when -p was
// given (recursive). In the plain `mkdir dir` case, EEXIST must

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do not you catch EEXIST itself directly at match arm?

// be an error — even if the directory was created by a concurrent
// process — to preserve the mkdir-as-mutex pattern.
if !is_parent && !config.recursive {
return Err(USimpleError::new(
1,
translate!("mkdir-error-file-exists", "path" => path.maybe_quote()),
));
}

let ends_with_parent_dir = matches!(
path.components().next_back(),
Some(std::path::Component::ParentDir)
Expand Down
62 changes: 62 additions & 0 deletions tests/by-util/test_mkdir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1134,3 +1134,65 @@ mod diagnostics {
assert!(!stderr.contains(":1:"), "{stderr}");
}
}

/// Regression test: plain `mkdir` on an existing directory must fail with
/// exit 1, not 0. Previously the EEXIST path checked `path.is_dir()` and
/// returned Ok(()), which broke the mkdir-as-mutex pattern under
/// concurrency (issue #13970).
#[test]
fn test_mkdir_eexist_returns_error() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let dir = at.plus("existing_dir");

let dir_str = dir.to_str().unwrap();

// Create the directory first
new_ucmd!().args(&[dir_str]).succeeds();

// Trying again without -p must fail
new_ucmd!()
.args(&[dir_str])
.fails_with_code(1)
.stderr_contains("File exists");
}

/// Regression test: concurrent `mkdir` (no -p) on the same path must
/// produce exactly one winner (issue #13970).
#[test]
fn test_mkdir_concurrent_eexist() {
use std::thread;

let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let dir = at.plus("race_dir");
let path_str = dir.to_string_lossy().to_string();
let bin_path = scene.bin_path.clone();

for _ in 0..10 {
// Remove directory before each round
let _ = std::fs::remove_dir(&path_str);

let mut handles = vec![];
for _ in 0..20 {
let path = path_str.clone();
let bin = bin_path.clone();
handles.push(thread::spawn(move || {
std::process::Command::new(&bin)
.arg("mkdir")
.arg(&path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}));
}

let winners: usize = handles
.into_iter()
.map(|h| h.join().unwrap() as usize)
.sum();
assert_eq!(winners, 1, "Expected exactly 1 winner, got {winners}");
}
}
Loading