syd 3.54.1

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/kernel/ptrace/setgroups.rs: ptrace setgroups handlers
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use libseccomp::ScmpArch;
use nix::{errno::Errno, unistd::Pid};

use crate::{
    compat::setgroups_none,
    config::NGROUPS_MAX,
    confine::{is_valid_ptr, scmp_arch_has_uid16},
    cookie::safe_kill,
    ptrace::ptrace_syscall_info_seccomp,
    req::RemoteProcess,
    warn,
};

// setgroups(2) is a ptrace(2) hook, not a seccomp hook!
// seccomp(2) hook is only used with trace/allow_unsafe_ptrace:1.
pub(crate) fn sysenter_setgroups(
    pid: Pid,
    arch: ScmpArch,
    data: ptrace_syscall_info_seccomp,
) -> Result<(), Errno> {
    // Accept 16-bit IDs on CONFIG_UID16 architectures.
    let is_16 = scmp_arch_has_uid16(arch);

    match handle_setgroups(pid, "setgroups", is_16, arch, data) {
        Ok(()) => Ok(()),
        Err(Errno::ESRCH) => Err(Errno::ESRCH),
        Err(errno) => {
            let _ = safe_kill(pid, libc::SIGKILL);
            Err(errno)
        }
    }
}

// setgroups32 is a ptrace(2) hook, not a seccomp hook!
// seccomp(2) hook is only used with trace/allow_unsafe_ptrace:1.
pub(crate) fn sysenter_setgroups32(
    pid: Pid,
    arch: ScmpArch,
    data: ptrace_syscall_info_seccomp,
) -> Result<(), Errno> {
    match handle_setgroups(pid, "setgroups32", false /*is_16*/, arch, data) {
        Ok(()) => Ok(()),
        Err(Errno::ESRCH) => Err(Errno::ESRCH),
        Err(errno) => {
            let _ = safe_kill(pid, libc::SIGKILL);
            Err(errno)
        }
    }
}

fn handle_setgroups(
    pid: Pid,
    name: &str,
    is_16: bool,
    arch: ScmpArch,
    data: ptrace_syscall_info_seccomp,
) -> Result<(), Errno> {
    // Linux truncates upper-bits of count.
    #[expect(clippy::cast_possible_truncation)]
    let count = data.args[0] as u32;

    // Linux limits count to NGROUPS_MAX.
    if count > NGROUPS_MAX {
        return Err(Errno::EINVAL);
    }
    let count = count as usize;

    // Linux doesn't dereference GID list for zero count.
    if count > 0 {
        let list = data.args[1];

        // Reject invalid list pointer.
        if !is_valid_ptr(list, arch) {
            return Err(Errno::EFAULT);
        }

        // Read remote GID list.
        let process = RemoteProcess::new(pid);

        // SAFETY: This is a ptrace(2) hook, the PID cannot be validated.
        let gids = unsafe { process.remote_gidlist(arch, list, count, is_16) }?;

        // Validate GID list.
        for gid in &gids {
            if *gid == u32::MAX {
                return Err(Errno::EINVAL);
            }
        }
    }

    // Attempt to drop Syd's supplementary groups.
    if let Err(errno) = setgroups_none() {
        if errno != Errno::EPERM {
            warn!("ctx": "safesetid", "op": "syd_nogroup",
                "err": errno as i32, "sys": name, "pid": pid.as_raw(),
                "msg": format!("drop additional groups for Syd failed: {errno}"),
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
        }
        return Err(errno);
    }

    // Continue to SetGroupsZero.
    Ok(())
}