macro_rules! syscall_handler {
($request:ident, $body:expr) => {{
let request_id = $request.scmpreq.id;
let _request_tid = $request.scmpreq.pid();
#[cfg(feature = "kcov")]
{
crate::kcov::abi::kcov_attach(_request_tid);
crate::kcov::abi::kcov_set_syscall(
$request.scmpreq.data.syscall.as_raw_syscall().into(),
);
let _ = crate::kcov::abi::kcov_enter_for(_request_tid);
crate::kcov_edge!();
}
let result = match $body($request) {
Ok(result) => result,
Err(Errno::UnknownErrno) => ScmpNotifResp::new(request_id, 0, -libc::ENOSYS, 0),
Err(errno) => {
let errno = (errno as i32).checked_neg().unwrap_or(-libc::ENOSYS);
ScmpNotifResp::new(request_id, 0, errno, 0)
}
};
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(_request_tid);
}
result
}};
}
pub(crate) mod access;
pub(crate) mod chdir;
pub(crate) mod chmod;
pub(crate) mod chown;
pub(crate) mod chroot;
pub(crate) mod exec;
pub(crate) mod fanotify;
pub(crate) mod fcntl;
pub(crate) mod getdents;
pub(crate) mod inotify;
pub(crate) mod ioctl;
pub(crate) mod link;
pub(crate) mod mem;
pub(crate) mod memfd;
pub(crate) mod mkdir;
pub(crate) mod mknod;
pub(crate) mod net;
pub(crate) mod open;
pub(crate) mod prctl;
pub(crate) mod readlink;
pub(crate) mod rename;
pub(crate) mod setid;
pub(crate) mod shm;
pub(crate) mod sigaction;
pub(crate) mod signal;
pub(crate) mod stat;
pub(crate) mod statfs;
pub(crate) mod symlink;
pub(crate) mod sysinfo;
pub(crate) mod syslog;
pub(crate) mod truncate;
pub(crate) mod uname;
pub(crate) mod utime;
pub(crate) mod unlink;
pub(crate) mod xattr;
pub(crate) mod ptrace;
pub(crate) mod sys_ptrace;
use libseccomp::ScmpNotifResp;
use nix::{
errno::Errno,
fcntl::AtFlags,
sys::{
signal::{kill, Signal},
stat::Mode,
},
unistd::Pid,
};
use crate::{
compat::RenameFlags,
err::cap2no,
fd::{to_fd, to_valid_fd},
log::log_is_main,
log_enabled,
lookup::{CanonicalPath, FileInfo, FileType},
notice,
path::XPath,
req::{PathArg, PathArgs, SysArg, UNotifyEventRequest},
sandbox::{Action, Capability, Sandbox, SandboxGuard},
syslog::LogLevel,
warn,
};
#[expect(clippy::cognitive_complexity)]
pub(crate) fn sandbox_path(
request: Option<&UNotifyEventRequest>,
sandbox: &Sandbox,
pid: Pid,
path: &XPath,
caps: Capability,
syscall_name: &str,
) -> Result<(), Errno> {
let caps_orig = caps & Capability::CAP_GLOB;
if caps != caps_orig {
return Err(Errno::EINVAL);
}
let caps = sandbox.getcaps(caps);
if caps.is_empty() {
return if caps_orig.can_write() && sandbox.is_write_protected(path) {
Err(Errno::EPERM)
} else {
Ok(())
};
}
let deny_errno = cap2no(caps);
if sandbox.is_chroot() {
return Err(deny_errno);
}
let path = path.replace_proc_self(pid);
let mut action = Action::Allow;
for cap in caps {
let new_action = sandbox.check_path(cap, &path);
if new_action > action {
action = new_action;
}
}
if action.is_logging() && log_enabled!(LogLevel::Warn) {
let is_warn = match caps {
Capability::CAP_STAT => !matches!(
sandbox.default_action(Capability::CAP_STAT),
Action::Filter | Action::Deny
),
Capability::CAP_WALK => !matches!(
sandbox.default_action(Capability::CAP_WALK),
Action::Filter | Action::Deny
),
_ => true,
};
if let Some(request) = request {
let args = request.scmpreq.data.args;
if sandbox.log_scmp() {
if is_warn {
warn!("ctx": "access", "cap": caps, "act": action,
"sys": syscall_name,
"path": &path, "args": args,
"tip": format!("configure `allow/{}+{}'",
caps.to_string().to_ascii_lowercase(),
path),
"req": request);
} else {
notice!("ctx": "access", "cap": caps, "act": action,
"sys": syscall_name,
"path": &path, "args": args,
"tip": format!("configure `allow/{}+{}'",
caps.to_string().to_ascii_lowercase(),
path),
"req": request);
}
} else if is_warn {
warn!("ctx": "access", "cap": caps, "act": action,
"sys": syscall_name,
"path": &path, "args": args,
"tip": format!("configure `allow/{}+{}'",
caps.to_string().to_ascii_lowercase(),
path),
"pid": request.scmpreq.pid);
} else {
notice!("ctx": "access", "cap": caps, "act": action,
"sys": syscall_name,
"path": &path, "args": args,
"tip": format!("configure `allow/{}+{}'",
caps.to_string().to_ascii_lowercase(),
path),
"pid": request.scmpreq.pid);
}
} else if is_warn {
warn!("ctx": "access", "cap": caps, "act": action,
"sys": syscall_name, "path": &path,
"tip": format!("configure `allow/{}+{}'",
caps.to_string().to_ascii_lowercase(),
path),
"pid": pid.as_raw());
} else {
notice!("ctx": "access", "cap": caps, "act": action,
"sys": syscall_name, "path": &path,
"tip": format!("configure `allow/{}+{}'",
caps.to_string().to_ascii_lowercase(),
path),
"pid": pid.as_raw());
}
}
match action {
Action::Allow | Action::Warn => {
if caps_orig.can_write() && sandbox.is_write_protected(&path) {
return Err(Errno::EPERM);
}
Ok(())
}
Action::Deny | Action::Filter => Err(deny_errno),
Action::Panic if log_is_main(std::thread::current().id()) => Err(deny_errno),
Action::Panic => panic!(),
Action::Exit => std::process::exit(deny_errno as i32),
Action::Stop => {
if let Some(request) = request {
let _ = request.pidfd_kill(libc::SIGSTOP);
} else {
let _ = kill(pid, Some(Signal::SIGSTOP));
}
Err(deny_errno)
}
Action::Abort => {
if let Some(request) = request {
let _ = request.pidfd_kill(libc::SIGABRT);
} else {
let _ = kill(pid, Some(Signal::SIGABRT));
}
Err(deny_errno)
}
Action::Kill => {
if let Some(request) = request {
let _ = request.pidfd_kill(libc::SIGKILL);
} else {
let _ = kill(pid, Some(Signal::SIGKILL));
}
Err(deny_errno)
}
}
}
#[expect(clippy::cognitive_complexity)]
pub(crate) fn syscall_path_handler<H>(
request: UNotifyEventRequest,
syscall_name: &str,
path_argv: &[SysArg],
handler: H,
) -> ScmpNotifResp
where
H: Fn(PathArgs, &UNotifyEventRequest, SandboxGuard) -> Result<ScmpNotifResp, Errno>,
{
syscall_handler!(request, |request: UNotifyEventRequest| {
let req = request.scmpreq;
let mut caps = Capability::try_from((req, syscall_name))?;
let is_fd = path_argv.iter().all(|arg| arg.path.is_none());
let sandbox = request.get_sandbox();
if sandbox.is_chroot() && !is_fd && !caps.contains(Capability::CAP_CHDIR) {
return Err(Errno::ENOENT);
}
let crypt = sandbox.enabled(Capability::CAP_CRYPT);
let mut magic = false;
let mut paths: [Option<PathArg>; 2] = [None, None];
for (idx, arg) in path_argv.iter().enumerate() {
if arg.path.is_some() {
let (path, is_magic, is_empty) = request.read_path(&sandbox, *arg)?;
magic = is_magic;
if sandbox.is_chroot() {
return if caps.contains(Capability::CAP_CHDIR) && path.abs().is_root() {
Ok(request.return_syscall(0))
} else {
Err(Errno::ENOENT)
};
}
let path = PathArg { path, is_empty };
paths[idx] = Some(path);
} else if let Some(arg_idx) = arg.dirfd {
let dirfd = if arg.path.is_some() {
to_valid_fd(req.data.args[arg_idx])?
} else {
to_fd(req.data.args[arg_idx])?
};
if dirfd != libc::AT_FDCWD {
let fd = request.get_fd(dirfd)?;
let crypt_path = if crypt {
#[expect(clippy::disallowed_methods)]
let files = request.cache.crypt_map.as_ref().unwrap();
if let Ok(info) = FileInfo::from_fd(&fd) {
let files = files.0.lock().unwrap_or_else(|e| e.into_inner());
files
.iter()
.find_map(|(path, map)| (map.info == info).then(|| path.clone()))
} else {
None
}
} else {
None
};
let path = if let Some(crypt_path) = crypt_path {
CanonicalPath::new_crypt(fd.into(), crypt_path)
} else {
CanonicalPath::new_fd(fd.into(), req.pid())?
};
let path = PathArg {
path,
is_empty: false,
};
paths[idx] = Some(path);
} else {
let path = CanonicalPath::new_fd(libc::AT_FDCWD.into(), req.pid())?;
let path = PathArg {
path,
is_empty: false,
};
paths[idx] = Some(path);
}
} else {
unreachable!("BUG: Both dirfd and path are None in SysArg!");
}
}
if magic && sandbox.locked_for(req.pid()) {
return Err(Errno::ENOENT);
}
if !magic {
match (&paths[0], &paths[1]) {
(Some(PathArg { path, .. }), None) => {
if caps.contains(Capability::CAP_CREATE) && path.typ.is_some() {
caps.remove(Capability::CAP_CREATE);
}
if caps.contains(Capability::CAP_DELETE) && path.typ.is_none() {
caps.remove(Capability::CAP_DELETE);
}
if caps.contains(Capability::CAP_CHDIR) && path.typ != Some(FileType::Dir) {
caps.remove(Capability::CAP_CHDIR);
}
if caps.contains(Capability::CAP_MKDIR) && path.typ.is_some() {
caps.remove(Capability::CAP_MKDIR);
}
sandbox_path(
Some(&request),
&sandbox,
request.scmpreq.pid(), path.abs(),
caps,
syscall_name,
)?
}
(Some(PathArg { path: path_0, .. }), Some(PathArg { path: path_1, .. })) => {
sandbox_path(
Some(&request),
&sandbox,
request.scmpreq.pid(), path_0.abs(),
caps,
syscall_name,
)?;
if path_1.typ.is_none() || !path_argv[1].fsflags.missing() {
let mut caps = Capability::CAP_CREATE;
if path_1.typ.is_some() {
caps.insert(Capability::CAP_DELETE);
}
if path_argv[1].fsflags.must_exist() {
caps.insert(Capability::CAP_RENAME);
}
sandbox_path(
Some(&request),
&sandbox,
request.scmpreq.pid(), path_1.abs(),
caps,
syscall_name,
)?;
}
}
_ => unreachable!("BUG: number of path arguments is not 1 or 2!"),
}
}
let path_args = PathArgs(paths[0].take(), paths[1].take());
handler(path_args, &request, sandbox)
})
}
pub(crate) fn to_atflags(arg: u64, valid: AtFlags) -> Result<AtFlags, Errno> {
#[expect(clippy::cast_possible_truncation)]
let flags = arg as libc::c_int;
let flags = AtFlags::from_bits_retain(flags);
if !flags.difference(valid).is_empty() {
return Err(Errno::EINVAL);
}
Ok(flags)
}
pub(crate) fn to_mode(arg: u64) -> Mode {
const S_IALLUGO: libc::mode_t = libc::S_ISUID
| libc::S_ISGID
| libc::S_ISVTX
| libc::S_IRWXU
| libc::S_IRWXG
| libc::S_IRWXO;
#[expect(clippy::cast_possible_truncation)]
Mode::from_bits_truncate((arg as libc::mode_t) & S_IALLUGO)
}
pub(crate) fn to_mode2(arg: u64) -> Result<Mode, Errno> {
let mode = arg.try_into().or(Err(Errno::EINVAL))?;
Mode::from_bits(mode).ok_or(Errno::EINVAL)
}
pub(crate) fn to_renameflags(arg: u64) -> Result<RenameFlags, Errno> {
#[expect(clippy::cast_possible_truncation)]
let flags = RenameFlags::from_bits(arg as u32).ok_or(Errno::EINVAL)?;
if flags.contains(RenameFlags::RENAME_EXCHANGE)
&& flags.intersects(RenameFlags::RENAME_NOREPLACE | RenameFlags::RENAME_WHITEOUT)
{
return Err(Errno::EINVAL);
}
Ok(flags)
}
pub(crate) fn to_id16(arg: u64) -> u64 {
to_id16_val(arg).unwrap_or(u64::from(u32::MAX))
}
pub(crate) fn to_id16_val(arg: u64) -> Result<u64, Errno> {
#[expect(clippy::cast_possible_truncation)]
match arg as u16 {
u16::MAX => Err(Errno::EINVAL),
value => Ok(u64::from(value)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fd::AT_EXECVE_CHECK;
#[test]
fn test_to_atflags() {
let valid = AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH | AT_EXECVE_CHECK;
assert_eq!(to_atflags(valid.bits() as u64, valid), Ok(valid));
let invalid = AtFlags::AT_REMOVEDIR;
assert_eq!(to_atflags(invalid.bits() as u64, valid), Err(Errno::EINVAL));
assert_eq!(
to_atflags((valid | invalid).bits() as u64, valid),
Err(Errno::EINVAL)
);
assert_eq!(
to_atflags((valid | invalid).bits() as u64, valid | invalid),
Ok(valid | invalid)
);
assert_eq!(to_atflags(1u64 << 32, valid), Ok(AtFlags::empty()));
assert_eq!(
to_atflags(valid.bits() as u64 | (1u64 << 32), valid),
Ok(valid)
);
assert_eq!(to_atflags(1u64 << 33, valid), Ok(AtFlags::empty()));
assert_eq!(
to_atflags(
AtFlags::AT_SYMLINK_NOFOLLOW.bits() as u64 | (0xFFFF_FFFFu64 << 32),
valid
),
Ok(AtFlags::AT_SYMLINK_NOFOLLOW)
);
assert_eq!(to_atflags(u64::MAX, valid), Err(Errno::EINVAL));
}
#[test]
fn test_to_mode_1() {
assert!(to_mode(0).is_empty());
}
#[test]
fn test_to_mode_2() {
let mode = to_mode(0o755);
assert!(mode.contains(Mode::S_IRWXU));
assert!(mode.contains(Mode::S_IRGRP | Mode::S_IXGRP));
assert!(mode.contains(Mode::S_IROTH | Mode::S_IXOTH));
}
#[test]
fn test_to_mode_3() {
let mode = to_mode(0o4755);
assert!(mode.contains(Mode::S_ISUID));
assert!(mode.contains(Mode::S_IRWXU));
}
#[test]
fn test_to_mode_4() {
let mode = to_mode(0o1777);
assert!(mode.contains(Mode::S_ISVTX));
assert!(mode.contains(Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO));
}
#[test]
fn test_to_mode_5() {
assert_eq!(to_mode(0o10755), to_mode(0o755));
assert_eq!(to_mode(0o777 | (1u64 << 32)), to_mode(0o777));
}
#[test]
fn test_to_mode_6() {
let mode = to_mode(u64::MAX);
assert!(mode.contains(Mode::S_ISUID | Mode::S_ISGID | Mode::S_ISVTX));
assert!(mode.contains(Mode::S_IRWXU | Mode::S_IRWXG | Mode::S_IRWXO));
}
#[test]
fn test_to_mode2_1() {
assert!(to_mode2(0o755).is_ok());
assert!(to_mode2(0).is_ok());
assert!(to_mode2(0o7777).is_ok());
}
#[test]
fn test_to_mode2_2() {
assert_eq!(to_mode2(0o10000), Err(Errno::EINVAL));
}
#[test]
fn test_to_mode2_3() {
assert_eq!(to_mode2(u64::MAX), Err(Errno::EINVAL));
assert_eq!(to_mode2(1u64 << 32), Err(Errno::EINVAL));
}
#[test]
fn test_to_renameflags_1() {
assert_eq!(to_renameflags(0), Ok(RenameFlags::empty()));
}
#[test]
fn test_to_renameflags_2() {
let result = to_renameflags(RenameFlags::RENAME_NOREPLACE.bits() as u64);
assert_eq!(result, Ok(RenameFlags::RENAME_NOREPLACE));
}
#[test]
fn test_to_renameflags_3() {
let result = to_renameflags(RenameFlags::RENAME_EXCHANGE.bits() as u64);
assert_eq!(result, Ok(RenameFlags::RENAME_EXCHANGE));
}
#[test]
fn test_to_renameflags_4() {
let result = to_renameflags(RenameFlags::RENAME_WHITEOUT.bits() as u64);
assert_eq!(result, Ok(RenameFlags::RENAME_WHITEOUT));
}
#[test]
fn test_to_renameflags_5() {
let arg = (RenameFlags::RENAME_EXCHANGE | RenameFlags::RENAME_NOREPLACE).bits() as u64;
assert_eq!(to_renameflags(arg), Err(Errno::EINVAL));
}
#[test]
fn test_to_renameflags_6() {
let arg = (RenameFlags::RENAME_EXCHANGE | RenameFlags::RENAME_WHITEOUT).bits() as u64;
assert_eq!(to_renameflags(arg), Err(Errno::EINVAL));
}
#[test]
fn test_to_renameflags_7() {
assert_eq!(to_renameflags(0x08), Err(Errno::EINVAL));
}
#[test]
fn test_to_renameflags_8() {
let arg = RenameFlags::RENAME_NOREPLACE.bits() as u64 | (1u64 << 32);
assert_eq!(to_renameflags(arg), Ok(RenameFlags::RENAME_NOREPLACE));
}
}