Skip to content

Commit

Permalink
Use AT_/O_RESOLVE_BENEATH on FreeBSD >= 13.0, fixes #180
Browse files Browse the repository at this point in the history
  • Loading branch information
valpackett authored and sunfishcode committed Dec 28, 2023
1 parent 9af71bd commit 27ff2a8
Show file tree
Hide file tree
Showing 16 changed files with 242 additions and 17 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?

Expand Down
4 changes: 2 additions & 2 deletions cap-primitives/src/fs/manually/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
2 changes: 1 addition & 1 deletion cap-primitives/src/fs/maybe_owned_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions cap-primitives/src/rustix/freebsd/fs/check.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
18 changes: 18 additions & 0 deletions cap-primitives/src/rustix/freebsd/fs/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
12 changes: 12 additions & 0 deletions cap-primitives/src/rustix/freebsd/fs/open_entry_impl.rs
Original file line number Diff line number Diff line change
@@ -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<fs::File> {
open_impl(start, path.as_ref(), options)
}
30 changes: 30 additions & 0 deletions cap-primitives/src/rustix/freebsd/fs/open_impl.rs
Original file line number Diff line number Diff line change
@@ -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<fs::File> {
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()),
}
}
16 changes: 16 additions & 0 deletions cap-primitives/src/rustix/freebsd/fs/remove_dir_impl.rs
Original file line number Diff line number Diff line change
@@ -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,
)?)
}
12 changes: 12 additions & 0 deletions cap-primitives/src/rustix/freebsd/fs/remove_file_impl.rs
Original file line number Diff line number Diff line change
@@ -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)?)
}
22 changes: 22 additions & 0 deletions cap-primitives/src/rustix/freebsd/fs/set_permissions_impl.rs
Original file line number Diff line number Diff line change
@@ -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,
)?)
}
45 changes: 45 additions & 0 deletions cap-primitives/src/rustix/freebsd/fs/set_times_impl.rs
Original file line number Diff line number Diff line change
@@ -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<SystemTimeSpec>,
mtime: Option<SystemTimeSpec>,
) -> 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, &times, AtFlags::RESOLVE_BENEATH)?)
}

pub(crate) fn set_times_nofollow_impl(
start: &fs::File,
path: &Path,
atime: Option<SystemTimeSpec>,
mtime: Option<SystemTimeSpec>,
) -> 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,
&times,
AtFlags::RESOLVE_BENEATH | AtFlags::SYMLINK_NOFOLLOW,
)?)
}
22 changes: 22 additions & 0 deletions cap-primitives/src/rustix/freebsd/fs/stat_impl.rs
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
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)?))
}
1 change: 1 addition & 0 deletions cap-primitives/src/rustix/freebsd/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod fs;
38 changes: 25 additions & 13 deletions cap-primitives/src/rustix/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,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: <https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=248335>
// 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,
Expand All @@ -66,27 +66,39 @@ 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(target_os = "freebsd")]
pub(crate) use set_permissions_impl::set_permissions_impl as set_permissions_manually;
#[cfg(not(target_os = "wasi"))]
pub(crate) use set_symlink_permissions_unchecked::set_symlink_permissions_unchecked;
#[cfg(not(any(target_os = "android", target_os = "linux")))]
#[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::{
via_parent::access as access_impl,
via_parent::hard_link as hard_link_impl,
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 = "wasi"))]
pub(crate) use crate::fs::via_parent::set_symlink_permissions as set_symlink_permissions_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 access_unchecked::access_unchecked;
pub(crate) use copy_impl::copy_impl;
Expand Down Expand Up @@ -116,7 +128,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: <https://man7.org/linux/man-pages/man7/path_resolution.7.html>
Expand Down
3 changes: 2 additions & 1 deletion cap-primitives/src/rustix/fs/times.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::path::Path;
use std::{fs, io};

#[allow(clippy::useless_conversion)]
fn to_timespec(ft: Option<SystemTimeSpec>) -> io::Result<Timespec> {
pub(crate) fn to_timespec(ft: Option<SystemTimeSpec>) -> io::Result<Timespec> {
Ok(match ft {
None => Timespec {
tv_sec: 0,
Expand All @@ -33,6 +33,7 @@ fn to_timespec(ft: Option<SystemTimeSpec>) -> io::Result<Timespec> {
})
}

#[allow(dead_code)]
pub(crate) fn set_times_nofollow_unchecked(
start: &fs::File,
path: &Path,
Expand Down
2 changes: 2 additions & 0 deletions cap-primitives/src/rustix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit 27ff2a8

Please sign in to comment.