use crate::error::JailError;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
use crate::openat2::{kernel_version as openat2_kernel_version, probe_openat2, MIN_OPENAT2_KERNEL};
pub struct GuardedFile {
pub(crate) file: File,
pub(crate) attestation: Attestation,
}
impl GuardedFile {
pub fn file(&self) -> &File {
&self.file
}
pub fn into_file(self) -> File {
self.file
}
pub fn attestation(&self) -> &Attestation {
&self.attestation
}
pub fn has_hard_links(&self) -> bool {
self.attestation.nlink > 1
}
pub fn sign_attestation<S: crate::guard::Signer>(
&self,
signer: &S,
) -> Result<Attestation, S::Error> {
let mut att = self.attestation.clone();
att.signature = Some(signer.sign(&att.signing_bytes())?);
Ok(att)
}
}
impl std::fmt::Debug for GuardedFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GuardedFile")
.field("attestation", &self.attestation)
.finish_non_exhaustive()
}
}
impl std::ops::Deref for GuardedFile {
type Target = File;
fn deref(&self) -> &File {
&self.file
}
}
impl std::ops::DerefMut for GuardedFile {
fn deref_mut(&mut self) -> &mut File {
&mut self.file
}
}
impl std::io::Read for GuardedFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.file.read(buf)
}
}
impl std::io::Write for GuardedFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.file.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.flush()
}
}
impl std::io::Seek for GuardedFile {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.file.seek(pos)
}
}
#[cfg(unix)]
impl std::os::unix::io::AsFd for GuardedFile {
fn as_fd(&self) -> std::os::unix::io::BorrowedFd<'_> {
self.file.as_fd()
}
}
#[cfg(unix)]
impl std::os::unix::io::AsRawFd for GuardedFile {
fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
self.file.as_raw_fd()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Attestation {
pub jail_root: PathBuf,
pub opened_path: PathBuf,
pub root_inode: u64,
pub file_inode: u64,
pub device: u64,
pub nlink: u64,
pub toctou_safe: bool,
pub opened_at: SystemTime,
pub signature: Option<[u8; 64]>,
}
impl Attestation {
pub fn content_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(256);
encode_path_field(&mut buf, &self.jail_root);
encode_path_field(&mut buf, &self.opened_path);
buf.extend_from_slice(&self.root_inode.to_le_bytes());
buf.extend_from_slice(&self.file_inode.to_le_bytes());
buf.extend_from_slice(&self.device.to_le_bytes());
buf.extend_from_slice(&self.nlink.to_le_bytes());
buf.push(if self.toctou_safe { 1 } else { 0 });
buf
}
pub fn signing_bytes(&self) -> Vec<u8> {
let mut buf = self.content_bytes();
let nanos = self
.opened_at
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
buf.extend_from_slice(&nanos.to_le_bytes());
buf
}
pub fn verify<V: crate::guard::Verifier>(
&self,
verifier: &V,
) -> Result<(), crate::guard::VerifyError<V::Error>> {
let sig = self.signature.ok_or(crate::guard::VerifyError::NotSigned)?;
verifier
.verify(&self.signing_bytes(), &sig)
.map_err(crate::guard::VerifyError::Invalid)
}
}
fn encode_path_field(buf: &mut Vec<u8>, path: &Path) {
let bytes = path.as_os_str().as_encoded_bytes();
let len = bytes.len() as u32;
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(bytes);
}
#[derive(Debug, Clone, Default)]
pub struct OpenOptions {
pub(crate) read: bool,
pub(crate) write: bool,
pub(crate) append: bool,
pub(crate) truncate: bool,
pub(crate) create: bool,
pub(crate) create_new: bool,
pub(crate) no_symlinks: bool,
pub(crate) no_xdev: bool,
}
impl OpenOptions {
pub fn new() -> Self {
Self::default()
}
pub fn read(mut self, v: bool) -> Self {
self.read = v;
self
}
pub fn write(mut self, v: bool) -> Self {
self.write = v;
self
}
pub fn append(mut self, v: bool) -> Self {
self.append = v;
self
}
pub fn truncate(mut self, v: bool) -> Self {
self.truncate = v;
self
}
pub fn create(mut self, v: bool) -> Self {
self.create = v;
self
}
pub fn create_new(mut self, v: bool) -> Self {
self.create_new = v;
self
}
pub fn no_symlinks(mut self, v: bool) -> Self {
self.no_symlinks = v;
self
}
pub fn no_xdev(mut self, v: bool) -> Self {
self.no_xdev = v;
self
}
}
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
mod linux_impl {
use super::*;
use crate::openat2::{
openat2, Errno, OpenHow, O_APPEND, O_CLOEXEC, O_CREAT, O_EXCL, O_RDONLY, O_TRUNC, O_WRONLY,
RESOLVE_BENEATH, RESOLVE_NO_MAGICLINKS, RESOLVE_NO_SYMLINKS, RESOLVE_NO_XDEV,
};
use std::ffi::CString;
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd};
pub(crate) fn jail_open(
dirfd: &OwnedFd,
jail_root: &Path,
root_inode: u64,
rel_path: &Path,
opts: &OpenOptions,
) -> Result<GuardedFile, JailError> {
let path_str = rel_path
.to_str()
.ok_or_else(|| JailError::InvalidPath("path contains invalid UTF-8".into()))?;
if path_str.contains('\0') {
return Err(JailError::InvalidPath("null bytes not allowed".into()));
}
let cpath = CString::new(path_str)
.map_err(|_| JailError::InvalidPath("could not convert path to C string".into()))?;
let mut flags: u64 = O_CLOEXEC;
if opts.append {
flags |= O_APPEND | O_WRONLY;
} else if opts.write {
flags |= O_WRONLY;
} else {
flags |= O_RDONLY;
}
if opts.create {
flags |= O_CREAT;
}
if opts.create_new {
flags |= O_CREAT | O_EXCL;
}
if opts.truncate {
flags |= O_TRUNC;
}
let mut resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
if opts.no_symlinks {
resolve |= RESOLVE_NO_SYMLINKS;
}
if opts.no_xdev {
resolve |= RESOLVE_NO_XDEV;
}
let mode: u64 = if flags & O_CREAT != 0 { 0o666 } else { 0 };
let how = OpenHow {
flags,
mode,
resolve,
};
let owned_fd = openat2(dirfd.as_raw_fd(), &cpath, &how)
.map_err(|e| map_errno_to_jail_error(e, rel_path))?;
let file: File = unsafe { File::from_raw_fd(owned_fd.into_raw_fd()) };
let meta = file.metadata().map_err(JailError::Io)?;
let attestation = Attestation {
jail_root: jail_root.to_path_buf(),
opened_path: rel_path.to_path_buf(),
root_inode,
file_inode: meta.ino(),
device: meta.dev(),
nlink: meta.nlink(),
toctou_safe: true,
opened_at: SystemTime::now(),
signature: None,
};
Ok(GuardedFile { file, attestation })
}
fn map_errno_to_jail_error(e: Errno, path: &Path) -> JailError {
match e {
Errno::EXDEV => JailError::Escape {
requested: path.to_path_buf(),
},
Errno::ELOOP => JailError::SymlinkRejected {
requested: path.to_path_buf(),
},
_ => JailError::Io(e.into()),
}
}
}
#[cfg(not(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
)))]
mod fallback_impl {
use super::*;
use std::os::unix::fs::OpenOptionsExt;
const O_NOFOLLOW: i32 = 0x0100;
pub(crate) fn jail_open(
jail_root: &Path,
root_inode: u64,
rel_path: &Path,
opts: &OpenOptions,
) -> Result<GuardedFile, JailError> {
let abs_path = {
let jail = crate::jail::Jail::new(jail_root)?;
jail.join(rel_path)?
};
let mut oo = std::fs::OpenOptions::new();
if opts.read {
oo.read(true);
}
if opts.write {
oo.write(true);
}
if opts.append {
oo.append(true);
}
if opts.truncate {
oo.truncate(true);
}
if opts.create {
oo.create(true);
}
if opts.create_new {
oo.create_new(true);
}
if !opts.read && !opts.write && !opts.append {
oo.read(true);
}
oo.custom_flags(O_NOFOLLOW);
let file = oo.open(&abs_path).map_err(JailError::Io)?;
let meta = file.metadata().map_err(JailError::Io)?;
use std::os::unix::fs::MetadataExt;
let attestation = Attestation {
jail_root: jail_root.to_path_buf(),
opened_path: rel_path.to_path_buf(),
root_inode,
file_inode: meta.ino(),
device: meta.dev(),
nlink: meta.nlink(),
toctou_safe: false, opened_at: SystemTime::now(),
signature: None,
};
Ok(GuardedFile { file, attestation })
}
}
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
fn libc_open_directory_flags() -> i32 {
0o0_200000 | 0o0_400000 | 0o2_000000 }
pub struct FdJail {
pub(crate) root: PathBuf,
pub(crate) root_inode: u64,
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
pub(crate) dirfd: std::os::unix::io::OwnedFd,
}
unsafe impl Send for FdJail {}
unsafe impl Sync for FdJail {}
impl Clone for FdJail {
fn clone(&self) -> Self {
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
{
use std::os::unix::io::{AsFd, OwnedFd};
let duped: OwnedFd = self
.dirfd
.as_fd()
.try_clone_to_owned()
.expect("dup of jail dirfd failed");
FdJail {
root: self.root.clone(),
root_inode: self.root_inode,
dirfd: duped,
}
}
#[cfg(not(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
)))]
{
FdJail {
root: self.root.clone(),
root_inode: self.root_inode,
}
}
}
}
impl FdJail {
pub fn new(root: impl AsRef<Path>) -> Result<Self, JailError> {
let root_input = root.as_ref().to_path_buf();
let root = root_input
.canonicalize()
.map_err(|e| JailError::InvalidRoot {
path: root_input.clone(),
source: Some(e),
})?;
if root.parent().is_none() || !root.is_dir() {
return Err(JailError::InvalidRoot {
path: root,
source: Some(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"not a directory or is filesystem root",
)),
});
}
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
{
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::os::unix::io::FromRawFd;
if let Some(kv) = openat2_kernel_version() {
if kv < MIN_OPENAT2_KERNEL {
return Err(JailError::UnsupportedKernel { version: Some(kv) });
}
}
probe_openat2().map_err(|_| JailError::UnsupportedKernel {
version: openat2_kernel_version(),
})?;
let dir_file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc_open_directory_flags())
.open(&root)
.map_err(JailError::Io)?;
let root_inode = dir_file.metadata().map_err(JailError::Io)?.ino();
let dirfd = unsafe {
std::os::unix::io::OwnedFd::from_raw_fd(std::os::unix::io::IntoRawFd::into_raw_fd(
dir_file,
))
};
Ok(FdJail {
root,
root_inode,
dirfd,
})
}
#[cfg(not(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
)))]
{
let meta = std::fs::metadata(&root).map_err(JailError::Io)?;
use std::os::unix::fs::MetadataExt;
Ok(FdJail {
root,
root_inode: meta.ino(),
})
}
}
pub fn open(
&self,
path: impl AsRef<Path>,
opts: OpenOptions,
) -> Result<GuardedFile, JailError> {
let rel = self.validate_relative(path.as_ref())?;
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
return linux_impl::jail_open(&self.dirfd, &self.root, self.root_inode, &rel, &opts);
#[cfg(not(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
)))]
return fallback_impl::jail_open(&self.root, self.root_inode, &rel, &opts);
}
pub fn create(&self, path: impl AsRef<Path>) -> Result<GuardedFile, JailError> {
self.open(path, OpenOptions::new().write(true).create_new(true))
}
pub fn check_path(&self, path: impl AsRef<Path>) -> Result<PathBuf, JailError> {
self.validate_relative(path.as_ref())
}
pub fn root(&self) -> &Path {
&self.root
}
fn validate_relative(&self, path: &Path) -> Result<PathBuf, JailError> {
let s = path
.to_str()
.ok_or_else(|| JailError::InvalidPath("path contains invalid UTF-8".into()))?;
if s.contains('\0') {
return Err(JailError::InvalidPath("null bytes not allowed".into()));
}
if path.is_absolute() {
return Err(JailError::InvalidPath("path must be relative".into()));
}
Ok(path.to_path_buf())
}
}
impl std::fmt::Debug for FdJail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FdJail")
.field("root", &self.root)
.field("root_inode", &self.root_inode)
.field("toctou_safe", &cfg!(target_os = "linux"))
.finish()
}
}