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
51 changes: 29 additions & 22 deletions src/uucore/src/lib/features/safe_traversal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,30 +548,37 @@ fn find_existing_ancestor(path: &Path) -> io::Result<(PathBuf, Vec<OsString>)> {
/// # Returns
/// A DirFd for the subdirectory
fn open_or_create_subdir(parent_fd: &DirFd, name: &OsStr, mode: u32) -> io::Result<DirFd> {
match parent_fd.stat_at(name, SymlinkBehavior::NoFollow) {
Ok(stat) => {
let file_type = (stat.st_mode as libc::mode_t) & libc::S_IFMT;
match file_type {
libc::S_IFDIR => parent_fd.open_subdir(name, SymlinkBehavior::NoFollow),
libc::S_IFLNK => {
parent_fd.unlink_at(name, false)?;
parent_fd.mkdir_at(name, mode)?;
parent_fd.open_subdir(name, SymlinkBehavior::NoFollow)
}
_ => Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"path component exists but is not a directory: {}",
name.display()
),
)),
// Loop to handle the TOCTOU race between `stat_at` and `mkdir_at`
// (issue #12355): a concurrent process can create `name` after our stat
// returns NotFound, causing mkdir_at to return EEXIST. Re-stat and
// proceed with whatever is now at `name`.
loop {
match parent_fd.stat_at(name, SymlinkBehavior::NoFollow) {
Ok(stat) => {
let file_type = (stat.st_mode as libc::mode_t) & libc::S_IFMT;
return match file_type {
libc::S_IFDIR => parent_fd.open_subdir(name, SymlinkBehavior::NoFollow),
libc::S_IFLNK => {
parent_fd.unlink_at(name, false)?;
parent_fd.mkdir_at(name, mode)?;
parent_fd.open_subdir(name, SymlinkBehavior::NoFollow)
}
_ => Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"path component exists but is not a directory: {}",
name.display()
),
)),
};
}
Err(e) if e.kind() == io::ErrorKind::NotFound => match parent_fd.mkdir_at(name, mode) {
Ok(()) => return parent_fd.open_subdir(name, SymlinkBehavior::NoFollow),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
Err(e) => return Err(e),
},
Err(e) => return Err(e),
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
parent_fd.mkdir_at(name, mode)?;
parent_fd.open_subdir(name, SymlinkBehavior::NoFollow)
}
Err(e) => Err(e),
}
}

Expand Down
31 changes: 31 additions & 0 deletions tests/by-util/test_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2670,3 +2670,34 @@ fn test_install_d_symlink_race_condition_concurrent() {
"Intermediate directory should be a real directory, not a symlink"
);
}

#[test]
#[cfg(unix)]
fn test_install_d_parallel_mkdir_race() {
// Regression test for issue #12355: concurrent `install -D` invocations
// that share parent directories must all succeed instead of one losing
// the stat/mkdir race and failing with `cannot create directory`.
const PARALLEL: usize = 32;
const ROUNDS: usize = 5;

let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("s");

for round in 0..ROUNDS {
let mut children = Vec::with_capacity(PARALLEL);
for k in 0..PARALLEL {
let mut cmd = scene.ucmd();
cmd.arg("-D").arg("s").arg(format!("o{round}/q/f{k}"));
children.push(cmd.run_no_wait());
}

for child in children {
child.wait().unwrap().success().no_stderr();
}

for k in 0..PARALLEL {
assert!(at.file_exists(format!("o{round}/q/f{k}")));
}
}
}
Loading