Skip to content

Commit 4191da8

Browse files
authored
Fix a race condition in remove_dir_all. (#222)
Port the fix from rust-lang/rust#93112 to cap-std.
1 parent 0a85201 commit 4191da8

17 files changed

+256
-102
lines changed

cap-primitives/src/fs/dir_entry.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::fs::{dir_options, DirEntryInner, FileType, Metadata, OpenOptions, ReadDir};
1+
use crate::fs::{
2+
dir_options, DirEntryInner, FileType, FollowSymlinks, Metadata, OpenOptions, ReadDir,
3+
};
24
#[cfg(not(windows))]
35
use rustix::fs::DirEntryExt;
46
use std::ffi::OsString;
@@ -57,7 +59,7 @@ impl DirEntry {
5759
/// Returns an iterator over the entries within the subdirectory.
5860
#[inline]
5961
pub fn read_dir(&self) -> io::Result<ReadDir> {
60-
self.inner.read_dir()
62+
self.inner.read_dir(FollowSymlinks::Yes)
6163
}
6264

6365
/// Returns the metadata for the file that this entry points at.

cap-primitives/src/fs/file_path_by_searching.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::fs::{is_root_dir, open_dir_unchecked, read_dir_unchecked, MaybeOwnedFile, Metadata};
1+
use crate::fs::{
2+
is_root_dir, open_dir_unchecked, read_dir_unchecked, FollowSymlinks, MaybeOwnedFile, Metadata,
3+
};
24
use std::fs;
35
use std::path::{Component, PathBuf};
46

@@ -13,7 +15,8 @@ pub(crate) fn file_path_by_searching(file: &fs::File) -> Option<PathBuf> {
1315
// Iterate with `..` until we reach the root directory.
1416
'next_component: loop {
1517
// Open `..`.
16-
let mut iter = read_dir_unchecked(&base, Component::ParentDir.as_ref()).ok()?;
18+
let mut iter =
19+
read_dir_unchecked(&base, Component::ParentDir.as_ref(), FollowSymlinks::No).ok()?;
1720
let metadata = Metadata::from_file(&*base).ok()?;
1821

1922
// Search the children until we find one with matching metadata, and

cap-primitives/src/fs/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ pub(crate) use super::rustix::fs::*;
5454
#[cfg(windows)]
5555
pub(crate) use super::windows::fs::*;
5656

57-
pub(crate) use read_dir::read_dir_unchecked;
57+
#[cfg(not(windows))]
58+
pub(crate) use read_dir::{read_dir_nofollow, read_dir_unchecked};
5859

5960
pub use canonicalize::canonicalize;
6061
pub use copy::copy;

cap-primitives/src/fs/open_dir.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ pub fn open_dir(start: &fs::File, path: &Path) -> io::Result<fs::File> {
1818

1919
/// Like `open_dir`, but additionally request the ability to read the directory
2020
/// entries.
21+
#[cfg(not(windows))]
2122
#[inline]
22-
pub fn open_dir_for_reading(start: &fs::File, path: &Path) -> io::Result<fs::File> {
23-
open(start, path, &readdir_options())
23+
pub(crate) fn open_dir_for_reading(
24+
start: &fs::File,
25+
path: &Path,
26+
follow: FollowSymlinks,
27+
) -> io::Result<fs::File> {
28+
open(start, path, readdir_options().follow(follow))
2429
}
2530

2631
/// Similar to `open_dir`, but fails if the path names a symlink.
@@ -43,8 +48,9 @@ pub(crate) fn open_dir_unchecked(start: &fs::File, path: &Path) -> io::Result<fs
4348
pub(crate) fn open_dir_for_reading_unchecked(
4449
start: &fs::File,
4550
path: &Path,
51+
follow: FollowSymlinks,
4652
) -> io::Result<fs::File> {
47-
open_unchecked(start, path, &readdir_options()).map_err(Into::into)
53+
open_unchecked(start, path, readdir_options().follow(follow)).map_err(Into::into)
4854
}
4955

5056
/// Open a directory named by a bare path, using the host process' ambient

cap-primitives/src/fs/read_dir.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::fs::{DirEntry, ReadDirInner};
1+
use crate::fs::{DirEntry, FollowSymlinks, ReadDirInner};
22
use std::path::Path;
33
use std::{fmt, fs, io};
44

@@ -8,7 +8,16 @@ use std::{fmt, fs, io};
88
#[inline]
99
pub fn read_dir(start: &fs::File, path: &Path) -> io::Result<ReadDir> {
1010
Ok(ReadDir {
11-
inner: ReadDirInner::new(start, path)?,
11+
inner: ReadDirInner::new(start, path, FollowSymlinks::Yes)?,
12+
})
13+
}
14+
15+
/// Like `read_dir`, but fails if `path` names a symlink.
16+
#[inline]
17+
#[cfg(not(windows))]
18+
pub(crate) fn read_dir_nofollow(start: &fs::File, path: &Path) -> io::Result<ReadDir> {
19+
Ok(ReadDir {
20+
inner: ReadDirInner::new(start, path, FollowSymlinks::No)?,
1221
})
1322
}
1423

@@ -23,9 +32,14 @@ pub fn read_base_dir(start: &fs::File) -> io::Result<ReadDir> {
2332

2433
/// Like `read_dir`, but doesn't perform sandboxing.
2534
#[inline]
26-
pub(crate) fn read_dir_unchecked(start: &fs::File, path: &Path) -> io::Result<ReadDir> {
35+
#[cfg(not(windows))]
36+
pub(crate) fn read_dir_unchecked(
37+
start: &fs::File,
38+
path: &Path,
39+
follow: FollowSymlinks,
40+
) -> io::Result<ReadDir> {
2741
Ok(ReadDir {
28-
inner: ReadDirInner::new_unchecked(start, path)?,
42+
inner: ReadDirInner::new_unchecked(start, path, follow)?,
2943
})
3044
}
3145

cap-primitives/src/rustix/fs/dir_entry_inner.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::fs::{FileType, FileTypeExt, Metadata, OpenOptions, ReadDir, ReadDirInner};
1+
use crate::fs::{
2+
FileType, FileTypeExt, FollowSymlinks, Metadata, OpenOptions, ReadDir, ReadDirInner,
3+
};
24
use rustix::fs::DirEntry;
35
use std::ffi::{OsStr, OsString};
46
#[cfg(unix)]
@@ -29,8 +31,8 @@ impl DirEntryInner {
2931
}
3032

3133
#[inline]
32-
pub(crate) fn read_dir(&self) -> io::Result<ReadDir> {
33-
self.read_dir.read_dir(self.file_name_bytes())
34+
pub(crate) fn read_dir(&self, follow: FollowSymlinks) -> io::Result<ReadDir> {
35+
self.read_dir.read_dir(self.file_name_bytes(), follow)
3436
}
3537

3638
#[inline]

cap-primitives/src/rustix/fs/read_dir_inner.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ pub(crate) struct ReadDirInner {
2222
}
2323

2424
impl ReadDirInner {
25-
pub(crate) fn new(start: &fs::File, path: &Path) -> io::Result<Self> {
26-
let dir = Dir::from(open_dir_for_reading(start, path)?)?;
25+
pub(crate) fn new(start: &fs::File, path: &Path, follow: FollowSymlinks) -> io::Result<Self> {
26+
let dir = Dir::from(open_dir_for_reading(start, path, follow)?)?;
2727
Ok(Self {
2828
raw_fd: dir.as_fd().as_raw_fd(),
2929
rustix: Arc::new(Mutex::new(dir)),
@@ -38,15 +38,20 @@ impl ReadDirInner {
3838
let dir = Dir::from(open_dir_for_reading_unchecked(
3939
start,
4040
Component::CurDir.as_ref(),
41+
FollowSymlinks::No,
4142
)?)?;
4243
Ok(Self {
4344
raw_fd: dir.as_fd().as_raw_fd(),
4445
rustix: Arc::new(Mutex::new(dir)),
4546
})
4647
}
4748

48-
pub(crate) fn new_unchecked(start: &fs::File, path: &Path) -> io::Result<Self> {
49-
let dir = open_dir_for_reading_unchecked(start, path)?;
49+
pub(crate) fn new_unchecked(
50+
start: &fs::File,
51+
path: &Path,
52+
follow: FollowSymlinks,
53+
) -> io::Result<Self> {
54+
let dir = open_dir_for_reading_unchecked(start, path, follow)?;
5055
Ok(Self {
5156
raw_fd: dir.as_fd().as_raw_fd(),
5257
rustix: Arc::new(Mutex::new(Dir::from(dir)?)),
@@ -73,8 +78,12 @@ impl ReadDirInner {
7378
Metadata::from_file(&self.as_file_view())
7479
}
7580

76-
pub(super) fn read_dir(&self, file_name: &OsStr) -> io::Result<ReadDir> {
77-
read_dir_unchecked(&self.as_file_view(), file_name.as_ref())
81+
pub(super) fn read_dir(
82+
&self,
83+
file_name: &OsStr,
84+
follow: FollowSymlinks,
85+
) -> io::Result<ReadDir> {
86+
read_dir_unchecked(&self.as_file_view(), file_name.as_ref(), follow)
7887
}
7988

8089
#[allow(unsafe_code)]

cap-primitives/src/rustix/fs/remove_dir_all_impl.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::fs::{
2-
read_dir, read_dir_unchecked, remove_dir, remove_file, remove_open_dir, stat, FollowSymlinks,
3-
ReadDir,
2+
read_dir_nofollow, read_dir_unchecked, remove_dir, remove_file, remove_open_dir, stat,
3+
FollowSymlinks, ReadDir,
44
};
55
use std::path::{Component, Path};
66
use std::{fs, io};
@@ -13,21 +13,25 @@ pub(crate) fn remove_dir_all_impl(start: &fs::File, path: &Path) -> io::Result<(
1313
if filetype.is_symlink() {
1414
remove_file(start, path)
1515
} else {
16-
remove_dir_all_recursive(read_dir(start, path)?)?;
16+
remove_dir_all_recursive(read_dir_nofollow(start, path)?)?;
1717
remove_dir(start, path)
1818
}
1919
}
2020

2121
pub(crate) fn remove_open_dir_all_impl(dir: fs::File) -> io::Result<()> {
22-
remove_dir_all_recursive(read_dir_unchecked(&dir, Component::CurDir.as_ref())?)?;
22+
remove_dir_all_recursive(read_dir_unchecked(
23+
&dir,
24+
Component::CurDir.as_ref(),
25+
FollowSymlinks::No,
26+
)?)?;
2327
remove_open_dir(dir)
2428
}
2529

2630
fn remove_dir_all_recursive(children: ReadDir) -> io::Result<()> {
2731
for child in children {
2832
let child = child?;
2933
if child.file_type()?.is_dir() {
30-
remove_dir_all_recursive(child.read_dir()?)?;
34+
remove_dir_all_recursive(child.inner.read_dir(FollowSymlinks::No)?)?;
3135
child.remove_dir()?;
3236
} else {
3337
child.remove_file()?;

cap-primitives/src/rustix/fs/remove_open_dir_by_searching.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::fs::{errors, is_root_dir, read_dir_unchecked, Metadata};
1+
use crate::fs::{errors, is_root_dir, read_dir_unchecked, FollowSymlinks, Metadata};
22
use std::path::Component;
33
use std::{fs, io};
44

@@ -7,7 +7,7 @@ use std::{fs, io};
77
/// available.
88
pub(crate) fn remove_open_dir_by_searching(dir: fs::File) -> io::Result<()> {
99
let metadata = Metadata::from_file(&dir)?;
10-
let mut iter = read_dir_unchecked(&dir, Component::ParentDir.as_ref())?;
10+
let mut iter = read_dir_unchecked(&dir, Component::ParentDir.as_ref(), FollowSymlinks::No)?;
1111
while let Some(child) = iter.next() {
1212
let child = child?;
1313
if child.is_same_file(&metadata)? {

cap-primitives/src/windows/fs/dir_entry_inner.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,12 @@ impl DirEntryInner {
6868
}
6969

7070
#[inline]
71-
pub(crate) fn read_dir(&self) -> io::Result<ReadDir> {
71+
pub(crate) fn read_dir(&self, follow: FollowSymlinks) -> io::Result<ReadDir> {
72+
assert_eq!(
73+
follow,
74+
FollowSymlinks::Yes,
75+
"`read_dir` without following symlinks is not implemented yet"
76+
);
7277
let std = fs::read_dir(self.std.path())?;
7378
let inner = ReadDirInner::from_std(std);
7479
Ok(ReadDir { inner })

cap-primitives/src/windows/fs/read_dir_inner.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::get_path::concatenate_or_return_absolute;
2-
use crate::fs::{open_dir, DirEntryInner};
2+
use crate::fs::{open_dir, DirEntryInner, FollowSymlinks};
33
use std::path::{Component, Path};
44
use std::{fmt, fs, io};
55

@@ -8,7 +8,12 @@ pub(crate) struct ReadDirInner {
88
}
99

1010
impl ReadDirInner {
11-
pub(crate) fn new(start: &fs::File, path: &Path) -> io::Result<Self> {
11+
pub(crate) fn new(start: &fs::File, path: &Path, follow: FollowSymlinks) -> io::Result<Self> {
12+
assert_eq!(
13+
follow,
14+
FollowSymlinks::Yes,
15+
"`read_dir` without following symlinks is not implemented yet"
16+
);
1217
let dir = open_dir(start, path)?;
1318
Self::new_unchecked(&dir, Component::CurDir.as_ref())
1419
}
Lines changed: 18 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,30 @@
1-
use crate::fs::{
2-
read_dir_unchecked, remove_dir, remove_file, remove_open_dir, stat, FollowSymlinks,
3-
};
1+
use super::get_path::get_path;
2+
use crate::fs::{open_dir, open_dir_nofollow, remove_dir, stat, FollowSymlinks};
43
#[cfg(windows_file_type_ext)]
54
use std::os::windows::fs::FileTypeExt;
6-
use std::path::{Component, Path};
5+
use std::path::Path;
76
use std::{fs, io};
87

98
pub(crate) fn remove_dir_all_impl(start: &fs::File, path: &Path) -> io::Result<()> {
10-
// Code derived from `remove_dir_all` in Rust's
11-
// library/std/src/sys/windows/fs.rs at revision
12-
// 108e90ca78f052c0c1c49c42a22c85620be19712.
13-
let filetype = stat(start, path, FollowSymlinks::No)?.file_type();
14-
if filetype.is_symlink() {
15-
// On Windows symlinks to files and directories are removed
16-
// differently. `remove_dir` only deletes dir symlinks and junctions,
17-
// not file symlinks.
9+
// Open the directory, following symlinks, to make sure it is a directory.
10+
let file = open_dir(start, path)?;
11+
// Test whether the path is a symlink.
12+
let md = stat(start, path, FollowSymlinks::No)?;
13+
drop(file);
14+
if md.is_symlink() {
15+
// If so, just remove the link.
1816
remove_dir(start, path)
1917
} else {
20-
remove_dir_all_recursive(start, path)?;
21-
remove_dir(start, path)
18+
// Otherwise, remove the tree.
19+
let dir = open_dir_nofollow(start, path)?;
20+
remove_open_dir_all_impl(dir)
2221
}
2322
}
2423

2524
pub(crate) fn remove_open_dir_all_impl(dir: fs::File) -> io::Result<()> {
26-
remove_dir_all_recursive(&dir, Component::CurDir.as_ref())?;
27-
remove_open_dir(dir)
28-
}
29-
30-
#[cfg(windows_file_type_ext)]
31-
fn remove_dir_all_recursive(start: &fs::File, path: &Path) -> io::Result<()> {
32-
// Code derived from `remove_dir_all_recursive` in Rust's
33-
// library/std/src/sys/windows/fs.rs at revision
34-
// 108e90ca78f052c0c1c49c42a22c85620be19712.
35-
for child in read_dir_unchecked(start, path)? {
36-
let child = child?;
37-
let child_type = child.file_type()?;
38-
if child_type.is_dir() {
39-
let path = path.join(child.file_name());
40-
remove_dir_all_recursive(start, &path)?;
41-
remove_dir(start, &path)?;
42-
} else if child_type.is_symlink_dir() {
43-
remove_dir(start, &path.join(child.file_name()))?;
44-
} else {
45-
remove_file(start, &path.join(child.file_name()))?;
46-
}
47-
}
48-
Ok(())
49-
}
50-
51-
#[cfg(not(windows_file_type_ext))]
52-
fn remove_dir_all_recursive(start: &fs::File, path: &Path) -> io::Result<()> {
53-
for child in read_dir_unchecked(start, path)? {
54-
let child = child?;
55-
let child_type = child.file_type()?;
56-
if child_type.is_dir() {
57-
let path = path.join(child.file_name());
58-
remove_dir_all_recursive(start, &path)?;
59-
remove_dir(start, &path)?;
60-
} else {
61-
match remove_dir(start, &path.join(child.file_name())) {
62-
Ok(()) => (),
63-
Err(e) => {
64-
if e.raw_os_error() == Some(winapi::shared::winerror::ERROR_DIRECTORY as i32) {
65-
remove_file(start, &path.join(child.file_name()))?;
66-
} else {
67-
return Err(e);
68-
}
69-
}
70-
}
71-
}
72-
}
73-
Ok(())
25+
// Close the directory so that we can delete it. This is racy; see the
26+
// comments in `remove_open_dir_impl` for details.
27+
let path = get_path(&dir)?;
28+
drop(dir);
29+
fs::remove_dir_all(&path)
7430
}

cap-tempfile/src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,11 @@ fn close_outer() {
218218
let t = tempdir(ambient_authority()).unwrap();
219219
let _s = tempdir_in(&t).unwrap();
220220
#[cfg(windows)]
221-
assert_eq!(
222-
t.close().unwrap_err().raw_os_error(),
223-
Some(winapi::shared::winerror::ERROR_SHARING_VIOLATION as i32)
224-
);
221+
assert!(matches!(
222+
t.close().unwrap_err().raw_os_error().map(|err| err as _),
223+
Some(winapi::shared::winerror::ERROR_SHARING_VIOLATION)
224+
| Some(winapi::shared::winerror::ERROR_DIR_NOT_EMPTY)
225+
));
225226
#[cfg(not(windows))]
226227
t.close().unwrap();
227228
}

cap-tempfile/src/utf8.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,11 @@ fn close_outer() {
214214
let t = tempdir(ambient_authority()).unwrap();
215215
let _s = tempdir_in(&t).unwrap();
216216
#[cfg(windows)]
217-
assert_eq!(
218-
t.close().unwrap_err().raw_os_error(),
219-
Some(winapi::shared::winerror::ERROR_SHARING_VIOLATION as i32)
220-
);
217+
assert!(matches!(
218+
t.close().unwrap_err().raw_os_error().map(|err| err as _),
219+
Some(winapi::shared::winerror::ERROR_SHARING_VIOLATION)
220+
| Some(winapi::shared::winerror::ERROR_DIR_NOT_EMPTY)
221+
));
221222
#[cfg(not(windows))]
222223
t.close().unwrap();
223224
}

0 commit comments

Comments
 (0)