use std::sync::{Arc, RwLock};
use nix::{errno::Errno, unistd::Pid};
#[cfg(feature = "kcov")]
use crate::confine::{
SYS_CHDIR, SYS_EXECVE, SYS_EXECVEAT, SYS_FCHDIR, SYS_MMAP, SYS_MMAP2, SYS_RT_SIGRETURN,
SYS_SETGROUPS, SYS_SETGROUPS32, SYS_SIGRETURN,
};
use crate::{
cache::SigreturnTrampolineIP,
config::{
PTRACE_DATA_CHDIR, PTRACE_DATA_EXECVE, PTRACE_DATA_EXECVEAT, PTRACE_DATA_FCHDIR,
PTRACE_DATA_MMAP, PTRACE_DATA_MMAP2, PTRACE_DATA_RT_SIGRETURN, PTRACE_DATA_SETGROUPS,
PTRACE_DATA_SETGROUPS32, PTRACE_DATA_SIGRETURN,
},
confine::{scmp_arch, scmp_arch_has_single_step, scmp_arch_raw, SydArch, SydNotifResp},
cookie::safe_kill,
error,
hash::hex_encode_lower,
kernel::ptrace::{
chdir::{sysenter_chdir, sysenter_fchdir, sysexit_chdir},
exec::sysenter_exec,
mmap::{ptrace_mmap_args, sysenter_mmap, sysexit_mmap, MmapSyscall},
setgroups::{sysenter_setgroups, sysenter_setgroups32},
},
proc::{proc_ip_in_sigtramp, proc_maps},
ptrace::{ptrace_set_return, ptrace_syscall_info},
req::RemoteProcess,
sandbox::{Action, Capability, Sandbox, SandboxGuard},
workers::WorkerCache,
};
pub(crate) mod chdir;
pub(crate) mod exec;
pub(crate) mod mmap;
pub(crate) mod setgroups;
pub(crate) mod event;
#[expect(clippy::cognitive_complexity)]
pub(crate) fn handle_ptrace_sysenter(
pid: Pid,
info: ptrace_syscall_info,
cache: &Arc<WorkerCache>,
sandbox: &Arc<RwLock<Sandbox>>,
) -> Option<SydNotifResp> {
#[expect(clippy::disallowed_methods)]
let arch: SydArch = scmp_arch(info.arch).unwrap().into();
#[expect(clippy::disallowed_methods)]
let info_scmp = info.seccomp().unwrap();
#[expect(clippy::cast_possible_truncation)]
let scmp_trace_data = info_scmp.ret_data as u16;
match scmp_trace_data {
PTRACE_DATA_CHDIR | PTRACE_DATA_FCHDIR => {
#[cfg(feature = "kcov")]
{
let scno = if scmp_trace_data == PTRACE_DATA_CHDIR {
*SYS_CHDIR
} else {
*SYS_FCHDIR
};
let scno = if let Some(scno) = scno {
scno
} else {
let scno: libc::c_long = 4000;
scno.saturating_add(scmp_trace_data.into())
};
crate::kcov::abi::kcov_attach(pid);
crate::kcov::abi::kcov_set_syscall(scno);
let _ = crate::kcov::abi::kcov_enter_for(pid);
crate::kcov_edge!();
}
let my_sandbox =
SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
let mut cont = false;
let result = if my_sandbox.flags.ghost() {
Err(Errno::ENOSYS)
} else if !my_sandbox.enabled(Capability::CAP_CHDIR) {
cont = true;
Err(Errno::ECANCELED)
} else if scmp_trace_data == PTRACE_DATA_CHDIR {
sysenter_chdir(pid, &my_sandbox, arch.into(), info_scmp)
} else {
sysenter_fchdir(pid, &my_sandbox, arch.into(), info_scmp)
};
drop(my_sandbox);
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(pid);
}
if cont {
return Some(SydNotifResp::Cont { pid, signal: None });
}
let file_info = match result {
Ok(info) => info,
Err(errno) => return deny_response(pid, info.arch, errno),
};
if cache.add_chdir(pid, scmp_trace_data, file_info).is_err() {
let _ = safe_kill(pid, libc::SIGKILL);
return None;
}
Some(SydNotifResp::Exit { pid, signal: None })
}
PTRACE_DATA_MMAP | PTRACE_DATA_MMAP2 => {
let data = if let Some(data) = info.seccomp() {
data
} else {
unreachable!("BUG: Invalid system call information returned by kernel!");
};
let syscall = if scmp_trace_data == PTRACE_DATA_MMAP {
MmapSyscall::Mmap
} else {
MmapSyscall::Mmap2
};
#[cfg(feature = "kcov")]
{
let scno = if scmp_trace_data == PTRACE_DATA_MMAP {
*SYS_MMAP
} else {
*SYS_MMAP2
};
let scno = if let Some(scno) = scno {
scno
} else {
let scno: libc::c_long = 4000;
scno.saturating_add(scmp_trace_data.into())
};
crate::kcov::abi::kcov_attach(pid);
crate::kcov::abi::kcov_set_syscall(scno);
let _ = crate::kcov::abi::kcov_enter_for(pid);
crate::kcov_edge!();
}
let args = if syscall == MmapSyscall::Mmap {
match ptrace_mmap_args(pid, arch.into(), data.args) {
Ok(args) => args,
Err(errno) => return deny_response(pid, info.arch, errno),
}
} else {
data.args
};
let my_sandbox =
SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
let result = if my_sandbox.flags.ghost() {
Err(Errno::ENOSYS)
} else {
sysenter_mmap(pid, &my_sandbox, syscall, &args)
};
drop(my_sandbox);
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(pid);
}
match result {
Ok(true) => {
if cache.add_mmap(pid, syscall).is_err() {
let _ = safe_kill(pid, libc::SIGKILL);
return None;
}
Some(SydNotifResp::Exit { pid, signal: None })
}
Ok(false) => {
Some(SydNotifResp::Cont { pid, signal: None })
}
Err(errno) => deny_response(pid, info.arch, errno),
}
}
PTRACE_DATA_EXECVE | PTRACE_DATA_EXECVEAT => {
#[cfg(feature = "kcov")]
{
let scno = if scmp_trace_data == PTRACE_DATA_EXECVE {
*SYS_EXECVE
} else {
*SYS_EXECVEAT
};
let scno = if let Some(scno) = scno {
scno
} else {
let scno: libc::c_long = 4000;
scno.saturating_add(scmp_trace_data.into())
};
crate::kcov::abi::kcov_attach(pid);
crate::kcov::abi::kcov_set_syscall(scno);
let _ = crate::kcov::abi::kcov_enter_for(pid);
crate::kcov_edge!();
}
let my_sandbox =
SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
let result = if my_sandbox.flags.ghost() {
Err(Errno::ENOSYS)
} else {
sysenter_exec(pid, info, cache, &my_sandbox)
};
drop(my_sandbox);
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(pid);
}
if let Err(errno) = result {
let errno = if errno == Errno::ECANCELED {
return Some(SydNotifResp::Deny {
pid,
errno,
arch: info.arch,
});
} else {
errno
};
return deny_response(pid, info.arch, errno);
}
Some(SydNotifResp::Cont { pid, signal: None })
}
PTRACE_DATA_SIGRETURN | PTRACE_DATA_RT_SIGRETURN => {
#[cfg(feature = "kcov")]
{
let scno = if scmp_trace_data == PTRACE_DATA_SIGRETURN {
*SYS_SIGRETURN
} else {
*SYS_RT_SIGRETURN
};
let scno = if let Some(scno) = scno {
scno
} else {
let scno: libc::c_long = 4000;
scno.saturating_add(scmp_trace_data.into())
};
crate::kcov::abi::kcov_attach(pid);
crate::kcov::abi::kcov_set_syscall(scno);
let _ = crate::kcov::abi::kcov_enter_for(pid);
crate::kcov_edge!();
}
let has_handler = cache.enter_sig_handle(pid);
let ip = info.instruction_pointer;
let has_savedip = if has_handler {
match cache.get_sig_trampoline_ip(pid) {
None if proc_ip_in_sigtramp(pid, ip) => {
cache.set_sig_trampoline_ip(pid, SigreturnTrampolineIP { lo: ip, hi: ip });
true
}
None if !scmp_arch_has_single_step(arch.into()) => {
cache.set_sig_trampoline_ip(pid, SigreturnTrampolineIP { lo: ip, hi: ip });
true
}
None => false,
Some(cookie) => cookie.matches(ip),
}
} else {
false
};
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(pid);
}
if has_savedip {
return Some(SydNotifResp::Exit { pid, signal: None });
}
let log_scmp = {
SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner())).log_scmp()
};
let memmap = if log_scmp {
match proc_maps(pid) {
Ok(memmap) => Some(memmap),
Err(Errno::ESRCH) => return None, Err(_) => None,
}
} else {
None
};
let ip = info.instruction_pointer;
let sp = (info.stack_pointer & !0xF).saturating_sub(16);
let ip_mem = if log_scmp { Some([0u8; 64]) } else { None };
let sp_mem = if log_scmp { Some([0u8; 64]) } else { None };
let process = RemoteProcess::new(pid);
#[expect(clippy::disallowed_methods)]
let arch: SydArch = scmp_arch(info.arch).unwrap().into();
let is_realtime = scmp_trace_data == PTRACE_DATA_RT_SIGRETURN;
if let Some(mut ip_mem) = ip_mem {
if unsafe { process.read_mem(arch.into(), &mut ip_mem, ip, 64) }
== Err(Errno::ESRCH)
{
return None; }
}
if let Some(mut sp_mem) = sp_mem {
if unsafe { process.read_mem(arch.into(), &mut sp_mem, sp, 64) }
== Err(Errno::ESRCH)
{
return None; }
}
if safe_kill(pid, libc::SIGKILL) == Err(Errno::ESRCH) {
return None;
}
let cookie = cache.get_sig_trampoline_ip(pid);
let depth = cache.depth_sig_handle(pid);
if !log_scmp {
error!("ctx": "sigreturn", "op": "check_SROP",
"msg": "Artificial sigreturn(2) detected: assume SROP!",
"act": Action::Kill,
"pid": process.pid.as_raw(), "arch": arch,
"sys": if is_realtime { "rt_sigreturn" } else { "sigreturn" },
"ip": ip, "depth": depth,
"trampoline_lo": cookie.map_or(0, |c| c.lo),
"trampoline_hi": cookie.map_or(0, |c| c.hi),
"tip": "configure `trace/allow_unsafe_sigreturn:1'");
} else {
let ip_hex = ip_mem.map(|m| hex_encode_lower(m.as_ref()).ok());
let sp_hex = sp_mem.map(|m| hex_encode_lower(m.as_ref()).ok());
error!("ctx": "sigreturn", "op": "check_SROP",
"msg": "Artificial sigreturn(2) detected: assume SROP!",
"act": Action::Kill,
"pid": process.pid.as_raw(), "arch": arch,
"sys": if is_realtime { "rt_sigreturn" } else { "sigreturn" },
"args": info_scmp.args, "ip": ip, "sp": sp,
"ip_mem": ip_hex,
"sp_mem": sp_hex,
"memmap": memmap,
"tip": "configure `trace/allow_unsafe_sigreturn:1'");
}
None
}
PTRACE_DATA_SETGROUPS | PTRACE_DATA_SETGROUPS32 => {
#[cfg(feature = "kcov")]
{
let scno = if scmp_trace_data == PTRACE_DATA_SETGROUPS {
*SYS_SETGROUPS
} else {
*SYS_SETGROUPS32
};
let scno = if let Some(scno) = scno {
scno
} else {
let scno: libc::c_long = 4000;
scno.saturating_add(scmp_trace_data.into())
};
crate::kcov::abi::kcov_attach(pid);
crate::kcov::abi::kcov_set_syscall(scno);
let _ = crate::kcov::abi::kcov_enter_for(pid);
crate::kcov_edge!();
}
let result = if scmp_trace_data == PTRACE_DATA_SETGROUPS {
sysenter_setgroups(pid, arch.into(), info_scmp)
} else {
sysenter_setgroups32(pid, arch.into(), info_scmp)
};
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(pid);
}
if let Err(errno) = result {
return deny_response(pid, info.arch, errno);
}
Some(SydNotifResp::SetGroupsZero {
pid,
arch: scmp_arch_raw(arch.into()),
})
}
data => unreachable!("BUG: invalid syscall data {data}!"),
}
}
pub(crate) fn handle_ptrace_sysexit(
pid: Pid,
info: ptrace_syscall_info,
cache: &Arc<WorkerCache>,
sandbox: &Arc<RwLock<Sandbox>>,
) -> Result<(), Errno> {
if let Some(entry) = cache.get_chdir(pid) {
#[cfg(feature = "kcov")]
{
let scno = if entry.data == PTRACE_DATA_CHDIR {
*SYS_CHDIR
} else {
*SYS_FCHDIR
};
let scno = if let Some(scno) = scno {
scno
} else {
let scno: libc::c_long = 4000;
scno.saturating_add(entry.data.into())
};
crate::kcov::abi::kcov_attach(pid);
crate::kcov::abi::kcov_set_syscall(scno);
let _ = crate::kcov::abi::kcov_enter_for(pid);
crate::kcov_edge!();
}
let result = sysexit_chdir(pid, info, entry.info);
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(pid);
}
result
} else if let Some(syscall) = cache.get_mmap(pid) {
#[cfg(feature = "kcov")]
{
let scno = if syscall == MmapSyscall::Mmap {
*SYS_MMAP
} else {
*SYS_MMAP2
};
let scno = if let Some(scno) = scno {
scno
} else if syscall == MmapSyscall::Mmap {
let scno: libc::c_long = 4000;
scno.saturating_add(PTRACE_DATA_MMAP.into())
} else {
let scno: libc::c_long = 4000;
scno.saturating_add(PTRACE_DATA_MMAP2.into())
};
crate::kcov::abi::kcov_attach(pid);
crate::kcov::abi::kcov_set_syscall(scno);
let _ = crate::kcov::abi::kcov_enter_for(pid);
crate::kcov_edge!();
}
let result = {
let sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
sysexit_mmap(pid, &sandbox, info, syscall)
};
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(pid);
}
result
} else if let Some((pid, errno)) = cache.get_error(pid) {
ptrace_set_return(pid, info.arch, errno)
} else if cache.has_sig_handle(pid) {
if cache.exit_sig_handle(pid) {
return Ok(());
}
let _ = safe_kill(pid, libc::SIGKILL);
#[expect(clippy::disallowed_methods)]
let arch: SydArch = scmp_arch(info.arch).unwrap().into();
error!("ctx": "sigreturn", "op": "check_SROP",
"msg": "Artificial sigreturn(2) without signal delivery cookie: assume SROP!",
"act": Action::Kill, "pid": pid.as_raw(), "arch": arch,
"tip": "configure `trace/allow_unsafe_sigreturn:1'");
Err(Errno::ESRCH)
} else {
unreachable!("BUG: Invalid syscall exit stop: {info:?}");
}
}
fn deny_response(pid: Pid, arch: u32, errno: Errno) -> Option<SydNotifResp> {
Some(SydNotifResp::Deny { pid, arch, errno })
}