#![forbid(unsafe_code)]
use std::os::fd::AsRawFd;
use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, fcntl::AtFlags, sys::stat::Mode, NixPath};
use crate::{
cookie::{safe_fchmod, safe_fchmodat, safe_fchmodat2},
error,
fd::{fd_mode, PROC_FILE},
kernel::{syscall_path_handler, to_atflags, to_mode},
lookup::{CanonicalPath, FileType, FsFlags},
path::XPathBuf,
req::{PathArgs, SysArg, SysFlags, UNotifyEventRequest},
sandbox::SandboxGuard,
};
pub(crate) fn sys_fchmod(request: UNotifyEventRequest) -> ScmpNotifResp {
let req = request.scmpreq;
let mode = to_mode(req.data.args[1]);
let argv = &[SysArg {
dirfd: Some(0),
flags: SysFlags::PASS_DELETE,
fsflags: FsFlags::MUST_PATH,
..Default::default()
}];
syscall_path_handler(request, "fchmod", argv, |path_args, request, sandbox| {
#[expect(clippy::disallowed_methods)]
let path = &path_args.0.as_ref().unwrap().path;
assert!(path.base().is_empty());
let umask = sandbox.umask.unwrap_or(Mode::empty());
let restrict_sticky = !sandbox.flags.allow_unsafe_sticky();
let log_scmp = sandbox.log_scmp();
drop(sandbox); let mut mode = mode;
safe_chmod_mode(request, path, &mut mode, umask, restrict_sticky, log_scmp)?;
safe_fchmod(path.dir(), mode).map(|_| request.return_syscall(0))
})
}
pub(crate) fn sys_chmod(request: UNotifyEventRequest) -> ScmpNotifResp {
let req = request.scmpreq;
let mode = to_mode(req.data.args[1]);
let argv = &[SysArg {
path: Some(0),
..Default::default()
}];
syscall_path_handler(request, "chmod", argv, |path_args, request, sandbox| {
syscall_chmod_handler(request, sandbox, path_args, mode)
})
}
pub(crate) fn sys_fchmodat(request: UNotifyEventRequest) -> ScmpNotifResp {
let req = request.scmpreq;
let mode = to_mode(req.data.args[2]);
let argv = &[SysArg {
dirfd: Some(0),
path: Some(1),
..Default::default()
}];
syscall_path_handler(request, "fchmodat", argv, |path_args, request, sandbox| {
syscall_chmod_handler(request, sandbox, path_args, mode)
})
}
pub(crate) fn sys_fchmodat2(request: UNotifyEventRequest) -> ScmpNotifResp {
let req = request.scmpreq;
let atflags = match to_atflags(
req.data.args[3],
AtFlags::AT_EMPTY_PATH | AtFlags::AT_SYMLINK_NOFOLLOW,
) {
Ok(atflags) => atflags,
Err(errno) => return request.fail_syscall(errno),
};
let mode = to_mode(req.data.args[2]);
let mut flags = SysFlags::empty();
let mut fsflags = FsFlags::MUST_PATH;
if atflags.contains(AtFlags::AT_EMPTY_PATH) {
flags |= SysFlags::EMPTY_PATH | SysFlags::PASS_DELETE;
}
if atflags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
fsflags |= FsFlags::NO_FOLLOW_LAST;
}
let argv = &[SysArg {
dirfd: Some(0),
path: Some(1),
flags,
fsflags,
}];
syscall_path_handler(request, "fchmodat2", argv, |path_args, request, sandbox| {
syscall_chmod_handler(request, sandbox, path_args, mode)
})
}
fn syscall_chmod_handler(
request: &UNotifyEventRequest,
sandbox: SandboxGuard,
args: PathArgs,
mut mode: Mode,
) -> Result<ScmpNotifResp, Errno> {
#[expect(clippy::disallowed_methods)]
let path = &args.0.as_ref().unwrap().path;
assert!(path.base().is_empty()); let fd = path.dir();
let umask = sandbox.umask.unwrap_or(Mode::empty());
let restrict_sticky = !sandbox.flags.allow_unsafe_sticky();
let log_scmp = sandbox.log_scmp();
drop(sandbox); safe_chmod_mode(request, path, &mut mode, umask, restrict_sticky, log_scmp)?;
match safe_fchmodat2(fd, mode) {
Ok(_) => Ok(()),
Err(Errno::ENOSYS) => {
let pfd = XPathBuf::from_self_fd(fd.as_raw_fd())?;
safe_fchmodat(PROC_FILE(), &pfd, mode)
}
Err(errno) => Err(errno),
}
.map(|_| request.return_syscall(0))
}
#[expect(clippy::cognitive_complexity)]
fn safe_chmod_mode(
request: &UNotifyEventRequest,
path: &CanonicalPath,
mode: &mut Mode,
umask: Mode,
restrict_sticky: bool,
log_scmp: bool,
) -> Result<(), Errno> {
match path.typ {
Some(FileType::Reg) => *mode &= !umask,
Some(FileType::Dir) if restrict_sticky => {
let need_setgid = !mode.contains(Mode::S_ISGID);
let need_sticky = !mode.contains(Mode::S_ISVTX);
if !need_setgid && !need_sticky {
return Ok(());
}
let my_mode = fd_mode(path.dir())?;
let fix_setgid = need_setgid && my_mode.contains(Mode::S_ISGID);
let fix_sticky = need_sticky && my_mode.contains(Mode::S_ISVTX);
if !fix_setgid && !fix_sticky {
return Ok(());
}
if log_scmp {
error!("ctx": "immutable_sticky",
"path": path.abs(), "mode": mode.bits(),
"msg": safe_chmod_errmsg(fix_setgid, fix_sticky),
"tip": "fix your program or use `trace/allow_unsafe_sticky:1'",
"req": request);
} else {
error!("ctx": "immutable_sticky",
"path": path.abs(), "mode": mode.bits(),
"msg": safe_chmod_errmsg(fix_setgid, fix_sticky),
"tip": "fix your program or use `trace/allow_unsafe_sticky:1'",
"pid": request.scmpreq.pid);
}
if fix_setgid {
mode.insert(Mode::S_ISGID);
}
if fix_sticky {
mode.insert(Mode::S_ISVTX);
}
}
_ => {}
}
Ok(())
}
fn safe_chmod_errmsg(fix_setgid: bool, fix_sticky: bool) -> &'static str {
match (fix_setgid, fix_sticky) {
(true, true) => "blocked attempt to unset setgid and sticky bits",
(true, false) => "blocked attempt to unset setgid bit",
(false, true) => "blocked attempt to unset sticky bit",
_ => unreachable!(),
}
}