use std::ffi::CString;
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::chroot::resolve::{confine, resolve_existing_in_root, resolve_in_root};
use crate::sys::fs::openat2_in_root;
use crate::seccomp::notif::{read_child_mem, write_child_mem, write_child_mem_force, NotifAction};
use crate::seccomp::state::{ChrootState, CowState};
use crate::sys::structs::{SeccompNotif, SeccompNotifAddfd, SECCOMP_IOCTL_NOTIF_ADDFD};
pub(crate) struct ChrootCtx<'a> {
pub root: &'a Path,
pub readable: &'a [PathBuf],
pub writable: &'a [PathBuf],
pub denied: &'a [PathBuf],
pub mounts: &'a [(PathBuf, PathBuf)],
pub mount_ro: &'a [PathBuf],
}
impl ChrootCtx<'_> {
fn is_denied(&self, virtual_path: &Path) -> bool {
self.denied.iter().any(|p| virtual_path.starts_with(p))
}
fn can_read(&self, virtual_path: &Path) -> bool {
if self.is_denied(virtual_path) {
return false;
}
if self.is_mounted(virtual_path) {
return true;
}
self.readable.is_empty()
|| self.readable.iter().any(|p| virtual_path.starts_with(p) || p.starts_with(virtual_path))
|| self.writable.iter().any(|p| virtual_path.starts_with(p) || p.starts_with(virtual_path))
}
fn is_mount_ro(&self, virtual_path: &Path) -> bool {
self.mount_ro.iter().any(|vp| virtual_path.starts_with(vp))
}
fn can_write(&self, virtual_path: &Path) -> bool {
if self.is_denied(virtual_path) {
return false;
}
if self.is_mount_ro(virtual_path) {
return false;
}
if self.is_mounted(virtual_path) {
return true;
}
self.writable.iter().any(|p| virtual_path.starts_with(p))
}
fn is_mounted(&self, virtual_path: &Path) -> bool {
self.mounts.iter().any(|(vp, _)| virtual_path.starts_with(vp))
}
fn mount_target(&self, virtual_path: &Path) -> Option<(&Path, String)> {
let mut best: Option<(&Path, &Path)> = None;
for (vp, hp) in self.mounts {
if virtual_path.starts_with(vp) {
if best.is_none() || vp.as_os_str().len() > best.unwrap().0.as_os_str().len() {
best = Some((vp.as_path(), hp.as_path()));
}
}
}
let (mount_vp, mount_hp) = best?;
let sub = virtual_path.strip_prefix(mount_vp).ok()?;
let sub_str = if sub.as_os_str().is_empty() {
"/".to_string()
} else {
format!("/{}", sub.to_string_lossy())
};
Some((mount_hp, sub_str))
}
fn resolve_mount(&self, virtual_path: &str) -> Option<(PathBuf, PathBuf)> {
let confined = confine(virtual_path);
let (mount_target, sub_path) = self.mount_target(&confined)?;
if let Some(result) = resolve_in_root(mount_target, &sub_path) {
let vp = confined;
return Some((result.0, vp));
}
None
}
fn resolve_mount_existing(&self, virtual_path: &str) -> Option<(PathBuf, PathBuf)> {
let confined = confine(virtual_path);
let (mount_target, sub_path) = self.mount_target(&confined)?;
if let Some(result) = resolve_existing_in_root(mount_target, &sub_path) {
let vp = confined;
return Some((result.0, vp));
}
None
}
fn host_to_virtual(&self, host_path: &Path) -> Option<PathBuf> {
crate::chroot::resolve::host_to_virtual(self.root, self.mounts, host_path)
}
}
fn read_path(notif: &SeccompNotif, addr: u64, notif_fd: RawFd) -> Option<String> {
if addr == 0 {
return None;
}
const PAGE_SIZE: u64 = 4096;
let mut result = Vec::with_capacity(256);
let mut cur = addr;
while result.len() < 4096 {
let page_remaining = PAGE_SIZE - (cur % PAGE_SIZE);
let to_read = page_remaining.min((4096 - result.len()) as u64) as usize;
let bytes = read_child_mem(notif_fd, notif.id, notif.pid, cur, to_read).ok()?;
if let Some(nul) = bytes.iter().position(|&b| b == 0) {
result.extend_from_slice(&bytes[..nul]);
return String::from_utf8(result).ok();
}
result.extend_from_slice(&bytes);
cur += to_read as u64;
}
String::from_utf8(result).ok()
}
fn canon_proc_self(virtual_path: &str, pid: u32) -> String {
for magic in ["/proc/self", "/proc/thread-self"] {
if virtual_path == magic {
return format!("/proc/{}", pid);
}
if let Some(rest) = virtual_path.strip_prefix(magic) {
if rest.starts_with('/') {
return format!("/proc/{}{}", pid, rest);
}
}
}
virtual_path.to_string()
}
fn build_virtual_path(
notif: &SeccompNotif,
dirfd: i64,
path: &str,
ctx: &ChrootCtx<'_>,
) -> Option<String> {
let vpath = if Path::new(path).is_absolute() {
path.to_string()
} else {
let dirfd32 = dirfd as i32;
let base_host = if dirfd32 == libc::AT_FDCWD {
std::fs::read_link(format!("/proc/{}/cwd", notif.pid)).ok()?
} else {
std::fs::read_link(format!("/proc/{}/fd/{}", notif.pid, dirfd)).ok()?
};
let base_virtual = ctx.host_to_virtual(&base_host)?;
let combined = base_virtual.join(path);
combined.to_string_lossy().to_string()
};
Some(canon_proc_self(&vpath, notif.pid))
}
fn resolve_chroot_path(
notif: &SeccompNotif,
dirfd: i64,
path: &str,
ctx: &ChrootCtx<'_>,
) -> Option<(PathBuf, PathBuf)> {
let full_path = build_virtual_path(notif, dirfd, path, ctx)?;
if let Some(result) = ctx.resolve_mount(&full_path) {
return Some(result);
}
resolve_in_root(ctx.root, &full_path)
}
fn resolve_chroot_path_existing(
notif: &SeccompNotif,
dirfd: i64,
path: &str,
ctx: &ChrootCtx<'_>,
) -> Option<(PathBuf, PathBuf)> {
let full_path = build_virtual_path(notif, dirfd, path, ctx)?;
if let Some(result) = ctx.resolve_mount_existing(&full_path) {
return Some(result);
}
resolve_existing_in_root(ctx.root, &full_path)
}
fn path_cstr(path: &Path, err: i32) -> Result<CString, NotifAction> {
CString::new(path.to_str().unwrap_or("")).map_err(|_| NotifAction::Errno(err))
}
fn last_errno(fallback: i32) -> i32 {
std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or(fallback)
}
async fn cow_resolve(
cow_state: &Arc<Mutex<CowState>>,
host_path: &Path,
) -> Result<PathBuf, NotifAction> {
let cs = cow_state.lock().await;
if let Some(ref cow) = cs.branch {
let host_str = host_path.to_string_lossy();
if cow.matches(&host_str) {
return cow
.handle_stat(&host_str)
.ok_or(NotifAction::Errno(libc::ENOENT));
}
}
Ok(host_path.to_path_buf())
}
fn read_and_resolve(
notif: &SeccompNotif,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
dirfd_idx: usize,
path_idx: usize,
) -> Result<(String, PathBuf, PathBuf), NotifAction> {
let path = read_path(notif, notif.data.args[path_idx], notif_fd)
.ok_or(NotifAction::Continue)?;
let dirfd = notif.data.args[dirfd_idx] as i64;
let (host_path, virtual_path) =
resolve_chroot_path(notif, dirfd, &path, ctx).ok_or(NotifAction::Errno(libc::EACCES))?;
Ok((path, host_path, virtual_path))
}
fn read_and_resolve_existing(
notif: &SeccompNotif,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
dirfd_idx: usize,
path_idx: usize,
) -> Result<(String, PathBuf, PathBuf), NotifAction> {
let path = read_path(notif, notif.data.args[path_idx], notif_fd)
.ok_or(NotifAction::Continue)?;
let dirfd = notif.data.args[dirfd_idx] as i64;
let (host_path, virtual_path) =
resolve_chroot_path_existing(notif, dirfd, &path, ctx)
.ok_or(NotifAction::Errno(libc::ENOENT))?;
Ok((path, host_path, virtual_path))
}
fn exec_on_host(f: impl FnOnce(*const libc::c_char) -> libc::c_int, host: &Path) -> NotifAction {
let c = match path_cstr(host, libc::EINVAL) {
Ok(c) => c,
Err(a) => return a,
};
if f(c.as_ptr()) < 0 {
NotifAction::Errno(last_errno(libc::EIO))
} else {
NotifAction::ReturnValue(0)
}
}
pub(crate) async fn handle_chroot_open(
notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let dirfd = notif.data.args[0] as i64;
let path_ptr = notif.data.args[1];
let flags = notif.data.args[2];
let rel_path = match read_path(notif, path_ptr, notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let (host_path, virtual_path) = match resolve_chroot_path(notif, dirfd, &rel_path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::EACCES),
};
let is_write = (flags as i32 & (libc::O_WRONLY | libc::O_RDWR)) != 0;
if is_write {
if !ctx.can_write(&virtual_path) {
return NotifAction::Errno(libc::EACCES);
}
} else if !ctx.can_read(&virtual_path) {
return NotifAction::Errno(libc::EACCES);
}
{
let mut cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_mut() {
let host_str = host_path.to_string_lossy();
if cow.matches(&host_str) {
match cow.handle_open(&host_str, flags) {
Ok(Some(real_path)) => {
drop(cs);
let c_path = match path_cstr(&real_path, libc::EINVAL) {
Ok(c) => c,
Err(a) => return a,
};
let fd = unsafe { libc::open(c_path.as_ptr(), flags as i32, 0o666) };
if fd < 0 {
return NotifAction::Errno(last_errno(libc::EIO));
}
let newfd_flags = if flags & libc::O_CLOEXEC as u64 != 0 {
libc::O_CLOEXEC as u32
} else {
0
};
let owned = unsafe { OwnedFd::from_raw_fd(fd) };
return NotifAction::InjectFdSend { srcfd: owned, newfd_flags };
}
Ok(None) => {
}
Err(crate::error::BranchError::QuotaExceeded) => {
return NotifAction::Errno(libc::ENOSPC);
}
Err(crate::error::BranchError::Exists) => {
return NotifAction::Errno(libc::EEXIST);
}
Err(crate::error::BranchError::Deleted) => {
return NotifAction::Errno(libc::ENOENT);
}
Err(_) => return NotifAction::Errno(libc::EIO),
}
}
}
}
let flags_i = flags as i32;
let creates = flags_i & libc::O_CREAT != 0 || flags_i & libc::O_TMPFILE == libc::O_TMPFILE;
let mode = if creates { 0o666 } else { 0 };
let newfd_flags = if flags & libc::O_CLOEXEC as u64 != 0 {
libc::O_CLOEXEC as u32
} else {
0
};
match open_in_namespace(ctx, notif.pid, &virtual_path, flags as i32, mode) {
Ok(srcfd) => NotifAction::InjectFdSend { srcfd, newfd_flags },
Err(errno) => NotifAction::Errno(errno),
}
}
fn open_in_namespace(
ctx: &ChrootCtx<'_>,
child_pid: u32,
virtual_path: &Path,
flags: i32,
mode: u32,
) -> Result<OwnedFd, i32> {
let vp_str = virtual_path.to_string_lossy();
if let Some(child_fd) = magic_self_fd(&vp_str, child_pid) {
return crate::seccomp::notif::dup_fd_from_pid(child_pid, child_fd)
.map_err(|_| libc::EBADF);
}
let (root, sub) = match ctx.mount_target(virtual_path) {
Some((mt, sub)) => (mt.to_path_buf(), sub),
None => (ctx.root.to_path_buf(), vp_str.to_string()),
};
match openat2_in_root(&root, &sub, flags, mode) {
Ok(fd) => Ok(unsafe { OwnedFd::from_raw_fd(fd) }),
Err(errno) if matches!(errno, libc::EXDEV | libc::ENOENT | libc::ENOTDIR) => {
match resolve_self_fd_magic(ctx, child_pid, &vp_str) {
Some(child_fd) => crate::seccomp::notif::dup_fd_from_pid(child_pid, child_fd)
.map_err(|_| errno),
None => Err(errno),
}
}
Err(errno) => Err(errno),
}
}
fn magic_self_fd(path: &str, child_pid: u32) -> Option<i32> {
let (who, fd_part) = path.strip_prefix("/proc/")?.split_once("/fd/")?;
let is_self =
who == "self" || who == "thread-self" || who.parse::<u32>().ok() == Some(child_pid);
if !is_self || fd_part.contains('/') {
return None;
}
fd_part.parse::<i32>().ok()
}
fn read_symlink_in_root(ctx: &ChrootCtx<'_>, virtual_path: &str) -> Option<String> {
let confined = crate::chroot::resolve::confine(virtual_path);
let (root, sub) = if let Some((mt, sub)) = ctx.mount_target(&confined) {
(mt.to_path_buf(), sub)
} else {
(ctx.root.to_path_buf(), confined.to_string_lossy().into_owned())
};
let fd = openat2_in_root(
&root,
&sub,
libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC,
0,
)
.ok()?;
let mut buf = [0u8; 4096];
let n = unsafe {
libc::readlinkat(
fd,
c"".as_ptr(),
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
)
};
unsafe { libc::close(fd) };
if n <= 0 {
return None; }
Some(String::from_utf8_lossy(&buf[..n as usize]).into_owned())
}
fn resolve_self_fd_magic(ctx: &ChrootCtx<'_>, child_pid: u32, virtual_path: &str) -> Option<i32> {
let mut cur = virtual_path.to_string();
for _ in 0..40 {
if let Some(fd) = magic_self_fd(&cur, child_pid) {
return Some(fd);
}
let target = read_symlink_in_root(ctx, &cur)?;
let next = if Path::new(&target).is_absolute() {
target
} else {
let parent = Path::new(&cur).parent().unwrap_or_else(|| Path::new("/"));
parent.join(&target).to_string_lossy().into_owned()
};
cur = crate::chroot::resolve::confine(&next).to_string_lossy().into_owned();
}
None
}
fn read_pt_interp(fd: RawFd) -> Option<(String, u64, usize)> {
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
let mut header = [0u8; 64]; if file.read_exact(&mut header).is_err() {
std::mem::forget(file); return None;
}
if &header[..4] != b"\x7fELF" {
std::mem::forget(file);
return None;
}
let e_phoff = u64::from_le_bytes(header[32..40].try_into().ok()?);
let e_phentsize = u16::from_le_bytes(header[54..56].try_into().ok()?) as u64;
let e_phnum = u16::from_le_bytes(header[56..58].try_into().ok()?) as usize;
const PT_INTERP: u32 = 3;
for i in 0..e_phnum {
let ph_offset = e_phoff + (i as u64) * e_phentsize;
let mut phdr = [0u8; 56]; if file.seek(SeekFrom::Start(ph_offset)).is_err() {
break;
}
if file.read_exact(&mut phdr).is_err() {
break;
}
let p_type = u32::from_le_bytes(phdr[0..4].try_into().ok()?);
if p_type != PT_INTERP {
continue;
}
let p_offset = u64::from_le_bytes(phdr[8..16].try_into().ok()?);
let p_filesz = u64::from_le_bytes(phdr[32..40].try_into().ok()?) as usize;
if p_filesz == 0 || p_filesz > 256 {
break;
}
let mut buf = vec![0u8; p_filesz];
if file.seek(SeekFrom::Start(p_offset)).is_err() {
break;
}
if file.read_exact(&mut buf).is_err() {
break;
}
let nul = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
let interp = String::from_utf8_lossy(&buf[..nul]).to_string();
std::mem::forget(file);
return Some((interp, p_offset, p_filesz));
}
std::mem::forget(file);
None
}
fn memfd_with_patched_interp(
src_fd: RawFd,
new_interp: &str,
interp_offset: u64,
interp_capacity: usize,
) -> Option<OwnedFd> {
let size = {
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(src_fd, &mut stat) } < 0 {
return None;
}
stat.st_size as usize
};
let memfd = crate::sys::syscall::memfd_create("sandlock-exec", 0).ok()?;
let mfd = memfd.as_raw_fd();
if unsafe { libc::ftruncate(mfd, size as libc::off_t) } < 0 {
return None;
}
let mut offset: libc::off_t = 0;
let mut remaining = size;
while remaining > 0 {
let n = unsafe {
libc::sendfile(mfd, src_fd, &mut offset, remaining)
};
if n <= 0 {
return None;
}
remaining -= n as usize;
}
let new_bytes = new_interp.as_bytes();
if new_bytes.len() >= interp_capacity {
return None; }
let mut patch = vec![0u8; interp_capacity];
patch[..new_bytes.len()].copy_from_slice(new_bytes);
let mut mfd_file = unsafe { std::fs::File::from_raw_fd(mfd) };
if mfd_file.seek(SeekFrom::Start(interp_offset)).is_err() {
std::mem::forget(mfd_file);
return None;
}
if mfd_file.write_all(&patch).is_err() {
std::mem::forget(mfd_file);
return None;
}
std::mem::forget(mfd_file);
Some(memfd)
}
pub(crate) async fn handle_chroot_exec(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
_cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let nr = notif.data.nr as i64;
let (dirfd, path_ptr, argv_ptr, envp_ptr) = if nr == libc::SYS_execveat {
(notif.data.args[0] as i64, notif.data.args[1], notif.data.args[2], notif.data.args[3])
} else {
(libc::AT_FDCWD as i64, notif.data.args[0], notif.data.args[1], notif.data.args[2])
};
let rel_path = match read_path(notif, path_ptr, notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let full_path = if Path::new(&rel_path).is_absolute() {
rel_path
} else {
let dirfd32 = dirfd as i32;
let base_host = if dirfd32 == libc::AT_FDCWD {
match std::fs::read_link(format!("/proc/{}/cwd", notif.pid)) {
Ok(p) => p,
Err(_) => return NotifAction::Errno(libc::EACCES),
}
} else {
match std::fs::read_link(format!("/proc/{}/fd/{}", notif.pid, dirfd)) {
Ok(p) => p,
Err(_) => return NotifAction::Errno(libc::EACCES),
}
};
match ctx.host_to_virtual(&base_host) {
Some(base) => base.join(&rel_path).to_string_lossy().to_string(),
None => return NotifAction::Errno(libc::EACCES),
}
};
let virtual_path = crate::chroot::resolve::confine(&full_path);
if !ctx.can_read(&virtual_path) {
return NotifAction::Errno(libc::EACCES);
}
let (exec_root, exec_path) = if let Some((mt, sub)) = ctx.mount_target(&virtual_path) {
(mt.to_path_buf(), sub)
} else {
(ctx.root.to_path_buf(), virtual_path.to_string_lossy().to_string())
};
let src_fd = match openat2_in_root(
&exec_root,
&exec_path,
libc::O_RDONLY | libc::O_CLOEXEC,
0,
) {
Ok(fd) => fd,
Err(_) => return NotifAction::Errno(libc::ENOENT),
};
let exec_fd = if let Some((interp_path, interp_offset, interp_cap)) = read_pt_interp(src_fd) {
let interp_src = match openat2_in_root(
ctx.root,
&interp_path,
libc::O_RDONLY | libc::O_CLOEXEC,
0,
) {
Ok(fd) => fd,
Err(_) => {
unsafe { libc::close(src_fd) };
return NotifAction::Errno(libc::ENOENT);
}
};
let addfd_interp = SeccompNotifAddfd {
id: notif.id,
flags: 0,
srcfd: interp_src as u32,
newfd: 0,
newfd_flags: 0,
};
let child_interp_fd = unsafe {
libc::ioctl(
notif_fd,
SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong,
&addfd_interp as *const _,
)
};
unsafe { libc::close(interp_src) };
if child_interp_fd < 0 {
unsafe { libc::close(src_fd) };
return NotifAction::Errno(libc::EIO);
}
let new_interp = format!("/proc/self/fd/{}", child_interp_fd);
match memfd_with_patched_interp(src_fd, &new_interp, interp_offset, interp_cap) {
Some(memfd) => {
unsafe { libc::close(src_fd) };
memfd
}
None => {
unsafe { OwnedFd::from_raw_fd(src_fd) }
}
}
} else {
unsafe { OwnedFd::from_raw_fd(src_fd) }
};
{
let mut cs = chroot_state.lock().await;
cs.chroot_exe = Some(virtual_path.clone());
}
let addfd = SeccompNotifAddfd {
id: notif.id,
flags: 0,
srcfd: exec_fd.as_raw_fd() as u32,
newfd: 0,
newfd_flags: 0, };
let child_fd = unsafe {
libc::ioctl(
notif_fd,
SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong,
&addfd as *const _,
)
};
drop(exec_fd);
if child_fd < 0 {
return NotifAction::Errno(libc::EIO);
}
if crate::seccomp::notif::rewrite_exec_path_to_fd(
notif_fd, notif.id, notif.pid, path_ptr, argv_ptr, envp_ptr, child_fd,
)
.is_err()
{
return NotifAction::Errno(libc::EFAULT);
}
NotifAction::Continue
}
pub(crate) async fn handle_chroot_write(
notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let nr = notif.data.nr as i64;
if nr == libc::SYS_unlinkat {
let (_, host_path, vp) = match read_and_resolve(notif, notif_fd, ctx, 0, 1) {
Ok(r) => r,
Err(a) => return a,
};
if !ctx.can_write(&vp) { return NotifAction::Errno(libc::EACCES); }
let is_dir = (notif.data.args[2] & libc::AT_REMOVEDIR as u64) != 0;
{
let mut cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_mut() {
let s = host_path.to_string_lossy();
if cow.matches(&s) {
match cow.handle_unlink(&s, is_dir) {
Ok(true) => return NotifAction::ReturnValue(0),
Err(errno) => return NotifAction::Errno(errno),
_ => {}
}
}
}
}
return exec_on_host(
|p| if is_dir { unsafe { libc::rmdir(p) } } else { unsafe { libc::unlink(p) } },
&host_path,
);
}
if nr == libc::SYS_mkdirat {
let (_, host_path, vp) = match read_and_resolve(notif, notif_fd, ctx, 0, 1) {
Ok(r) => r,
Err(a) => return a,
};
if !ctx.can_write(&vp) { return NotifAction::Errno(libc::EACCES); }
let mode = notif.data.args[2] as u32;
{
let mut cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_mut() {
let s = host_path.to_string_lossy();
if cow.matches(&s) {
match cow.handle_mkdir(&s) {
Ok(true) => return NotifAction::ReturnValue(0),
Err(crate::error::BranchError::QuotaExceeded) => return NotifAction::Errno(libc::ENOSPC),
_ => {}
}
}
}
}
return exec_on_host(|p| unsafe { libc::mkdir(p, mode) }, &host_path);
}
if nr == libc::SYS_renameat2 {
let old_path = match read_path(notif, notif.data.args[1], notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let new_path = match read_path(notif, notif.data.args[3], notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let (old_host, old_vp) = match resolve_chroot_path(notif, notif.data.args[0] as i64, &old_path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::EACCES),
};
let (new_host, new_vp) = match resolve_chroot_path(notif, notif.data.args[2] as i64, &new_path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::EACCES),
};
if !ctx.can_write(&old_vp) || !ctx.can_write(&new_vp) {
return NotifAction::Errno(libc::EACCES);
}
{
let mut cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_mut() {
let old_str = old_host.to_string_lossy();
if cow.matches(&old_str) {
match cow.handle_rename(&old_str, &new_host.to_string_lossy()) {
Ok(true) => return NotifAction::ReturnValue(0),
Err(crate::error::BranchError::QuotaExceeded) => return NotifAction::Errno(libc::ENOSPC),
_ => {}
}
}
}
}
let c_old = match path_cstr(&old_host, libc::EINVAL) { Ok(c) => c, Err(a) => return a };
let c_new = match path_cstr(&new_host, libc::EINVAL) { Ok(c) => c, Err(a) => return a };
return if unsafe { libc::rename(c_old.as_ptr(), c_new.as_ptr()) } < 0 {
NotifAction::Errno(last_errno(libc::EIO))
} else {
NotifAction::ReturnValue(0)
};
}
if nr == libc::SYS_symlinkat {
let target = match read_path(notif, notif.data.args[0], notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let linkpath = match read_path(notif, notif.data.args[2], notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let (host_link, link_vp) = match resolve_chroot_path(notif, notif.data.args[1] as i64, &linkpath, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::EACCES),
};
if !ctx.can_write(&link_vp) { return NotifAction::Errno(libc::EACCES); }
{
let mut cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_mut() {
let s = host_link.to_string_lossy();
if cow.matches(&s) {
match cow.handle_symlink(&target, &s) {
Ok(true) => return NotifAction::ReturnValue(0),
Err(crate::error::BranchError::QuotaExceeded) => return NotifAction::Errno(libc::ENOSPC),
_ => {}
}
}
}
}
let c_target = match CString::new(target.as_str()) { Ok(c) => c, Err(_) => return NotifAction::Errno(libc::EINVAL) };
let c_link = match path_cstr(&host_link, libc::EINVAL) { Ok(c) => c, Err(a) => return a };
return if unsafe { libc::symlink(c_target.as_ptr(), c_link.as_ptr()) } < 0 {
NotifAction::Errno(last_errno(libc::EIO))
} else {
NotifAction::ReturnValue(0)
};
}
if nr == libc::SYS_linkat {
let old_path = match read_path(notif, notif.data.args[1], notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let new_path = match read_path(notif, notif.data.args[3], notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let (old_host, _) = match resolve_chroot_path(notif, notif.data.args[0] as i64, &old_path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::EACCES),
};
let (new_host, new_vp) = match resolve_chroot_path(notif, notif.data.args[2] as i64, &new_path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::EACCES),
};
if !ctx.can_write(&new_vp) { return NotifAction::Errno(libc::EACCES); }
{
let mut cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_mut() {
let s = new_host.to_string_lossy();
if cow.matches(&s) {
match cow.handle_link(&old_host.to_string_lossy(), &s) {
Ok(true) => return NotifAction::ReturnValue(0),
Err(crate::error::BranchError::QuotaExceeded) => return NotifAction::Errno(libc::ENOSPC),
_ => {}
}
}
}
}
let c_old = match path_cstr(&old_host, libc::EINVAL) { Ok(c) => c, Err(a) => return a };
let c_new = match path_cstr(&new_host, libc::EINVAL) { Ok(c) => c, Err(a) => return a };
let flags = notif.data.args[4] as i32;
return if unsafe { libc::linkat(libc::AT_FDCWD, c_old.as_ptr(), libc::AT_FDCWD, c_new.as_ptr(), flags) } < 0 {
NotifAction::Errno(last_errno(libc::EIO))
} else {
NotifAction::ReturnValue(0)
};
}
if nr == libc::SYS_fchmodat {
let (_, host_path, vp) = match read_and_resolve(notif, notif_fd, ctx, 0, 1) {
Ok(r) => r,
Err(a) => return a,
};
if !ctx.can_write(&vp) { return NotifAction::Errno(libc::EACCES); }
let mode = (notif.data.args[2] & 0o7777) as u32;
{
let mut cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_mut() {
let s = host_path.to_string_lossy();
if cow.matches(&s) {
match cow.handle_chmod(&s, mode) {
Ok(true) => return NotifAction::ReturnValue(0),
Err(crate::error::BranchError::QuotaExceeded) => return NotifAction::Errno(libc::ENOSPC),
_ => {}
}
}
}
}
return exec_on_host(|p| unsafe { libc::chmod(p, mode) }, &host_path);
}
if nr == libc::SYS_fchownat {
let (_, host_path, vp) = match read_and_resolve(notif, notif_fd, ctx, 0, 1) {
Ok(r) => r,
Err(a) => return a,
};
if !ctx.can_write(&vp) { return NotifAction::Errno(libc::EACCES); }
let uid = notif.data.args[2] as u32;
let gid = notif.data.args[3] as u32;
{
let mut cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_mut() {
let s = host_path.to_string_lossy();
if cow.matches(&s) {
match cow.handle_chown(&s, uid, gid) {
Ok(true) => return NotifAction::ReturnValue(0),
Err(crate::error::BranchError::QuotaExceeded) => return NotifAction::Errno(libc::ENOSPC),
_ => {}
}
}
}
}
return exec_on_host(|p| unsafe { libc::chown(p, uid, gid) }, &host_path);
}
if nr == libc::SYS_truncate {
let path = match read_path(notif, notif.data.args[0], notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let (host_path, vp) = match resolve_chroot_path(notif, libc::AT_FDCWD as i64, &path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::EACCES),
};
if !ctx.can_write(&vp) { return NotifAction::Errno(libc::EACCES); }
let length = notif.data.args[1] as i64;
{
let mut cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_mut() {
let s = host_path.to_string_lossy();
if cow.matches(&s) {
match cow.handle_truncate(&s, length) {
Ok(true) => return NotifAction::ReturnValue(0),
Err(crate::error::BranchError::QuotaExceeded) => return NotifAction::Errno(libc::ENOSPC),
_ => {}
}
}
}
}
return exec_on_host(|p| unsafe { libc::truncate(p, length) }, &host_path);
}
NotifAction::Continue
}
fn stat_and_write(notif: &SeccompNotif, notif_fd: RawFd, path: &Path) -> NotifAction {
let statbuf_addr = notif.data.args[2];
let flags = notif.data.args[3];
let follow = (flags & libc::AT_SYMLINK_NOFOLLOW as u64) == 0;
let meta = if follow {
std::fs::metadata(path)
} else {
std::fs::symlink_metadata(path)
};
let meta = match meta {
Ok(m) => m,
Err(_) => return NotifAction::Errno(libc::ENOENT),
};
use std::os::unix::fs::MetadataExt;
let mut buf = vec![0u8; std::mem::size_of::<libc::stat>()];
let mut off = 0;
macro_rules! pack_u64 { ($v:expr) => { buf[off..off+8].copy_from_slice(&($v as u64).to_ne_bytes()); off += 8; }; }
macro_rules! pack_u32 { ($v:expr) => { buf[off..off+4].copy_from_slice(&($v as u32).to_ne_bytes()); off += 4; }; }
pack_u64!(meta.dev()); pack_u64!(meta.ino()); pack_u64!(meta.nlink());
pack_u32!(meta.mode()); pack_u32!(meta.uid()); pack_u32!(meta.gid()); pack_u32!(0u32);
pack_u64!(meta.rdev()); pack_u64!(meta.size() as u64);
pack_u64!(meta.blksize()); pack_u64!(meta.blocks() as u64);
pack_u64!(meta.atime() as u64); pack_u64!(meta.atime_nsec() as u64);
pack_u64!(meta.mtime() as u64); pack_u64!(meta.mtime_nsec() as u64);
pack_u64!(meta.ctime() as u64); pack_u64!(meta.ctime_nsec() as u64);
let _ = off;
if write_child_mem(notif_fd, notif.id, notif.pid, statbuf_addr, &buf).is_err() {
return NotifAction::Continue;
}
NotifAction::ReturnValue(0)
}
pub(crate) async fn handle_chroot_stat(
notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let nr = notif.data.nr as i64;
let flags = notif.data.args[3];
if (flags & libc::AT_EMPTY_PATH as u64) != 0 {
return NotifAction::Continue;
}
let (_, host_path, vp) = match read_and_resolve_existing(notif, notif_fd, ctx, 0, 1) {
Ok(r) => r,
Err(a) => return a,
};
if !ctx.can_read(&vp) { return NotifAction::Errno(libc::EACCES); }
let real_path = match cow_resolve(cow_state, &host_path).await {
Ok(p) => p,
Err(a) => return a,
};
if nr == libc::SYS_faccessat || nr == crate::arch::SYS_FACCESSAT2 {
return if real_path.exists() || real_path.is_symlink() {
NotifAction::ReturnValue(0)
} else {
NotifAction::Errno(libc::ENOENT)
};
}
stat_and_write(notif, notif_fd, &real_path)
}
pub(crate) async fn handle_chroot_statx(
notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let dirfd = notif.data.args[0] as i64;
let path_ptr = notif.data.args[1];
let flags = notif.data.args[2] as i32;
let mask = notif.data.args[3] as u32;
let statxbuf_addr = notif.data.args[4];
if (flags & libc::AT_EMPTY_PATH) != 0 {
return NotifAction::Continue;
}
let path = match read_path(notif, path_ptr, notif_fd) {
Some(p) if !p.is_empty() => p,
_ => return NotifAction::Continue,
};
let (host_path, vp) = match resolve_chroot_path_existing(notif, dirfd, &path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::ENOENT),
};
if !ctx.can_read(&vp) { return NotifAction::Errno(libc::EACCES); }
let real_path = match cow_resolve(cow_state, &host_path).await {
Ok(p) => p,
Err(a) => return a,
};
let c_path = match path_cstr(&real_path, libc::ENOENT) {
Ok(c) => c,
Err(a) => return a,
};
let mut stx_buf = vec![0u8; 256];
let ret = unsafe {
libc::syscall(libc::SYS_statx, libc::AT_FDCWD, c_path.as_ptr(), flags, mask, stx_buf.as_mut_ptr())
};
if ret < 0 {
return NotifAction::Errno(last_errno(libc::ENOENT));
}
if write_child_mem(notif_fd, notif.id, notif.pid, statxbuf_addr, &stx_buf).is_err() {
return NotifAction::Continue;
}
NotifAction::ReturnValue(0)
}
pub(crate) async fn handle_chroot_readlink(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let dirfd = notif.data.args[0] as i64;
let path = match read_path(notif, notif.data.args[1], notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let buf_addr = notif.data.args[2];
let bufsiz = (notif.data.args[3] & 0xFFFFFFFF) as usize;
let write_target = |target: &[u8]| -> NotifAction {
let len = target.len().min(bufsiz);
if write_child_mem(notif_fd, notif.id, notif.pid, buf_addr, &target[..len]).is_err() {
return NotifAction::Continue;
}
NotifAction::ReturnValue(len as i64)
};
if path == "/proc/self/root" {
return write_target(b"/");
}
if path == "/proc/self/exe" {
let cs = chroot_state.lock().await;
if let Some(ref exe) = cs.chroot_exe {
let s = exe.to_string_lossy();
return write_target(s.as_bytes());
}
drop(cs);
if let Ok(real_exe) = std::fs::read_link(format!("/proc/{}/exe", notif.pid)) {
let virtual_exe = ctx.host_to_virtual(&real_exe).unwrap_or(real_exe);
let s = virtual_exe.to_string_lossy();
return write_target(s.as_bytes());
}
return NotifAction::Continue;
}
let full_path = if Path::new(&path).is_absolute() {
path.clone()
} else {
let dirfd32 = dirfd as i32;
let base_host = if dirfd32 == libc::AT_FDCWD {
match std::fs::read_link(format!("/proc/{}/cwd", notif.pid)) {
Ok(p) => p,
Err(_) => return NotifAction::Errno(libc::EACCES),
}
} else {
match std::fs::read_link(format!("/proc/{}/fd/{}", notif.pid, dirfd)) {
Ok(p) => p,
Err(_) => return NotifAction::Errno(libc::EACCES),
}
};
let base_virtual = match ctx.host_to_virtual(&base_host) {
Some(p) => p,
None => return NotifAction::Errno(libc::EACCES),
};
base_virtual.join(&path).to_string_lossy().to_string()
};
let confined = crate::chroot::resolve::confine(&full_path);
let file_name = match confined.file_name() {
Some(f) => f.to_os_string(),
None => return NotifAction::Errno(libc::EINVAL),
};
let parent = confined.parent().unwrap_or(Path::new("/"));
let parent_str = parent.to_str().unwrap_or("/");
let parent_host = if let Some((mt, sub)) = ctx.mount_target(parent) {
match resolve_in_root(mt, &sub) {
Some((hp, _)) => hp,
None => return NotifAction::Errno(libc::EACCES),
}
} else {
match resolve_in_root(ctx.root, parent_str) {
Some((hp, _)) => hp,
None => return NotifAction::Errno(libc::EACCES),
}
};
let host_path = parent_host.join(&file_name);
{
let cs = cow_state.lock().await;
if let Some(cow) = cs.branch.as_ref() {
let host_str = host_path.to_string_lossy();
if cow.matches(&host_str) {
let target = match cow.handle_readlink(&host_str) {
Some(t) => t,
None => return NotifAction::Errno(libc::ENOENT),
};
drop(cs);
return write_target(target.as_bytes());
}
}
}
let target = match std::fs::read_link(&host_path) {
Ok(t) => t,
Err(_) => return NotifAction::Errno(libc::ENOENT),
};
let display = if target.is_absolute() {
ctx.host_to_virtual(&target).unwrap_or(target)
} else {
target
};
write_target(display.to_string_lossy().as_bytes())
}
#[derive(Clone, Copy)]
enum XattrOp {
Get,
Set,
List,
Remove,
}
fn classify_xattr(nr: i64) -> Option<(XattrOp, bool)> {
Some(match nr {
libc::SYS_getxattr => (XattrOp::Get, true),
libc::SYS_lgetxattr => (XattrOp::Get, false),
libc::SYS_setxattr => (XattrOp::Set, true),
libc::SYS_lsetxattr => (XattrOp::Set, false),
libc::SYS_listxattr => (XattrOp::List, true),
libc::SYS_llistxattr => (XattrOp::List, false),
libc::SYS_removexattr => (XattrOp::Remove, true),
libc::SYS_lremovexattr => (XattrOp::Remove, false),
_ => return None,
})
}
const XATTR_MAX: usize = 65536;
fn xattr_read(
notif: &SeccompNotif,
notif_fd: RawFd,
c_path: &CString,
name: Option<&CString>,
follow: bool,
buf_idx: usize,
size_idx: usize,
) -> NotifAction {
let buf_addr = notif.data.args[buf_idx];
let size = (notif.data.args[size_idx] as usize).min(XATTR_MAX);
let mut buf = vec![0u8; size];
let buf_ptr = if size == 0 {
std::ptr::null_mut()
} else {
buf.as_mut_ptr() as *mut libc::c_void
};
let ret = unsafe {
match name {
Some(n) if follow => {
libc::syscall(libc::SYS_getxattr, c_path.as_ptr(), n.as_ptr(), buf_ptr, size)
}
Some(n) => {
libc::syscall(libc::SYS_lgetxattr, c_path.as_ptr(), n.as_ptr(), buf_ptr, size)
}
None if follow => libc::syscall(libc::SYS_listxattr, c_path.as_ptr(), buf_ptr, size),
None => libc::syscall(libc::SYS_llistxattr, c_path.as_ptr(), buf_ptr, size),
}
};
if ret < 0 {
return NotifAction::Errno(last_errno(libc::ENODATA));
}
if size > 0 && ret as usize > 0 {
let written = write_child_mem(notif_fd, notif.id, notif.pid, buf_addr, &buf[..ret as usize]);
if written.is_err() {
return NotifAction::Errno(libc::EFAULT);
}
}
NotifAction::ReturnValue(ret)
}
pub(crate) async fn handle_chroot_xattr(
notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let (op, follow) = match classify_xattr(notif.data.nr as i64) {
Some(x) => x,
None => return NotifAction::Continue,
};
let path = match read_path(notif, notif.data.args[0], notif_fd) {
Some(p) if !p.is_empty() => p,
_ => return NotifAction::Continue,
};
let (host_path, vp) =
match resolve_chroot_path_existing(notif, libc::AT_FDCWD as i64, &path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::ENOENT),
};
let writing = matches!(op, XattrOp::Set | XattrOp::Remove);
let allowed = if writing { ctx.can_write(&vp) } else { ctx.can_read(&vp) };
if !allowed {
return NotifAction::Errno(libc::EACCES);
}
let real_path = match cow_resolve(cow_state, &host_path).await {
Ok(p) => p,
Err(a) => return a,
};
let c_path = match path_cstr(&real_path, libc::ENOENT) {
Ok(c) => c,
Err(a) => return a,
};
let read_name = || -> Result<CString, NotifAction> {
let n = read_path(notif, notif.data.args[1], notif_fd)
.ok_or(NotifAction::Errno(libc::EFAULT))?;
CString::new(n).map_err(|_| NotifAction::Errno(libc::EINVAL))
};
match op {
XattrOp::Get => {
let name = match read_name() {
Ok(n) => n,
Err(a) => return a,
};
xattr_read(notif, notif_fd, &c_path, Some(&name), follow, 2, 3)
}
XattrOp::List => xattr_read(notif, notif_fd, &c_path, None, follow, 1, 2),
XattrOp::Set => {
let name = match read_name() {
Ok(n) => n,
Err(a) => return a,
};
let size = notif.data.args[3] as usize;
let flags = notif.data.args[4] as i32;
let value = match read_child_mem(notif_fd, notif.id, notif.pid, notif.data.args[2], size) {
Ok(v) => v,
Err(_) => return NotifAction::Errno(libc::EFAULT),
};
let nr = if follow { libc::SYS_setxattr } else { libc::SYS_lsetxattr };
let ret = unsafe {
libc::syscall(
nr,
c_path.as_ptr(),
name.as_ptr(),
value.as_ptr() as *const libc::c_void,
size,
flags,
)
};
if ret < 0 {
NotifAction::Errno(last_errno(libc::EIO))
} else {
NotifAction::ReturnValue(0)
}
}
XattrOp::Remove => {
let name = match read_name() {
Ok(n) => n,
Err(a) => return a,
};
let nr = if follow { libc::SYS_removexattr } else { libc::SYS_lremovexattr };
let ret = unsafe { libc::syscall(nr, c_path.as_ptr(), name.as_ptr()) };
if ret < 0 {
NotifAction::Errno(last_errno(libc::EIO))
} else {
NotifAction::ReturnValue(0)
}
}
}
}
pub(crate) async fn handle_chroot_getdents(
_notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
_cow_state: &Arc<Mutex<CowState>>,
_notif_fd: RawFd,
_ctx: &ChrootCtx<'_>,
) -> NotifAction {
NotifAction::Continue
}
pub(crate) async fn handle_chroot_chdir(
notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
_cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let path_ptr = notif.data.args[0];
let path = match read_path(notif, path_ptr, notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let orig_path_buf_len = path.len() + 1;
let was_absolute = Path::new(&path).is_absolute();
let virtual_path = if was_absolute {
path
} else {
match std::fs::read_link(format!("/proc/{}/cwd", notif.pid)) {
Ok(cwd) => match ctx.host_to_virtual(&cwd) {
Some(base) => base.join(&path).to_string_lossy().to_string(),
None => return NotifAction::Errno(libc::EACCES),
},
Err(_) => return NotifAction::Errno(libc::EACCES),
}
};
let full_path = canon_proc_self(&virtual_path, notif.pid);
let confined = confine(&full_path);
let (chdir_root, chdir_path) = if let Some((mt, sub)) = ctx.mount_target(&confined) {
(mt.to_path_buf(), sub)
} else {
(ctx.root.to_path_buf(), full_path.clone())
};
let src_fd = match openat2_in_root(
&chdir_root,
&chdir_path,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
0,
) {
Ok(fd) => fd,
Err(errno) => return NotifAction::Errno(errno),
};
if was_absolute
&& std::fs::read_link(format!("/proc/self/fd/{}", src_fd))
.ok()
.as_deref()
== Some(Path::new(&full_path))
{
unsafe { libc::close(src_fd) };
return NotifAction::Continue;
}
let addfd = SeccompNotifAddfd {
id: notif.id,
flags: 0,
srcfd: src_fd as u32,
newfd: 0,
newfd_flags: libc::O_CLOEXEC as u32,
};
let child_fd = unsafe {
libc::ioctl(
notif_fd,
SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong,
&addfd as *const _,
)
};
unsafe { libc::close(src_fd) };
if child_fd < 0 {
return NotifAction::Errno(libc::EIO);
}
let fd_path = format!("/proc/self/fd/{}\0", child_fd);
if orig_path_buf_len < fd_path.len() {
return NotifAction::Errno(libc::ENAMETOOLONG);
}
if write_child_mem_force(notif_fd, notif.id, notif.pid, path_ptr, fd_path.as_bytes()).is_err() {
return NotifAction::Errno(libc::EFAULT);
}
NotifAction::Continue
}
pub(crate) async fn handle_chroot_getcwd(
notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
_cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let buf_addr = notif.data.args[0];
let buf_size = (notif.data.args[1] & 0xFFFFFFFF) as usize;
let cwd = match std::fs::read_link(format!("/proc/{}/cwd", notif.pid)) {
Ok(c) => c,
Err(_) => return NotifAction::Continue,
};
let virtual_cwd = ctx.host_to_virtual(&cwd).unwrap_or_else(|| PathBuf::from("/"));
let cwd_str = virtual_cwd.to_string_lossy();
let cwd_bytes = cwd_str.as_bytes();
if cwd_bytes.len() + 1 > buf_size {
return NotifAction::Errno(libc::ERANGE);
}
let mut write_buf = cwd_bytes.to_vec();
write_buf.push(0);
if write_child_mem(notif_fd, notif.id, notif.pid, buf_addr, &write_buf).is_err() {
return NotifAction::Continue;
}
NotifAction::ReturnValue(write_buf.len() as i64)
}
pub(crate) async fn handle_chroot_statfs(
notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
_cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let path_ptr = notif.data.args[0];
let statfsbuf_addr = notif.data.args[1];
let path = match read_path(notif, path_ptr, notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let (host_path, _) = match resolve_chroot_path_existing(notif, libc::AT_FDCWD as i64, &path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::ENOENT),
};
let c_path = match path_cstr(&host_path, libc::ENOENT) {
Ok(c) => c,
Err(a) => return a,
};
let mut statfs_buf: libc::statfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statfs(c_path.as_ptr(), &mut statfs_buf) } < 0 {
return NotifAction::Errno(last_errno(libc::ENOENT));
}
let buf_bytes = unsafe {
std::slice::from_raw_parts(
&statfs_buf as *const libc::statfs as *const u8,
std::mem::size_of::<libc::statfs>(),
)
};
if write_child_mem(notif_fd, notif.id, notif.pid, statfsbuf_addr, buf_bytes).is_err() {
return NotifAction::Continue;
}
NotifAction::ReturnValue(0)
}
pub(crate) async fn handle_chroot_utimensat(
notif: &SeccompNotif,
_chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let dirfd = notif.data.args[0] as i64;
let path_ptr = notif.data.args[1];
let times_ptr = notif.data.args[2];
let flags = notif.data.args[3] as i32;
if path_ptr == 0 {
return NotifAction::Continue;
}
let path = match read_path(notif, path_ptr, notif_fd) {
Some(p) => p,
None => return NotifAction::Continue,
};
let (host_path, vp) = match resolve_chroot_path(notif, dirfd, &path, ctx) {
Some(r) => r,
None => return NotifAction::Errno(libc::EACCES),
};
if !ctx.can_write(&vp) { return NotifAction::Errno(libc::EACCES); }
let real_path = match cow_resolve(cow_state, &host_path).await {
Ok(p) => p,
Err(a) => return a,
};
let times = if times_ptr != 0 {
match read_child_mem(notif_fd, notif.id, notif.pid, times_ptr, 32) {
Ok(data) => {
let mut ts: [libc::timespec; 2] = unsafe { std::mem::zeroed() };
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), &mut ts as *mut _ as *mut u8, 32);
}
Some(ts)
}
Err(_) => return NotifAction::Errno(libc::EFAULT),
}
} else {
None
};
let c_path = match path_cstr(&real_path, libc::ENOENT) {
Ok(c) => c,
Err(a) => return a,
};
let times_raw = times.as_ref().map(|t| t.as_ptr()).unwrap_or(std::ptr::null());
if unsafe { libc::utimensat(libc::AT_FDCWD, c_path.as_ptr(), times_raw, flags) } < 0 {
return NotifAction::Errno(last_errno(libc::EIO));
}
NotifAction::ReturnValue(0)
}
fn notif_with_args(notif: &SeccompNotif, args: [u64; 6]) -> SeccompNotif {
let mut copy = *notif;
copy.data.args = args;
copy
}
pub(crate) async fn handle_chroot_legacy_open(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], notif.data.args[1], notif.data.args[2], 0, 0,
]);
handle_chroot_open(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_stat(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], notif.data.args[1], 0, 0, 0,
]);
handle_chroot_stat(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_lstat(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], notif.data.args[1], libc::AT_SYMLINK_NOFOLLOW as u64,
0, 0,
]);
handle_chroot_stat(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_access(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let mut synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], 0, 0, 0, 0,
]);
synth.data.nr = libc::SYS_faccessat as i32;
handle_chroot_stat(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_readlink(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], notif.data.args[1], notif.data.args[2], 0, 0,
]);
handle_chroot_readlink(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_unlink(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let mut synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], 0, 0, 0, 0,
]);
synth.data.nr = libc::SYS_unlinkat as i32;
handle_chroot_write(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_rmdir(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let mut synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], libc::AT_REMOVEDIR as u64,
0, 0, 0,
]);
synth.data.nr = libc::SYS_unlinkat as i32;
handle_chroot_write(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_mkdir(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let mut synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], notif.data.args[1], 0, 0, 0,
]);
synth.data.nr = libc::SYS_mkdirat as i32;
handle_chroot_write(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_rename(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let mut synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], libc::AT_FDCWD as u64,
notif.data.args[1], 0, 0,
]);
synth.data.nr = libc::SYS_renameat2 as i32;
handle_chroot_write(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_symlink(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let mut synth = notif_with_args(notif, [
notif.data.args[0], libc::AT_FDCWD as u64,
notif.data.args[1], 0, 0, 0,
]);
synth.data.nr = libc::SYS_symlinkat as i32;
handle_chroot_write(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_link(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let mut synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], libc::AT_FDCWD as u64,
notif.data.args[1], 0, 0,
]);
synth.data.nr = libc::SYS_linkat as i32;
handle_chroot_write(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_chmod(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
) -> NotifAction {
let mut synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], notif.data.args[1], 0, 0, 0,
]);
synth.data.nr = libc::SYS_fchmodat as i32;
handle_chroot_write(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
pub(crate) async fn handle_chroot_legacy_chown(
notif: &SeccompNotif,
chroot_state: &Arc<Mutex<ChrootState>>,
cow_state: &Arc<Mutex<CowState>>,
notif_fd: RawFd,
ctx: &ChrootCtx<'_>,
nofollow: bool,
) -> NotifAction {
let flags = if nofollow { libc::AT_SYMLINK_NOFOLLOW as u64 } else { 0 };
let mut synth = notif_with_args(notif, [
libc::AT_FDCWD as u64,
notif.data.args[0], notif.data.args[1], notif.data.args[2], flags,
0,
]);
synth.data.nr = libc::SYS_fchownat as i32;
handle_chroot_write(&synth, chroot_state, cow_state, notif_fd, ctx).await
}
#[cfg(test)]
mod self_rewrite_tests {
use super::canon_proc_self;
#[test]
fn rewrites_self_and_thread_self_to_pid() {
assert_eq!(canon_proc_self("/proc/self", 42), "/proc/42");
assert_eq!(canon_proc_self("/proc/self/status", 42), "/proc/42/status");
assert_eq!(canon_proc_self("/proc/self/fd/3", 42), "/proc/42/fd/3");
assert_eq!(canon_proc_self("/proc/thread-self", 42), "/proc/42");
assert_eq!(canon_proc_self("/proc/thread-self/maps", 42), "/proc/42/maps");
}
#[test]
fn leaves_other_paths_untouched() {
assert_eq!(canon_proc_self("/proc/7/status", 42), "/proc/7/status");
assert_eq!(canon_proc_self("/proc/cpuinfo", 42), "/proc/cpuinfo");
assert_eq!(canon_proc_self("/proc/selfish", 42), "/proc/selfish");
assert_eq!(canon_proc_self("/etc/passwd", 42), "/etc/passwd");
}
}
#[cfg(test)]
mod mount_ro_tests {
use super::ChrootCtx;
use std::path::{Path, PathBuf};
fn ctx<'a>(
mounts: &'a [(PathBuf, PathBuf)],
mount_ro: &'a [PathBuf],
writable: &'a [PathBuf],
) -> ChrootCtx<'a> {
ChrootCtx {
root: Path::new("/rootfs"),
readable: &[],
writable,
denied: &[],
mounts,
mount_ro,
}
}
#[test]
fn read_only_mount_denies_writes_but_allows_reads() {
let mounts = vec![(PathBuf::from("/proc"), PathBuf::from("/proc"))];
let ro = vec![PathBuf::from("/proc")];
let writable = vec![PathBuf::from("/")];
let c = ctx(&mounts, &ro, &writable);
assert!(c.can_read(Path::new("/proc/version")));
assert!(!c.can_write(Path::new("/proc/sys/kernel/core_pattern")));
assert!(!c.can_write(Path::new("/proc/self/oom_score_adj")));
}
#[test]
fn writable_mount_still_allows_writes() {
let mounts = vec![(PathBuf::from("/data"), PathBuf::from("/host/data"))];
let ro: Vec<PathBuf> = vec![];
let writable = vec![PathBuf::from("/data")];
let c = ctx(&mounts, &ro, &writable);
assert!(c.can_read(Path::new("/data/file")));
assert!(c.can_write(Path::new("/data/file")));
}
}