#![forbid(unsafe_code)]
use std::os::fd::{AsFd, AsRawFd};
use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, fcntl::AtFlags, unistd::AccessFlags, NixPath};
use crate::{
compat::{fstatx, AT_EACCESS, STATX_MODE},
cookie::{safe_faccess, safe_fdlink, safe_linkat},
fd::PROC_FILE,
kernel::{syscall_path_handler, to_atflags},
lookup::{FileType, FsFlags},
path::XPathBuf,
req::{PathArgs, SysArg, SysFlags, UNotifyEventRequest},
};
pub(crate) fn sys_link(request: UNotifyEventRequest) -> ScmpNotifResp {
let argv = &[
SysArg {
path: Some(0),
fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
..Default::default()
},
SysArg {
path: Some(1),
fsflags: FsFlags::MISS_LAST | FsFlags::NO_FOLLOW_LAST | FsFlags::DOTLAST_EEXIST,
..Default::default()
},
];
syscall_path_handler(request, "link", argv, |path_args, request, sandbox| {
let restrict_hardlinks = !sandbox.flags.allow_unsafe_hardlinks();
drop(sandbox);
syscall_link_handler(request, path_args, restrict_hardlinks)
})
}
pub(crate) fn sys_linkat(request: UNotifyEventRequest) -> ScmpNotifResp {
let req = request.scmpreq;
let atflags = match to_atflags(
req.data.args[4],
AtFlags::AT_EMPTY_PATH | AtFlags::AT_SYMLINK_FOLLOW,
) {
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;
}
if !atflags.contains(AtFlags::AT_SYMLINK_FOLLOW) {
fsflags |= FsFlags::NO_FOLLOW_LAST;
}
let argv = &[
SysArg {
dirfd: Some(0),
path: Some(1),
flags,
fsflags,
},
SysArg {
dirfd: Some(2),
path: Some(3),
fsflags: FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST | FsFlags::DOTLAST_EEXIST,
..Default::default()
},
];
syscall_path_handler(request, "linkat", argv, |path_args, request, sandbox| {
let restrict_hardlinks = !sandbox.flags.allow_unsafe_hardlinks();
drop(sandbox);
syscall_link_handler(request, path_args, restrict_hardlinks)
})
}
fn syscall_link_handler(
request: &UNotifyEventRequest,
args: PathArgs,
restrict_hardlinks: bool,
) -> Result<ScmpNotifResp, Errno> {
let req = request.scmpreq;
#[expect(clippy::disallowed_methods)]
let new_path = &args.1.as_ref().unwrap().path;
#[expect(clippy::disallowed_methods)]
let old_parg = &args.0.as_ref().unwrap();
let old_path = &old_parg.path;
let is_empty = old_parg.is_empty;
assert!(old_path.base().is_empty()); let fd = old_path.dir();
if restrict_hardlinks {
safe_hardlink_source(fd, old_path.typ.unwrap_or(FileType::Unk))?;
}
request.cache.add_sys_block(req, false)?;
let result = if is_empty {
safe_fdlink(fd, new_path.dir(), new_path.base())
} else {
safe_linkat(
PROC_FILE(),
&XPathBuf::from_self_fd(fd.as_raw_fd())?,
new_path.dir(),
new_path.base(),
AtFlags::AT_SYMLINK_FOLLOW,
)
};
request.cache.del_sys_block(req.id)?;
result.map(|_| request.return_syscall(0))
}
fn safe_hardlink_source<Fd: AsFd>(fd: Fd, typ: FileType) -> Result<(), Errno> {
if typ.is_symlink() {
return Ok(());
} else if !typ.is_file() {
return Err(Errno::EPERM);
}
let mode = fstatx(&fd, STATX_MODE).map(|stx| libc::mode_t::from(stx.stx_mode))?;
if (mode & libc::S_ISUID) != 0 {
return Err(Errno::EPERM);
}
if (mode & (libc::S_ISGID | libc::S_IXGRP)) == (libc::S_ISGID | libc::S_IXGRP) {
return Err(Errno::EPERM);
}
safe_faccess(
fd,
AccessFlags::R_OK | AccessFlags::W_OK,
AT_EACCESS | AtFlags::AT_EMPTY_PATH,
)
.or(Err(Errno::EPERM))
}