Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ptfs: implement perfile based chroot by using openat2 #171

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
2 changes: 0 additions & 2 deletions src/passthrough/file_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,7 @@ impl OpenableFileHandle {
#[cfg(test)]
mod tests {
use super::*;
use nix::unistd::getuid;
use std::ffi::CString;
use std::io::Read;

fn generate_c_file_handle(
handle_bytes: usize,
Expand Down
34 changes: 13 additions & 21 deletions src/passthrough/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub use self::config::{CachePolicy, Config};
use self::file_handle::{FileHandle, OpenableFileHandle};
use self::inode_store::{InodeId, InodeStore};
use self::mount_fd::MountFds;
use self::os_compat::SafeOpenAt;
use self::statx::{statx, StatExt};
use self::util::{
ebadf, einval, enosys, eperm, is_dir, is_safe_inode, openat, reopen_fd_through_proc, stat_fd,
Expand Down Expand Up @@ -358,6 +359,7 @@ pub struct PassthroughFs<S: BitmapSlice + Send + Sync = ()> {
ino_allocator: UniqueInodeGenerator,
// Maps mount IDs to an open FD on the respective ID for the purpose of open_by_handle_at().
mount_fds: MountFds,
opener: SafeOpenAt,

// File descriptor pointing to the `/proc/self/fd` directory. This is used to convert an fd from
// `inodes` into one that can go into `handles`. This is accomplished by reading the
Expand Down Expand Up @@ -439,6 +441,7 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {

mount_fds,
proc_self_fd,
opener: SafeOpenAt::new(),

writeback: AtomicBool::new(false),
no_open: AtomicBool::new(false),
Expand All @@ -459,8 +462,8 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
pub fn import(&self) -> io::Result<()> {
let root = CString::new(self.cfg.root_dir.as_str()).expect("CString::new failed");

let (path_fd, handle_opt, st) = Self::open_file_and_handle(self, &libc::AT_FDCWD, &root)
.map_err(|e| {
let (path_fd, handle_opt, st) =
Self::open_file_and_handle(self, &libc::AT_FDCWD, &root, false).map_err(|e| {
error!("fuse: import: failed to get file or handle: {:?}", e);
e
})?;
Expand Down Expand Up @@ -556,30 +559,19 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
openat(dfd, pathname, flags, mode)
}

fn open_file_restricted(
&self,
dir: &impl AsRawFd,
pathname: &CStr,
flags: i32,
mode: u32,
) -> io::Result<File> {
let flags = libc::O_NOFOLLOW | libc::O_CLOEXEC | flags;

// TODO
//if self.os_facts.has_openat2 {
// oslib::do_open_relative_to(dir, pathname, flags, mode)
//} else {
openat(dir, pathname, flags, mode)
//}
}

/// Create a File or File Handle for `name` under directory `dir_fd` to support `lookup()`.
fn open_file_and_handle(
&self,
dir: &impl AsRawFd,
name: &CStr,
use_openat2: bool,
) -> io::Result<(File, Option<FileHandle>, StatExt)> {
let path_file = self.open_file_restricted(dir, name, libc::O_PATH, 0)?;
let flags = libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_PATH;
let path_file = if use_openat2 {
self.opener.openat(dir, name, flags, 0)?
} else {
openat(dir, name, flags, 0)?
};
let st = statx(&path_file, None)?;
let handle = if self.cfg.inode_file_handles {
FileHandle::from_fd(&path_file)?
Expand Down Expand Up @@ -640,7 +632,7 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {

let dir = self.inode_map.get(parent)?;
let dir_file = dir.get_file()?;
let (path_fd, handle_opt, st) = Self::open_file_and_handle(self, &dir_file, name)?;
let (path_fd, handle_opt, st) = Self::open_file_and_handle(self, &dir_file, name, true)?;
let id = InodeId::from_stat(&st);

let mut found = None;
Expand Down
199 changes: 199 additions & 0 deletions src/passthrough/os_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
// found in the LICENSE-BSD-3-Clause file.
// SPDX-License-Identifier: Apache-2.0

use std::ffi::{CStr, CString};
use std::fs::File;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, RawFd};

use vm_memory::ByteValued;

use super::util::openat;

#[repr(C, packed)]
#[derive(Clone, Copy, Debug, Default)]
pub struct LinuxDirent64 {
Expand Down Expand Up @@ -66,3 +73,195 @@ pub const STATX_BASIC_STATS: libc::c_uint = 0x07ff;

#[cfg(not(target_env = "gnu"))]
pub const STATX_MNT_ID: libc::c_uint = 0x1000;

pub struct SafeOpenAt {
has_openat2: bool,
}

impl SafeOpenAt {
pub fn new() -> Self {
// Checking for `openat2()` since it first appeared in Linux 5.6.
// SAFETY: all-zero byte-pattern is a valid `libc::open_how`
let how: libc::open_how = unsafe { std::mem::zeroed() };
let cwd = CString::new(".").unwrap();
// SAFETY: `cwd.as_ptr()` points to a valid NUL-terminated string,
// and the `how` pointer is a valid pointer to an `open_how` struct.
let fd = unsafe {
libc::syscall(
libc::SYS_openat2,
libc::AT_FDCWD,
cwd.as_ptr(),
std::ptr::addr_of!(how),
std::mem::size_of::<libc::open_how>(),
)
};

let has_openat2 = fd >= 0;
if has_openat2 {
// SAFETY: `fd` is an open file descriptor
unsafe {
libc::close(fd as libc::c_int);
}
}

Self { has_openat2 }
}

/// An utility function that uses `openat2(2)` to restrict the how the provided pathname
/// is resolved. It uses the following flags:
/// - `RESOLVE_IN_ROOT`: Treat the directory referred to by dirfd as the root directory while
/// resolving pathname. This has the effect as though virtiofsd had used chroot(2) to modify its
/// root directory to dirfd.
/// - `RESOLVE_NO_MAGICLINKS`: Disallow all magic-link (i.e., proc(2) link-like files) resolution
/// during path resolution.
///
/// Additionally, the flags `O_NOFOLLOW` and `O_CLOEXEC` are added.
///
/// # Error
///
/// Will return `Err(errno)` if `openat2(2)` fails, see the man page for details.
///
/// # Safety
///
/// The caller must ensure that dirfd is a valid file descriptor.
pub fn openat(
&self,
dir: &impl AsRawFd,
path: &CStr,
flags: libc::c_int,
mode: u32,
) -> io::Result<File> {
// Fallback to openat
if !self.has_openat2 {
return openat(dir, path, flags, mode);
}

// `openat2(2)` returns an error if `how.mode` contains bits other than those in range 07777,
// let's ignore the extra bits to be compatible with `openat(2)`.
let mode = mode as u64 & 0o7777;

// SAFETY: all-zero byte-pattern represents a valid `libc::open_how`
let mut how: libc::open_how = unsafe { std::mem::zeroed() };
// - RESOLVE_IN_ROOT
// Treat the directory referred to by dirfd as the root directory while resolving pathname.
// Absolute symbolic links are interpreted relative to dirfd. If a prefix component of
// pathname equates to dirfd, then an immediately following .. component likewise equates
// to dirfd (just as /.. is traditionally equivalent to /). If pathname is an absolute
// path, it is also interpreted relative to dirfd.
//
// The effect of this flag is as though the calling process had used chroot(2) to
// (temporarily) modify its root directory (to the directory referred to by dirfd).
// However, unlike chroot(2) (which changes the filesystem root permanently for a process),
// RESOLVE_IN_ROOT allows a program to efficiently restrict path resolution on a per-open
// basis.
//
// Currently, this flag also disables magic-link resolution. However, this may change
// in the future. Therefore, to ensure that magic links are not resolved, the caller should
// explicitly specify RESOLVE_NO_MAGICLINKS.
//
// - RESOLVE_NO_MAGICLINKS
// Disallow all magic-link resolution during path resolution.
//
// Magic links are symbolic link-like objects that are most notably found in proc(5);
// examples include /proc/pid/exe and /proc/pid/fd/*. (See symlink(7) for more details.)
//
// Unknowingly opening magic links can be risky for some applications. Examples of such
// risks include the following:
// • If the process opening a pathname is a controlling process that currently has no
// controlling terminal (see credentials(7)), then opening a magic link inside
// /proc/pid/fd that happens to refer to a terminal would cause the process to acquire
// a controlling terminal.
//
// • In a containerized environment, a magic link inside /proc may refer to an object
// outside the container, and thus may provide a means to escape from the container.
//
// Because of such risks, an application may prefer to disable magic link resolution using
// the RESOLVE_NO_MAGICLINKS flag.
//
// If the trailing component (i.e., basename) of pathname is a magic link, how.resolve
// contains RESOLVE_NO_MAGICLINKS, and how.flags contains both O_PATH and O_NOFOLLOW,
// then an O_PATH file descriptor referencing the magic link will be returned.
how.resolve = libc::RESOLVE_IN_ROOT | libc::RESOLVE_NO_MAGICLINKS;
how.flags = flags as u64;
how.mode = mode;

// SAFETY: `pathname` points to a valid NUL-terminated string, and the `how` pointer is a valid
// pointer to an `open_how` struct. However, the caller must ensure that `dir` can provide a
// valid file descriptor (this can be changed to BorrowedFd).
let ret = unsafe {
libc::syscall(
libc::SYS_openat2,
dir.as_raw_fd(),
path.as_ptr(),
std::ptr::addr_of!(how),
std::mem::size_of::<libc::open_how>(),
)
};
if ret == -1 {
Err(io::Error::last_os_error())
} else {
// Safe because we have just open the RawFd.
let file = unsafe { File::from_raw_fd(ret as RawFd) };
Ok(file)
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::os::unix::fs;
use vmm_sys_util::tempdir::TempDir;

#[test]
fn test_openat2() {
let topdir = env!("CARGO_MANIFEST_DIR");
let dir = File::open(topdir).unwrap();
let filename = CString::new("build.rs").unwrap();

let opener = SafeOpenAt::new();
assert!(opener.has_openat2);
opener.openat(&dir, &filename, libc::O_RDONLY, 0).unwrap();
}

#[test]
// If pathname is an absolute path, it is also interpreted relative to dirfd.
fn test_openat2_absolute() {
let topdir = env!("CARGO_MANIFEST_DIR");
let dir = File::open(topdir).unwrap();
let filename = CString::new("/build.rs").unwrap();

let opener = SafeOpenAt::new();
assert!(opener.has_openat2);
opener.openat(&dir, &filename, libc::O_RDONLY, 0).unwrap();
}

#[test]
// If a prefix component of pathname equates to dirfd, then an immediately following ..
// component likewise equates to dirfd
fn test_openat2_parent() {
let topdir = env!("CARGO_MANIFEST_DIR");
let dir = File::open(topdir).unwrap();
let filename = CString::new("/../../build.rs").unwrap();

let opener = SafeOpenAt::new();
assert!(opener.has_openat2);
opener.openat(&dir, &filename, libc::O_RDONLY, 0).unwrap();
}

#[test]
// Absolute symbolic links are interpreted relative to dirfd.
fn test_openat2_symlink() {
let topdir = env!("CARGO_MANIFEST_DIR");
let dir = File::open(topdir).unwrap();
let tmpdir = TempDir::new().unwrap();
let dest = tmpdir.as_path().join("build.rs");
fs::symlink("/build.rs", dest).unwrap();
let filename = CString::new("build.rs").unwrap();

let opener = SafeOpenAt::new();
assert!(opener.has_openat2);
opener.openat(&dir, &filename, libc::O_RDONLY, 0).unwrap();
}
}