Skip to content

Commit 7007ee7

Browse files
committed
Make remove_file_or_symlink_dir non-racy.
On Windows, `remove_file` doesn't remove the file until the last handle is closed, so hold the handle open while we check the symlink's type to avoid race conditions.
1 parent 86ad757 commit 7007ee7

File tree

4 files changed

+101
-32
lines changed

4 files changed

+101
-32
lines changed

cap-fs-ext/src/dir_ext.rs

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,9 @@ pub trait DirExtUtf8 {
167167

168168
/// Removes a file or symlink from a filesystem.
169169
///
170-
/// Removal of symlinks has different behavior under Windows - if a symlink
171-
/// points to a directory, it cannot be removed with the remove_file
172-
/// operation. This method will remove files and all symlinks.
170+
/// This is similar to [`std::fs::remove_file`], except that it also works
171+
/// on symlinks to directories on Windows, similar to how `unlink` works
172+
/// on symlinks to directories on Posix-ish platforms.
173173
fn remove_file_or_symlink<P: AsRef<str>>(&self, path: P) -> io::Result<()>;
174174
}
175175

@@ -262,37 +262,39 @@ impl DirExt for cap_std::fs::Dir {
262262
#[cfg(windows)]
263263
#[inline]
264264
fn remove_file_or_symlink<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
265-
// This operation may race, because it checks the metadata before deleting
266-
// the symlink. We tried to do this atomically by ReOpenFile with DELETE_ON_CLOSE but could
267-
// not get it to work.
268-
fn delete_symlink_to_dir(dir: &cap_std::fs::Dir, path: &Path) -> io::Result<()> {
269-
use crate::{FollowSymlinks, OpenOptionsFollowExt};
270-
use cap_std::fs::OpenOptions;
271-
use std::os::windows::fs::OpenOptionsExt;
272-
use winapi::um::winbase::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT};
273-
274-
let mut opts = OpenOptions::new();
275-
opts.read(true);
276-
opts.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS);
277-
opts.follow(FollowSymlinks::No);
278-
let file = dir.open_with(path, &opts)?;
279-
280-
let meta = file.metadata()?;
281-
if meta.file_type().is_symlink() {
282-
drop(file);
283-
// Symlinks that point to directories use the remove_dir, not remove_file,
284-
// operation on windows:
285-
dir.remove_dir(path)?;
286-
Ok(())
287-
} else {
288-
Err(io::Error::from_raw_os_error(
289-
winapi::shared::winerror::ERROR_DIRECTORY as i32,
290-
))
291-
}
265+
use crate::{FollowSymlinks, OpenOptionsFollowExt};
266+
use cap_primitives::fs::_WindowsByHandle;
267+
use cap_std::fs::OpenOptions;
268+
use std::os::windows::fs::OpenOptionsExt;
269+
use winapi::um::{
270+
winbase::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT},
271+
winnt::{DELETE, FILE_ATTRIBUTE_DIRECTORY},
272+
};
273+
let path = path.as_ref();
274+
275+
let mut opts = OpenOptions::new();
276+
opts.access_mode(DELETE);
277+
opts.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS);
278+
opts.follow(FollowSymlinks::No);
279+
let file = self.open_with(path, &opts)?;
280+
281+
let meta = file.metadata()?;
282+
if meta.file_type().is_symlink()
283+
&& unsafe { meta.file_attributes() } & FILE_ATTRIBUTE_DIRECTORY
284+
== FILE_ATTRIBUTE_DIRECTORY
285+
{
286+
self.remove_dir(path)?;
287+
} else {
288+
self.remove_file(path)?;
292289
}
293290

294-
self.remove_file(path.as_ref())
295-
.or_else(|e| delete_symlink_to_dir(&self, path.as_ref()).map_err(|_| e))
291+
// Drop the file after calling `remove_file` or `remove_dir`, since
292+
// Windows doesn't actually remove the file until after the last open
293+
// handle is closed, and this protects us from race conditions where
294+
// other processes replace the file out from underneath us.
295+
drop(file);
296+
297+
Ok(())
296298
}
297299
}
298300

cap-primitives/src/fs/metadata.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ impl std::os::windows::fs::MetadataExt for Metadata {
396396
#[cfg(windows)]
397397
#[doc(hidden)]
398398
pub unsafe trait _WindowsByHandle {
399+
unsafe fn file_attributes(&self) -> u32;
399400
unsafe fn volume_serial_number(&self) -> Option<u32>;
400401
unsafe fn number_of_links(&self) -> Option<u32>;
401402
unsafe fn file_index(&self) -> Option<u64>;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ impl std::os::windows::fs::MetadataExt for MetadataExt {
182182
#[cfg(windows)]
183183
#[doc(hidden)]
184184
unsafe impl crate::fs::_WindowsByHandle for crate::fs::Metadata {
185+
#[inline]
186+
unsafe fn file_attributes(&self) -> u32 {
187+
self.ext.file_attributes
188+
}
189+
185190
#[inline]
186191
unsafe fn volume_serial_number(&self) -> Option<u32> {
187192
self.ext.volume_serial_number

tests/dir-ext.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use cap_fs_ext::DirExt;
2+
use cap_tempfile::TempDir;
3+
4+
use sys_common::{io::tmpdir, symlink_supported};
5+
6+
#[test]
7+
fn remove_file() {
8+
let tempdir = unsafe { TempDir::new() }.expect("create tempdir");
9+
let file = tempdir.create("file").expect("create file to delete");
10+
drop(file);
11+
tempdir.remove_file_or_symlink("file").expect("delete file");
12+
assert!(!tempdir.exists("file"), "deletion worked");
13+
}
14+
15+
#[test]
16+
fn remove_symlink_to_file() {
17+
if !symlink_supported() {
18+
return;
19+
}
20+
21+
let tempdir = unsafe { TempDir::new() }.expect("create tempdir");
22+
let target = tempdir.create("target").expect("create target file");
23+
drop(target);
24+
tempdir.symlink("target", "link").expect("create symlink");
25+
assert!(tempdir.exists("link"), "link exists");
26+
tempdir
27+
.remove_file_or_symlink("link")
28+
.expect("delete symlink");
29+
assert!(!tempdir.exists("link"), "link deleted");
30+
assert!(tempdir.exists("target"), "target not deleted");
31+
}
32+
33+
#[test]
34+
fn remove_symlink_to_dir() {
35+
if !symlink_supported() {
36+
return;
37+
}
38+
39+
let tempdir = unsafe { TempDir::new() }.expect("create tempdir");
40+
let target = tempdir.create_dir("target").expect("create target dir");
41+
drop(target);
42+
tempdir.symlink("target", "link").expect("create symlink");
43+
assert!(tempdir.exists("link"), "link exists");
44+
tempdir
45+
.remove_file_or_symlink("link")
46+
.expect("delete symlink");
47+
assert!(!tempdir.exists("link"), "link deleted");
48+
assert!(tempdir.exists("target"), "target not deleted");
49+
}
50+
51+
#[test]
52+
fn do_not_remove_dir() {
53+
let tempdir = unsafe { TempDir::new() }.expect("create tempdir");
54+
let subdir = tempdir.create_dir("subdir").expect("create dir");
55+
drop(subdir);
56+
assert!(tempdir.exists("subdir"), "subdir created");
57+
tempdir
58+
.remove_file_or_symlink("subdir")
59+
.expect_err("should not delete empty directory");
60+
assert!(tempdir.exists("subdir"), "subdir not deleted");
61+
}

0 commit comments

Comments
 (0)