use std::{
io::Seek,
sync::{Arc, RwLock},
};
use nix::{
errno::Errno,
fcntl::OFlag,
sys::signal::{kill, Signal},
unistd::Pid,
};
use crate::{
compat::{fstatx, FsType, ResolveFlag, STATX_INO},
debug,
elf::{ElfError, ElfFileType, ElfType, ExecutableFile, LinkingType},
err::err2no,
error,
fd::{SafeOwnedFd, AT_BADFD, PROC_FILE},
log_enabled,
lookup::{safe_open, safe_open_msym},
path::XPathBuf,
proc::{proc_executables, proc_set_at_secure, SydExecMap},
ptrace::ptrace_cont,
sandbox::{Action, Capability, IntegrityError, Sandbox, SandboxGuard},
syslog::LogLevel,
warn,
workers::WorkerCache,
};
#[expect(clippy::cognitive_complexity)]
pub(crate) fn sysevent_exec(pid: Pid, cache: &Arc<WorkerCache>, sandbox: &Arc<RwLock<Sandbox>>) {
#[cfg(feature = "kcov")]
{
crate::kcov::abi::kcov_attach(pid);
crate::kcov::abi::kcov_set_syscall(libc::SYS_execve);
let _ = crate::kcov::abi::kcov_enter_for(pid);
crate::kcov_edge!();
}
let bins = match exec_get_proc(pid) {
Some(bins) => bins,
None => return,
};
let path = &bins[0].path;
let mut fds = Vec::with_capacity(2);
let flags = OFlag::O_RDONLY | OFlag::O_NOCTTY;
for (idx, bin) in bins.iter().enumerate() {
let result = (|| -> Result<SafeOwnedFd, Errno> {
if idx == 0 {
let mut pfd = XPathBuf::from_pid(pid)?;
pfd.push(b"exe");
safe_open_msym(PROC_FILE(), &pfd, flags, ResolveFlag::empty())
} else {
safe_open(AT_BADFD, &bin.path, flags, ResolveFlag::empty())
}
})();
match result {
Ok(fd) => {
let dev_check = match FsType::get(&fd).map(|fs_type| !fs_type.has_broken_devid()) {
Ok(dev_check) => dev_check,
Err(Errno::ENOSYS) => {
true
}
Err(errno) => {
error!("ctx": "exec", "op": "open_elf",
"msg": format!("statfs error: {errno}"),
"err": errno as i32,
"pid": pid.as_raw(), "path": path);
let _ = kill(pid, Some(Signal::SIGKILL));
return;
}
};
let statx = match fstatx(&fd, STATX_INO) {
Ok(stat) => stat,
Err(errno) => {
error!("ctx": "exec", "op": "open_elf",
"msg": format!("statx error: {errno}"),
"err": errno as i32,
"pid": pid.as_raw(), "path": path);
let _ = kill(pid, Some(Signal::SIGKILL));
return;
}
};
#[expect(clippy::cast_sign_loss)]
let dev_major = bin.dev_major as libc::c_uint;
#[expect(clippy::cast_sign_loss)]
let dev_minor = bin.dev_minor as libc::c_uint;
if bin.inode != statx.stx_ino
|| (dev_check
&& (dev_major != statx.stx_dev_major || dev_minor != statx.stx_dev_minor))
{
let error = format!(
"metadata mismatch: {}:{}={} is not {}:{}={}",
statx.stx_dev_major,
statx.stx_dev_minor,
statx.stx_ino,
dev_major,
dev_minor,
bin.inode
);
error!("ctx": "exec", "op": "open_elf",
"msg": error,
"pid": pid.as_raw(),"path": path);
let _ = kill(pid, Some(Signal::SIGKILL));
return;
}
fds.push(fd);
}
Err(errno) => {
error!("ctx": "exec", "op": "open_elf",
"msg": format!("open error: {errno}"),
"err": errno as i32,
"pid": pid.as_raw(), "path": path);
let _ = kill(pid, Some(Signal::SIGKILL));
return;
}
}
}
let my_sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
if !my_sandbox.options.allow_unsafe_sigreturn() {
cache.del_sig_trampoline_ip(pid);
}
let mut deny_action: Option<Action> = None;
if let Some(action) = my_sandbox.check_segvguard(path) {
if action != Action::Filter {
error!("ctx": "exec", "op": "segvguard",
"msg": format!("Max crashes {} exceeded, kill process {}",
my_sandbox.segvguard_maxcrashes,
pid.as_raw()),
"tip": "increase `segvguard/maxcrashes'",
"pid": pid.as_raw(), "path": path);
}
if action == Action::Exit {
std::process::exit(libc::EACCES);
} else if action.is_signaling() {
deny_action = Some(action);
} else if action.is_denying() {
deny_action = Some(Action::Kill);
}
}
if deny_action.is_none() && my_sandbox.enabled(Capability::CAP_EXEC) {
for bin in &bins {
let path = &bin.path;
let mut action = my_sandbox.check_path(Capability::CAP_EXEC, path);
if action == Action::Deny {
action = Action::Kill;
}
if action.is_logging() {
warn!("ctx": "access", "cap": Capability::CAP_EXEC, "act": action,
"pid": pid.as_raw(), "sys": "exec", "path": path,
"tip": format!("configure `allow/exec+{path}'"));
}
match action {
Action::Allow | Action::Warn => {}
Action::Stop => {
deny_action = Some(Action::Stop);
break;
}
Action::Abort => {
deny_action = Some(Action::Abort);
break;
}
Action::Exit => std::process::exit(libc::EACCES),
_ => {
deny_action = Some(Action::Kill);
break;
}
}
}
}
if deny_action.is_none() && my_sandbox.enabled(Capability::CAP_TPE) {
for (idx, bin) in bins.iter().enumerate() {
let file = &fds[idx];
let path = &bin.path;
let (action, msg) = my_sandbox.check_tpe(file, path);
if !matches!(action, Action::Allow | Action::Filter) {
let msg = msg.as_deref().unwrap_or("?");
error!("ctx": "exec", "op": "trusted_path_execution", "err": libc::EACCES,
"pid": pid.as_raw(), "sys": "exec", "path": path, "act": action,
"msg": format!("exec from untrusted path blocked: {msg}"),
"tip": "move the binary to a safe location or use `sandbox/tpe:off'");
}
match action {
Action::Allow | Action::Warn => {}
Action::Stop => deny_action = Some(Action::Stop),
Action::Abort => deny_action = Some(Action::Abort),
Action::Exit => std::process::exit(libc::EACCES),
_ => {
deny_action = Some(Action::Kill);
}
}
}
}
let restrict_32 = my_sandbox.flags.deny_exec_elf32();
let restrict_dyn = my_sandbox.flags.deny_exec_elf_dynamic();
let restrict_sta = my_sandbox.flags.deny_exec_elf_static();
let restrict_ldd = !my_sandbox.flags.allow_unsafe_exec_ldso();
let restrict_pie = !my_sandbox.flags.allow_unsafe_exec_nopie();
let restrict_xs = !my_sandbox.flags.allow_unsafe_exec_stack();
let check_linking = restrict_ldd || restrict_dyn || restrict_sta || restrict_pie || restrict_xs;
let mut need_rewind = false;
let mut exe = None;
if deny_action.is_none() {
match ExecutableFile::parse(&mut fds[0], check_linking) {
Ok(exe_bin) => {
exe = Some(exe_bin);
need_rewind = true;
}
Err(ElfError::IoError(err)) => {
deny_action = Some(Action::Kill);
error!("ctx": "exec", "op": "parse_elf",
"msg": format!("io error: {}", err2no(&err)),
"err": err2no(&err) as i32,
"pid": pid.as_raw(), "path": path);
}
Err(ElfError::BadMagic) => {
deny_action = Some(Action::Kill);
error!("ctx": "exec", "op": "parse_elf",
"msg": format!("BUG: not an ELF"),
"pid": pid.as_raw(), "path": path);
}
Err(ElfError::Malformed) => {
deny_action = Some(Action::Kill);
error!("ctx": "exec", "op": "parse_elf",
"msg": format!("BUG: malformed ELF"),
"pid": pid.as_raw(), "path": path);
}
}
}
if deny_action.is_none()
&& restrict_ldd
&& !matches!(
exe,
Some(ExecutableFile::Elf {
file_type: ElfFileType::Executable,
..
})
)
{
deny_action = Some(Action::Kill);
#[expect(clippy::disallowed_methods)]
let exe = exe.unwrap();
error!("ctx": "exec", "op": "check_elf",
"msg": "ld.so(8) exec-indirection prevented",
"pid": pid.as_raw(), "path": path,
"tip": "configure `trace/allow_unsafe_exec_ldso:1'",
"exe": format!("{exe}"));
}
if deny_action.is_none()
&& restrict_pie
&& matches!(exe, Some(ExecutableFile::Elf { pie: false, .. }))
{
deny_action = Some(Action::Kill);
#[expect(clippy::disallowed_methods)]
let exe = exe.unwrap();
error!("ctx": "exec", "op": "check_elf",
"msg": "ELF is not a Position Independent Executable (PIE)",
"pid": pid.as_raw(), "path": path,
"tip": "configure `trace/allow_unsafe_exec_nopie:1'",
"exe": format!("{exe}"));
}
if deny_action.is_none()
&& restrict_xs
&& matches!(exe, Some(ExecutableFile::Elf { xs: true, .. }))
{
deny_action = Some(Action::Kill);
#[expect(clippy::disallowed_methods)]
let exe = exe.unwrap();
error!("ctx": "exec", "op": "check_elf",
"msg": "ELF has Executable Stack (PT_GNU_STACK)",
"pid": pid.as_raw(), "path": path,
"tip": "configure `trace/allow_unsafe_exec_stack:1'",
"exe": format!("{exe}"));
}
if deny_action.is_none()
&& restrict_32
&& matches!(
exe,
Some(ExecutableFile::Elf {
elf_type: ElfType::Elf32,
..
})
)
{
deny_action = Some(Action::Kill);
#[expect(clippy::disallowed_methods)]
let exe = exe.unwrap();
error!("ctx": "exec", "op": "check_elf",
"msg": "32-bit execution prevented",
"pid": pid.as_raw(), "path": path,
"tip": "configure `trace/deny_exec_elf32:0'",
"exe": format!("{exe}"));
}
if deny_action.is_none()
&& restrict_dyn
&& matches!(
exe,
Some(ExecutableFile::Elf {
linking_type: Some(LinkingType::Dynamic),
..
})
)
{
deny_action = Some(Action::Kill);
#[expect(clippy::disallowed_methods)]
let exe = exe.unwrap();
error!("ctx": "exec", "op": "check_elf",
"msg": "dynamic-link execution prevented",
"pid": pid.as_raw(), "path": path,
"tip": "configure `trace/deny_exec_elf_dynamic:0'",
"exe": format!("{exe}"));
}
if deny_action.is_none()
&& restrict_sta
&& matches!(
exe,
Some(ExecutableFile::Elf {
linking_type: Some(LinkingType::Static),
..
})
)
{
deny_action = Some(Action::Kill);
#[expect(clippy::disallowed_methods)]
let exe = exe.unwrap();
error!("ctx": "exec", "op": "check_elf",
"msg": "static-link execution prevented",
"pid": pid.as_raw(), "path": path,
"tip": "configure `trace/deny_exec_elf_static:0'",
"exe": format!("{exe}"));
}
if deny_action.is_none() && my_sandbox.enabled(Capability::CAP_FORCE) {
for (idx, bin) in bins.iter().enumerate() {
let path = &bin.path;
let result = (|file: &mut SafeOwnedFd,
idx: usize,
need_rewind: bool|
-> Result<Action, IntegrityError> {
if idx == 0 && need_rewind {
file.rewind().map_err(IntegrityError::from)?;
}
my_sandbox.check_force2(file, path)
})(&mut fds[idx], idx, need_rewind);
match result {
Ok(Action::Allow) => {}
Ok(Action::Warn) => {
warn!("ctx": "exec", "op": "verify_elf", "act": Action::Warn,
"pid": pid.as_raw(), "path": path,
"tip": format!("configure `force+{path}:<checksum>'"));
}
Ok(Action::Stop) => {
deny_action = Some(Action::Stop);
warn!("ctx": "exec", "op": "verify_elf", "act": Action::Stop,
"pid": pid.as_raw(), "path": path,
"tip": format!("configure `force+{path}:<checksum>'"));
}
Ok(Action::Abort) => {
deny_action = Some(Action::Abort);
warn!("ctx": "exec", "op": "verify_elf", "act": Action::Abort,
"pid": pid.as_raw(), "path": path,
"tip": format!("configure `force+{path}:<checksum>'"));
}
Ok(Action::Exit) => {
error!("ctx": "exec", "op": "verify_elf", "act": Action::Exit,
"pid": pid.as_raw(), "path": path,
"tip": format!("configure `force+{path}:<checksum>'"));
std::process::exit(libc::EACCES);
}
Ok(mut action) => {
deny_action = Some(Action::Kill);
if action == Action::Deny {
action = Action::Kill;
}
if action != Action::Filter {
warn!("ctx": "exec", "op": "verify_elf", "act": action,
"pid": pid.as_raw(), "path": path,
"tip": format!("configure `force+{path}:<checksum>'"));
}
}
Err(IntegrityError::Sys(errno)) => {
deny_action = Some(Action::Kill);
error!("ctx": "exec", "op": "verify_elf",
"msg": format!("system error during ELF checksum calculation: {errno}"),
"err": errno as i32,
"pid": pid.as_raw(), "path": path,
"tip": format!("configure `force+{path}:<checksum>'"));
}
Err(IntegrityError::Hash {
mut action,
expected,
found,
}) => {
if action == Action::Deny {
action = Action::Kill;
}
if !matches!(action, Action::Allow | Action::Filter) {
error!("ctx": "exec", "op": "verify_elf", "act": action,
"msg": format!("ELF checksum mismatch: {found} is not {expected}"),
"pid": pid.as_raw(), "path": path,
"tip": format!("configure `force+{path}:<checksum>'"));
}
match action {
Action::Allow | Action::Warn => {}
Action::Stop => deny_action = Some(Action::Stop),
Action::Abort => deny_action = Some(Action::Abort),
Action::Exit => std::process::exit(libc::EACCES),
_ =>
{
deny_action = Some(Action::Kill)
}
};
}
}
}
}
if deny_action.is_none() && !my_sandbox.options.allow_unsafe_exec_libc() {
let elf_type = match exe {
Some(ExecutableFile::Elf { elf_type, .. }) => elf_type,
_ => unreachable!(), };
match proc_set_at_secure(pid, elf_type, my_sandbox.flags.deny_vdso()) {
Ok(_) | Err(Errno::ESRCH) => {}
Err(errno) => {
deny_action = Some(Action::Kill);
error!("ctx": "exec", "op": "secure_exec",
"msg": format!("error setting AT_SECURE: {errno}"),
"err": errno as i32,
"tip": "configure `trace/allow_unsafe_exec_libc:1'",
"pid": pid.as_raw(), "path": path);
}
}
}
drop(my_sandbox);
#[cfg(feature = "kcov")]
{
crate::kcov_edge!();
let _ = crate::kcov::abi::kcov_exit_for(pid);
}
if let Some(action) = deny_action {
let _ = kill(
pid,
Some(
Signal::try_from(
action
.signal()
.map(|sig| sig as i32)
.unwrap_or(libc::SIGKILL),
)
.unwrap_or(Signal::SIGKILL),
),
);
} else {
if log_enabled!(LogLevel::Debug) {
let exe = exe
.map(|exe| exe.to_string())
.unwrap_or_else(|| "?".to_string());
debug!("ctx": "exec", "op": "verify_exec",
"msg": format!("execution of `{path}' of type {exe} approved"),
"pid": pid.as_raw(), "path": &path, "exe": &exe);
}
let _ = ptrace_cont(pid, None);
}
}
fn exec_get_proc(pid: Pid) -> Option<Vec<SydExecMap>> {
match proc_executables(pid) {
Ok(bins) => Some(bins),
Err(errno) => {
error!("ctx": "exec", "op": "read_maps",
"msg": format!("failed to read /proc/{}/maps: {errno}", pid.as_raw()),
"err": errno as i32,
"tip": "check with SYD_LOG=debug and/or submit a bug report");
let _ = kill(pid, Some(Signal::SIGKILL));
None
}
}
}