Skip to content
Draft
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
8 changes: 5 additions & 3 deletions cap-primitives/src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,16 @@ pub use canonicalize::canonicalize;
pub use copy::copy;
pub use create_dir::create_dir;
pub use dir_builder::*;
pub use dir_entry::DirEntry;
#[cfg(windows)]
pub use dir_entry::_WindowsDirEntryExt;
pub use dir_entry::DirEntry;
pub use dir_options::DirOptions;
pub use file::FileExt;
#[cfg(windows)]
pub use file_type::_WindowsFileTypeExt;
pub use file_type::FileType;
#[cfg(any(unix, target_os = "vxworks", all(windows, windows_file_type_ext)))]
pub use file_type::FileTypeExt;
#[cfg(windows)]
pub use file_type::_WindowsFileTypeExt;
pub use follow_symlinks::FollowSymlinks;
pub use hard_link::hard_link;
pub use is_file_read_write::is_file_read_write;
Expand All @@ -94,6 +94,8 @@ pub use remove_dir_all::remove_dir_all;
pub use remove_file::remove_file;
pub use remove_open_dir::{remove_open_dir, remove_open_dir_all};
pub use rename::rename;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
pub use rename::rename_exclusive;
pub use reopen::reopen;
#[cfg(not(target_os = "wasi"))]
pub use set_permissions::{set_permissions, set_symlink_permissions};
Expand Down
83 changes: 70 additions & 13 deletions cap-primitives/src/fs/rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#[cfg(all(racy_asserts, not(windows)))]
use crate::fs::append_dir_suffix;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
use crate::fs::rename_excl_impl;
use crate::fs::rename_impl;
use std::path::Path;
use std::{fs, io};
Expand All @@ -14,6 +16,12 @@ use {
std::path::PathBuf,
};

#[cfg(all(
racy_asserts,
any(target_os = "macos", target_os = "linux", target_os = "redox")
))]
use crate::fs::rename_excl_unchecked;

/// Perform a `renameat`-like operation, ensuring that the resolution of both
/// the old and new paths never escape the directory tree rooted at their
/// respective starts.
Expand Down Expand Up @@ -51,6 +59,54 @@ pub fn rename(
&result,
&old_metadata_after,
&new_metadata_after,
rename_unchecked,
);

result
}

