use std::{
fmt,
io::Seek,
os::fd::{AsRawFd, RawFd},
};
use libc::c_long;
use libseccomp::ScmpArch;
use nix::{
errno::Errno,
fcntl::OFlag,
sys::signal::{kill, Signal},
unistd::Pid,
};
use serde::{Serialize, Serializer};
use crate::{
compat::ResolveFlag,
config::PAGE_SIZE,
confine::scmp_arch_is_old_mmap,
cookie::{safe_pidfd_getfd, safe_pidfd_open},
elf::ExecutableFile,
err::err2no,
error,
fd::{fd_status_flags, PIDFD_THREAD, PROC_FILE},
kernel::{ptrace::SYS_MMAP2, sandbox_path},
lookup::{safe_open_msym, CanonicalPath},
path::XPathBuf,
proc::{proc_executables, proc_mem, proc_statm},
ptrace::{ptrace_get_error, ptrace_syscall_info},
req::RemoteProcess,
sandbox::{Action, Capability, IntegrityError, SandboxGuard},
warn,
};
const PROT_EXEC: u64 = libc::PROT_EXEC as u64;
const MAP_ANONYMOUS: u64 = libc::MAP_ANONYMOUS as u64;
const MAP_SHARED: u64 = libc::MAP_SHARED as u64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum MmapSyscall {
Mmap,
Mmap2,
}
impl MmapSyscall {
pub(crate) const fn name(self) -> &'static str {
match self {
Self::Mmap => "mmap",
Self::Mmap2 => "mmap2",
}
}
pub(crate) fn from_scno(scno: c_long) -> Self {
if scno == *SYS_MMAP2 {
Self::Mmap2
} else {
Self::Mmap
}
}
}
impl fmt::Display for MmapSyscall {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
impl Serialize for MmapSyscall {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.name())
}
}
pub(crate) fn sysenter_mmap(
pid: Pid,
sandbox: &SandboxGuard,
syscall: MmapSyscall,
args: &[u64; 6],
) -> Result<bool, Errno> {
handle_mmap(pid, sandbox, syscall, args)
}
pub(crate) fn sysexit_mmap(
pid: Pid,
sandbox: &SandboxGuard,
info: ptrace_syscall_info,
scno: c_long,
args: &[u64; 6],
) -> Result<(), Errno> {
match ptrace_get_error(pid, info.arch) {
Ok(None) => {
}
Ok(Some(_)) => {
return Ok(());
}
Err(Errno::ESRCH) => return Err(Errno::ESRCH),
Err(errno) => {
error!("ctx": "mmap", "op": "read_return",
"msg": format!("failed to read mmap return: {errno}"),
"err": errno as i32, "pid": pid.as_raw(),
"tip": "check with SYD_LOG=debug and/or submit a bug report");
let _ = kill(pid, Some(Signal::SIGKILL));
return Err(Errno::ESRCH);
}
};
let syscall = MmapSyscall::from_scno(scno);
if sandbox.enabled(Capability::CAP_EXEC) {
check_exec(pid, sandbox, syscall)?;
}
check_mmap(pid, sandbox, syscall, args)?;
Ok(())
}
fn check_mmap(
pid: Pid,
sandbox: &SandboxGuard,
syscall: MmapSyscall,
args: &[u64; 6],
) -> Result<(), Errno> {
if handle_mmap(pid, sandbox, syscall, args).is_err() {
let _ = kill(pid, Some(Signal::SIGKILL));
return Err(Errno::ESRCH);
}
Ok(())
}
#[expect(clippy::cognitive_complexity)]
fn check_exec(pid: Pid, sandbox: &SandboxGuard, syscall: MmapSyscall) -> Result<(), Errno> {
let bins = match proc_executables(pid) {
Ok(bins) => bins,
Err(errno) => {
error!("ctx": "mmap", "op": "read_proc_maps", "sys": syscall,
"msg": format!("failed to read proc maps: {errno}"),
"err": errno as i32, "pid": pid.as_raw(),
"tip": "check with SYD_LOG=debug and/or submit a bug report");
let _ = kill(pid, Some(Signal::SIGKILL));
return Err(Errno::ESRCH);
}
};
for exec in bins {
let path = &exec.path;
let action = sandbox.check_path(Capability::CAP_EXEC, path);
if action.is_allowing() {
continue;
}
error!("ctx": "mmap", "op": "map_mismatch", "sys": syscall,
"msg": format!("map mismatch detected for executable `{path}': assume TOCTTOU!"),
"pid": pid.as_raw(), "path": &path,
"inode": exec.inode,
"dev_major": exec.dev_major,
"dev_minor": exec.dev_minor);
let _ = kill(pid, Some(Signal::SIGKILL));
return Err(Errno::ESRCH);
}
Ok(())
}
#[expect(clippy::cognitive_complexity)]
fn handle_mmap(
pid: Pid,
sandbox: &SandboxGuard,
syscall: MmapSyscall,
args: &[u64; 6],
) -> Result<bool, Errno> {
let size = args[1];
let caps = sandbox.getcaps(Capability::CAP_MMAP);
let exec = caps.contains(Capability::CAP_EXEC);
let force = caps.contains(Capability::CAP_FORCE);
let tpe = caps.contains(Capability::CAP_TPE);
let mem = caps.contains(Capability::CAP_MEM);
let mem_max = sandbox.mem_max;
let mem_vm_max = sandbox.mem_vm_max;
let mem_act = sandbox.default_action(Capability::CAP_MEM);
let restrict_exec_memory = !sandbox.options.allow_unsafe_exec_memory();
let restrict_exec_stack = !sandbox.flags.allow_unsafe_exec_stack();
let restrict_append_only = sandbox.has_append() || sandbox.enabled(Capability::CAP_CRYPT);
if !exec
&& !force
&& !tpe
&& !restrict_exec_memory
&& !restrict_exec_stack
&& !restrict_append_only
&& (!mem || (mem_max == 0 && mem_vm_max == 0))
{
return Ok(false);
}
if restrict_exec_memory {
const PROT_WRITE: u64 = libc::PROT_WRITE as u64;
const WRITE_EXEC: u64 = PROT_WRITE | PROT_EXEC;
if args[2] & WRITE_EXEC == WRITE_EXEC {
return Err(Errno::EACCES);
}
if args[2] & PROT_EXEC != 0 && args[3] & MAP_ANONYMOUS != 0 {
return Err(Errno::EACCES);
}
if args[2] & PROT_EXEC != 0 && args[3] & MAP_SHARED != 0 {
return Err(Errno::EACCES);
}
}
let check_exec = (exec || force || tpe || restrict_exec_memory || restrict_exec_stack)
&& args[2] & PROT_EXEC != 0
&& args[3] & MAP_ANONYMOUS == 0;
let check_append_only =
restrict_append_only && args[3] & MAP_SHARED != 0 && args[3] & MAP_ANONYMOUS == 0;
let fd = if check_exec || check_append_only {
#[expect(clippy::cast_possible_truncation)]
let remote_fd = args[4] as RawFd;
if remote_fd < 0 {
return Err(Errno::EBADF);
}
let pid_fd = safe_pidfd_open(pid, PIDFD_THREAD)?;
match safe_pidfd_getfd(pid_fd, remote_fd) {
Ok(fd) => Some(fd),
Err(_) => return Err(Errno::EBADF),
}
} else {
None
};
#[expect(clippy::disallowed_methods)]
let oflags = if check_append_only || (check_exec && restrict_exec_memory) {
fd_status_flags(fd.as_ref().unwrap()).ok()
} else {
None
};
if check_append_only {
let deny = oflags
.map(|fl| {
fl.contains(OFlag::O_APPEND)
&& (fl.contains(OFlag::O_RDWR) || fl.contains(OFlag::O_WRONLY))
})
.unwrap_or(true);
if deny {
return Err(Errno::EPERM);
}
}
if check_exec {
if restrict_exec_memory {
let deny = oflags
.map(|fl| fl.contains(OFlag::O_RDWR) || fl.contains(OFlag::O_WRONLY))
.unwrap_or(true);
if deny {
return Err(Errno::EACCES);
}
}
#[expect(clippy::disallowed_methods)]
let mut path = CanonicalPath::new_fd(fd.unwrap().into(), pid)?;
if exec {
sandbox_path(
None,
sandbox,
pid,
path.abs(),
Capability::CAP_EXEC,
syscall.name(),
)?;
}
if tpe {
let (action, msg) = sandbox.check_tpe(path.dir(), path.abs());
if !matches!(action, Action::Allow | Action::Filter) {
let msg = msg.as_deref().unwrap_or("?");
error!("ctx": "trusted_path_execution",
"msg": format!("library load from untrusted path blocked: {msg}"),
"sys": syscall, "path": &path,
"pid": pid.as_raw(),
"tip": "move the library to a safe location or use `sandbox/tpe:off'");
}
match action {
Action::Allow | Action::Warn => {}
Action::Panic | Action::Deny | Action::Filter => return Err(Errno::EACCES),
Action::Exit => std::process::exit(libc::EACCES),
Action::Stop => {
let _ = kill(pid, Some(Signal::SIGSTOP));
return Err(Errno::EACCES);
}
Action::Abort => {
let _ = kill(pid, Some(Signal::SIGABRT));
return Err(Errno::EACCES);
}
Action::Kill => {
let _ = kill(pid, Some(Signal::SIGKILL));
return Err(Errno::EACCES);
}
}
}
if force || restrict_exec_stack {
#[expect(clippy::disallowed_methods)]
let fd = path.dir.take().unwrap();
let mut fd = XPathBuf::from_self_fd(fd.as_raw_fd()).and_then(|pfd| {
safe_open_msym(
PROC_FILE(),
&pfd,
OFlag::O_RDONLY | OFlag::O_NOCTTY,
ResolveFlag::empty(),
)
})?;
if restrict_exec_stack {
let exe = ExecutableFile::parse(&mut fd, true).or(Err(Errno::EACCES))?;
if matches!(exe, ExecutableFile::Elf { xs: true, .. }) {
error!("ctx": "check_lib",
"msg": "library load with executable stack blocked",
"sys": syscall, "path": path.abs(),
"tip": "configure `trace/allow_unsafe_exec_stack:1'",
"lib": format!("{exe}"),
"pid": pid.as_raw());
return Err(Errno::EACCES);
}
}
if force {
if restrict_exec_stack {
fd.rewind().map_err(|err| err2no(&err))?;
}
let result = sandbox.check_force2(fd, path.abs());
let deny = match result {
Ok(action) => {
if !matches!(action, Action::Allow | Action::Filter) {
warn!("ctx": "verify_lib", "act": action,
"sys": syscall, "path": path.abs(),
"tip": format!("configure `force+{}:<checksum>'", path.abs()),
"pid": pid.as_raw());
}
match action {
Action::Allow | Action::Warn => false,
Action::Panic | Action::Deny | Action::Filter => true,
Action::Exit => std::process::exit(libc::EACCES),
Action::Stop => {
let _ = kill(pid, Some(Signal::SIGSTOP));
true
}
Action::Abort => {
let _ = kill(pid, Some(Signal::SIGABRT));
true
}
Action::Kill => {
let _ = kill(pid, Some(Signal::SIGKILL));
true
}
}
}
Err(IntegrityError::Sys(errno)) => {
error!("ctx": "verify_lib",
"msg": format!("system error during library checksum calculation: {errno}"),
"sys": syscall, "path": path.abs(),
"tip": format!("configure `force+{}:<checksum>'", path.abs()),
"pid": pid.as_raw());
true
}
Err(IntegrityError::Hash {
action,
expected,
found,
}) => {
if action != Action::Filter {
error!("ctx": "verify_lib", "act": action,
"msg": format!("library checksum mismatch: {found} is not {expected}"),
"sys": syscall, "path": path.abs(),
"tip": format!("configure `force+{}:<checksum>'", path.abs()),
"pid": pid.as_raw());
}
match action {
Action::Allow => unreachable!(),
Action::Warn => false,
Action::Panic | Action::Deny | Action::Filter => true,
Action::Exit => std::process::exit(libc::EACCES),
Action::Stop => {
let _ = kill(pid, Some(Signal::SIGSTOP));
true
}
Action::Abort => {
let _ = kill(pid, Some(Signal::SIGABRT));
true
}
Action::Kill => {
let _ = kill(pid, Some(Signal::SIGKILL));
true
}
}
}
};
if deny {
return Err(Errno::EACCES);
}
}
}
}
if !mem || (mem_max == 0 && mem_vm_max == 0) {
return Ok(check_exec || check_append_only);
}
if mem_vm_max > 0 {
let mem_vm_cur = match proc_statm(pid) {
Ok(statm) => statm.size.saturating_mul(*PAGE_SIZE),
Err(errno) => return Err(errno),
};
if mem_vm_cur.saturating_add(size) >= mem_vm_max {
if mem_act != Action::Filter {
warn!("ctx": "access", "cap": Capability::CAP_MEM, "act": mem_act,
"sys": syscall, "mem_vm_max": mem_vm_max, "mem_vm_cur": mem_vm_cur,
"mem_size": size, "tip": "increase `mem/vm_max'",
"pid": pid.as_raw());
}
match mem_act {
Action::Allow => unreachable!(),
Action::Warn => {}
Action::Panic | Action::Deny | Action::Filter => return Err(Errno::ENOMEM),
Action::Exit => std::process::exit(libc::ENOMEM),
Action::Stop => {
let _ = kill(pid, Some(Signal::SIGSTOP));
return Err(Errno::ENOMEM);
}
Action::Abort => {
let _ = kill(pid, Some(Signal::SIGABRT));
return Err(Errno::ENOMEM);
}
Action::Kill => {
let _ = kill(pid, Some(Signal::SIGKILL));
return Err(Errno::ENOMEM);
}
}
}
}
if mem_max > 0 {
let mem_cur = proc_mem(pid)?;
if mem_cur.saturating_add(size) >= mem_max {
if mem_act != Action::Filter {
warn!("ctx": "access", "cap": Capability::CAP_MEM, "act": mem_act,
"sys": syscall, "mem_max": mem_max, "mem_cur": mem_cur,
"mem_size": size, "tip": "increase `mem/max'",
"pid": pid.as_raw());
}
return match mem_act {
Action::Allow => unreachable!(),
Action::Warn => Ok(check_exec),
Action::Panic | Action::Deny | Action::Filter => Err(Errno::ENOMEM),
Action::Exit => std::process::exit(libc::ENOMEM),
Action::Stop => {
let _ = kill(pid, Some(Signal::SIGSTOP));
return Err(Errno::ENOMEM);
}
Action::Abort => {
let _ = kill(pid, Some(Signal::SIGABRT));
return Err(Errno::ENOMEM);
}
Action::Kill => {
let _ = kill(pid, Some(Signal::SIGKILL));
return Err(Errno::ENOMEM);
}
};
}
}
Ok(check_exec || check_append_only || mem_max > 0 || mem_vm_max > 0)
}
pub(crate) fn ptrace_mmap_args(pid: Pid, arch: ScmpArch, raw: [u64; 6]) -> Result<[u64; 6], Errno> {
if !scmp_arch_is_old_mmap(arch) {
return Ok(raw);
}
let process = RemoteProcess::new(pid);
unsafe { process.remote_old_mmap_args(arch, raw[0]) }
}