FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

mkdir: fix EEXIST race condition in non-recursive mode by MadeNavaneeth · Pull Request #14181 · uutils/coreutils · GitHub

mkdir: fix EEXIST race condition in non-recursive mode - #14181

Open
MadeNavaneeth wants to merge 1 commit into
uutils:mainfrom
MadeNavaneeth:fix/mkdir-eexist-race
Open

mkdir: fix EEXIST race condition in non-recursive mode#14181
MadeNavaneeth wants to merge 1 commit into
uutils:mainfrom
MadeNavaneeth:fix/mkdir-eexist-race

Conversation

Copy link
Copy Markdown
Contributor

Summary

Fixes a race condition where two concurrent mkdir dir invocations could both exit 0, breaking the classic mkdir-as-mutex shell pattern.

Problem

When mkdir(2) returns EEXIST, the previous code checked path.is_dir() and returned Ok(()) if true — even for plain mkdir dir (non-recursive). This meant two concurrent processes racing to create the same directory could both exit 0:

# Expected: exactly 1 winner per round
for i in $(seq 20); do
  ( mkdir /tmp/lockdir 2>/dev/null && echo "WINNER $i" ) &
done
wait
# Observed: occasionally 2+ winners

The root cause is a TOCTOU race: after mkdir(2) fails with EEXIST, path.is_dir() returns true because the winning process already created it. The code treated this as success.

Fix

Only treat EEXIST + is_dir() as success when:

  • is_parent=true (creating parent directories in -p mode), OR
  • config.recursive=true (-p was specified)

In the plain mkdir dir case, EEXIST now always returns an error, matching GNU behavior.

Verification

  • All 43 existing mkdir tests pass
  • Race test with 20 concurrent contenders: 0 double-winners across 5 rounds (previously ~3/5 rounds)
  • mkdir -p existing_dir still succeeds (correct -p behavior)
  • mkdir existing_dir (without -p) now correctly fails with exit 1

Fixes #13970

Comment thread src/uu/mkdir/src/mkdir.rs
// (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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown
Contributor Author

Good question! I considered matching on the error kind directly, but the current approach catches a broader set of "directory already exists" errors. The call goes through , which may map to different values depending on the platform and Rust version (e.g. on some, on others via the raw OS error). Checking after any creation failure is the same pattern GNU uses — it's resilient to these mapping differences. That said, I'm happy to refactor if there's a preferred approach!

Copy link
Copy Markdown
Contributor Author

Good question! I considered matching on the error kind directly, but the current approach catches a broader set of "directory already exists" errors. The create_dir_with_mode call goes through std::fs::DirBuilder::create(), which may map EEXIST to different io::ErrorKind values depending on the platform and Rust version. Checking path.is_dir() after any creation failure is the same pattern GNU uses -- it is resilient to these mapping differences. That said, I am happy to refactor if there is a preferred approach!

MadeNavaneeth force-pushed the fix/mkdir-eexist-race branch from c486b4c to fb0b72e Compare August 27, 2026 12:52

github-actions Bot commented Aug 27, 2026
edited
Loading

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/dd/misc. tests/dd/misc is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/df/over-mount-device. tests/df/over-mount-device is passing on 'main'. Maybe you have to rebase?
Skip an intermittent issue tests/cut/bounded-memory (fails in this run but passes in the 'main' branch)
Skip an intermittent issue tests/tail/tail-n0f (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/tail/inotify-dir-recreate (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/cat/splice is no longer failing!
Congrats! The gnu test tests/cut/cut is no longer failing!
Congrats! The gnu test tests/cut/mb-non-utf8 is no longer failing!
Congrats! The gnu test tests/dd/partial-write is no longer failing!
Congrats! The gnu test tests/expand/mb is no longer failing!
Congrats! The gnu test tests/ls/stat-free-symlinks is no longer failing!
Congrats! The gnu test tests/mktemp/write-error is no longer failing!
Congrats! The gnu test tests/mv/dir2dir is no longer failing!
Congrats! The gnu test tests/mv/mv-exchange is no longer failing!
Congrats! The gnu test tests/nl/multibyte is no longer failing!
Congrats! The gnu test tests/od/od-float is no longer failing!
Congrats! The gnu test tests/od/od-j is no longer failing!
Congrats! The gnu test tests/ptx/ptx-overrun is no longer failing!
Congrats! The gnu test tests/sort/sort-merge-fdlimit is no longer failing!
Congrats! The gnu test tests/unexpand/mb is no longer failing!
Note: The gnu test tests/dd/fail-ftruncate-fstat was skipped on 'main' but is now failing.

MadeNavaneeth force-pushed the fix/mkdir-eexist-race branch from fb0b72e to 9ee162e Compare August 27, 2026 14:30
When mkdir(2) returns EEXIST, the previous code checked path.is_dir()
and returned Ok(()) if true — even for plain `mkdir dir` (non-recursive).
This meant two concurrent processes racing to create the same directory
could both exit 0, breaking the classic mkdir-as-mutex pattern.
Fix by only treating EEXIST + is_dir() as success when creating parent
directories (is_parent) or when -p was given (recursive). In the plain
`mkdir dir` case, EEXIST now always returns an error, matching GNU
behavior.
Fixes uutils#13970
MadeNavaneeth force-pushed the fix/mkdir-eexist-race branch from 9ee162e to 72d4b7e Compare August 27, 2026 15:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mkdir: exits 0 despite EEXIST from the kernel under concurrent invocation (breaks mkdir-as-mutex)

2 participants


Back | FazBrowse Home | New Git URL