syd 3.52.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/kernel/ptrace/event/sig.rs: ptrace(2) signal event handler
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::sync::{Arc, RwLock};

use libc::{PTRACE_CONT, PTRACE_SINGLESTEP};
use nix::{
    errno::Errno,
    sys::signal::{kill, Signal},
    unistd::Pid,
};

#[cfg(any(
    target_arch = "aarch64",
    target_arch = "powerpc",
    target_arch = "powerpc64",
    target_arch = "s390x",
))]
use crate::ptrace::ptrace_get_link_register;
use crate::{
    cache::{SigreturnTrampolineIP, SIG_NEST_DEEP},
    confine::{is_coredump, scmp_arch, scmp_arch_has_single_step},
    cookie::safe_ptrace,
    error,
    ptrace::{ptrace_get_arch, ptrace_getsiginfo},
    sandbox::{Action, Sandbox, SandboxGuard},
    workers::WorkerCache,
};
#[cfg(any(target_arch = "x86_64", target_arch = "x86", target_arch = "m68k"))]
use crate::{ptrace::ptrace_get_stack_ptr, req::RemoteProcess};

pub(crate) fn sysevent_sig(
    pid: Pid,
    sig: i32,
    cache: &Arc<WorkerCache>,
    sandbox: &Arc<RwLock<Sandbox>>,
) {
    // Determine whether SROP mitigations are enabled.
    let restrict_sigreturn = {
        !SandboxGuard::Read(sandbox.read().unwrap_or_else(|err| err.into_inner()))
            .options
            .allow_unsafe_sigreturn()
    };

    if !restrict_sigreturn {
        // SAFETY:
        // 1. Continue process with ptrace(2).
        // 2. nix Signal type does not include realtime signals.
        let _ = unsafe {
            safe_ptrace(
                PTRACE_CONT,
                pid.as_raw(),
                std::ptr::null_mut(),
                sig as *mut libc::c_void,
            )
        };
        return;
    }

    // SIGTRAP from a previous PTRACE_SINGLESTEP at signal-delivery:
    // Save trampoline IP and continue without delivering any trap.
    if sig == libc::SIGTRAP && cache.get_sig_in_singlestep(pid) {
        let si_code = ptrace_getsiginfo(pid).map(|i| i.si_code).unwrap_or(0);

        if si_code == libc::TRAP_TRACE {
            cache.set_sig_in_singlestep(pid, false);
        } else if let Some(ip) = read_sig_trampoline_ip(pid) {
            cache.set_sig_trampoline_ip(pid, ip);
        } else {
            cache.set_sig_in_singlestep(pid, false);
        }

        // SAFETY: PTRACE_CONT with NULL signal skips SIGTRAP, clears TIF_SINGLESTEP.
        let _ = unsafe {
            safe_ptrace(
                PTRACE_CONT,
                pid.as_raw(),
                std::ptr::null_mut(),
                std::ptr::null_mut(),
            )
        };

        return;
    }

    if handle_srop(pid, sig, cache).is_err() {
        return;
    }

    // Check if architecture supports PTRACE_SINGLESTEP.
    let has_single_step = ptrace_get_arch(pid)
        .ok()
        .and_then(|a| scmp_arch(a).ok())
        .is_some_and(scmp_arch_has_single_step);

    let request = if has_single_step {
        cache.set_sig_in_singlestep(pid, true);
        PTRACE_SINGLESTEP
    } else {
        PTRACE_CONT
    };

    // SAFETY:
    // 1. Continue/single-step process with ptrace(2).
    // 2. nix Signal type does not include realtime signals.
    let _ = unsafe {
        safe_ptrace(
            request,
            pid.as_raw(),
            std::ptr::null_mut(),
            sig as *mut libc::c_void,
        )
    };
}

