syd 3.52.0

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, scmp_arch_raw},
    ptrace::{ptrace_set_arg, 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);
    handle_setgroups(pid, "setgroups", is_16, arch, data)
}

// 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> {
    handle_setgroups(pid, "setgroups32", false /*is_16*/, arch, data)
}

#[expect(clippy::cognitive_complexity)]
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);
    }

    // Change setgroups(2) count argument to zero, and continue process.
    //
    // SAFETY:
    // 1. Linux doesn't dereference gidlist if size is zero.
    // 2. There's no pointer dereference in syscall handler.
    if let Err(errno) = ptrace_set_arg(pid, scmp_arch_raw(arch), 0, 0) {
        if errno != Errno::ESRCH {
            warn!("ctx": "safesetid", "op": "set_nogroup",
                "err": errno as i32, "sys": name, "pid": pid.as_raw(),
                "msg": format!("drop additional groups failed: {errno}"),
                "tip": "check with SYD_LOG=debug and/or submit a bug report");
        }
        return Err(errno);
    }

    // Continue process to execute setgroups(2) system call.
    //
    // The count argument is zero, and at this point the sandbox process
    // can NOT prevent removing all additional groups.
    Ok(())
}