use std::{
io::Seek,
sync::{Arc, RwLock},
};
use nix::{errno::Errno, fcntl::OFlag, unistd::Pid};
use crate::{
compat::{fstatx, FsType, ResolveFlag, STATX_INO},
cookie::safe_kill,
elf::{ElfError, ElfFileType, ElfType, ExecutableFile, LinkingType},
err::err2no,
error,
fd::{SafeOwnedFd, AT_BADFD, PROC_FILE},
kernel::ptrace::deny_action,
lookup::{safe_open, safe_open_msym},
path::{XPath, XPathBuf},
proc::{proc_executables, proc_set_at_secure, SydExecMap},
ptrace::ptrace_cont,
sandbox::{Action, Capability, IntegrityError, Sandbox, SandboxGuard},
warn,
workers::WorkerCache,
xfmt,
};
pub(crate) fn sysevent_exec(pid: Pid, cache: &Arc<WorkerCache>, sandbox: &Arc<RwLock<Sandbox>>) {
#[cfg(feature = "kcov")]
let _kcov = crate::kcov::abi::KcovScope::enter(pid, libc::SYS_execve);
let bins = match exec_get_proc(pid) {
Some(bins) => bins,
None => return,
};
let path = &bins[0].path;
let mut fds = match exec_open_bins(pid, &bins, path) {
Some(fds) => fds,
None => return,
};
let my_sandbox = SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()));
let mut deny = check_segvguard(pid, cache, &my_sandbox, &bins);
if deny.is_none() && my_sandbox.enabled(Capability::CAP_EXEC) {
deny = check_exec(pid, &my_sandbox, &bins);
}
if deny.is_none() && my_sandbox.enabled(Capability::CAP_TPE) {
match check_tpe(pid, &my_sandbox, &bins, &fds) {
Ok(action) => deny = action,
Err(()) => return,
}
}
let mut exe = None;
if deny.is_none() {
match check_elf(pid, &my_sandbox, &bins, &mut fds) {
Ok(elf) => exe = Some(elf),
Err(action) => deny = Some(action),
}
}
if deny.is_none() && my_sandbox.enabled(Capability::CAP_FORCE) {
deny = check_force(pid, &my_sandbox, &bins, &mut fds);
}
if deny.is_none() && !my_sandbox.options.allow_unsafe_exec_libc() {
deny = set_at_secure(pid, &my_sandbox, exe, path);
}
if deny.is_none() {
my_sandbox.move_on_exec(path);
}
drop(my_sandbox);
if let Some(action) = deny {
let sig = action
.signal()
.map(|sig| sig as libc::c_int)
.unwrap_or(libc::SIGKILL);
let _ = safe_kill(pid, sig);
} else {
let _ = ptrace_cont(pid, None);
}
}
#[expect(clippy::cognitive_complexity)]
fn exec_open_bins(pid: Pid, bins: &[SydExecMap], path: &XPath) -> Option<Vec<SafeOwnedFd>> {
let mut fds = Vec::new();
fds.try_reserve_exact(2).ok()?;
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.try_push(b"exe")?;
safe_open_msym(PROC_FILE(), &pfd, flags, ResolveFlag::empty())
} else {
safe_open(AT_BADFD, &bin.path, flags, ResolveFlag::empty())
}
})();
let fd = match result {
Ok(fd) => fd,
Err(errno) => {
error!("ctx": "exec", "op": "open_elf", "pid": pid.as_raw(),
"err": errno as i32, "path": path,
"msg": xfmt!("open ÈLF `{path}' failed: {errno}"),
"tip": "check with SYD_LOG=debug and/or submit a bug report");
let _ = safe_kill(pid, libc::SIGKILL);
return None;
}
};
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", "pid": pid.as_raw(),
"err": errno as i32, "path": path,
"msg": xfmt!("statfs ELF `{path}' failed: {errno}"),
"tip": "check with SYD_LOG=debug and/or submit a bug report");
let _ = safe_kill(pid, libc::SIGKILL);
return None;
}
};
let statx = match fstatx(&fd, STATX_INO) {
Ok(stat) => stat,
Err(errno) => {
error!("ctx": "exec", "op": "open_elf", "pid": pid.as_raw(),
"err": errno as i32, "path": path,
"msg": xfmt!("statx ELF `{path}' failed: {errno}"),
"tip": "check with SYD_LOG=debug and/or submit a bug report");
let _ = safe_kill(pid, libc::SIGKILL);
return None;
}
};
#[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!(
"ELF metadata mismatch: {}:{}={} is not {}:{}={}",
statx.stx_dev_major,
statx.stx_dev_minor,
statx.stx_ino,
dev_major,
dev_minor,
bin.inode
);
warn!("ctx": "exec", "op": "open_elf", "pid": pid.as_raw(),
"path": path, "msg": error);
let _ = safe_kill(pid, libc::SIGKILL);
return None;
}
fds.push(fd);
}
Some(fds)
}
fn check_segvguard(
pid: Pid,
cache: &Arc<WorkerCache>,
sandbox: &SandboxGuard,
bins: &[SydExecMap],
) -> Option<Action> {
for bin in bins {
let path = &bin.path;
let Some(action) = cache.check_segvguard(
path,
sandbox.segvguard_act(),
sandbox.get_segvguard_expiry(),
) else {
continue;
};
if action != Action::Filter {
error!("ctx": "exec", "op": "segvguard",
"pid": pid.as_raw(), "path": path,
"msg": xfmt!("Max crashes {} exceeded, kill process {}",
sandbox.segvguard_maxcrashes,
pid.as_raw()),
"tip": "increase `segvguard/maxcrashes'");
}
if let Some(act) = deny_action(action) {
return Some(act);
}
}
None
}
fn check_exec(pid: Pid, sandbox: &SandboxGuard, bins: &[SydExecMap]) -> Option<Action> {
for bin in bins {
let path = &bin.path;
let mut action = 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": xfmt!("configure `allow/exec+{path}'"));
}
if let Some(act) = deny_action(action) {
return Some(act);
}
}
None
}
#[expect(clippy::cognitive_complexity)]
fn check_tpe(
pid: Pid,
sandbox: &SandboxGuard,
bins: &[SydExecMap],
fds: &[SafeOwnedFd],
) -> Result<Option<Action>, ()> {
let mut deny = None;
for (idx, bin) in bins.iter().enumerate() {
let file = &fds[idx];
let path = &bin.path;
let (action, msg) = match sandbox.check_tpe(file, path) {
Ok(result) => result,
Err(errno) => {
error!("ctx": "exec", "op": "check_tpe", "sys": "exec",
"act": Action::Kill, "pid": pid.as_raw(), "err": errno as i32, "path": path,
"msg": xfmt!("check ELF `{path}' for TPE failed: {errno}"),
"tip": "move the binary to a safe location or use `sandbox/tpe:off'");
let _ = safe_kill(pid, libc::SIGKILL);
return Err(());
}
};
if !matches!(action, Action::Allow | Action::Filter) {
let msg = msg.as_deref().unwrap_or("?");
error!("ctx": "exec", "op": "check_tpe", "sys": "exec",
"act": action, "pid": pid.as_raw(), "err": libc::EACCES, "path": path,
"msg": xfmt!("exec from untrusted path `{path}' blocked: {msg}"),
"tip": "move the binary to a safe location or use `sandbox/tpe:off'");
}
if let Some(act) = deny_action(action) {
deny = Some(act);
}
}
Ok(deny)
}
#[expect(clippy::cognitive_complexity)]
fn check_elf(
pid: Pid,
sandbox: &SandboxGuard,
bins: &[SydExecMap],
fds: &mut [SafeOwnedFd],
) -> Result<ExecutableFile, Action> {
let restrict_32 = sandbox.options.deny_exec_elf32();
let restrict_dyn = sandbox.options.deny_exec_elf_dynamic();
let restrict_sta = sandbox.options.deny_exec_elf_static();
let restrict_ldd = !sandbox.options.allow_unsafe_exec_ldso();
let restrict_pie = !sandbox.options.allow_unsafe_exec_nopie();
let restrict_xs = !sandbox.options.allow_unsafe_exec_stack();
let check_linking = restrict_ldd || restrict_dyn || restrict_sta || restrict_pie || restrict_xs;
let mut exe = None;
for (idx, bin) in bins.iter().enumerate() {
let path = &bin.path;
let elf = match ExecutableFile::parse(&mut fds[idx], check_linking) {
Ok(elf) => elf,
Err(ElfError::IoError(err)) => {
error!("ctx": "exec", "op": "parse_elf", "pid": pid.as_raw(),
"err": err2no(&err) as i32, "path": path,
"msg": xfmt!("parse ELF `{path}' failed: {}", err2no(&err)));
return Err(Action::Kill);
}
Err(ElfError::BadMagic) => {
error!("ctx": "exec", "op": "parse_elf", "pid": pid.as_raw(),
"path": path, "msg": "BUG: not an ELF");
return Err(Action::Kill);
}
Err(ElfError::Malformed) => {
error!("ctx": "exec", "op": "parse_elf", "pid": pid.as_raw(),
"path": path, "msg": "BUG: malformed ELF");
return Err(Action::Kill);
}
};
if idx == 0 {
if restrict_ldd
&& !matches!(
elf,
ExecutableFile::Elf {
file_type: ElfFileType::Executable,
..
}
)
{
warn!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
"path": path, "exe": xfmt!("{elf}"),
"msg": "ld.so(8) exec-indirection prevented",
"tip": "configure `trace/allow_unsafe_exec_ldso:1'");
return Err(Action::Kill);
}
if restrict_pie && matches!(elf, ExecutableFile::Elf { pie: false, .. }) {
warn!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
"path": path, "exe": xfmt!("{elf}"),
"msg": "ELF is not a Position Independent Executable (PIE)",
"tip": "configure `trace/allow_unsafe_exec_nopie:1'");
return Err(Action::Kill);
}
}
if restrict_xs && matches!(elf, ExecutableFile::Elf { xs: true, .. }) {
warn!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
"path": path, "exe": xfmt!("{elf}"),
"msg": "ELF has Executable Stack (PT_GNU_STACK)",
"tip": "configure `trace/allow_unsafe_exec_stack:1'");
return Err(Action::Kill);
}
if restrict_32
&& matches!(
elf,
ExecutableFile::Elf {
elf_type: ElfType::Elf32,
..
}
)
{
warn!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
"path": path, "exe": xfmt!("{elf}"),
"msg": "32-bit execution prevented",
"tip": "configure `trace/deny_exec_elf32:0'");
return Err(Action::Kill);
}
if restrict_dyn
&& matches!(
elf,
ExecutableFile::Elf {
linking_type: Some(LinkingType::Dynamic),
..
}
)
{
warn!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
"path": path, "exe": xfmt!("{elf}"),
"msg": "dynamic-link execution prevented",
"tip": "configure `trace/deny_exec_elf_dynamic:0'");
return Err(Action::Kill);
}
if restrict_sta
&& matches!(
elf,
ExecutableFile::Elf {
linking_type: Some(LinkingType::Static),
..
}
)
{
error!("ctx": "exec", "op": "check_elf", "pid": pid.as_raw(),
"path": path, "exe": xfmt!("{elf}"),
"msg": "static-link execution prevented",
"tip": "configure `trace/deny_exec_elf_static:0'");
return Err(Action::Kill);
}
if idx == 0 {
exe = Some(elf);
}
}
exe.ok_or(Action::Kill)
}
#[expect(clippy::cognitive_complexity)]
fn check_force(
pid: Pid,
sandbox: &SandboxGuard,
bins: &[SydExecMap],
fds: &mut [SafeOwnedFd],
) -> Option<Action> {
let mut deny = None;
for (idx, bin) in bins.iter().enumerate() {
let path = &bin.path;
let fd = &mut fds[idx];
let result = match fd.rewind() {
Ok(()) => sandbox.check_force2(fd, path),
Err(err) => Err(IntegrityError::from(err)),
};
match result {
Ok(mut action) => {
if action == Action::Deny {
action = Action::Kill;
}
match action {
Action::Allow | Action::Filter => {}
Action::Exit => {
error!("ctx": "exec", "op": "verify_elf", "act": action,
"pid": pid.as_raw(), "path": path,
"tip": xfmt!("configure `force+{path}:<checksum>'"));
}
_ => {
warn!("ctx": "exec", "op": "verify_elf", "act": action,
"pid": pid.as_raw(), "path": path,
"tip": xfmt!("configure `force+{path}:<checksum>'"));
}
}
if let Some(act) = deny_action(action) {
deny = Some(act);
}
}
Err(IntegrityError::Sys(errno)) => {
deny = Some(Action::Kill);
error!("ctx": "exec", "op": "verify_elf", "act": Action::Kill,
"pid": pid.as_raw(), "err": errno as i32, "path": path,
"msg": xfmt!("system error during ELF checksum calculation: {errno}"),
"tip": xfmt!("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) {
warn!("ctx": "exec", "op": "verify_elf", "act": action,
"pid": pid.as_raw(), "path": path,
"msg": xfmt!("ELF checksum mismatch: {found} is not {expected}"),
"tip": xfmt!("configure `force+{path}:<checksum>'"));
}
if let Some(act) = deny_action(action) {
deny = Some(act);
}
}
}
}
deny
}
fn set_at_secure(
pid: Pid,
sandbox: &SandboxGuard,
exe: Option<ExecutableFile>,
path: &XPath,
) -> Option<Action> {
let elf_type = match exe {
Some(ExecutableFile::Elf { elf_type, .. }) => elf_type,
_ => unreachable!("BUG: set_at_secure called with `{exe:?}', report a bug!"),
};
match proc_set_at_secure(pid, elf_type, sandbox.flags.deny_vdso()) {
Ok(_) | Err(Errno::ESRCH) => None,
Err(errno) => {
error!("ctx": "exec", "op": "secure_exec", "pid": pid.as_raw(),
"err": errno as i32, "path": path,
"msg": xfmt!("error setting AT_SECURE for ELF `{path}': {errno}"),
"tip": "configure `trace/allow_unsafe_exec_libc:1'");
Some(Action::Kill)
}
}
}
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",
"pid": pid.as_raw(), "err": errno as i32,
"msg": xfmt!("failed to read /proc/{}/maps: {errno}", pid.as_raw()),
"tip": "check with SYD_LOG=debug and/or submit a bug report");
let _ = safe_kill(pid, libc::SIGKILL);
None
}
}
}