Skip to content

Commit ae7a077

Browse files
committed
Add a test for Windows filesystem behavior that we depend on.
1 parent 184fedf commit ae7a077

File tree

11 files changed

+141
-2
lines changed

11 files changed

+141
-2
lines changed

cap-async-std/src/fs/dir.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ pub struct Dir {
4646

4747
impl Dir {
4848
/// Constructs a new instance of `Self` from the given `async_std::fs::File`.
49+
///
50+
/// To prevent race conditions on Windows, the file must be opened without
51+
/// `FILE_SHARE_DELETE`.
4952
#[inline]
5053
pub fn from_std_file(std_file: fs::File) -> Self {
5154
Self { std_file }
@@ -624,6 +627,8 @@ impl FromRawFd for Dir {
624627

625628
#[cfg(windows)]
626629
impl FromRawHandle for Dir {
630+
/// To prevent race conditions on Windows, the handle must be opened without
631+
/// `FILE_SHARE_DELETE`.
627632
#[inline]
628633
unsafe fn from_raw_handle(handle: RawHandle) -> Self {
629634
Self::from_std_file(fs::File::from_raw_handle(handle))

cap-async-std/src/fs_utf8/dir.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ pub struct Dir {
3131

3232
impl Dir {
3333
/// Constructs a new instance of `Self` from the given `async_std::fs::File`.
34+
///
35+
/// To prevent race conditions on Windows, the file must be opened without
36+
/// `FILE_SHARE_DELETE`.
3437
#[inline]
3538
pub fn from_std_file(std_file: fs::File) -> Self {
3639
Self::from_cap_std(crate::fs::Dir::from_std_file(std_file))
@@ -543,6 +546,8 @@ impl FromRawFd for Dir {
543546

544547
#[cfg(windows)]
545548
impl FromRawHandle for Dir {
549+
/// To prevent race conditions on Windows, the handle must be opened without
550+
/// `FILE_SHARE_DELETE`.
546551
#[inline]
547552
unsafe fn from_raw_handle(handle: RawHandle) -> Self {
548553
Self::from_std_file(fs::File::from_raw_handle(handle))

cap-primitives/src/fs/open_options.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ impl std::os::windows::fs::OpenOptionsExt for OpenOptions {
174174
self
175175
}
176176

177+
/// To prevent race conditions on Windows, handles for directories must be opened
178+
/// without `FILE_SHARE_DELETE`.
177179
#[inline]
178180
fn share_mode(&mut self, val: u32) -> &mut Self {
179181
self.ext.share_mode(val);

cap-primitives/src/winx/fs/dir_utils.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::{
99
},
1010
path::{Path, PathBuf},
1111
};
12+
use winapi::um::winnt;
1213
use winx::file::Flags;
1314

1415
/// Rust's `Path` implicitly strips redundant slashes and `.` components, however
@@ -46,10 +47,14 @@ pub(crate) fn strip_dir_suffix(path: &Path) -> impl Deref<Target = Path> + '_ {
4647

4748
/// Return an `OpenOptions` for opening directories.
4849
pub(crate) fn dir_options() -> OpenOptions {
50+
// Set `FILE_FLAG_BACKUP_SEMANTICS` so that we can open directories. Unset
51+
// `FILE_SHARE_DELETE` so that directories can't be renamed or deleted
52+
// underneath us, since we use paths to implement many directory operations.
4953
OpenOptions::new()
5054
.read(true)
5155
.dir_required(true)
5256
.custom_flags(Flags::FILE_FLAG_BACKUP_SEMANTICS.bits())
57+
.share_mode(winnt::FILE_SHARE_READ | winnt::FILE_SHARE_WRITE)
5358
.clone()
5459
}
5560

cap-primitives/src/winx/fs/open_options_ext.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ impl std::os::windows::fs::OpenOptionsExt for OpenOptionsExt {
2929
self
3030
}
3131

32+
/// To prevent race conditions on Windows, handles must be opened without
33+
/// `FILE_SHARE_DELETE`.
3234
fn share_mode(&mut self, share: u32) -> &mut Self {
3335
self.share_mode = share;
3436
self

cap-primitives/src/winx/fs/remove_open_dir_impl.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,22 @@ use std::{fs, io};
33

44
pub(crate) fn remove_open_dir_impl(dir: fs::File) -> io::Result<()> {
55
let path = get_path(&dir)?;
6+
7+
// Drop the directory before removing it, since we open directories without
8+
// `FILE_SHARE_DELETE`, and removing it requires accessing via its name
9+
// rather than its handle.
10+
//
11+
// There is a window here in which another process could remove or rename
12+
// a directory with this path after the handle is dropped, however it's
13+
// unlikely to happen by accident, and unlikely to cause major problems.
14+
// It may cause spurious failures, or failures with different error codes,
15+
// but this appears to be unaoidable.
16+
//
17+
// Even if we did have `FILE_SHARE_DELETE` and we kept the handle open
18+
// while doing the `remove_dir, `FILE_SHARE_DELETE` would grant other
19+
// processes the right to remove or rename the directory. So there
20+
// doesn't seem to be a race-free way of removing opened directories.
21+
drop(dir);
22+
623
fs::remove_dir(path)
724
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ pub(crate) fn remove_open_dir_by_searching(dir: fs::File) -> io::Result<()> {
1010
while let Some(child) = iter.next() {
1111
let child = child?;
1212
if child.is_same_file(&metadata)? {
13-
drop(dir);
1413
return child.remove_dir();
1514
}
1615
}

cap-std/src/fs/dir.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ pub struct Dir {
4646

4747
impl Dir {
4848
/// Constructs a new instance of `Self` from the given `std::fs::File`.
49+
///
50+
/// To prevent race conditions on Windows, the file must be opened without
51+
/// `FILE_SHARE_DELETE`.
4952
#[inline]
5053
pub fn from_std_file(std_file: fs::File) -> Self {
5154
Self { std_file }
@@ -602,6 +605,8 @@ impl FromRawFd for Dir {
602605

603606
#[cfg(windows)]
604607
impl FromRawHandle for Dir {
608+
/// To prevent race conditions on Windows, the handle must be opened without
609+
/// `FILE_SHARE_DELETE`.
605610
#[inline]
606611
unsafe fn from_raw_handle(handle: RawHandle) -> Self {
607612
Self::from_std_file(fs::File::from_raw_handle(handle))

cap-std/src/fs_utf8/dir.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ pub struct Dir {
3030

3131
impl Dir {
3232
/// Constructs a new instance of `Self` from the given `std::fs::File`.
33+
///
34+
/// To prevent race conditions on Windows, the file must be opened without
35+
/// `FILE_SHARE_DELETE`.
3336
#[inline]
3437
pub fn from_std_file(std_file: fs::File) -> Self {
3538
Self::from_cap_std(crate::fs::Dir::from_std_file(std_file))
@@ -556,6 +559,8 @@ impl FromRawFd for Dir {
556559

557560
#[cfg(windows)]
558561
impl FromRawHandle for Dir {
562+
/// To prevent race conditions on Windows, the handle must be opened without
563+
/// `FILE_SHARE_DELETE`.
559564
#[inline]
560565
unsafe fn from_raw_handle(handle: RawHandle) -> Self {
561566
Self::from_std_file(fs::File::from_raw_handle(handle))

cap-tempfile/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ fn close_outer() {
191191
#[cfg(windows)]
192192
assert_eq!(
193193
t.close().unwrap_err().raw_os_error(),
194-
Some(winapi::shared::winerror::ERROR_DIR_NOT_EMPTY as i32)
194+
Some(winapi::shared::winerror::ERROR_SHARING_VIOLATION as i32)
195195
);
196196
#[cfg(not(windows))]
197197
t.close().unwrap();

tests/windows-open.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! On Windows, cap-std uses the technique of looking up absolute paths
2+
//! for file and directory handles. This would be racy, except that Windows
3+
//! doesn't allow open files or directories to be removed or renamed. Test
4+
//! that this is the case.
5+
6+
#[cfg(windows)]
7+
#[macro_use]
8+
mod sys_common;
9+
10+
#[cfg(windows)]
11+
use sys_common::io::tmpdir;
12+
13+
#[test]
14+
#[cfg(windows)]
15+
fn windows_open_one() {
16+
let tmpdir = tmpdir();
17+
check!(tmpdir.create_dir("aaa"));
18+
19+
let dir = tmpdir.open_dir("aaa");
20+
21+
// Attempts to remove or rename the open directory should fail.
22+
tmpdir.remove_dir("aaa").unwrap_err();
23+
tmpdir.rename("aaa", &tmpdir, "zzz").unwrap_err();
24+
25+
drop(dir);
26+
27+
// Now that we've droped the handle, the same operations should succeed.
28+
check!(tmpdir.rename("aaa", &tmpdir, "xxx"));
29+
check!(tmpdir.remove_dir("xxx"));
30+
}
31+
32+
#[test]
33+
#[cfg(windows)]
34+
fn windows_open_multiple() {
35+
let tmpdir = tmpdir();
36+
check!(tmpdir.create_dir_all("aaa/bbb"));
37+
38+
let dir = tmpdir.open_dir("aaa/bbb");
39+
40+
// Attempts to remove or rename any component of the open directory should fail.
41+
tmpdir.remove_dir("aaa/bbb").unwrap_err();
42+
tmpdir.remove_dir("aaa").unwrap_err();
43+
tmpdir.rename("aaa/bbb", &tmpdir, "aaa/yyy").unwrap_err();
44+
tmpdir.rename("aaa", &tmpdir, "zzz").unwrap_err();
45+
46+
drop(dir);
47+
48+
// Now that we've droped the handle, the same operations should succeed.
49+
check!(tmpdir.rename("aaa/bbb", &tmpdir, "aaa/www"));
50+
check!(tmpdir.rename("aaa", &tmpdir, "xxx"));
51+
check!(tmpdir.remove_dir("xxx/www"));
52+
check!(tmpdir.remove_dir("xxx"));
53+
}
54+
55+
/// Like `windows_open_multiple`, but does so within a directory that we
56+
/// can close and then independently mutate.
57+
#[test]
58+
#[cfg(windows)]
59+
fn windows_open_tricky() {
60+
let tmpdir = tmpdir();
61+
check!(tmpdir.create_dir("qqq"));
62+
63+
let qqq = check!(tmpdir.open_dir("qqq"));
64+
check!(qqq.create_dir_all("aaa/bbb"));
65+
66+
let dir = check!(qqq.open_dir("aaa/bbb"));
67+
68+
// Now drop `qqq`.
69+
drop(qqq);
70+
71+
// Attempts to remove or rename any component of the open directory should fail.
72+
dir.remove_dir("aaa/bbb").unwrap_err();
73+
dir.remove_dir("aaa").unwrap_err();
74+
dir.rename("aaa/bbb", &tmpdir, "aaa/yyy").unwrap_err();
75+
dir.rename("aaa", &tmpdir, "zzz").unwrap_err();
76+
tmpdir.remove_dir("qqq/aaa/bbb").unwrap_err();
77+
tmpdir.remove_dir("qqq/aaa").unwrap_err();
78+
tmpdir.remove_dir("qqq").unwrap_err();
79+
tmpdir
80+
.rename("qqq/aaa/bbb", &tmpdir, "qqq/aaa/yyy")
81+
.unwrap_err();
82+
tmpdir.rename("qqq/aaa", &tmpdir, "qqq/zzz").unwrap_err();
83+
tmpdir.rename("qqq", &tmpdir, "vvv").unwrap_err();
84+
85+
drop(dir);
86+
87+
// Now that we've droped the handle, the same operations should succeed.
88+
check!(tmpdir.rename("qqq/aaa/bbb", &tmpdir, "qqq/aaa/www"));
89+
check!(tmpdir.rename("qqq/aaa", &tmpdir, "qqq/xxx"));
90+
check!(tmpdir.rename("qqq", &tmpdir, "uuu"));
91+
check!(tmpdir.remove_dir("uuu/xxx/www"));
92+
check!(tmpdir.remove_dir("uuu/xxx"));
93+
check!(tmpdir.remove_dir("uuu"));
94+
}

0 commit comments

Comments
 (0)