use std::sync::{Arc, LazyLock, RwLock};
use data_encoding::HEXLOWER;
use libc::c_long;
use libseccomp::ScmpSyscall;
use nix::{
errno::Errno,
sys::signal::{kill, Signal},
unistd::Pid,
};
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, SydArch},
error,
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_skip_syscall, 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;
static SYS_CHDIR: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("chdir")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
static SYS_FCHDIR: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("fchdir")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
static SYS_MMAP: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("mmap")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
static SYS_MMAP2: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("mmap2")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
#[allow(unused)]
static SYS_EXECVE: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("execve")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
#[allow(unused)]
static SYS_EXECVEAT: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("execveat")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
#[allow(unused)]
static SYS_SIGRETURN: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("sigreturn")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
#[allow(unused)]
static SYS_RT_SIGRETURN: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("rt_sigreturn")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
#[allow(unused)]
static SYS_SETGROUPS: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("setgroups")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
#[allow(unused)]
static SYS_SETGROUPS32: LazyLock<c_long> = LazyLock::new(|| {
ScmpSyscall::from_name("setgroups32")
.map(i32::from)
.map(c_long::from)
.unwrap_or(-1) });
#[expect(clippy::cognitive_complexity)]
pub(crate) fn handle_ptrace_sysenter(
pid: Pid,
info: ptrace_syscall_info,
cache: &Arc<WorkerCache>,
sandbox: &Arc<RwLock<Sandbox>>,
) -> Result<(), Errno> {
#[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 => {
let scno = if scmp_trace_data == PTRACE_DATA_CHDIR {
*SYS_CHDIR
} else {
*SYS_FCHDIR
};
#[cfg(feature = "kcov")]
{
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()));
if !my_sandbox.enabled(Capability::CAP_CHDIR) {
return Err(Errno::ECANCELED);
}
let result = 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 let Err(errno) = result {
return if let Err(errno) = ptrace_skip_syscall(pid, info.arch, Some(errno)) {
if errno != Errno::ESRCH {
let _ = kill(pid, Some(Signal::SIGKILL));
}
Err(Errno::ESRCH)
} else if cfg!(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
target_arch = "s390x"
)) {
cache.add_error(pid, Some(errno));
Ok(())
} else {
Err(Errno::ECANCELED)
};
}
cache.add_chdir(pid, scno);
Ok(())
}
PTRACE_DATA_MMAP | PTRACE_DATA_MMAP2 => {
let scno = if scmp_trace_data == PTRACE_DATA_MMAP {
*SYS_MMAP
} else {
*SYS_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")]
{
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 if let Err(errno) = ptrace_skip_syscall(pid, info.arch, Some(errno))
{
if errno != Errno::ESRCH {
let _ = kill(pid, Some(Signal::SIGKILL));
}
Err(Errno::ESRCH)
} else if cfg!(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
target_arch = "s390x"
)) {
cache.add_error(pid, Some(errno));
Ok(())
} else {
Err(Errno::ECANCELED)
};
}
}
} else {
data.args
};
let my_sandbox =
SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
let res = 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 res {
Ok(true) => {
cache.add_mmap(pid, scno, args);
Ok(()) }
Ok(false) => {
Err(Errno::ECANCELED)
}
Err(errno) => {
if let Err(errno) = ptrace_skip_syscall(pid, info.arch, Some(errno)) {
if errno != Errno::ESRCH {
let _ = kill(pid, Some(Signal::SIGKILL));
}
Err(Errno::ESRCH)
} else if cfg!(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
target_arch = "s390x"
)) {
cache.add_error(pid, Some(errno));
Ok(())
} else {
Err(Errno::ECANCELED)
}
}
}
}
PTRACE_DATA_EXECVE | PTRACE_DATA_EXECVEAT => {
#[cfg(feature = "kcov")]
{
let scno = if scmp_trace_data == PTRACE_DATA_EXECVE {
*SYS_EXECVE
} else {
*SYS_EXECVEAT
};
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 = sysenter_exec(pid, &my_sandbox, info);
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 {
None
} else {
Some(errno)
};
return if let Err(errno) = ptrace_skip_syscall(pid, info.arch, errno) {
if errno != Errno::ESRCH {
error!("ctx": "skip_syscall",
"msg": format!("skip exec syscall error: {errno}"),
"err": errno as i32,
"tip": "check with SYD_LOG=debug and/or submit a bug report");
let _ = kill(pid, Some(Signal::SIGKILL));
}
Err(Errno::ESRCH)
} else if cfg!(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
target_arch = "s390x"
)) {
cache.add_error(pid, errno);
Ok(())
} else {
Err(Errno::ECANCELED)
};
}
Err(Errno::ECANCELED)
}
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
};
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 Ok(());
}
let log_scmp = {
SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner())).log_scmp()
};
let memmap = if log_scmp { proc_maps(pid).ok() } 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 {
let _ = unsafe { process.read_mem(arch.into(), &mut ip_mem, ip, 64) };
}
if let Some(mut sp_mem) = sp_mem {
let _ = unsafe { process.read_mem(arch.into(), &mut sp_mem, sp, 64) };
}
let _ = kill(pid, Some(Signal::SIGKILL));
let cookie = cache.get_sig_trampoline_ip(pid);
let depth = cache.depth_sig_handle(pid);
#[expect(clippy::disallowed_methods)]
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 {
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": HEXLOWER.encode(ip_mem.as_ref().unwrap()),
"sp_mem": HEXLOWER.encode(sp_mem.as_ref().unwrap()),
"memmap": memmap,
"tip": "configure `trace/allow_unsafe_sigreturn:1'");
}
Err(Errno::ESRCH)
}
PTRACE_DATA_SETGROUPS | PTRACE_DATA_SETGROUPS32 => {
#[cfg(feature = "kcov")]
{
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 if let Err(errno) = ptrace_skip_syscall(pid, info.arch, Some(errno)) {
if errno != Errno::ESRCH {
let _ = kill(pid, Some(Signal::SIGKILL));
}
Err(Errno::ESRCH)
} else if cfg!(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
target_arch = "s390x"
)) {
cache.add_error(pid, Some(errno));
Ok(())
} else {
Err(Errno::ECANCELED)
};
}
Err(Errno::ECANCELED)
}
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(_scno) = cache.get_chdir(pid) {
#[cfg(feature = "kcov")]
{
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 sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
let result = sysexit_chdir(pid, info, &sandbox);
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(pid);
}
result
} else if let Some((scno, args)) = cache.get_mmap(pid) {
#[cfg(feature = "kcov")]
{
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 sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
let result = sysexit_mmap(pid, &sandbox, info, scno, &args);
#[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 _ = kill(pid, Some(Signal::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:?}");
}
}