use std::io::BufReader;
use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, fcntl::AtFlags, NixPath};
use crate::{
compat::{
fstatat64, statx, FileStat, FileStat64, FileStatx, FileStatxTimestamp, STATX_BASIC_STATS,
STATX_MODE, STATX_TYPE,
},
config::{API_VERSION, MAGIC_LOAD, MAGIC_PREFIX},
confine::{is_valid_ptr, scmp_arch_bits, EOWNERDEAD},
fd::{is_file, parse_fd},
hash::SydHashSet,
kernel::to_atflags,
lookup::{CanonicalPath, FileInfo, FileType, FsFlags},
path::XPath,
req::{SysArg, SysFlags, UNotifyEventRequest},
sandbox::{Capability, Options},
};
const AT_STATX_FORCE_SYNC: AtFlags = AtFlags::from_bits_retain(0x2000);
const AT_STATX_DONT_SYNC: AtFlags = AtFlags::from_bits_retain(0x4000);
pub(crate) fn sys_stat(request: UNotifyEventRequest) -> ScmpNotifResp {
let arg = SysArg {
path: Some(0),
flags: SysFlags::CHECK_MAGIC,
fsflags: FsFlags::MUST_PATH,
..Default::default()
};
syscall_stat_handler(request, arg, 1, false)
}
pub(crate) fn sys_stat64(request: UNotifyEventRequest) -> ScmpNotifResp {
let arg = SysArg {
path: Some(0),
flags: SysFlags::CHECK_MAGIC,
fsflags: FsFlags::MUST_PATH,
..Default::default()
};
syscall_stat_handler(request, arg, 1, true)
}
pub(crate) fn sys_fstat(request: UNotifyEventRequest) -> ScmpNotifResp {
let arg = SysArg {
dirfd: Some(0),
..Default::default()
};
syscall_stat_handler(request, arg, 1, false)
}
pub(crate) fn sys_fstat64(request: UNotifyEventRequest) -> ScmpNotifResp {
let arg = SysArg {
dirfd: Some(0),
..Default::default()
};
syscall_stat_handler(request, arg, 1, true)
}
pub(crate) fn sys_lstat(request: UNotifyEventRequest) -> ScmpNotifResp {
let arg = SysArg {
path: Some(0),
flags: SysFlags::CHECK_MAGIC,
fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
..Default::default()
};
syscall_stat_handler(request, arg, 1, false)
}
pub(crate) fn sys_lstat64(request: UNotifyEventRequest) -> ScmpNotifResp {
let arg = SysArg {
path: Some(0),
flags: SysFlags::CHECK_MAGIC,
fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
..Default::default()
};
syscall_stat_handler(request, arg, 1, true)
}
pub(crate) fn sys_statx(request: UNotifyEventRequest) -> ScmpNotifResp {
let req = request.scmpreq;
let atflags = match to_atflags(
req.data.args[2],
AtFlags::AT_EMPTY_PATH
| AtFlags::AT_SYMLINK_NOFOLLOW
| AtFlags::AT_NO_AUTOMOUNT
| AT_STATX_FORCE_SYNC
| AT_STATX_DONT_SYNC,
) {
Ok(atflags) => atflags,
Err(errno) => return request.fail_syscall(errno),
};
if atflags.contains(AT_STATX_FORCE_SYNC | AT_STATX_DONT_SYNC) {
return request.fail_syscall(Errno::EINVAL);
}
const STATX__RESERVED: u64 = 0x80000000;
if req.data.args[3] & STATX__RESERVED != 0 {
return request.fail_syscall(Errno::EINVAL);
}
let mut flags = SysFlags::empty();
let mut fsflags = FsFlags::MUST_PATH;
if atflags.contains(AtFlags::AT_EMPTY_PATH) {
flags |= SysFlags::EMPTY_PATH | SysFlags::MAYBE_NULL;
} else {
flags |= SysFlags::CHECK_MAGIC;
}
if atflags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
fsflags |= FsFlags::NO_FOLLOW_LAST;
}
let arg = SysArg {
dirfd: Some(0),
path: Some(1),
flags,
fsflags,
};
syscall_stat_handler(request, arg, 4, false)
}
pub(crate) fn sys_newfstatat(request: UNotifyEventRequest) -> ScmpNotifResp {
let req = request.scmpreq;
let atflags = match to_atflags(
req.data.args[3],
AtFlags::AT_EMPTY_PATH
| AtFlags::AT_SYMLINK_NOFOLLOW
| AtFlags::AT_NO_AUTOMOUNT
| AT_STATX_FORCE_SYNC
| AT_STATX_DONT_SYNC,
) {
Ok(atflags) => atflags,
Err(errno) => return request.fail_syscall(errno),
};
let mut flags = SysFlags::empty();
let mut fsflags = FsFlags::MUST_PATH;
if atflags.contains(AtFlags::AT_EMPTY_PATH) {
flags |= SysFlags::EMPTY_PATH | SysFlags::MAYBE_NULL;
} else {
flags |= SysFlags::CHECK_MAGIC;
}
if atflags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
fsflags |= FsFlags::NO_FOLLOW_LAST;
}
let arg = SysArg {
dirfd: Some(0),
path: Some(1),
flags,
fsflags,
};
syscall_stat_handler(request, arg, 2, true)
}
#[expect(clippy::cognitive_complexity)]
fn syscall_stat_handler(
request: UNotifyEventRequest,
arg: SysArg,
arg_stat: usize,
compat64: bool,
) -> ScmpNotifResp {
syscall_handler!(request, |request: UNotifyEventRequest| {
let req = request.scmpreq;
let sandbox = request.get_sandbox();
let (mut path, magic, empty_path) = request.read_path(&sandbox, arg)?;
let is_fd = empty_path || arg.path.is_none();
if sandbox.is_chroot() {
return Err(if is_fd { Errno::EACCES } else { Errno::ENOENT });
}
let has_crypt = sandbox.enabled(Capability::CAP_CRYPT);
let restrict_stat_bdev = !sandbox.flags.allow_unsafe_stat_bdev();
let restrict_stat_cdev = !sandbox.flags.allow_unsafe_stat_cdev();
let mut ghost = false;
let caps = *sandbox.state;
let opts = *sandbox.options;
if magic {
if sandbox.locked_drop_for(req.pid()) {
return Err(Errno::ENOENT);
}
drop(sandbox);
let cmd = path
.abs()
.strip_prefix(MAGIC_PREFIX)
.unwrap_or_else(|| XPath::from_bytes(&path.abs().as_bytes()[MAGIC_PREFIX.len()..]));
ghost = handle_magic_stat(&request, cmd)?;
} else {
#[expect(clippy::disallowed_methods)]
if is_fd && has_crypt {
let files = request.cache.crypt_map.as_ref().unwrap();
if let Ok(info) = FileInfo::from_fd(path.dir()) {
let files = files.0.lock().unwrap_or_else(|err| err.into_inner());
for (enc_path, map) in files.iter() {
if info == map.info {
path = CanonicalPath::new_crypt(
path.dir.take().unwrap(),
enc_path.clone(),
);
break;
}
}
} }
if is_fd && path.is_memory_fd() && path.abs().starts_with(b"!memfd:syd") {
let mut p = path.take();
p.drain(0..b"!memfd:syd".len());
path = CanonicalPath::new_mask(&p, &p)?;
}
if !is_fd {
if let Some(mask) = sandbox.is_masked(path.abs()) {
let mask = if let Some(mask_dir) = &mask.mask_dir {
if path.is_dir() {
Some(mask_dir)
} else {
mask.mask_all.as_ref()
}
} else {
mask.mask_all.as_ref()
};
match mask {
None => path = CanonicalPath::new_null(),
Some(mask) => path = CanonicalPath::new_mask(mask, path.abs())?,
};
}
}
drop(sandbox); }
assert!(path.base().is_empty()); let fd = path.dir();
let mut flags = libc::AT_EMPTY_PATH;
if !is_valid_ptr(req.data.args[arg_stat], req.data.arch) {
return Err(Errno::EFAULT);
}
#[expect(clippy::cast_possible_truncation)]
if arg_stat == 4 {
flags |= req.data.args[2] as libc::c_int
& !(libc::AT_SYMLINK_NOFOLLOW | libc::AT_EMPTY_PATH);
let mut mask = req.data.args[3] as libc::c_uint;
let orig_mask = mask;
let basic_stx = (orig_mask & STATX_BASIC_STATS) != 0;
if !basic_stx {
mask |= STATX_TYPE | STATX_MODE;
}
request.cache.add_sys_block(req, false)?;
let result = statx(fd, c"", flags, mask);
request.cache.del_sys_block(req.id)?;
let mut statx = result?;
if restrict_stat_bdev || restrict_stat_cdev {
let filetype = FileType::from(libc::mode_t::from(statx.stx_mode));
if (restrict_stat_bdev && filetype.is_block_device())
|| (restrict_stat_cdev && filetype.is_char_device())
{
statx.stx_atime = statx.stx_ctime;
statx.stx_mtime = statx.stx_ctime;
}
}
if magic {
magic_statx(&mut statx, caps, opts);
}
let statx = unsafe {
std::slice::from_raw_parts(
std::ptr::addr_of!(statx) as *const u8,
size_of_val(&statx),
)
};
let addr = req.data.args[4];
if addr != 0 {
request.write_mem_all(statx, addr)?;
}
} else {
request.cache.add_sys_block(req, false)?;
let result = fstatat64(fd, c"", flags);
request.cache.del_sys_block(req.id)?;
let mut stat = result?;
if restrict_stat_bdev || restrict_stat_cdev {
let filetype = FileType::from(stat.st_mode);
if (restrict_stat_bdev && filetype.is_block_device())
|| (restrict_stat_cdev && filetype.is_char_device())
{
stat.st_atime = stat.st_ctime;
stat.st_mtime = stat.st_ctime;
stat.st_atime_nsec = stat.st_ctime_nsec;
stat.st_mtime_nsec = stat.st_ctime_nsec;
}
}
if magic {
magic_stat(&mut stat, caps, opts);
}
let addr = req.data.args[arg_stat];
if addr != 0 {
let is32 = scmp_arch_bits(req.data.arch) == 32;
if is32 && compat64 {
let stat64: crate::compat::stat64 = stat.into();
let stat = unsafe {
std::slice::from_raw_parts(
std::ptr::addr_of!(stat64).cast::<u8>(),
size_of_val(&stat64),
)
};
request.write_mem_all(stat, addr)?;
} else if is32 {
let stat32: crate::compat::stat32 = stat.try_into()?;
let stat = unsafe {
std::slice::from_raw_parts(
std::ptr::addr_of!(stat32) as *const u8,
size_of_val(&stat32),
)
};
request.write_mem_all(stat, addr)?;
} else {
#[allow(clippy::useless_conversion)]
let stat: FileStat = stat.into();
let stat = unsafe {
std::slice::from_raw_parts(
std::ptr::addr_of!(stat) as *const u8,
size_of_val(&stat),
)
};
request.write_mem_all(stat, addr)?;
}
}
}
if ghost {
return Ok(ScmpNotifResp::new(0, 0, EOWNERDEAD, 0));
}
Ok(request.return_syscall(0))
})
}
fn handle_magic_stat(request: &UNotifyEventRequest, cmd: &XPath) -> Result<bool, Errno> {
let mut ghost = false;
let mut sandbox = request.get_mut_sandbox();
if cmd.is_empty() || cmd.is_equal(b".el") || cmd.is_equal(b".sh") {
sandbox.config("")?;
} else if cmd.is_equal(b"panic") {
sandbox.panic()?;
} else if cmd.is_equal(b"ghost") {
sandbox.reset(true)?;
ghost = true;
} else if let Some(cmd) = cmd.strip_prefix(b"load") {
match parse_fd(cmd) {
Ok(remote_fd) => {
let name = XPath::from_bytes(MAGIC_LOAD);
let file = request.get_fd(remote_fd)?;
if !is_file(&file)? {
return Err(Errno::EBADFD);
}
let file = BufReader::new(file);
sandbox.parse_config(file, name, &mut SydHashSet::default() )?;
}
Err(Errno::EBADF) => {
sandbox.parse_profile(cmd.as_bytes())?;
}
Err(errno) => return Err(errno),
}
} else {
std::str::from_utf8(cmd.as_bytes())
.or(Err(Errno::EINVAL))
.and_then(|cmd| sandbox.config(cmd))?;
}
Ok(ghost)
}
fn magic_stat(stat: &mut FileStat64, caps: Capability, opts: Options) {
stat.st_ino = 0;
stat.st_nlink = caps.nlink().into();
stat.st_mode = magic_mode(caps, opts).into();
stat.st_rdev = API_VERSION.dev();
stat.st_atime = 505958400; stat.st_ctime = -2036448000; stat.st_mtime = -842745600; }
fn magic_statx(statx: &mut FileStatx, caps: Capability, opts: Options) {
statx.stx_ino = 0;
statx.stx_nlink = caps.nlink();
statx.stx_mode = magic_mode(caps, opts);
statx.stx_rdev_major = API_VERSION.major().into();
statx.stx_rdev_minor = API_VERSION.minor().into();
statx.stx_atime = FileStatxTimestamp {
tv_sec: 505958400, ..Default::default()
};
statx.stx_ctime = FileStatxTimestamp {
tv_sec: -2036448000, ..Default::default()
};
statx.stx_mtime = FileStatxTimestamp {
tv_sec: -842745600, ..Default::default()
};
}
#[expect(clippy::cast_possible_truncation)]
fn magic_mode(caps: Capability, opts: Options) -> u16 {
let mut mode: u16 = libc::S_IFCHR as u16;
if opts.contains(Options::OPT_UNSHARE_MOUNT) {
mode |= libc::S_ISVTX as u16;
}
if opts.contains(Options::OPT_UNSHARE_USER) {
mode |= libc::S_ISUID as u16;
}
if opts.contains(Options::OPT_UNSHARE_NET) {
mode |= libc::S_ISGID as u16;
}
if caps.contains(Capability::CAP_READ) {
mode |= libc::S_IRUSR as u16;
}
if caps.contains(Capability::CAP_WRITE) {
mode |= libc::S_IWUSR as u16;
}
if caps.contains(Capability::CAP_EXEC) {
mode |= libc::S_IXUSR as u16;
}
if caps.contains(Capability::CAP_STAT) {
mode |= libc::S_IRGRP as u16;
}
if caps.contains(Capability::CAP_PROXY) {
mode |= libc::S_IWGRP as u16;
}
if caps.contains(Capability::CAP_TPE) {
mode |= libc::S_IXGRP as u16;
}
if caps.contains(Capability::CAP_LOCK) {
mode |= libc::S_IROTH as u16;
}
if caps.contains(Capability::CAP_CRYPT) {
mode |= libc::S_IWOTH as u16;
}
if caps.contains(Capability::CAP_FORCE) {
mode |= libc::S_IXOTH as u16;
}
mode
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sandbox::{Capability, Options};
#[test]
fn test_magic_mode_empty_caps_1() {
let mode = magic_mode(Capability::empty(), Options::empty());
assert_eq!(mode, libc::S_IFCHR as u16);
}
#[test]
fn test_magic_mode_cap_read_1() {
let mode = magic_mode(Capability::CAP_READ, Options::empty());
assert!(mode & libc::S_IRUSR as u16 != 0);
}
#[test]
fn test_magic_mode_cap_write_1() {
let mode = magic_mode(Capability::CAP_WRITE, Options::empty());
assert!(mode & libc::S_IWUSR as u16 != 0);
}
#[test]
fn test_magic_mode_cap_exec_1() {
let mode = magic_mode(Capability::CAP_EXEC, Options::empty());
assert!(mode & libc::S_IXUSR as u16 != 0);
}
#[test]
fn test_magic_mode_cap_stat_1() {
let mode = magic_mode(Capability::CAP_STAT, Options::empty());
assert!(mode & libc::S_IRGRP as u16 != 0);
}
#[test]
fn test_magic_mode_cap_proxy_1() {
let mode = magic_mode(Capability::CAP_PROXY, Options::empty());
assert!(mode & libc::S_IWGRP as u16 != 0);
}
#[test]
fn test_magic_mode_cap_tpe_1() {
let mode = magic_mode(Capability::CAP_TPE, Options::empty());
assert!(mode & libc::S_IXGRP as u16 != 0);
}
#[test]
fn test_magic_mode_cap_lock_1() {
let mode = magic_mode(Capability::CAP_LOCK, Options::empty());
assert!(mode & libc::S_IROTH as u16 != 0);
}
#[test]
fn test_magic_mode_cap_crypt_1() {
let mode = magic_mode(Capability::CAP_CRYPT, Options::empty());
assert!(mode & libc::S_IWOTH as u16 != 0);
}
#[test]
fn test_magic_mode_cap_force_1() {
let mode = magic_mode(Capability::CAP_FORCE, Options::empty());
assert!(mode & libc::S_IXOTH as u16 != 0);
}
#[test]
fn test_magic_mode_opt_unshare_mount_1() {
let mode = magic_mode(Capability::empty(), Options::OPT_UNSHARE_MOUNT);
assert!(mode & libc::S_ISVTX as u16 != 0);
}
#[test]
fn test_magic_mode_opt_unshare_user_1() {
let mode = magic_mode(Capability::empty(), Options::OPT_UNSHARE_USER);
assert!(mode & libc::S_ISUID as u16 != 0);
}
#[test]
fn test_magic_mode_opt_unshare_net_1() {
let mode = magic_mode(Capability::empty(), Options::OPT_UNSHARE_NET);
assert!(mode & libc::S_ISGID as u16 != 0);
}
#[test]
fn test_magic_mode_always_has_s_ifchr_1() {
let caps = Capability::CAP_READ | Capability::CAP_WRITE | Capability::CAP_EXEC;
let mode = magic_mode(caps, Options::OPT_UNSHARE_MOUNT);
assert!(mode & libc::S_IFMT as u16 == libc::S_IFCHR as u16);
}
}