//
// Syd: rock-solid application kernel
// src/kernel/setid.rs: Set UID/GID syscall handlers
//
// Copyright (c) 2023, 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    unistd::{getresgid, getresuid, Gid, Uid},
};

use crate::{
    caps,
    config::{GID_MIN, UID_MIN},
    confine::safe_drop_cap,
    req::UNotifyEventRequest,
    warn,
};

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sys_setuid(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;

        let source_uid = Uid::current();
        let target_uid =
            Uid::from_raw(libc::uid_t::try_from(req.data.args[0]).or(Err(Errno::EINVAL))?);
        if target_uid.as_raw() <= UID_MIN.as_raw() {
            // SAFETY: This is already asserted with the parent
            // seccomp-bpf filter, this is the second layer.
            return Ok(request.fail_syscall(Errno::EACCES));
        } else if source_uid == target_uid {
            // SAFETY: No UID change or no ptr-deref in check.
            return unsafe { Ok(request.continue_syscall()) };
        }

        let sandbox = request.get_sandbox();
        let allowed = sandbox.chk_uid_transit(source_uid, target_uid);
        let log_scmp = sandbox.log_scmp();
        drop(sandbox); // release the read lock.

        if !allowed {
            if log_scmp {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "req": request,
                    "target_uid": target_uid.as_raw(), "source_uid": source_uid.as_raw(),
                    "msg": "UID change without UID transit blocked",
                    "tip": format!("define UID transit `setuid+{}:{}'",
                        source_uid.as_raw(), target_uid.as_raw()));
            } else {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_uid": target_uid.as_raw(), "source_uid": source_uid.as_raw(),
                    "msg": "UID change without UID transit blocked",
                    "tip": format!("define UID transit `setuid+{}:{}'",
                        source_uid.as_raw(), target_uid.as_raw()));
            }
            return Err(Errno::EACCES);
        }

        // SAFETY: nix version of setuid does not allow -1 as argument.
        if let Err(errno) = Errno::result(unsafe { libc::setuid(target_uid.as_raw()) }) {
            if log_scmp {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "req": request,
                    "target_uid": target_uid.as_raw(), "source_uid": source_uid.as_raw(),
                    "msg": format!("UID change {}->{} failed: {errno}",
                        source_uid.as_raw(), target_uid.as_raw()),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            } else {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_uid": target_uid.as_raw(), "source_uid": source_uid.as_raw(),
                    "msg": format!("UID change {}->{} failed: {errno}",
                        source_uid.as_raw(), target_uid.as_raw()),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            }
            return Err(errno);
        } else if safe_drop_cap(caps::Capability::CAP_SETUID).is_err() {
            // SAFETY: We cannot do much on errors,
            // and on panic the thread will be restarted.
            // The best we can do from a security POV is
            // to enter Ghost mode. This is certainly
            // unexpected but it's safe.
            return Err(Errno::EOWNERDEAD);
        }

        // SAFETY: There's no pointer dereference in the access check.
        unsafe { Ok(request.continue_syscall()) }
    })
}

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sys_setgid(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;

        let source_gid = Gid::current();
        let target_gid =
            Gid::from_raw(libc::gid_t::try_from(req.data.args[0]).or(Err(Errno::EINVAL))?);
        if target_gid.as_raw() <= GID_MIN.as_raw() {
            // SAFETY: This is already asserted with the parent
            // seccomp-bpf filter, this is the second layer.
            return Ok(request.fail_syscall(Errno::EACCES));
        } else if source_gid == target_gid {
            // SAFETY: No GID change or no ptr-deref in check.
            return unsafe { Ok(request.continue_syscall()) };
        }

        let sandbox = request.get_sandbox();
        let allowed = sandbox.chk_gid_transit(source_gid, target_gid);
        let log_scmp = sandbox.log_scmp();
        drop(sandbox); // release the read lock.

        if !allowed {
            if log_scmp {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "req": request,
                    "target_gid": target_gid.as_raw(), "source_gid": source_gid.as_raw(),
                    "msg": "GID change without GID transit blocked",
                    "tip": format!("define GID transit `setgid+{}:{}'",
                        source_gid.as_raw(), target_gid.as_raw()));
            } else {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_gid": target_gid.as_raw(), "source_gid": source_gid.as_raw(),
                    "msg": "GID change without GID transit blocked",
                    "tip": format!("define GID transit `setgid+{}:{}'",
                        source_gid.as_raw(), target_gid.as_raw()));
            }
            return Err(Errno::EACCES);
        }

        // SAFETY: nix version of setgid does not allow -1 as argument.
        if let Err(errno) = Errno::result(unsafe { libc::setgid(target_gid.as_raw()) }) {
            if log_scmp {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "req": request,
                    "target_gid": target_gid.as_raw(), "source_gid": source_gid.as_raw(),
                    "msg": format!("GID change {}->{} failed: {errno}",
                        source_gid.as_raw(), target_gid.as_raw()),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            } else {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_gid": target_gid.as_raw(), "source_gid": source_gid.as_raw(),
                    "msg": format!("GID change {}->{} failed: {errno}",
                        source_gid.as_raw(), target_gid.as_raw()),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            }
            return Err(errno);
        } else if safe_drop_cap(caps::Capability::CAP_SETGID).is_err() {
            // SAFETY: We cannot do much on errors,
            // and on panic the thread will be restarted.
            // The best we can do from a security POV is
            // to enter Ghost mode. This is certainly
            // unexpected but it's safe.
            return Err(Errno::EOWNERDEAD);
        }

        // SAFETY: There's no pointer dereference in the access check.
        unsafe { Ok(request.continue_syscall()) }
    })
}

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sys_setreuid(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;

        #[expect(clippy::cast_possible_truncation)]
        let target_ruid = match req.data.args[0] as i32 {
            -1 => None,
            n if n >= 0 => Some(Uid::from_raw(
                libc::uid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };
        #[expect(clippy::cast_possible_truncation)]
        let target_euid = match req.data.args[1] as i32 {
            -1 => None,
            n if n >= 0 => Some(Uid::from_raw(
                libc::uid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };

        if target_ruid.is_none() && target_euid.is_none() {
            // No change of UID requested, return success.
            return Ok(request.return_syscall(0));
        }

        // getresuid can only fail with EFAULT which should not happen.
        let resuid = getresuid()?;
        let source_ruid = resuid.real;
        let source_euid = resuid.effective;

        let mut change = false;
        if let Some(target_ruid) = target_ruid {
            if target_ruid.as_raw() <= UID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_ruid != target_ruid {
                change = true;
            }
        }
        if let Some(target_euid) = target_euid {
            if target_euid.as_raw() <= UID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_euid != target_euid {
                change = true;
            }
        }

        if !change {
            // SAFETY: No UID change or no ptr-deref in check.
            return unsafe { Ok(request.continue_syscall()) };
        }

        let sandbox = request.get_sandbox();
        let log_scmp = sandbox.log_scmp();

        // SAFETY: We do not support RUID != EUID
        if let Some(target_ruid) = target_ruid {
            if let Some(target_euid) = target_euid {
                if target_ruid != target_euid {
                    if log_scmp {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "req": &request,
                            "target_euid": target_euid.as_raw(), "target_ruid": target_ruid.as_raw(),
                            "source_euid": source_euid.as_raw(), "source_ruid": source_ruid.as_raw(),
                            "msg": format!("unsafe UID change with real-UID:{} != effective-UID:{} blocked",
                                target_ruid, target_euid),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    } else {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "pid": request.scmpreq.pid,
                            "target_euid": target_euid.as_raw(), "target_ruid": target_ruid.as_raw(),
                            "source_euid": source_euid.as_raw(), "source_ruid": source_ruid.as_raw(),
                            "msg": format!("unsafe UID change with real-UID:{} != effective-UID:{} blocked",
                                target_ruid, target_euid),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    }
                    return Err(Errno::EACCES);
                }
            }
        }

        let mut allowed = true;
        if let Some(target_ruid) = target_ruid {
            if !sandbox.chk_uid_transit(source_ruid, target_ruid) {
                allowed = false;
            }
        }
        if allowed {
            if let Some(target_euid) = target_euid {
                if !sandbox.chk_uid_transit(source_euid, target_euid) {
                    allowed = false;
                }
            }
        }
        drop(sandbox); // release the read lock.

        let target_ruid = target_ruid.map(|uid| i64::from(uid.as_raw())).unwrap_or(-1);
        let target_euid = target_euid.map(|uid| i64::from(uid.as_raw())).unwrap_or(-1);
        if !allowed {
            if log_scmp {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "req": request,
                    "target_euid": target_euid, "target_ruid": target_ruid,
                    "source_euid": source_euid.as_raw(), "source_ruid": source_ruid.as_raw(),
                    "msg": "UID change without UID transit blocked",
                    "tip": format!("define UID transit `setuid+{}:{}'",
                        source_euid.as_raw(), target_euid));
            } else {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_euid": target_euid, "target_ruid": target_ruid,
                    "source_euid": source_euid.as_raw(), "source_ruid": source_ruid.as_raw(),
                    "msg": "UID change without UID transit blocked",
                    "tip": format!("define UID transit `setuid+{}:{}'",
                        source_euid.as_raw(), target_euid));
            }
            return Err(Errno::EACCES);
        }

        if let Err(errno) =
            // SAFETY: nix version of setreuid does not allow -1 as argument.
            Errno::result(unsafe {
                libc::syscall(libc::SYS_setreuid, target_ruid, target_euid)
            })
        {
            if log_scmp {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "req": request,
                    "target_euid": target_euid, "target_ruid": target_ruid,
                    "source_euid": source_euid.as_raw(), "source_ruid": source_ruid.as_raw(),
                    "msg": format!("UID change {}->{} failed: {errno}",
                        source_euid.as_raw(), target_euid),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            } else {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_euid": target_euid, "target_ruid": target_ruid,
                    "source_euid": source_euid.as_raw(), "source_ruid": source_ruid.as_raw(),
                    "msg": format!("UID change {}->{} failed: {errno}",
                        source_euid.as_raw(), target_euid),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            }
            return Err(errno);
        } else if safe_drop_cap(caps::Capability::CAP_SETUID).is_err() {
            // SAFETY: We cannot do much on errors,
            // and on panic the thread will be restarted.
            // The best we can do from a security POV is
            // to enter Ghost mode. This is certainly
            // unexpected but it's safe.
            return Err(Errno::EOWNERDEAD);
        }

        // SAFETY: There's no pointer dereference in the access check.
        unsafe { Ok(request.continue_syscall()) }
    })
}

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sys_setregid(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;

        #[expect(clippy::cast_possible_truncation)]
        let target_rgid = match req.data.args[0] as i32 {
            -1 => None,
            n if n >= 0 => Some(Gid::from_raw(
                libc::gid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };
        #[expect(clippy::cast_possible_truncation)]
        let target_egid = match req.data.args[1] as i32 {
            -1 => None,
            n if n >= 0 => Some(Gid::from_raw(
                libc::gid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };

        if target_rgid.is_none() && target_egid.is_none() {
            // No change of GID requested, return success.
            return Ok(request.return_syscall(0));
        }

        // getresgid can only fail with EFAULT which should not happen.
        let resgid = getresgid()?;
        let source_rgid = resgid.real;
        let source_egid = resgid.effective;

        let mut change = false;
        if let Some(target_rgid) = target_rgid {
            if target_rgid.as_raw() <= GID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_rgid != target_rgid {
                change = true;
            }
        }
        if let Some(target_egid) = target_egid {
            if target_egid.as_raw() <= GID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_egid != target_egid {
                change = true;
            }
        }

        if !change {
            // SAFETY: No GID change or no ptr-deref in check.
            return unsafe { Ok(request.continue_syscall()) };
        }

        let sandbox = request.get_sandbox();
        let log_scmp = sandbox.log_scmp();

        // SAFETY: We do not support Rgid != Egid
        if let Some(target_rgid) = target_rgid {
            if let Some(target_egid) = target_egid {
                if target_rgid != target_egid {
                    if log_scmp {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "req": &request,
                            "target_egid": target_egid.as_raw(), "target_rgid": target_rgid.as_raw(),
                            "source_egid": source_egid.as_raw(), "source_rgid": source_rgid.as_raw(),
                            "msg": format!("unsafe GID change with real-GID:{} != effective-GID:{} blocked",
                                target_rgid.as_raw(), target_egid.as_raw()),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    } else {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "pid": request.scmpreq.pid,
                            "target_egid": target_egid.as_raw(), "target_rgid": target_rgid.as_raw(),
                            "source_egid": source_egid.as_raw(), "source_rgid": source_rgid.as_raw(),
                            "msg": format!("unsafe GID change with real-GID:{} != effective-GID:{} blocked",
                                target_rgid.as_raw(), target_egid.as_raw()),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    }
                    return Err(Errno::EACCES);
                }
            }
        }

        let mut allowed = true;
        if let Some(target_rgid) = target_rgid {
            if !sandbox.chk_gid_transit(source_rgid, target_rgid) {
                allowed = false;
            }
        }
        if allowed {
            if let Some(target_egid) = target_egid {
                if !sandbox.chk_gid_transit(source_egid, target_egid) {
                    allowed = false;
                }
            }
        }
        drop(sandbox); // release the read lock.

        let target_rgid = target_rgid.map(|gid| i64::from(gid.as_raw())).unwrap_or(-1);
        let target_egid = target_egid.map(|gid| i64::from(gid.as_raw())).unwrap_or(-1);
        if !allowed {
            if log_scmp {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "req": request,
                    "target_egid": target_egid, "target_rgid": target_rgid,
                    "source_egid": source_egid.as_raw(), "source_rgid": source_rgid.as_raw(),
                    "msg": "GID change without GID transit blocked",
                    "tip": format!("define GID transit `setgid+{}:{}'",
                        source_egid.as_raw(), target_egid));
            } else {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_egid": target_egid, "target_rgid": target_rgid,
                    "source_egid": source_egid.as_raw(), "source_rgid": source_rgid.as_raw(),
                    "msg": "GID change without GID transit blocked",
                    "tip": format!("define GID transit `setgid+{}:{}'",
                        source_egid.as_raw(), target_egid));
            }
            return Err(Errno::EACCES);
        }

        if let Err(errno) =
            // SAFETY: nix version of setregid does not allow -1 as argument.
            Errno::result(unsafe {
                libc::syscall(libc::SYS_setregid, target_rgid, target_egid)
            })
        {
            if log_scmp {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "req": request,
                    "target_egid": target_egid, "target_rgid": target_rgid,
                    "source_egid": source_egid.as_raw(), "source_rgid": source_rgid.as_raw(),
                    "msg": format!("GID change {}->{} failed: {errno}",
                        source_egid.as_raw(), target_egid),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            } else {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_egid": target_egid, "target_rgid": target_rgid,
                    "source_egid": source_egid.as_raw(), "source_rgid": source_rgid.as_raw(),
                    "msg": format!("GID change {}->{} failed: {errno}",
                        source_egid.as_raw(), target_egid),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            }
            return Err(errno);
        } else if safe_drop_cap(caps::Capability::CAP_SETGID).is_err() {
            // SAFETY: We cannot do much on errors,
            // and on panic the thread will be restarted.
            // The best we can do from a security POV is
            // to enter Ghost mode. This is certainly
            // unexpected but it's safe.
            return Err(Errno::EOWNERDEAD);
        }

        // SAFETY: There's no pointer dereference in the access check.
        unsafe { Ok(request.continue_syscall()) }
    })
}

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sys_setresuid(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;

        #[expect(clippy::cast_possible_truncation)]
        let target_ruid = match req.data.args[0] as i32 {
            -1 => None,
            n if n >= 0 => Some(Uid::from_raw(
                libc::uid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };
        #[expect(clippy::cast_possible_truncation)]
        let target_euid = match req.data.args[1] as i32 {
            -1 => None,
            n if n >= 0 => Some(Uid::from_raw(
                libc::uid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };
        #[expect(clippy::cast_possible_truncation)]
        let target_suid = match req.data.args[2] as i32 {
            -1 => None,
            n if n >= 0 => Some(Uid::from_raw(
                libc::uid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };

        if target_ruid.is_none() && target_euid.is_none() && target_suid.is_none() {
            // No change of UID requested, return success.
            return Ok(request.return_syscall(0));
        }

        // getresuid can only fail with EFAULT which should not happen.
        let resuid = getresuid()?;
        let source_ruid = resuid.real;
        let source_euid = resuid.effective;
        let source_suid = resuid.saved;

        let mut change = false;
        if let Some(target_ruid) = target_ruid {
            if target_ruid.as_raw() <= UID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_ruid != target_ruid {
                change = true;
            }
        }
        if let Some(target_euid) = target_euid {
            if target_euid.as_raw() <= UID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_euid != target_euid {
                change = true;
            }
        }
        if let Some(target_suid) = target_suid {
            if target_suid.as_raw() <= UID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_suid != target_suid {
                change = true;
            }
        }

        if !change {
            // SAFETY: No UID change or no ptr-deref in check.
            return unsafe { Ok(request.continue_syscall()) };
        }

        let sandbox = request.get_sandbox();
        let log_scmp = sandbox.log_scmp();

        // SAFETY: We do not support RUID != EUID != SUID
        if let Some(target_ruid) = target_ruid {
            if let Some(target_euid) = target_euid {
                if target_ruid != target_euid {
                    if log_scmp {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "req": &request,
                            "target_suid": target_suid.map(|u| u.as_raw()),
                            "target_euid": target_euid.as_raw(),
                            "target_ruid": target_ruid.as_raw(),
                            "source_euid": source_euid.as_raw(),
                            "source_ruid": source_ruid.as_raw(),
                            "source_suid": source_suid.as_raw(),
                            "msg": format!("unsafe UID change with real-UID:{} != effective-UID:{} blocked",
                                target_ruid, target_euid),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    } else {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "pid": request.scmpreq.pid,
                            "target_suid": target_suid.map(|u| u.as_raw()),
                            "target_euid": target_euid.as_raw(),
                            "target_ruid": target_ruid.as_raw(),
                            "source_euid": source_euid.as_raw(),
                            "source_ruid": source_ruid.as_raw(),
                            "source_suid": source_suid.as_raw(),
                            "msg": format!("unsafe UID change with real-UID:{} != effective-UID:{} blocked",
                                target_ruid, target_euid),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    }
                    return Err(Errno::EACCES);
                }
            }
        }
        if let Some(target_ruid) = target_ruid {
            if let Some(target_suid) = target_suid {
                if target_ruid != target_suid {
                    if log_scmp {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "req": &request,
                            "target_suid": target_suid.as_raw(),
                            "target_euid": target_euid.map(|u| u.as_raw()),
                            "target_ruid": target_ruid.as_raw(),
                            "source_euid": source_euid.as_raw(),
                            "source_ruid": source_ruid.as_raw(),
                            "source_suid": source_suid.as_raw(),
                            "msg": format!("unsafe UID change with real-UID:{} != saved-UID:{} blocked",
                                target_ruid, target_suid),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    } else {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "pid": request.scmpreq.pid,
                            "target_suid": target_suid.as_raw(),
                            "target_euid": target_euid.map(|u| u.as_raw()),
                            "target_ruid": target_ruid.as_raw(),
                            "source_euid": source_euid.as_raw(),
                            "source_ruid": source_ruid.as_raw(),
                            "source_suid": source_suid.as_raw(),
                            "msg": format!("unsafe UID change with real-UID:{} != saved-UID:{} blocked",
                                target_ruid, target_suid),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    }
                    return Err(Errno::EACCES);
                }
            }
        }
        if let Some(target_euid) = target_euid {
            if let Some(target_suid) = target_suid {
                if target_euid != target_suid {
                    if log_scmp {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "req": &request,
                            "target_suid": target_suid.as_raw(),
                            "target_euid": target_euid.as_raw(),
                            "target_ruid": target_ruid.map(|u| u.as_raw()),
                            "source_euid": source_euid.as_raw(),
                            "source_ruid": source_ruid.as_raw(),
                            "source_suid": source_suid.as_raw(),
                            "msg": format!("unsafe UID change with effective-UID:{} != saved-UID:{} blocked",
                                target_euid, target_suid),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    } else {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "pid": request.scmpreq.pid,
                            "target_suid": target_suid.as_raw(),
                            "target_euid": target_euid.as_raw(),
                            "target_ruid": target_ruid.map(|u| u.as_raw()),
                            "source_euid": source_euid.as_raw(),
                            "source_ruid": source_ruid.as_raw(),
                            "source_suid": source_suid.as_raw(),
                            "msg": format!("unsafe UID change with effective-UID:{} != saved-UID:{} blocked",
                                target_euid, target_suid),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    }
                    return Err(Errno::EACCES);
                }
            }
        }

        let mut allowed = true;
        if let Some(target_ruid) = target_ruid {
            if !sandbox.chk_uid_transit(source_ruid, target_ruid) {
                allowed = false;
            }
        }
        if allowed {
            if let Some(target_euid) = target_euid {
                if !sandbox.chk_uid_transit(source_euid, target_euid) {
                    allowed = false;
                }
            }
        }
        if allowed {
            if let Some(target_suid) = target_suid {
                if !sandbox.chk_uid_transit(source_suid, target_suid) {
                    allowed = false;
                }
            }
        }
        drop(sandbox); // release the read lock.

        let target_ruid = target_ruid.map(|uid| i64::from(uid.as_raw())).unwrap_or(-1);
        let target_euid = target_euid.map(|uid| i64::from(uid.as_raw())).unwrap_or(-1);
        let target_suid = target_suid.map(|uid| i64::from(uid.as_raw())).unwrap_or(-1);
        if !allowed {
            if log_scmp {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "req": request,
                    "target_suid": target_suid,
                    "target_euid": target_euid,
                    "target_ruid": target_ruid,
                    "source_euid": source_euid.as_raw(),
                    "source_ruid": source_ruid.as_raw(),
                    "source_suid": source_suid.as_raw(),
                    "msg": "UID change without UID transit blocked",
                    "tip": format!("define UID transit `setuid+{}:{}'",
                        source_euid.as_raw(), target_euid));
            } else {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_suid": target_suid,
                    "target_euid": target_euid,
                    "target_ruid": target_ruid,
                    "source_euid": source_euid.as_raw(),
                    "source_ruid": source_ruid.as_raw(),
                    "source_suid": source_suid.as_raw(),
                    "msg": "UID change without UID transit blocked",
                    "tip": format!("define UID transit `setuid+{}:{}'",
                        source_euid.as_raw(), target_euid));
            }
            return Err(Errno::EACCES);
        }

        // SAFETY: nix version of setresuid does not allow -1 as argument.
        if let Err(errno) = Errno::result(unsafe {
            libc::syscall(libc::SYS_setresuid, target_ruid, target_euid, target_suid)
        }) {
            if log_scmp {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "req": request,
                    "target_suid": target_suid,
                    "target_euid": target_euid,
                    "target_ruid": target_ruid,
                    "source_euid": source_euid.as_raw(),
                    "source_ruid": source_ruid.as_raw(),
                    "source_suid": source_suid.as_raw(),
                    "msg": format!("UID change {}->{} failed: {errno}",
                        source_euid.as_raw(), target_euid),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            } else {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_suid": target_suid,
                    "target_euid": target_euid,
                    "target_ruid": target_ruid,
                    "source_euid": source_euid.as_raw(),
                    "source_ruid": source_ruid.as_raw(),
                    "source_suid": source_suid.as_raw(),
                    "msg": format!("UID change {}->{} failed: {errno}",
                        source_euid.as_raw(), target_euid),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            }
            return Err(errno);
        } else if safe_drop_cap(caps::Capability::CAP_SETUID).is_err() {
            // SAFETY: We cannot do much on errors,
            // and on panic the thread will be restarted.
            // The best we can do from a security POV is
            // to enter Ghost mode. This is certainly
            // unexpected but it's safe.
            return Err(Errno::EOWNERDEAD);
        }

        // SAFETY: There's no pointer dereference in the access check.
        unsafe { Ok(request.continue_syscall()) }
    })
}

#[expect(clippy::cognitive_complexity)]
pub(crate) fn sys_setresgid(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;

        #[expect(clippy::cast_possible_truncation)]
        let target_rgid = match req.data.args[0] as i32 {
            -1 => None,
            n if n >= 0 => Some(Gid::from_raw(
                libc::gid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };
        #[expect(clippy::cast_possible_truncation)]
        let target_egid = match req.data.args[1] as i32 {
            -1 => None,
            n if n >= 0 => Some(Gid::from_raw(
                libc::gid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };
        #[expect(clippy::cast_possible_truncation)]
        let target_sgid = match req.data.args[2] as i32 {
            -1 => None,
            n if n >= 0 => Some(Gid::from_raw(
                libc::gid_t::try_from(n).or(Err(Errno::EINVAL))?,
            )),
            _ => return Err(Errno::EINVAL),
        };

        if target_rgid.is_none() && target_egid.is_none() && target_sgid.is_none() {
            // No change of GID requested, return success.
            return Ok(request.return_syscall(0));
        }

        // getresgid can only fail with EFAULT which should not happen.
        let resgid = getresgid()?;
        let source_rgid = resgid.real;
        let source_egid = resgid.effective;
        let source_sgid = resgid.saved;

        let mut change = false;
        if let Some(target_rgid) = target_rgid {
            if target_rgid.as_raw() <= GID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_rgid != target_rgid {
                change = true;
            }
        }
        if let Some(target_egid) = target_egid {
            if target_egid.as_raw() <= GID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_egid != target_egid {
                change = true;
            }
        }
        if let Some(target_sgid) = target_sgid {
            if target_sgid.as_raw() <= GID_MIN.as_raw() {
                // SAFETY: This is already asserted with the parent
                // seccomp-bpf filter, this is the second layer.
                return Ok(request.fail_syscall(Errno::EACCES));
            } else if source_sgid != target_sgid {
                change = true;
            }
        }

        if !change {
            // SAFETY: No GID change or no ptr-deref in check.
            return unsafe { Ok(request.continue_syscall()) };
        }

        let sandbox = request.get_sandbox();
        let log_scmp = sandbox.log_scmp();

        // SAFETY: We do not support Rgid != Egid != Sgid
        if let Some(target_rgid) = target_rgid {
            if let Some(target_egid) = target_egid {
                if target_rgid != target_egid {
                    if log_scmp {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "req": &request,
                            "target_sgid": target_sgid.map(|u| u.as_raw()),
                            "target_egid": target_egid.as_raw(),
                            "target_rgid": target_rgid.as_raw(),
                            "source_egid": source_egid.as_raw(),
                            "source_rgid": source_rgid.as_raw(),
                            "source_sgid": source_sgid.as_raw(),
                            "msg": format!("unsafe GID change with real-GID:{} != effective-GID:{} blocked",
                                target_rgid.as_raw(), target_egid.as_raw()),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    } else {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "pid": request.scmpreq.pid,
                            "target_sgid": target_sgid.map(|u| u.as_raw()),
                            "target_egid": target_egid.as_raw(),
                            "target_rgid": target_rgid.as_raw(),
                            "source_egid": source_egid.as_raw(),
                            "source_rgid": source_rgid.as_raw(),
                            "source_sgid": source_sgid.as_raw(),
                            "msg": format!("unsafe GID change with real-GID:{} != effective-GID:{} blocked",
                                target_rgid.as_raw(), target_egid.as_raw()),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    }
                    return Err(Errno::EACCES);
                }
            }
        }
        if let Some(target_rgid) = target_rgid {
            if let Some(target_sgid) = target_sgid {
                if target_rgid != target_sgid {
                    if log_scmp {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "req": &request,
                            "target_sgid": target_sgid.as_raw(),
                            "target_egid": target_egid.map(|u| u.as_raw()),
                            "target_rgid": target_rgid.as_raw(),
                            "source_egid": source_egid.as_raw(),
                            "source_rgid": source_rgid.as_raw(),
                            "source_sgid": source_sgid.as_raw(),
                            "msg": format!("unsafe GID change with real-GID:{} != saved-GID:{} blocked",
                                target_rgid.as_raw(), target_sgid.as_raw()),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    } else {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "pid": request.scmpreq.pid,
                            "target_sgid": target_sgid.as_raw(),
                            "target_egid": target_egid.map(|u| u.as_raw()),
                            "target_rgid": target_rgid.as_raw(),
                            "source_egid": source_egid.as_raw(),
                            "source_rgid": source_rgid.as_raw(),
                            "source_sgid": source_sgid.as_raw(),
                            "msg": format!("unsafe GID change with real-GID:{} != saved-GID:{} blocked",
                                target_rgid.as_raw(), target_sgid.as_raw()),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    }
                    return Err(Errno::EACCES);
                }
            }
        }
        if let Some(target_egid) = target_egid {
            if let Some(target_sgid) = target_sgid {
                if target_egid != target_sgid {
                    if log_scmp {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "req": &request,
                            "target_sgid": target_sgid.as_raw(),
                            "target_egid": target_egid.as_raw(),
                            "target_rgid": target_rgid.map(|u| u.as_raw()),
                            "source_egid": source_egid.as_raw(),
                            "source_rgid": source_rgid.as_raw(),
                            "source_sgid": source_sgid.as_raw(),
                            "msg": format!("unsafe GID change with effective-GID:{} != saved-GID:{} blocked",
                                target_egid.as_raw(), target_sgid.as_raw()),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    } else {
                        warn!("ctx": "safesetid", "err": libc::EACCES,
                            "sys": request.syscall, "pid": request.scmpreq.pid,
                            "target_sgid": target_sgid.as_raw(),
                            "target_egid": target_egid.as_raw(),
                            "target_rgid": target_rgid.map(|u| u.as_raw()),
                            "source_egid": source_egid.as_raw(),
                            "source_rgid": source_rgid.as_raw(),
                            "source_sgid": source_sgid.as_raw(),
                            "msg": format!("unsafe GID change with effective-GID:{} != saved-GID:{} blocked",
                                target_egid.as_raw(), target_sgid.as_raw()),
                            "tip": "check with SYD_LOG=debug and/or submit a bug report");
                    }
                    return Err(Errno::EACCES);
                }
            }
        }

        let mut allowed = true;
        if let Some(target_rgid) = target_rgid {
            if !sandbox.chk_gid_transit(source_rgid, target_rgid) {
                allowed = false;
            }
        }
        if allowed {
            if let Some(target_egid) = target_egid {
                if !sandbox.chk_gid_transit(source_egid, target_egid) {
                    allowed = false;
                }
            }
        }
        if allowed {
            if let Some(target_sgid) = target_sgid {
                if !sandbox.chk_gid_transit(source_sgid, target_sgid) {
                    allowed = false;
                }
            }
        }
        drop(sandbox); // release the read lock.

        let target_rgid = target_rgid.map(|gid| i64::from(gid.as_raw())).unwrap_or(-1);
        let target_egid = target_egid.map(|gid| i64::from(gid.as_raw())).unwrap_or(-1);
        let target_sgid = target_sgid.map(|gid| i64::from(gid.as_raw())).unwrap_or(-1);
        if !allowed {
            if log_scmp {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "req": request,
                    "target_sgid": target_sgid,
                    "target_egid": target_egid,
                    "target_rgid": target_rgid,
                    "source_egid": source_egid.as_raw(),
                    "source_rgid": source_rgid.as_raw(),
                    "source_sgid": source_sgid.as_raw(),
                    "msg": "GID change without GID transit blocked",
                    "tip": format!("define GID transit `setgid+{}:{}'",
                        source_egid.as_raw(), target_egid));
            } else {
                warn!("ctx": "safesetid", "err": libc::EACCES,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_sgid": target_sgid,
                    "target_egid": target_egid,
                    "target_rgid": target_rgid,
                    "source_egid": source_egid.as_raw(),
                    "source_rgid": source_rgid.as_raw(),
                    "source_sgid": source_sgid.as_raw(),
                    "msg": "GID change without GID transit blocked",
                    "tip": format!("define GID transit `setgid+{}:{}'",
                        source_egid.as_raw(), target_egid));
            }
            return Err(Errno::EACCES);
        }

        // SAFETY: nix version of setregid does not allow -1 as argument.
        if let Err(errno) = Errno::result(unsafe {
            libc::syscall(libc::SYS_setresgid, target_rgid, target_egid, target_sgid)
        }) {
            if log_scmp {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "req": request,
                    "target_sgid": target_sgid,
                    "target_egid": target_egid,
                    "target_rgid": target_rgid,
                    "source_egid": source_egid.as_raw(),
                    "source_rgid": source_rgid.as_raw(),
                    "source_sgid": source_sgid.as_raw(),
                    "msg": format!("GID change {}->{} failed: {errno}",
                        source_egid.as_raw(), target_egid),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            } else {
                warn!("ctx": "safesetid", "err": errno as i32,
                    "sys": request.syscall, "pid": request.scmpreq.pid,
                    "target_sgid": target_sgid,
                    "target_egid": target_egid,
                    "target_rgid": target_rgid,
                    "source_egid": source_egid.as_raw(),
                    "source_rgid": source_rgid.as_raw(),
                    "source_sgid": source_sgid.as_raw(),
                    "msg": format!("GID change {}->{} failed: {errno}",
                        source_egid.as_raw(), target_egid),
                    "tip": "check with SYD_LOG=debug and/or submit a bug report");
            }
            return Err(errno);
        } else if safe_drop_cap(caps::Capability::CAP_SETGID).is_err() {
            // SAFETY: We cannot do much on errors,
            // and on panic the thread will be restarted.
            // The best we can do from a security POV is
            // to enter Ghost mode. This is certainly
            // unexpected but it's safe.
            return Err(Errno::EOWNERDEAD);
        }

        // SAFETY: There's no pointer dereference in the access check.
        unsafe { Ok(request.continue_syscall()) }
    })
}
