diff --git a/README.md b/README.md index dcecbf7f..941694a4 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,11 @@ utilize [`openat2`], [`O_PATH`], and [`/proc/self/fd`] (though only when /proc is mounted, it's really `procfs`, and there are no mounts on top of it) for fast path resolution as well. +On FreeBSD 13.0 and newer, `cap-std` uses [`openat(O_RESOLVE_BENEATH)`] to +implement `Dir::open` with a single system call in common cases. +Several other operations internally utilize `AT_RESOLVE_BENEATH` and `O_PATH` for +fast path resolution as well. + Otherwise, `cap-std` opens each component of a path individually, in order to specially handle `..` and symlinks. The algorithm is carefully designed to minimize system calls, so opening `red/green/blue` performs just 5 system @@ -177,6 +182,7 @@ and `green`. [`openat2`]: https://lwn.net/Articles/796868/ [`O_PATH`]: https://man7.org/linux/man-pages/man2/open.2.html [`/proc/self/fd`]: https://man7.org/linux/man-pages/man5/proc.5.html +[`openat(O_RESOLVE_BENEATH)`]: https://man.freebsd.org/cgi/man.cgi?openat ## What about networking? diff --git a/cap-primitives/src/fs/manually/mod.rs b/cap-primitives/src/fs/manually/mod.rs index c2c3bd32..34da44f9 100644 --- a/cap-primitives/src/fs/manually/mod.rs +++ b/cap-primitives/src/fs/manually/mod.rs @@ -5,7 +5,7 @@ mod canonical_path; mod canonicalize; mod cow_component; mod open; -#[cfg(not(windows))] +#[cfg(not(any(windows, target_os = "freebsd")))] mod open_entry; mod read_link_one; @@ -19,5 +19,5 @@ pub(super) use canonicalize::canonicalize_with; pub(crate) use canonicalize::canonicalize; pub(crate) use open::{open, stat}; -#[cfg(not(windows))] +#[cfg(not(any(windows, target_os = "freebsd")))] pub(crate) use open_entry::open_entry; diff --git a/cap-primitives/src/fs/maybe_owned_file.rs b/cap-primitives/src/fs/maybe_owned_file.rs index 2e1a888c..443cba24 100644 --- a/cap-primitives/src/fs/maybe_owned_file.rs +++ b/cap-primitives/src/fs/maybe_owned_file.rs @@ -120,7 +120,7 @@ impl<'borrow> MaybeOwnedFile<'borrow> { } /// Assuming `self` holds an owned `File`, return it. - #[cfg_attr(windows, allow(dead_code))] + #[cfg_attr(any(windows, target_os = "freebsd"), allow(dead_code))] pub(super) fn unwrap_owned(self) -> fs::File { match self.inner { MaybeOwned::Owned(file) => file, diff --git a/cap-primitives/src/rustix/freebsd/fs/check.rs b/cap-primitives/src/rustix/freebsd/fs/check.rs new file mode 100644 index 00000000..2a823e69 --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/fs/check.rs @@ -0,0 +1,26 @@ +use rustix::fs::{statat, AtFlags}; +use std::fs; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; + +static WORKING: AtomicBool = AtomicBool::new(false); +static CHECKED: AtomicBool = AtomicBool::new(false); + +#[inline] +pub(crate) fn beneath_supported(start: &fs::File) -> bool { + if WORKING.load(Relaxed) { + return true; + } + if CHECKED.load(Relaxed) { + return false; + } + // Unknown O_ flags get ignored but AT_ flags have strict checks, so we use that. + if let Err(rustix::io::Errno::INVAL) = + statat(start, "", AtFlags::EMPTY_PATH | AtFlags::RESOLVE_BENEATH) + { + CHECKED.store(true, Relaxed); + false + } else { + WORKING.store(true, Relaxed); + true + } +} diff --git a/cap-primitives/src/rustix/freebsd/fs/mod.rs b/cap-primitives/src/rustix/freebsd/fs/mod.rs new file mode 100644 index 00000000..2f1ef261 --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/fs/mod.rs @@ -0,0 +1,18 @@ +mod check; +mod open_entry_impl; +mod open_impl; +mod remove_dir_impl; +mod remove_file_impl; +mod set_permissions_impl; +mod set_times_impl; +mod stat_impl; + +pub(crate) use crate::fs::manually::canonicalize as canonicalize_impl; +pub(crate) use check::beneath_supported; +pub(crate) use open_entry_impl::open_entry_impl; +pub(crate) use open_impl::open_impl; +pub(crate) use remove_dir_impl::remove_dir_impl; +pub(crate) use remove_file_impl::remove_file_impl; +pub(crate) use set_permissions_impl::set_permissions_impl; +pub(crate) use set_times_impl::{set_times_impl, set_times_nofollow_impl}; +pub(crate) use stat_impl::stat_impl; diff --git a/cap-primitives/src/rustix/freebsd/fs/open_entry_impl.rs b/cap-primitives/src/rustix/freebsd/fs/open_entry_impl.rs new file mode 100644 index 00000000..2bf5645b --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/fs/open_entry_impl.rs @@ -0,0 +1,12 @@ +use crate::fs::{open_impl, OpenOptions}; +use std::ffi::OsStr; +use std::{fs, io}; + +#[inline(always)] +pub(crate) fn open_entry_impl( + start: &fs::File, + path: &OsStr, + options: &OpenOptions, +) -> io::Result { + open_impl(start, path.as_ref(), options) +} diff --git a/cap-primitives/src/rustix/freebsd/fs/open_impl.rs b/cap-primitives/src/rustix/freebsd/fs/open_impl.rs new file mode 100644 index 00000000..7924f4d5 --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/fs/open_impl.rs @@ -0,0 +1,30 @@ +use super::super::super::fs::compute_oflags; +use crate::fs::{errors, manually, OpenOptions}; +use io_lifetimes::FromFd; +use rustix::fs::{openat, Mode, OFlags, RawMode}; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn open_impl( + start: &fs::File, + path: &Path, + options: &OpenOptions, +) -> io::Result { + if !super::beneath_supported(start) { + return manually::open(start, path, options); + } + + let oflags = compute_oflags(options)? | OFlags::RESOLVE_BENEATH; + + let mode = if oflags.contains(OFlags::CREATE) { + Mode::from_bits((options.ext.mode & 0o7777) as RawMode).unwrap() + } else { + Mode::empty() + }; + + match openat(start, path, oflags, mode) { + Ok(file) => Ok(fs::File::from_into_fd(file)), + Err(rustix::io::Errno::NOTCAPABLE) => Err(errors::escape_attempt()), + Err(err) => Err(err.into()), + } +} diff --git a/cap-primitives/src/rustix/freebsd/fs/remove_dir_impl.rs b/cap-primitives/src/rustix/freebsd/fs/remove_dir_impl.rs new file mode 100644 index 00000000..0c2fa4b2 --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/fs/remove_dir_impl.rs @@ -0,0 +1,16 @@ +use crate::fs::via_parent; +use rustix::fs::{unlinkat, AtFlags}; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn remove_dir_impl(start: &fs::File, path: &Path) -> io::Result<()> { + if !super::beneath_supported(start) { + return via_parent::remove_dir(start, path); + } + + Ok(unlinkat( + start, + path, + AtFlags::RESOLVE_BENEATH | AtFlags::REMOVEDIR, + )?) +} diff --git a/cap-primitives/src/rustix/freebsd/fs/remove_file_impl.rs b/cap-primitives/src/rustix/freebsd/fs/remove_file_impl.rs new file mode 100644 index 00000000..79e72287 --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/fs/remove_file_impl.rs @@ -0,0 +1,12 @@ +use crate::fs::via_parent; +use rustix::fs::{unlinkat, AtFlags}; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn remove_file_impl(start: &fs::File, path: &Path) -> io::Result<()> { + if !super::beneath_supported(start) { + return via_parent::remove_file(start, path); + } + + Ok(unlinkat(start, path, AtFlags::RESOLVE_BENEATH)?) +} diff --git a/cap-primitives/src/rustix/freebsd/fs/set_permissions_impl.rs b/cap-primitives/src/rustix/freebsd/fs/set_permissions_impl.rs new file mode 100644 index 00000000..47c36b7d --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/fs/set_permissions_impl.rs @@ -0,0 +1,22 @@ +use crate::fs::Permissions; +use rustix::fs::{chmodat, AtFlags, Mode}; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn set_permissions_impl( + start: &fs::File, + path: &Path, + perm: Permissions, +) -> io::Result<()> { + if !super::beneath_supported(start) { + return super::super::super::fs::set_permissions_manually(start, path, perm); + } + + Ok(chmodat( + start, + path, + Mode::from_raw_mode(perm.mode() as _), + AtFlags::RESOLVE_BENEATH, + )?) +} diff --git a/cap-primitives/src/rustix/freebsd/fs/set_times_impl.rs b/cap-primitives/src/rustix/freebsd/fs/set_times_impl.rs new file mode 100644 index 00000000..85dc8e60 --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/fs/set_times_impl.rs @@ -0,0 +1,45 @@ +use crate::fs::{to_timespec, via_parent, SystemTimeSpec}; +use rustix::fs::{utimensat, AtFlags, Timestamps}; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn set_times_impl( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + if !super::beneath_supported(start) { + return super::super::super::fs::set_times_manually(start, path, atime, mtime); + } + + let times = Timestamps { + last_access: to_timespec(atime)?, + last_modification: to_timespec(mtime)?, + }; + + Ok(utimensat(start, path, ×, AtFlags::RESOLVE_BENEATH)?) +} + +pub(crate) fn set_times_nofollow_impl( + start: &fs::File, + path: &Path, + atime: Option, + mtime: Option, +) -> io::Result<()> { + if !super::beneath_supported(start) { + return via_parent::set_times_nofollow(start, path, atime, mtime); + } + + let times = Timestamps { + last_access: to_timespec(atime)?, + last_modification: to_timespec(mtime)?, + }; + + Ok(utimensat( + start, + path, + ×, + AtFlags::RESOLVE_BENEATH | AtFlags::SYMLINK_NOFOLLOW, + )?) +} diff --git a/cap-primitives/src/rustix/freebsd/fs/stat_impl.rs b/cap-primitives/src/rustix/freebsd/fs/stat_impl.rs new file mode 100644 index 00000000..3dfe62b4 --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/fs/stat_impl.rs @@ -0,0 +1,22 @@ +use crate::fs::{manually, FollowSymlinks, Metadata, MetadataExt}; +use rustix::fs::{statat, AtFlags}; +use std::path::Path; +use std::{fs, io}; + +pub(crate) fn stat_impl( + start: &fs::File, + path: &Path, + follow: FollowSymlinks, +) -> io::Result { + if !super::beneath_supported(start) { + return manually::stat(start, path, follow); + } + + let flags = AtFlags::RESOLVE_BENEATH + | if follow == FollowSymlinks::Yes { + AtFlags::empty() + } else { + AtFlags::SYMLINK_NOFOLLOW + }; + Ok(MetadataExt::from_rustix(statat(start, path, flags)?)) +} diff --git a/cap-primitives/src/rustix/freebsd/mod.rs b/cap-primitives/src/rustix/freebsd/mod.rs new file mode 100644 index 00000000..aabc2afc --- /dev/null +++ b/cap-primitives/src/rustix/freebsd/mod.rs @@ -0,0 +1 @@ +pub(crate) mod fs; diff --git a/cap-primitives/src/rustix/fs/mod.rs b/cap-primitives/src/rustix/fs/mod.rs index e69edc19..29129d30 100644 --- a/cap-primitives/src/rustix/fs/mod.rs +++ b/cap-primitives/src/rustix/fs/mod.rs @@ -34,18 +34,18 @@ mod times; pub(crate) mod errors; -// On Linux, use optimized implementations of `open` and `stat` using `openat2` -// and `O_PATH` when available. +// On Linux, use optimized implementations based on +// `openat2` and `O_PATH` when available. // -// FreeBSD has a similar mechanism in `O_BENEATH`, however it appears to have -// different behavior on absolute and `..` paths in ways that make it -// unsuitable for `cap-std`'s style of sandboxing. For more information, see -// the bug filed upstream: +// On FreeBSD, use optimized implementations based on +// `O_RESOLVE_BENEATH`/`AT_RESOLVE_BENEATH` and `O_PATH` when available. #[cfg(any(target_os = "macos", target_os = "ios"))] pub(crate) use crate::rustix::darwin::fs::*; +#[cfg(target_os = "freebsd")] +pub(crate) use crate::rustix::freebsd::fs::*; #[cfg(any(target_os = "android", target_os = "linux"))] pub(crate) use crate::rustix::linux::fs::*; -#[cfg(not(any(target_os = "android", target_os = "linux")))] +#[cfg(not(any(target_os = "android", target_os = "linux", target_os = "freebsd")))] #[rustfmt::skip] pub(crate) use crate::fs::{ manually::open_entry as open_entry_impl, @@ -63,10 +63,19 @@ pub(super) use file_path::file_path_by_ttyname_or_seaching; target_os = "ios" )))] pub(crate) use file_path::file_path_by_ttyname_or_seaching as file_path; -#[cfg(not(any(target_os = "android", target_os = "linux", target_os = "wasi")))] +#[cfg(not(any( + target_os = "android", + target_os = "linux", + target_os = "freebsd", + target_os = "wasi" +)))] pub(crate) use set_permissions_impl::set_permissions_impl; -#[cfg(not(any(target_os = "android", target_os = "linux")))] +#[cfg(target_os = "freebsd")] +pub(crate) use set_permissions_impl::set_permissions_impl as set_permissions_manually; +#[cfg(not(any(target_os = "android", target_os = "linux", target_os = "freebsd")))] pub(crate) use set_times_impl::set_times_impl; +#[cfg(target_os = "freebsd")] +pub(crate) use set_times_impl::set_times_impl as set_times_manually; #[rustfmt::skip] pub(crate) use crate::fs::{ @@ -74,11 +83,15 @@ pub(crate) use crate::fs::{ via_parent::create_dir as create_dir_impl, via_parent::read_link as read_link_impl, via_parent::rename as rename_impl, - via_parent::remove_dir as remove_dir_impl, via_parent::symlink as symlink_impl, - via_parent::remove_file as remove_file_impl, remove_open_dir_by_searching as remove_open_dir_impl, }; +#[cfg(not(target_os = "freebsd"))] +#[rustfmt::skip] +pub(crate) use crate::fs::{ + via_parent::remove_dir as remove_dir_impl, + via_parent::remove_file as remove_file_impl, +}; pub(crate) use copy_impl::copy_impl; pub(crate) use create_dir_unchecked::create_dir_unchecked; @@ -107,7 +120,7 @@ pub(crate) use reopen_impl::reopen_impl; pub(crate) use stat_unchecked::stat_unchecked; pub(crate) use symlink_unchecked::symlink_unchecked; #[allow(unused_imports)] -pub(crate) use times::{set_times_follow_unchecked, set_times_nofollow_unchecked}; +pub(crate) use times::{set_times_follow_unchecked, set_times_nofollow_unchecked, to_timespec}; // On Linux, there is a limit of 40 symlink expansions. // Source: diff --git a/cap-primitives/src/rustix/fs/times.rs b/cap-primitives/src/rustix/fs/times.rs index d4641c9d..555ebe63 100644 --- a/cap-primitives/src/rustix/fs/times.rs +++ b/cap-primitives/src/rustix/fs/times.rs @@ -7,7 +7,7 @@ use std::path::Path; use std::{fs, io}; #[allow(clippy::useless_conversion)] -fn to_timespec(ft: Option) -> io::Result { +pub(crate) fn to_timespec(ft: Option) -> io::Result { Ok(match ft { None => Timespec { tv_sec: 0, @@ -33,6 +33,7 @@ fn to_timespec(ft: Option) -> io::Result { }) } +#[allow(dead_code)] pub(crate) fn set_times_nofollow_unchecked( start: &fs::File, path: &Path, diff --git a/cap-primitives/src/rustix/mod.rs b/cap-primitives/src/rustix/mod.rs index 6a19568a..2a7a2f10 100644 --- a/cap-primitives/src/rustix/mod.rs +++ b/cap-primitives/src/rustix/mod.rs @@ -5,5 +5,7 @@ pub(crate) mod fs; #[cfg(any(target_os = "macos", target_os = "ios"))] mod darwin; +#[cfg(target_os = "freebsd")] +mod freebsd; #[cfg(any(target_os = "android", target_os = "linux"))] mod linux;