use std::os::fd::AsRawFd;
use libc::c_int;
use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, NixPath};
use crate::{
compat::{inotify_add_watch, readlinkat, AddWatchFlags, FsType, InotifyInitFlags},
confine::SydSys::SysInotifyAddWatch,
cookie::{safe_exit_group, safe_inotify_init1},
fd::{to_fd, PROC_FILE},
kernel::syscall_path_handler,
log_enabled,
lookup::FsFlags,
path::{XPath, XPathBuf},
req::{SysArg, UNotifyEventRequest},
sandbox::{Action, Capability, SandboxGuard},
syslog::LogLevel,
warn, xfmt,
};
pub(crate) fn sys_inotify_init(request: UNotifyEventRequest) -> ScmpNotifResp {
syscall_handler!(request, |request: UNotifyEventRequest| {
handle_inotify_init(&request, InotifyInitFlags::empty())
})
}
pub(crate) fn sys_inotify_init1(request: UNotifyEventRequest) -> ScmpNotifResp {
syscall_handler!(request, |request: UNotifyEventRequest| {
let flags = to_inotify_init_flags(request.scmpreq.data.args[0])?;
handle_inotify_init(&request, flags)
})
}
pub(crate) fn sys_inotify_add_watch(request: UNotifyEventRequest) -> ScmpNotifResp {
let req = request.scmpreq;
#[expect(clippy::cast_possible_truncation)]
let mask = req.data.args[2] as u32;
let mask = match AddWatchFlags::from_bits(mask) {
Some(mask) if !mask.is_empty() => mask,
_ => return request.fail_syscall(Errno::EINVAL),
};
let infd = match to_fd(req.data.args[0]) {
Ok(fd) => fd,
Err(errno) => return request.fail_syscall(errno),
};
let infd = match request.get_fd(infd) {
Ok(fd) => fd,
Err(errno) => return request.fail_syscall(errno),
};
if mask.contains(AddWatchFlags::IN_MASK_ADD | AddWatchFlags::IN_MASK_CREATE) {
return request.fail_syscall(Errno::EINVAL);
}
match FsType::get(&infd) {
Ok(fst) if fst.is_anon_inode() => {
let pfd = match XPathBuf::from_self_fd(infd.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:inotify") => {}
_ => return request.fail_syscall(Errno::EINVAL),
}
}
Ok(_) => return request.fail_syscall(Errno::EINVAL),
Err(errno) => return request.fail_syscall(errno),
}
let mut fsflags = FsFlags::MUST_PATH;
if mask.contains(AddWatchFlags::IN_DONT_FOLLOW) {
fsflags |= FsFlags::NO_FOLLOW_LAST;
}
let argv = &[SysArg {
dirfd: None,
path: Some(1),
fsflags,
..Default::default()
}];
syscall_path_handler(
request,
SysInotifyAddWatch,
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 path = &path_args.0.as_ref().unwrap().path;
assert!(path.base().is_empty());
let mut mask = mask & !AddWatchFlags::IN_DONT_FOLLOW;
if mask.is_empty() {
mask = AddWatchFlags::IN_UNMOUNT;
}
if restrict_notify_bdev || restrict_notify_cdev {
#[expect(clippy::disallowed_methods)]
let typ = path.typ.as_ref().unwrap();
if (restrict_notify_bdev && typ.is_block_device())
|| (restrict_notify_cdev && typ.is_char_device())
{
mask.remove(AddWatchFlags::IN_ACCESS);
mask.remove(AddWatchFlags::IN_MODIFY);
}
}
let mut pfd = XPathBuf::try_from("/proc/thread-self/fd")?;
pfd.try_push_fd(path.dir().as_raw_fd())?;
inotify_add_watch(&infd, &pfd, mask)
.map(|retval| request.return_syscall(i64::from(retval)))
},
)
}
fn handle_inotify_init(
request: &UNotifyEventRequest,
mut flags: InotifyInitFlags,
) -> 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_inotify_init(request, &sandbox)?;
drop(sandbox);
let cloexec = force_cloexec || flags.contains(InotifyInitFlags::IN_CLOEXEC);
flags.insert(InotifyInitFlags::IN_CLOEXEC);
let fd = safe_inotify_init1(flags)?;
request.send_fd(fd, cloexec, force_rand_fd)
}
#[expect(clippy::cognitive_complexity)]
fn sandbox_inotify_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"!inotify");
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": "inotify_init", "path": &name,
"tip": xfmt!("configure `allow/{caps}+{name}'"),
"req": request);
} else {
warn!("ctx": "access", "cap": caps, "act": action,
"sys": "inotify_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::ENOMEM),
Action::Panic => panic!(),
Action::Exit => safe_exit_group(Errno::ENOMEM as i32),
action => {
let _ = request.kill(action);
Err(Errno::ENOMEM)
}
}
}
fn to_inotify_init_flags(arg: u64) -> Result<InotifyInitFlags, Errno> {
#[expect(clippy::cast_possible_truncation)]
let flags = arg as c_int;
InotifyInitFlags::from_bits(flags).ok_or(Errno::EINVAL)
}