| // 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)) |
| } |
| } |