blob: b298be554392514c062b577809ef107e39143db2 [file] [log] [blame] [edit]
// Copyright 2024 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Rust port of libbrillo's `OpenSafely` API.
use std::ffi::{CString, OsStr};
use std::fs::File;
use std::io;
use std::mem::MaybeUninit;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt;
use std::path::{Component, Path};
use libc::{
c_int, fcntl, mode_t, stat64, F_GETFL, F_SETFL, O_APPEND, O_CLOEXEC, O_CREAT, O_DIRECTORY,
O_EXCL, O_NOFOLLOW, O_NONBLOCK, O_PATH, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY, S_IFDIR, S_IFIFO,
S_IFMT, S_IFREG,
};
use thiserror::Error;
use crate::handle_eintr_errno;
#[derive(Error, Debug)]
pub enum OpenSafelyError {
#[error("open_safely() requires an absolute path")]
AbsolutePathRequired,
#[error("got an empty path")]
EmptyPath,
#[error("expected a directory but got type {0:#}")]
ExpectedDirectory(mode_t),
#[error("expected a fifo but got type {0:#}")]
ExpectedFifo(mode_t),
#[error("expected a regular file but got type {0:#}")]
ExpectedRegularFile(mode_t),
#[error("fstat() failed: {0}")]
Fstat(io::Error),
#[error("fcntl(F_GETFL) failed: {0}")]
GetFlags(io::Error),
#[error("invalid path component")]
InvalidPathComponent,
#[error("open failed: {0}")]
Open(io::Error),
#[error("parent fd required for relative path")]
ParentFdRequired,
#[error("fcntl(F_SETFL) failed: {0}")]
SetFlags(io::Error),
}
impl From<OpenSafelyError> for io::Error {
fn from(value: OpenSafelyError) -> Self {
match value {
OpenSafelyError::Open(io_error) => io_error,
_ => io::Error::new(io::ErrorKind::Other, "open_safely failed"),
}
}
}
pub type OpenSafelyResult<T> = std::result::Result<T, OpenSafelyError>;
/// Drop-in replacement for `std::fs::OpenOptions` that provides `open_safely()`.
#[derive(Clone, Debug)]
pub struct OpenSafelyOptions {
read: bool,
write: bool,
append: bool,
truncate: bool,
create: bool,
create_new: bool,
mode: mode_t,
custom_flags: c_int,
}
impl OpenSafelyOptions {
pub fn new() -> Self {
OpenSafelyOptions {
read: false,
write: false,
append: false,
truncate: false,
create: false,
create_new: false,
mode: 0o666, // rw-rw-rw-
custom_flags: 0,
}
}
pub fn read(&mut self, read: bool) -> &mut Self {
self.read = read;
self
}
pub fn write(&mut self, write: bool) -> &mut Self {
self.write = write;
self
}
pub fn append(&mut self, append: bool) -> &mut Self {
self.append = append;
self
}
pub fn truncate(&mut self, truncate: bool) -> &mut Self {
self.truncate = truncate;
self
}
pub fn create(&mut self, create: bool) -> &mut Self {
self.create = create;
self
}
pub fn create_new(&mut self, create_new: bool) -> &mut Self {
self.create_new = create_new;
self
}
pub fn mode(&mut self, mode: mode_t) -> &mut Self {
self.mode = mode;
self
}
pub fn custom_flags(&mut self, custom_flags: c_int) -> &mut Self {
self.custom_flags = custom_flags;
self
}
fn flags(&self) -> c_int {
let mut flags = 0;
if self.create {
flags |= O_CREAT;
}
if self.truncate {
flags |= O_TRUNC;
}
if self.append {
flags |= O_APPEND;
}
if self.create_new {
// NOTE: if create_new is specified, create, truncate, and append are ignored.
flags = O_CREAT | O_EXCL;
}
if self.read && self.write {
flags |= O_RDWR;
} else if self.write {
flags |= O_WRONLY;
} else if self.read {
flags |= O_RDONLY;
}
flags
}
pub fn open_safely(&self, path: impl AsRef<Path>) -> OpenSafelyResult<OwnedFd> {
open_safely(path, self.flags(), self.mode)
}
pub fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
let fd = self.open_safely(path)?;
Ok(File::from(fd))
}
pub fn open_at_safely(
&self,
parent_fd: BorrowedFd<'_>,
path: impl AsRef<Path>,
) -> OpenSafelyResult<OwnedFd> {
open_at_safely(parent_fd, path, self.flags(), self.mode)
}
pub fn open_at(&self, file: &File, path: impl AsRef<Path>) -> io::Result<File> {
let fd = self.open_at_safely(file.as_fd(), path)?;
Ok(File::from(fd))
}
pub fn open_fifo_safely(&self, path: impl AsRef<Path>) -> OpenSafelyResult<OwnedFd> {
open_fifo_safely(path, self.flags(), self.mode)
}
}
impl Default for OpenSafelyOptions {
fn default() -> Self {
Self::new()
}
}
enum MaybeBorrowedFd<'fd> {
Borrowed(BorrowedFd<'fd>),
Owned(OwnedFd),
}
impl AsFd for MaybeBorrowedFd<'_> {
fn as_fd(&self) -> BorrowedFd<'_> {
match self {
MaybeBorrowedFd::Borrowed(fd) => fd.as_fd(),
MaybeBorrowedFd::Owned(fd) => fd.as_fd(),
}
}
}
// Safe wrapper for `openat()`.
fn openat(
dirfd: Option<BorrowedFd>,
pathname: &OsStr,
flags: c_int,
mode: mode_t,
) -> io::Result<OwnedFd> {
let name_cstr = CString::new(pathname.as_bytes())?;
// SAFETY: We pass a valid C string pointer and borrowed file descriptor.
let fd = handle_eintr_errno!(unsafe {
libc::openat64(
dirfd.as_ref().map(BorrowedFd::as_raw_fd).unwrap_or(-1),
name_cstr.as_ptr(),
flags,
mode,
)
});
if fd < 0 {
Err(io::Error::last_os_error())
} else {
// SAFETY: We have ownership of the raw file descriptor returned by `openat`.
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
}
// Safe wrapper for `fstat()`.
fn fstat(fd: BorrowedFd) -> io::Result<stat64> {
let mut st = MaybeUninit::<stat64>::zeroed();
// SAFETY: We pass a valid `struct stat` buffer for `fstat()`
if unsafe { libc::fstat64(fd.as_raw_fd(), st.as_mut_ptr()) } < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: We check that the `stat()` function succeeded and filled out `st` above.
Ok(unsafe { st.assume_init() })
}
fn open_safely_internal(
parent_fd: Option<BorrowedFd>,
path: &Path,
flags: c_int,
mode: mode_t,
) -> OpenSafelyResult<OwnedFd> {
let final_component = path.file_name().ok_or(OpenSafelyError::EmptyPath)?;
let parent_path = path.parent().ok_or(OpenSafelyError::EmptyPath)?;
let mut components = parent_path.components().peekable();
let parent_flags = O_NONBLOCK | O_RDONLY | O_DIRECTORY | O_PATH | O_NOFOLLOW | O_CLOEXEC;
let mut parent_fd = if components.next_if(|c| c == &Component::RootDir).is_some() {
// Absolute path - open the root directory.
MaybeBorrowedFd::Owned(
openat(None, OsStr::new("/"), parent_flags, 0).map_err(OpenSafelyError::Open)?,
)
} else {
// Relative paths may begin with `CurDir` ("./a/b") or just a normal component ("a/b").
let _cur_dir = components.next_if(|c| c == &Component::CurDir);
// Relative path - use provided `parent_fd`.
MaybeBorrowedFd::Borrowed(parent_fd.ok_or(OpenSafelyError::ParentFdRequired)?)
};
for component in components {
let name = match component {
Component::Normal(component) => component,
Component::ParentDir => OsStr::new(".."),
// "/" can only occur as the first component.
// `Path::components()` normalizes "." away aside from the first component.
// Prefix is only used on Windows.
Component::RootDir | Component::CurDir | Component::Prefix(..) => {
return Err(OpenSafelyError::InvalidPathComponent)
}
};
parent_fd = MaybeBorrowedFd::Owned(
openat(Some(parent_fd.as_fd()), name, parent_flags, 0)
.map_err(OpenSafelyError::Open)?,
);
}
// O_NONBLOCK is used to avoid hanging on edge cases (e.g. a serial port with flow control, or a
// FIFO without a writer).
let fd = openat(
Some(parent_fd.as_fd()),
final_component,
flags | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC,
mode,
)
.map_err(OpenSafelyError::Open)?;
// Remove the O_NONBLOCK flag unless the original `flags` have it.
if (flags & O_NONBLOCK) == 0 {
// SAFETY: We pass a valid file descriptor.
let flags = unsafe { fcntl(fd.as_raw_fd(), F_GETFL) };
if flags == -1 {
return Err(OpenSafelyError::GetFlags(io::Error::last_os_error()));
}
// SAFETY: We pass a valid file descriptor and flags.
if unsafe { fcntl(fd.as_raw_fd(), F_SETFL, flags & !O_NONBLOCK) } != 0 {
return Err(OpenSafelyError::SetFlags(io::Error::last_os_error()));
}
}
Ok(fd)
}
/// Opens the absolute `path` to a regular file or directory ensuring that none of the path
/// components are symbolic links and returns a FD.
///
/// If `path` is relative, or contains any symbolic links, or points to a non-regular file or
/// directory, an error is returned instead. `mode` is ignored unless `flags` has either `O_CREAT`
/// or `O_TMPFILE`. Note that `O_CLOEXEC` is set so the file descriptor will not be inherited across
/// exec calls.
///
/// The opened FD is verified to be the correct type of file: if `flags` has `O_DIRECTORY`, the path
/// is expected to end in a directory; otherwise, the path must be to a regular file. (C++ libbrillo
/// porting note: this behavior differs from `OpenSafely()`, which accepts a directory even when
/// `O_DIRECTORY` was not specified in `flags`).
///
/// # Parameters
///
/// - `path` - An absolute path of the file to open.
/// - `flags` - Flags to pass to open.
/// - `mode - Mode to pass to open.
pub fn open_safely(
path: impl AsRef<Path>,
flags: c_int,
mode: mode_t,
) -> OpenSafelyResult<OwnedFd> {
let path = path.as_ref();
if !path.is_absolute() {
return Err(OpenSafelyError::AbsolutePathRequired);
}
let fd = open_safely_internal(None, path, flags, mode)?;
// Ensure the opened file is a regular file or directory.
let st = fstat(fd.as_fd()).map_err(OpenSafelyError::Fstat)?;
let fd_type = st.st_mode & S_IFMT;
// This detects a FIFO opened for reading, for example.
match flags & O_DIRECTORY {
O_DIRECTORY => {
if fd_type != S_IFDIR {
return Err(OpenSafelyError::ExpectedDirectory(fd_type));
}
}
_ => {
if fd_type != S_IFREG {
return Err(OpenSafelyError::ExpectedRegularFile(fd_type));
}
}
}
Ok(fd)
}
/// Opens the `path` relative to the `parent_fd` to a regular file or directory ensuring that none
/// of the path components are symbolic links and returns a FD.
///
/// If `path` contains any symbolic links, or points to a non-regular file or directory, an error is
/// returned instead. `mode` is ignored unless `flags` has either `O_CREAT` or `O_TMPFILE`. Note
/// that `O_CLOEXEC` is set so the file descriptor will not be inherited across exec calls.
///
/// # Parameters
///
/// - `parent_fd` - The file descriptor of the parent directory
/// - `path` - An absolute path of the file to open
/// - `flags` - Flags to pass to open.
/// - `mode` - Mode to pass to open.
pub fn open_at_safely(
parent_fd: BorrowedFd<'_>,
path: impl AsRef<Path>,
flags: c_int,
mode: mode_t,
) -> OpenSafelyResult<OwnedFd> {
let path = path.as_ref();
let fd = open_safely_internal(Some(parent_fd), path, flags, mode)?;
// Ensure the opened file is a regular file or directory.
let st = fstat(fd.as_fd()).map_err(OpenSafelyError::Fstat)?;
let fd_type = st.st_mode & S_IFMT;
// This detects a FIFO opened for reading, for example.
match flags & O_DIRECTORY {
O_DIRECTORY => {
if fd_type != S_IFDIR {
return Err(OpenSafelyError::ExpectedDirectory(fd_type));
}
}
_ => {
if fd_type != S_IFREG {
return Err(OpenSafelyError::ExpectedRegularFile(fd_type));
}
}
}
Ok(fd)
}
/// Opens the absolute `path` to a FIFO ensuring that none of the path components
/// are symbolic links and returns a FD.
///
/// If `path` is relative, or contains any symbolic links, or points to a non-regular file or
/// directory, an error is returned instead. `mode` is ignored unless `flags` has either
/// `O_CREAT` or `O_TMPFILE`.
///
/// # Parameters
///
/// - `path` - An absolute path of the file to open
/// - `flags` - Flags to pass to open.
/// - `mode` - Mode to pass to open.
pub fn open_fifo_safely(
path: impl AsRef<Path>,
flags: c_int,
mode: mode_t,
) -> OpenSafelyResult<OwnedFd> {
let path = path.as_ref();
if !path.is_absolute() {
return Err(OpenSafelyError::AbsolutePathRequired);
}
let fd = open_safely_internal(None, path, flags, mode)?;
// Ensure the opened file is a FIFO.
let st = fstat(fd.as_fd()).map_err(OpenSafelyError::Fstat)?;
let fd_type = st.st_mode & S_IFMT;
if fd_type == S_IFIFO {
Ok(fd)
} else {
Err(OpenSafelyError::ExpectedFifo(fd_type))
}
}