/// Perform a `renameat`-like operation, ensuring that the resolution of both
/// the old and new paths never escape the directory tree rooted at their
/// respective starts.
///
/// Unlike [`rename`], the rename fails if the target exists. The check is atomic on supported
/// platform which mitigates potential races (TOCTOU).
#[cfg_attr(not(racy_asserts), allow(clippy::let_and_return))]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
#[inline]
pub fn rename_exclusive(
old_start: &fs::File,
old_path: &Path,
new_start: &fs::File,
new_path: &Path,
) -> io::Result<()> {
#[cfg(racy_asserts)]
let (old_metadata_before, new_metadata_before) = (
stat_unchecked(old_start, old_path, FollowSymlinks::No),
stat_unchecked(new_start, new_path, FollowSymlinks::No),
);

// Call the underlying implementation.
let result = rename_excl_impl(old_start, old_path, new_start, new_path);

#[cfg(racy_asserts)]
let (old_metadata_after, new_metadata_after) = (
stat_unchecked(old_start, old_path, FollowSymlinks::No),
stat_unchecked(new_start, new_path, FollowSymlinks::No),
);

#[cfg(racy_asserts)]
check_rename(
old_start,
old_path,
new_start,
new_path,
&old_metadata_before,
&new_metadata_before,
&result,
&old_metadata_after,
&new_metadata_after,
rename_excl_unchecked,
);

result
Expand All @@ -69,6 +125,7 @@ fn check_rename(
result: &io::Result<()>,
old_metadata_after: &io::Result<Metadata>,
new_metadata_after: &io::Result<Metadata>,
rename_impl: impl Fn(&fs::File, &Path, &fs::File, &Path) -> io::Result<()>,
) {
use io::ErrorKind::*;

Expand Down Expand Up @@ -97,20 +154,20 @@ fn check_rename(
map_result(&canonicalize_for_rename(old_start, old_path)),
map_result(&canonicalize_for_rename(new_start, new_path)),
) {
(Ok(old_canon), Ok(new_canon)) => match map_result(&rename_unchecked(
old_start, &old_canon, new_start, &new_canon,
)) {
Err((_unchecked_kind, _unchecked_message)) => {
/* TODO: Check error messages.
assert_eq!(kind, unchecked_kind);
assert_eq!(message, unchecked_message);
*/
(Ok(old_canon), Ok(new_canon)) => {
match map_result(&rename_impl(old_start, &old_canon, new_start, &new_canon)) {
Err((_unchecked_kind, _unchecked_message)) => {
/* TODO: Check error messages.
assert_eq!(kind, unchecked_kind);
assert_eq!(message, unchecked_message);
*/
}
other => panic!(
"unsandboxed rename success:\n{:#?}\n{:?} {:?}",
other, kind, message
),
}
other => panic!(
"unsandboxed rename success:\n{:#?}\n{:?} {:?}",
other, kind, message
),
},
}
(Err((_old_canon_kind, _old_canon_message)), _) => {
/* TODO: Check error messages.
assert_eq!(kind, old_canon_kind);
Expand Down
2 changes: 2 additions & 0 deletions cap-primitives/src/fs/via_parent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub(crate) use read_link::read_link;
pub(crate) use remove_dir::remove_dir;
pub(crate) use remove_file::remove_file;
pub(crate) use rename::rename;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
pub(crate) use rename::rename_exclusive;
#[cfg(windows)]
pub(crate) use set_permissions::set_permissions;
#[cfg(not(target_os = "wasi"))]
Expand Down
32 changes: 30 additions & 2 deletions cap-primitives/src/fs/via_parent/rename.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::open_parent;
#[cfg(unix)]
use crate::fs::{append_dir_suffix, path_has_trailing_slash};
use crate::fs::{rename_unchecked, strip_dir_suffix, MaybeOwnedFile};
use crate::fs::{rename_excl_unchecked, rename_unchecked, strip_dir_suffix, MaybeOwnedFile};
use std::path::Path;
use std::{fs, io};

Expand All @@ -12,6 +12,34 @@ pub(crate) fn rename(
old_path: &Path,
new_start: &fs::File,
new_path: &Path,
) -> io::Result<()> {
do_rename(old_start, old_path, new_start, new_path, rename_unchecked)
}

/// Implement `rename_exclusive` by `open`ing up the parent component of the path and then
/// calling `rename_excl_unchecked` on the last component.
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
pub(crate) fn rename_exclusive(
old_start: &fs::File,
old_path: &Path,
new_start: &fs::File,
new_path: &Path,
) -> io::Result<()> {
do_rename(
old_start,
old_path,
new_start,
new_path,
rename_excl_unchecked,
)
}

fn do_rename(
old_start: &fs::File,
old_path: &Path,
new_start: &fs::File,
new_path: &Path,
rename_impl: impl Fn(&fs::File, &Path, &fs::File, &Path) -> io::Result<()>,
) -> io::Result<()> {
let old_start = MaybeOwnedFile::borrowed(old_start);
let new_start = MaybeOwnedFile::borrowed(new_start);
Expand Down Expand Up @@ -41,7 +69,7 @@ pub(crate) fn rename(
old_basename
};

rename_unchecked(
rename_impl(
&old_dir,
old_basename.as_ref(),
&new_dir,
Expand Down
6 changes: 6 additions & 0 deletions cap-primitives/src/rustix/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ mod remove_dir_all_impl;
mod remove_dir_unchecked;
mod remove_file_unchecked;
mod remove_open_dir_by_searching;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
mod rename_excl_unchecked;
mod rename_unchecked;
mod reopen_impl;
#[cfg(not(any(target_os = "android", target_os = "linux", target_os = "wasi")))]
Expand Down Expand Up @@ -106,6 +108,8 @@ pub(crate) use crate::fs::{
via_parent::symlink as symlink_impl,
remove_open_dir_by_searching as remove_open_dir_impl,
};
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
pub(crate) use crate::fs::via_parent::rename_exclusive as rename_excl_impl;
#[cfg(not(target_os = "wasi"))]
pub(crate) use crate::fs::via_parent::set_symlink_permissions as set_symlink_permissions_impl;
#[cfg(not(target_os = "freebsd"))]
Expand Down Expand Up @@ -138,6 +142,8 @@ pub(crate) use remove_dir_all_impl::{remove_dir_all_impl, remove_open_dir_all_im
pub(crate) use remove_dir_unchecked::remove_dir_unchecked;
pub(crate) use remove_file_unchecked::remove_file_unchecked;
pub(crate) use remove_open_dir_by_searching::remove_open_dir_by_searching;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
pub(crate) use rename_excl_unchecked::rename_excl_unchecked;
pub(crate) use rename_unchecked::rename_unchecked;
pub(crate) use reopen_impl::reopen_impl;
pub(crate) use stat_unchecked::stat_unchecked;
Expand Down
19 changes: 19 additions & 0 deletions cap-primitives/src/rustix/fs/rename_excl_unchecked.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use rustix::fs::{renameat_with, RenameFlags};
use std::path::Path;
use std::{fs, io};

pub(crate) fn rename_excl_unchecked(
old_start: &fs::File,
old_path: &Path,
new_start: &fs::File,
new_path: &Path,
) -> io::Result<()> {
renameat_with(
old_start,
old_path,
new_start,
new_path,
RenameFlags::NOREPLACE,
)
.map_err(Into::into)
}
2 changes: 2 additions & 0 deletions cap-primitives/src/windows/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod remove_dir_all_impl;
mod remove_dir_unchecked;
mod remove_file_unchecked;
mod remove_open_dir_impl;
mod rename_excl_unchecked;
mod rename_unchecked;
mod reopen_impl;
mod set_permissions_unchecked;
Expand Down Expand Up @@ -69,6 +70,7 @@ pub(crate) use remove_dir_all_impl::*;
pub(crate) use remove_dir_unchecked::*;
pub(crate) use remove_file_unchecked::*;
pub(crate) use remove_open_dir_impl::*;
pub(crate) use rename_excl_unchecked::*;
pub(crate) use rename_unchecked::*;
pub(crate) use reopen_impl::reopen_impl;
pub(crate) use set_permissions_unchecked::*;
Expand Down
93 changes: 93 additions & 0 deletions cap-primitives/src/windows/fs/rename_excl_unchecked.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use super::get_path::concatenate;
use std::{fs, io, iter, path::Path, ptr};
use windows_sys::Win32::{
Foundation::{LocalFree, FALSE},
Security::{
Authorization::{GetNamedSecurityInfoW, SetNamedSecurityInfoW, SE_FILE_OBJECT},
ACL, DACL_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION,
PSECURITY_DESCRIPTOR, PSID,
},
Storage::FileSystem::{MoveFileExW, MOVEFILE_COPY_ALLOWED},
};

pub(crate) fn rename_excl_unchecked(
old_start: &fs::File,
old_path: &Path,
new_start: &fs::File,
new_path: &Path,
) -> io::Result<()> {
let old_full_path: Vec<u16> = concatenate(old_start, old_path)?
.into_iter()
.chain(iter::once(0u16))
.collect();
let new_full_path: Vec<u16> = concatenate(new_start, new_path)?
.into_iter()
.chain(iter::once(0u16))
.collect();

// Save permissions in case file will be moved across volumes
// https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-getnamedsecurityinfoa
// https://learn.microsoft.com/en-us/windows/win32/secauthz/security-information
let mut owner: PSID = ptr::null_mut();
let mut group: PSID = ptr::null_mut();
let mut dacl: ACL = ptr::null_mut();
// According to the docs, the pointers above are pointers into the PSECURITY_DESCRIPTOR
// struct, so `security` should only be freed after using `owner`, `group`, and `dacl`
let mut security: PSECURITY_DESCRIPTOR = ptr::null_mut();

let move_result = unsafe {
// SAFETY: `old_full_path` is a valid pointer to a NUL terminated wide string
GetNamedSecurityInfoW(
old_full_path.as_ptr(),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&mut owner,
&mut group,
&mut dacl,
ptr::null_mut(),
&mut security,
)
};

// Set/GetNamedSecurityInfoW return the error code directly rather than a bool
if move_result != 0 {
return Err(io::Error::from_raw_os_error(move_result));
}

unsafe {
// SAFETY:
// * `concatenate` calls `get_path` which calls `encode_wide` so we have a wide string
// * Both paths are NUL terminated above
if MoveFileExW(
old_full_path.as_ptr(),
new_full_path.as_ptr(),
MOVEFILE_COPY_ALLOWED,
) == FALSE
{
LocalFree(security);
return Err(io::Error::last_os_error());
}
}

let set_result = unsafe {
SetNamedSecurityInfoW(
new_full_path.as_ptr(),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
owner,
group,
dacl,
ptr::null_mut(),
)
};

unsafe {
LocalFree(security);
}

if set_result != 0 {
Err(io::Error::from_raw_os_error(set_result))
} else {
Ok(())
}
}
14 changes: 14 additions & 0 deletions cap-std/src/fs/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use crate::fs::{DirBuilder, File, Metadata, OpenOptions, ReadDir};
use crate::fs_utf8::Dir as DirUtf8;
#[cfg(unix)]
use crate::os::unix::net::{UnixDatagram, UnixListener, UnixStream};
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
use cap_primitives::fs::rename_exclusive;
#[cfg(not(target_os = "wasi"))]
use cap_primitives::fs::set_permissions;
use cap_primitives::fs::{
Expand Down Expand Up @@ -397,6 +399,18 @@ impl Dir {
rename(&self.std_file, from.as_ref(), &to_dir.std_file, to.as_ref())
}

/// Rename a file or a directory to a new name but only if the target does not exist.
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "redox"))]
#[inline]
pub fn rename_exclusive<P: AsRef<Path>, Q: AsRef<Path>>(
&self,
from: P,
to_dir: &Self,
to: Q,
) -> io::Result<()> {
rename_exclusive(&self.std_file, from.as_ref(), &to_dir.std_file, to.as_ref())
}

/// Changes the permissions found on a file or a directory.
///
/// This corresponds to [`std::fs::set_permissions`], but only accesses
Expand Down
Loading
Loading