Skip to content

mkdir: create directories atomically with correct permissions#10036

Merged
sylvestre merged 4 commits into
uutils:mainfrom
rynewang:fix-mkdir-mode-umask-race-10022
Jan 18, 2026
Merged

mkdir: create directories atomically with correct permissions#10036
sylvestre merged 4 commits into
uutils:mainfrom
rynewang:fix-mkdir-mode-umask-race-10022

Conversation

@rynewang

@rynewang rynewang commented Jan 3, 2026

Copy link
Copy Markdown
Contributor

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.

Fix uutils#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.
@alippai

alippai commented Jan 3, 2026

Copy link
Copy Markdown

Is coreutils supposed to be used as a multithreaded lib? Umask changes the whole process, all threads. If safety is needed adding Drop + mutex can make it more robust

@github-actions

github-actions Bot commented Jan 3, 2026

Copy link
Copy Markdown

GNU testsuite comparison:

Congrats! The gnu test tests/tail/follow-name is no longer failing!

@sylvestre

Copy link
Copy Markdown
Contributor

Is coreutils supposed to be used as a multithreaded lib? Umask changes the whole process, all threads. If safety is needed adding Drop + mutex can make it more robust

Yes and no. We focus on the cli first but we have some users like nushell.

@rynewang

rynewang commented Jan 3, 2026

Copy link
Copy Markdown
Contributor Author

I can add mutex and Drop but I wonder how much it can help. If a downstream user users uutils/coreutils as a library then their own umask or fork calls can't be protected by any of our mutexes.

@rynewang

rynewang commented Jan 3, 2026

Copy link
Copy Markdown
Contributor Author

One approach is to fork a subprocess that does umask, mkdir and exit. But I wonder if this is an overkill and a mutex + drop guard is enough?

@rynewang

rynewang commented Jan 4, 2026

Copy link
Copy Markdown
Contributor Author

@sylvestre can you provide some directions on whether I should go with:

  1. a drop guard, or
  2. fork a subprocess to umask and mkdir and exit ?

@sylvestre

Copy link
Copy Markdown
Contributor

for now, just focus on the binary itself, we will see for the lib later

Add RAII guard to ensure umask is restored even on panic. Encapsulate
unsafe umask calls in UmaskGuard::set() for a safe interface.
@rynewang

rynewang commented Jan 4, 2026

Copy link
Copy Markdown
Contributor Author

updated with the drop guard

- Add RAII to spell-checker ignore list
- Narrow chmod cfg to Linux only (only used for ACL bits)
@github-actions

github-actions Bot commented Jan 4, 2026

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/sort/sort-stale-thread-mem. tests/sort/sort-stale-thread-mem is passing on 'main'. Maybe you have to rebase?
Skip an intermittent issue tests/timeout/timeout (fails in this run but passes in the 'main' branch)

@sylvestre

Copy link
Copy Markdown
Contributor

some jobs are failing

FromIo is only used in chmod, which is now Linux-only.
@github-actions

github-actions Bot commented Jan 4, 2026

Copy link
Copy Markdown

GNU testsuite comparison:

Skip an intermittent issue tests/timeout/timeout (fails in this run but passes in the 'main' branch)
Note: The gnu test tests/csplit/csplit-heap is now being skipped but was previously passing.

@rynewang

rynewang commented Jan 5, 2026

Copy link
Copy Markdown
Contributor Author

Looks like the benchmark failure is from flaky test on join not mkdir

@sylvestre sylvestre merged commit 037b958 into uutils:main Jan 18, 2026
150 of 155 checks passed
@sylvestre

Copy link
Copy Markdown
Contributor

well done

@0xferrous

0xferrous commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

seems like this also fixed a bug in gid bit not getting inherited, i bisected with the following test:

#!/usr/bin/env nu

def main [] {
    cargo b -p uu_mkdir
    alias mkdir = ./target/debug/mkdir
    if ("test-dir" | path exists) {
        rm -rv test-dir
    }

    mkdir test-dir
    chgrp agent test-dir
    chmod g+rwxs test-dir

    mkdir -p test-dir/foo/bar/baz
    let perms = ls -lD test-dir/foo/bar/baz | get mode | str substring 5..5
    print $"perms: ($perms)"
    print ($perms == "s")
}

fails for d72d6f7
but works for 037b958
hence, identifying this is the pr where the problem gets fixed

although, seems like an unrelated side effect from this pr.

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 -m creates directory with wrong permissions, briefly

4 participants