#![forbid(unsafe_code)]
use std::os::fd::AsRawFd;
use libseccomp::ScmpNotifResp;
use nix::errno::Errno;
use crate::{
caps::{util::capable, Capabilities},
compat::{readlinkat, EventFFlags, Fanotify, FsType, InitFlags, MarkFlags, MaskFlags},
confine::{scmp_arch_bits, scmp_arch_is_big_endian, ScmpNotifReq, SydSys::SysFanotifyMark},
cookie::{safe_exit_group, safe_fanotify_init},
fd::{to_fd, PROC_FILE},
kernel::syscall_path_handler,
log_enabled,
lookup::{file_type, FsFlags},
path::{XPath, XPathBuf},
req::{SysArg, SysFlags, UNotifyEventRequest},
sandbox::{Action, Capability, SandboxGuard},
syslog::LogLevel,
warn, xfmt,
};
const MARK_CMD: MarkFlags = MarkFlags::from_bits_retain(
MarkFlags::FAN_MARK_ADD.bits()
| MarkFlags::FAN_MARK_REMOVE.bits()
| MarkFlags::FAN_MARK_FLUSH.bits(),
);
const MARK_TYPE: MarkFlags = MarkFlags::from_bits_retain(
MarkFlags::FAN_MARK_FILESYSTEM.bits()
| MarkFlags::FAN_MARK_MNTNS.bits()
| MarkFlags::FAN_MARK_MOUNT.bits(),
);
const FAN_MARK_INODE: MarkFlags = MarkFlags::from_bits_retain(0);
const FANOTIFY_ADMIN_INIT_FLAGS: InitFlags = InitFlags::FAN_CLASS_CONTENT
.union(InitFlags::FAN_CLASS_PRE_CONTENT)
.union(InitFlags::FAN_REPORT_TID)
.union(InitFlags::FAN_REPORT_PIDFD)
.union(InitFlags::FAN_REPORT_FD_ERROR)
.union(InitFlags::FAN_UNLIMITED_QUEUE)
.union(InitFlags::FAN_UNLIMITED_MARKS);
const FANOTIFY_FID_BITS: InitFlags = InitFlags::FAN_REPORT_DIR_FID
.union(InitFlags::FAN_REPORT_NAME)
.union(InitFlags::FAN_REPORT_FID)
.union(InitFlags::FAN_REPORT_TARGET_FID);
const FANOTIFY_CLASS_BITS: InitFlags =
InitFlags::FAN_CLASS_CONTENT.union(InitFlags::FAN_CLASS_PRE_CONTENT);
const FANOTIFY_INIT_FLAGS: InitFlags = InitFlags::all().difference(InitFlags::FAN_ENABLE_AUDIT);
pub(crate) fn sys_fanotify_init(request: UNotifyEventRequest) -> ScmpNotifResp {
syscall_handler!(request, |request: UNotifyEventRequest| {
let req = request.scmpreq;
#[expect(clippy::cast_possible_truncation)]
let flags = InitFlags::from_bits_retain(req.data.args[0] as libc::c_uint);
#[expect(clippy::cast_possible_truncation)]
let event_f_flags = EventFFlags::from_bits_retain(req.data.args[1] as libc::c_uint);
let fid_mode = flags & FANOTIFY_FID_BITS;
let class = flags & FANOTIFY_CLASS_BITS;
if (flags.intersects(FANOTIFY_ADMIN_INIT_FLAGS)
|| !flags.intersects(FANOTIFY_FID_BITS | InitFlags::FAN_REPORT_MNT))
&& !capable(Capabilities::CAP_SYS_ADMIN)
{
return Err(Errno::EPERM);
}
if !flags.difference(FANOTIFY_INIT_FLAGS).is_empty() {
return Err(Errno::EINVAL);
}
if flags.contains(InitFlags::FAN_REPORT_MNT) {
if !class.is_empty() {
return Err(Errno::EINVAL);
}
if flags.intersects(FANOTIFY_FID_BITS | InitFlags::FAN_REPORT_FD_ERROR) {
return Err(Errno::EINVAL);
}
}
if !event_f_flags.difference(EventFFlags::all()).is_empty() {
return Err(Errno::EINVAL);
}
if event_f_flags & EventFFlags::O_ACCMODE == EventFFlags::O_ACCMODE {
return Err(Errno::EINVAL);
}
if !fid_mode.is_empty() && !class.is_empty() {
return Err(Errno::EINVAL);
}
if fid_mode.contains(InitFlags::FAN_REPORT_NAME)
&& !fid_mode.contains(InitFlags::FAN_REPORT_DIR_FID)
{
return Err(Errno::EINVAL);
}
if fid_mode.contains(InitFlags::FAN_REPORT_TARGET_FID)
&& !(fid_mode.contains(InitFlags::FAN_REPORT_NAME)
&& fid_mode.contains(InitFlags::FAN_REPORT_FID))
{
return Err(Errno::EINVAL);
}
if class == FANOTIFY_CLASS_BITS {
return Err(Errno::EINVAL);
}
handle_fanotify_init(&request, flags, event_f_flags)
})
}
#[expect(clippy::arithmetic_side_effects)]
pub(crate) fn sys_fanotify_mark(request: UNotifyEventRequest) -> ScmpNotifResp {
let req = request.scmpreq;
#[expect(clippy::cast_possible_truncation)]
let flags = req.data.args[1] as libc::c_uint;
let flags = match MarkFlags::from_bits(flags) {
Some(flags) => flags,
None => return request.fail_syscall(Errno::EINVAL),
};
let mark_type = flags & MARK_TYPE;
if !matches!(
mark_type,
FAN_MARK_INODE
| MarkFlags::FAN_MARK_MOUNT
| MarkFlags::FAN_MARK_FILESYSTEM
| MarkFlags::FAN_MARK_MNTNS
) {
return request.fail_syscall(Errno::EINVAL);
}
if flags.contains(MarkFlags::FAN_MARK_IGNORE | MarkFlags::FAN_MARK_IGNORED_MASK) {
return request.fail_syscall(Errno::EINVAL);
}
let mark_cmd = flags & MARK_CMD;
if !matches!(
mark_cmd,
MarkFlags::FAN_MARK_ADD | MarkFlags::FAN_MARK_REMOVE | MarkFlags::FAN_MARK_FLUSH
) {
return request.fail_syscall(Errno::EINVAL);
}
let (mask, narg) = match parse_mark_mask(req) {
Ok(mask) => mask,
Err(errno) => return request.fail_syscall(errno),
};
if mask.is_empty()
&& matches!(
mark_cmd,
MarkFlags::FAN_MARK_ADD | MarkFlags::FAN_MARK_REMOVE
)
{
return request.fail_syscall(Errno::EINVAL);
}
if mark_cmd == MarkFlags::FAN_MARK_FLUSH
&& !flags
.difference(MARK_TYPE | MarkFlags::FAN_MARK_FLUSH)
.is_empty()
{
return request.fail_syscall(Errno::EINVAL);
}
let notify_fd = match to_fd(req.data.args[0]) {
Ok(fd) => fd,
Err(errno) => return request.fail_syscall(errno),
};
let notify_fd = match request.get_fd(notify_fd).map(Fanotify::from) {
Ok(fd) => fd,
Err(errno) => return request.fail_syscall(errno),
};
match FsType::get(¬ify_fd) {
Ok(fst) if fst.is_anon_inode() => {
let pfd = match XPathBuf::from_self_fd(notify_fd.as_raw_fd()) {
Ok(pfd) => pfd,
Err(errno) => return request.fail_syscall(errno),
};
match readlinkat(PROC_FILE(), &pfd) {
Ok(target) if target.is_equal(b"anon_inode:[fanotify]") => {}
_ => return request.fail_syscall(Errno::EINVAL),
}
}
Ok(_) => return request.fail_syscall(Errno::EINVAL),
Err(errno) => return request.fail_syscall(errno),
}
if let Err(errno) = check_mark_type(flags, mask, mark_type) {
return request.fail_syscall(errno);
}
let mut fsflags = FsFlags::MUST_PATH;
if flags.contains(MarkFlags::FAN_MARK_DONT_FOLLOW) {
fsflags |= FsFlags::NO_FOLLOW_LAST;
}
let pidx = narg + 2;
let argv = &[SysArg {
dirfd: Some(narg + 1),
path: Some(pidx),
flags: SysFlags::EMPTY_PATH | SysFlags::MAYBE_NULL | SysFlags::PASS_DELETE,
fsflags,
}];
syscall_path_handler(
request,
SysFanotifyMark,
argv,
|path_args, request, sandbox| {
let restrict_notify_bdev = !sandbox.options.allow_unsafe_notify_bdev();
let restrict_notify_cdev = !sandbox.options.allow_unsafe_notify_cdev();
drop(sandbox);
#[expect(clippy::disallowed_methods)]
let fd = path_args.0.as_ref().unwrap().path.dir();
let mut mask = mask;
if restrict_notify_bdev || restrict_notify_cdev {
let filetype = file_type(fd, None, false)?;
if (restrict_notify_bdev && filetype.is_block_device())
|| (restrict_notify_cdev && filetype.is_char_device())
{
mask.remove(MaskFlags::FAN_ACCESS);
mask.remove(MaskFlags::FAN_ACCESS_PERM);
mask.remove(MaskFlags::FAN_MODIFY);
}
}
let mut flags = flags;
flags.remove(MarkFlags::FAN_MARK_DONT_FOLLOW);
let pfd = XPathBuf::from_self_fd(fd.as_raw_fd())?;
notify_fd
.mark(flags, mask, PROC_FILE(), Some(&pfd))
.map(|_| request.return_syscall(0))
},
)
}
fn handle_fanotify_init(
request: &UNotifyEventRequest,
mut flags: InitFlags,
event_f_flags: EventFFlags,
) -> Result<ScmpNotifResp, Errno> {
let sandbox = request.get_sandbox();
let force_cloexec = sandbox.flags.force_cloexec();
let force_rand_fd = sandbox.flags.force_rand_fd();
sandbox_fanotify_init(request, &sandbox)?;
drop(sandbox);
let cloexec = force_cloexec || flags.contains(InitFlags::FAN_CLOEXEC);
flags.insert(InitFlags::FAN_CLOEXEC);
let fd = safe_fanotify_init(flags, event_f_flags)?;
request.send_fd(fd, cloexec, force_rand_fd)
}
#[expect(clippy::cognitive_complexity)]
fn sandbox_fanotify_init(
request: &UNotifyEventRequest,
sandbox: &SandboxGuard<'_>,
) -> Result<(), Errno> {
let caps = Capability::CAP_CREATE;
if sandbox.getcaps(caps).is_empty() {
return Ok(());
}
let name = XPath::from_bytes(b"!fanotify");
let action = sandbox.check_name(caps, name);
if action.is_logging() && log_enabled!(LogLevel::Warn) {
if sandbox.log_scmp() {
warn!("ctx": "access", "cap": caps, "act": action,
"sys": "fanotify_init", "path": &name,
"tip": xfmt!("configure `allow/{caps}+{name}'"),
"req": request);
} else {
warn!("ctx": "access", "cap": caps, "act": action,
"sys": "fanotify_init", "path": &name,
"tip": xfmt!("configure `allow/{caps}+{name}'"),
"pid": request.scmpreq.pid);
}
}
match action {
Action::Allow | Action::Warn => Ok(()),
Action::Deny | Action::Filter => Err(Errno::EPERM),
Action::Panic => panic!(),
Action::Exit => safe_exit_group(Errno::EPERM as i32),
action => {
let _ = request.kill(action);
Err(Errno::EPERM)
}
}
}
fn check_mark_type(flags: MarkFlags, mask: MaskFlags, mark_type: MarkFlags) -> Result<(), Errno> {
if mark_type != FAN_MARK_INODE && !capable(Capabilities::CAP_SYS_ADMIN) {
return Err(Errno::EPERM);
}
if mask.contains(MaskFlags::FAN_FS_ERROR) && mark_type != MarkFlags::FAN_MARK_FILESYSTEM {
return Err(Errno::EINVAL);
}
if flags.contains(MarkFlags::FAN_MARK_EVICTABLE) && mark_type != FAN_MARK_INODE {
return Err(Errno::EINVAL);
}
if mask.contains(MaskFlags::FAN_PRE_ACCESS) && mask.contains(MaskFlags::FAN_ONDIR) {
return Err(Errno::EINVAL);
}
Ok(())
}
fn parse_mark_mask(req: ScmpNotifReq) -> Result<(MaskFlags, usize), Errno> {
let is32 = scmp_arch_bits(req.data.arch) == 32;
let (mask, narg) = if is32 {
let (lo, hi) = if scmp_arch_is_big_endian(req.data.arch) {
(req.data.args[3], req.data.args[2])
} else {
(req.data.args[2], req.data.args[3])
};
if hi != 0 {
return Err(Errno::EINVAL);
}
(lo, 3)
} else {
(req.data.args[2], 2)
};
MaskFlags::from_bits(mask)
.map(|mask| (mask, narg))
.ok_or(Errno::EINVAL)
}