From 72d4b7ede4daacef406bb2f187ac314d2e70e37f Mon Sep 17 00:00:00 2001 From: Navaneeth Yadamreddy Date: Thu, 27 Aug 2026 17:55:42 +0530 Subject: [PATCH] =?UTF-8?q?mkdir:=20fix=20EEXIST=20race=20condition=20in?= =?UTF-8?q?=20non-recursive=20mode=20When=20mkdir(2)=20returns=20EEXIST,?= =?UTF-8?q?=20the=20previous=20code=20checked=20path.is=5Fdir()=20and=20re?= =?UTF-8?q?turned=20Ok(())=20if=20true=20=E2=80=94=20even=20for=20plain=20?= =?UTF-8?q?`mkdir=20dir`=20(non-recursive).=20This=20meant=20two=20concurr?= =?UTF-8?q?ent=20processes=20racing=20to=20create=20the=20same=20directory?= =?UTF-8?q?=20could=20both=20exit=200,=20breaking=20the=20classic=20mkdir-?= =?UTF-8?q?as-mutex=20pattern.=20Fix=20by=20only=20treating=20EEXIST=20+?= =?UTF-8?q?=20is=5Fdir()=20as=20success=20when=20creating=20parent=20direc?= =?UTF-8?q?tories=20(is=5Fparent)=20or=20when=20-p=20was=20given=20(recurs?= =?UTF-8?q?ive).=20In=20the=20plain=20`mkdir=20dir`=20case,=20EEXIST=20now?= =?UTF-8?q?=20always=20returns=20an=20error,=20matching=20GNU=20behavior.?= =?UTF-8?q?=20Fixes=20#13970?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/uu/mkdir/src/mkdir.rs | 14 +++++++-- tests/by-util/test_mkdir.rs | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/uu/mkdir/src/mkdir.rs b/src/uu/mkdir/src/mkdir.rs index 1d34d62bb7e..36be5a7dbfe 100644 --- a/src/uu/mkdir/src/mkdir.rs +++ b/src/uu/mkdir/src/mkdir.rs @@ -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 + // 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) diff --git a/tests/by-util/test_mkdir.rs b/tests/by-util/test_mkdir.rs index e335a3f0566..82db044daa2 100644 --- a/tests/by-util/test_mkdir.rs +++ b/tests/by-util/test_mkdir.rs @@ -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}"); + } +}