#[cfg(any(target_arch = "x86_64", target_arch = "x86", target_arch = "m68k"))]
fn read_sig_trampoline_ip(pid: Pid) -> Option<SigreturnTrampolineIP> {
    use libseccomp_sys::{SCMP_ARCH_M68K, SCMP_ARCH_X32, SCMP_ARCH_X86, SCMP_ARCH_X86_64};

    let arch = ptrace_get_arch(pid).ok()?;
    let sp = ptrace_get_stack_ptr(pid, Some(arch)).ok()?;

    let scmp = scmp_arch(arch).ok()?;

    let (ptr_size, is_be) = match arch {
        SCMP_ARCH_X86_64 | SCMP_ARCH_X32 => (8usize, false),
        SCMP_ARCH_X86 => (4usize, false),
        SCMP_ARCH_M68K => (4usize, true),
        _ => return None,
    };
    let mut buf = [0u8; 8];

    // SAFETY:
    // 1. ptrace(2) hook, request cannot be validated.
    // 2. read_mem is bounds-checked internally.
    let n = unsafe { RemoteProcess::new(pid).read_mem(scmp, &mut buf[..ptr_size], sp, ptr_size) }
        .ok()?;
    if n != ptr_size {
        return None;
    }

    let mut ip = [0u8; 8];
    #[expect(clippy::arithmetic_side_effects)]
    let ip = if is_be {
        ip[8 - ptr_size..].copy_from_slice(&buf[..ptr_size]);
        u64::from_be_bytes(ip)
    } else {
        ip[..ptr_size].copy_from_slice(&buf[..ptr_size]);
        u64::from_le_bytes(ip)
    };

    Some(SigreturnTrampolineIP { lo: ip, hi: ip })
}

#[cfg(any(
    target_arch = "aarch64",
    target_arch = "powerpc",
    target_arch = "powerpc64",
    target_arch = "s390x",
))]
fn read_sig_trampoline_ip(pid: Pid) -> Option<SigreturnTrampolineIP> {
    let lr = ptrace_get_link_register(pid).ok()?;
    Some(SigreturnTrampolineIP { lo: lr, hi: lr })
}

#[cfg(not(any(
    target_arch = "x86_64",
    target_arch = "x86",
    target_arch = "m68k",
    target_arch = "aarch64",
    target_arch = "powerpc",
    target_arch = "powerpc64",
    target_arch = "s390x",
)))]
fn read_sig_trampoline_ip(_pid: Pid) -> Option<SigreturnTrampolineIP> {
    // Architectures without PTRACE_SINGLESTEP support.
    None
}

#[expect(clippy::cognitive_complexity)]
fn handle_srop(pid: Pid, sig: i32, cache: &Arc<WorkerCache>) -> Result<(), Errno> {
    // Fatal signal during handler dispatch: assume SROP.
    //
    // 1. User-sent (SI_FROMUSER) coredump signal at any depth is a
    //    direct SROP indicator, unless si_pid is the receiving TID
    //    itself.
    // 2. Kernel-sent coredump signal at deep nesting (>= SIG_NEST_DEEP)
    //    is unreachable for any sane program and indicates a sigaction
    //    TOCTOU stress pattern.
    if is_coredump(sig) {
        let depth = cache.depth_sig_handle(pid);
        if depth > 0 {
            let user_sig = match ptrace_getsiginfo(pid) {
                // SAFETY: si_code <= 0 means siginfo.si_pid is valid.
                Ok(info) => info.si_code <= 0 && unsafe { info.si_pid() } != pid.as_raw(),
                Err(Errno::ESRCH) => return Err(Errno::ESRCH),
                Err(_) => true,
            };

            if user_sig || usize::from(depth) >= SIG_NEST_DEEP {
                error!("ctx": "sigreturn", "op": "check_SROP", "act": Action::Kill,
                    "pid": pid.as_raw(), "sig": sig, "depth": depth,
                    "msg": "fatal signal during handler dispatch: assume SROP!",
                    "tip": "configure `trace/allow_unsafe_sigreturn:1'");
                let _ = kill(pid, Some(Signal::SIGKILL));
                return Err(Errno::ESRCH);
            }
        }
    }

    // Increment per-TID delivery depth to reject artificial sigreturn(2).
    if let Err(errno) = cache.push_sig_handle(pid) {
        error!("ctx": "handle_signal", "op": "push_sig_handle",
            "pid": pid.as_raw(), "err": errno as i32,
            "msg": format!("per-TID signal delivery cookie ring full: {errno}"),
            "tip": "configure `trace/allow_unsafe_sigreturn:1'");
        let _ = kill(pid, Some(Signal::SIGKILL));
        return Err(Errno::ESRCH);
    }

    Ok(())
}