//
// Syd: rock-solid application kernel
// src/ptrace.rs: Utilities for ptrace(2)
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
// Based in part upon strace which is:
//   Copyright (c) 2016-2021 The strace developers.
//   SPDX-License-Identifier: LGPL-2.1-or-later
//
// SPDX-License-Identifier: GPL-3.0

use std::{ffi::CStr, mem};

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

use crate::{libseccomp_sys::seccomp_syscall_resolve_num_arch, path::XPath};

/// Skip the syscall for the specified process.
/// Set the syscall to fail with the given errno.
///
/// This function modifies the architecture-specific register that holds
/// the system call and the return value.
#[allow(unused_variables)]
pub fn ptrace_skip_syscall(pid: Pid, arch: u32, errno: Errno) -> Result<(), Errno> {
    // Quoting seccomp(2):
    // The tracer can skip the system call by changing the system call
    // number to -1. Alternatively, the tracer can change the system
    // call requested by changing the system call to a valid system call
    // number.  If the tracer asks to skip the system call, then the
    // system call will appear to return the value that the tracer puts
    // in the return value register.
    #[cfg(any(
        target_arch = "x86_64",
        target_arch = "x86",
        target_arch = "aarch64",
        target_arch = "arm",
        target_arch = "powerpc64",
        target_arch = "powerpc",
        target_arch = "s390x",
        target_arch = "mips",
        target_arch = "mips32r6",
        target_arch = "mips64",
        target_arch = "mips64r6",
        target_arch = "loongarch64",
    ))]
    {
        use crate::{scmp_arch, scmp_arch_bits};

        // Define -1 for the target architecture.
        let sys_invalid = if cfg!(any(
            target_arch = "mips",
            target_arch = "mips32r6",
            target_arch = "mips64",
            target_arch = "mips64r6",
            target_arch = "s390x",
        )) {
            return ptrace_set_return(pid, arch, Some(errno));
        } else if scmp_arch_bits(scmp_arch(arch)?) == 32 {
            u32::MAX.into()
        } else {
            u64::MAX
        };

        ptrace_set_syscall(pid, arch, sys_invalid)?;
        ptrace_set_return(pid, arch, Some(errno))
    }

    #[cfg(target_arch = "riscv64")]
    {
        use libc::{c_void, iovec, user_regs_struct, PTRACE_GETREGSET, PTRACE_SETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_RISCV64;

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_RISCV64 {
            return Err(Errno::EINVAL);
        }

        // Define the user_regs_struct for the tracee.
        // SAFETY: Zero-initialize the struct.
        let mut regs: user_regs_struct = unsafe { mem::zeroed() };

        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: mem::size_of::<user_regs_struct>(),
        };

        // SAFETY: Retrieve the current register state.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        // Modify the syscall number (a7 holds the syscall number on RISC-V)
        regs.a7 = u64::MAX;

        // RISC-V requires to set return value for system call number tampering.
        regs.a0 = (-(errno as i64)) as u64;

        // SAFETY: Set the modified register state.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &io)
        })
        .map(drop)
    }

    #[cfg(not(any(
        target_arch = "x86_64",
        target_arch = "x86",
        target_arch = "aarch64",
        target_arch = "arm",
        target_arch = "s390x",
        target_arch = "riscv64",
        target_arch = "powerpc",
        target_arch = "powerpc64",
        target_arch = "mips",
        target_arch = "mips32r6",
        target_arch = "mips64",
        target_arch = "mips64r6",
        target_arch = "loongarch64",
    )))]
    {
        compile_error!("BUG: ptrace_skip_syscall is not implemented for this architecture!");
    }
}

/// Set the syscall return value for the specified process.
/// Sets success if `errno` is `None`.
///
/// This function modifies the architecture-specific register that holds
/// the return value.
#[allow(unused_variables)]
pub fn ptrace_set_return(pid: Pid, arch: u32, errno: Option<Errno>) -> Result<(), Errno> {
    #[cfg(target_arch = "x86_64")]
    {
        use nix::sys::ptrace;

        // Define offsets for registers.
        //
        // x86_64 RAX offset.
        // This works the same for x32 and x86.
        const RAX_OFFSET: u64 = 10 * 8;

        // Determine the value to set.
        #[allow(clippy::arithmetic_side_effects)]
        let rval = if let Some(e) = errno {
            // Error case: Set the error code as a negative value.
            -(e as i64)
        } else {
            // Success case: Set the return value to 0.
            0
        };

        // x86_64 architecture (64-bit)
        ptrace::write_user(pid, RAX_OFFSET as ptrace::AddressType, rval as libc::c_long)
    }

    #[cfg(target_arch = "x86")]
    {
        use nix::{errno::Errno, sys::ptrace};

        use crate::libseccomp_sys::SCMP_ARCH_X86;

        // Define offset for EAX in the user area.
        const EAX_OFFSET: u64 = 6 * 4; // EAX offset (32-bit).

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_X86 {
            return Err(Errno::EINVAL);
        }

        // Determine the value to set.
        #[allow(clippy::arithmetic_side_effects)]
        let rval = if let Some(e) = errno {
            // Error case: Set the error code as a negative value.
            -(e as i32)
        } else {
            // Success case: Set the return value to 0.
            0
        };

        // Write the value into the EAX register
        ptrace::write_user(pid, EAX_OFFSET as ptrace::AddressType, rval.into())
    }

    #[cfg(target_arch = "aarch64")]
    {
        use libc::{c_void, iovec, PTRACE_GETREGSET, PTRACE_SETREGSET};

        use crate::libseccomp_sys::{SCMP_ARCH_AARCH64, SCMP_ARCH_ARM};

        // Define the user_regs_struct for aarch64 as described in the system headers.
        #[repr(C)]
        #[derive(Copy, Clone)]
        struct Aarch64UserRegsStruct {
            regs: [u64; 31], // General-purpose registers
            sp: u64,         // Stack pointer
            pc: u64,         // Program counter
            pstate: u64,     // Processor state
        }

        // Define the arm_pt_regs for arm as described in the system headers
        #[repr(C)]
        #[derive(Copy, Clone)]
        struct ArmPtRegs {
            uregs: [u32; 18], // ARM registers
        }

        // Allocate a union for multipersonality support.
        #[repr(C)]
        union ArmRegsUnion {
            aarch64: Aarch64UserRegsStruct,
            arm: ArmPtRegs,
        }

        let mut regs = ArmRegsUnion {
            // SAFETY: Zero initialize the ARM register union.
            aarch64: unsafe { mem::zeroed() },
        };

        // IOVEC for PTRACE_GETREGSET and PTRACE_SETREGSET.
        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: mem::size_of::<ArmRegsUnion>(),
        };

        // SAFETY: Retrieve the current register state
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        match arch {
            SCMP_ARCH_AARCH64 => {
                // SAFETY: aarch64 personality
                let regs_ref = unsafe { &mut regs.aarch64 };
                #[allow(clippy::arithmetic_side_effects)]
                let rval = if let Some(e) = errno {
                    -(e as i64) // Error case
                } else {
                    0 // Success case
                };

                #[allow(clippy::cast_sign_loss)]
                {
                    // Set return value in X0.
                    regs_ref.regs[0] = rval as u64;
                }
            }
            SCMP_ARCH_ARM => {
                // SAFETY: arm personality
                let regs_ref = unsafe { &mut regs.arm };
                #[allow(clippy::arithmetic_side_effects)]
                let rval = if let Some(e) = errno {
                    -(e as i32) // Error case
                } else {
                    0 // Success case
                };

                #[allow(clippy::cast_sign_loss)]
                {
                    // Set return value in R0.
                    regs_ref.uregs[0] = rval as u32;
                }
            }
            _ => return Err(Errno::EINVAL),
        }

        // SAFETY: Write the modified register state back.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })
        .map(drop)
    }

    #[cfg(target_arch = "arm")]
    {
        use libc::{c_void, iovec, PTRACE_GETREGSET, PTRACE_SETREGSET};
        use nix::errno::Errno;

        use crate::libseccomp_sys::SCMP_ARCH_ARM;

        // Define the ARM register structure.
        #[repr(C)]
        struct ArmPtRegs {
            uregs: [u32; 18],
        }

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_ARM {
            return Err(Errno::EINVAL);
        }

        // SAFETY: Zero initialize the register structuer.
        let mut regs: ArmPtRegs = unsafe { mem::zeroed() };

        // IOVEC for PTRACE_GETREGSET and PTRACE_SETREGSET.
        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: mem::size_of::<ArmPtRegs>(),
        };

        // SAFETY: Retrieve the current register state
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        // Modify the return value in R0.
        #[allow(clippy::arithmetic_side_effects)]
        let rval = if let Some(e) = errno {
            -(e as i32) // Error case.
        } else {
            0 // Success case.
        };

        #[allow(clippy::cast_sign_loss)]
        {
            regs.uregs[0] = rval as u32;
        }

        // SAFETY: Write the modified register state back.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })
        .map(drop)
    }

    #[cfg(any(
        target_arch = "mips",
        target_arch = "mips32r6",
        target_arch = "mips64",
        target_arch = "mips64r6"
    ))]
    {
        use libc::{c_void, PTRACE_GETREGS, PTRACE_SETREGS};
        use nix::errno::Errno;

        #[repr(C)]
        #[derive(Copy, Clone)]
        struct MipsPtRegs {
            uregs: [u64; 38],
        }

        let mut regs = mem::MaybeUninit::<MipsPtRegs>::uninit();

        // SAFETY: Retrieve the current register state.
        Errno::result(unsafe {
            libc::ptrace(
                PTRACE_GETREGS,
                pid.as_raw(),
                std::ptr::null_mut::<c_void>(),
                regs.as_mut_ptr(),
            )
        })?;

        // SAFETY: PTRACE_GETREGS returned success.
        let mut regs = unsafe { regs.assume_init() };

        // Modify the return value.
        const REG_V0: usize = 2;
        const REG_A0: usize = 4;
        const REG_A3: usize = REG_A0 + 3;

        #[allow(clippy::arithmetic_side_effects)]
        if let Some(e) = errno {
            // Error case
            regs.uregs[REG_V0] = e as u64;
            regs.uregs[REG_A3] = u64::MAX; // -1
        } else {
            // Success case
            regs.uregs[REG_V0] = 0;
            regs.uregs[REG_A3] = 0;
        }

        // SAFETY: Write the modified register state back.
        Errno::result(unsafe {
            libc::ptrace(
                PTRACE_SETREGS,
                pid.as_raw(),
                std::ptr::null_mut::<c_void>(),
                &regs as *const MipsPtRegs as *const c_void,
            )
        })
        .map(drop)
    }

    #[cfg(target_arch = "riscv64")]
    {
        use libc::{c_void, iovec, user_regs_struct, PTRACE_GETREGSET, PTRACE_SETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_RISCV64;

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_RISCV64 {
            return Err(Errno::EINVAL);
        }

        // SAFETY: Allocate the register structure.
        let mut regs: user_regs_struct = unsafe { mem::zeroed() };

        // IOVEC for PTRACE_GETREGSET and PTRACE_SETREGSET.
        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: std::mem::size_of::<user_regs_struct>(),
        };

        // SAFETY: Retrieve the current register state
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        #[allow(clippy::arithmetic_side_effects)]
        let rval = if let Some(e) = errno {
            -(e as i64) // Error case
        } else {
            0 // Success case
        };

        #[allow(clippy::cast_sign_loss)]
        {
            // Modify the return value in A0.
            regs.a0 = rval as u64;
        }

        // SAFETY: Write the modified register state back
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })
        .map(drop)
    }

    #[cfg(target_arch = "s390x")]
    {
        use libc::{c_void, iovec, PTRACE_GETREGSET, PTRACE_SETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_S390X;

        #[repr(C, align(8))]
        struct psw_t {
            mask: u64,
            addr: u64,
        }

        #[repr(C)]
        struct s390_regs {
            psw: psw_t,
            gprs: [u64; 16],
            acrs: [u32; 16],
            orig_gpr2: u64,
        }

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_S390X {
            return Err(Errno::EINVAL);
        }

        // SAFETY: Zero-initialize the struct.
        let mut regs: s390_regs = unsafe { mem::zeroed() };

        // Define the IOVEC structure for the register set.
        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: std::mem::size_of::<s390_regs>(),
        };

        // SAFETY: Retrieve the current register state.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        #[allow(clippy::arithmetic_side_effects)]
        let rval = if let Some(e) = errno {
            -(e as i64) // Error case
        } else {
            0 // Success case
        };

        #[allow(clippy::cast_sign_loss)]
        {
            // Modify the return value in GPR2
            regs.gprs[2] = rval as u64;
        }

        // SAFETY: Write the modified register state back.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &io)
        })
        .map(drop)
    }

    #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
    {
        use libc::{c_void, iovec, PTRACE_GETREGSET, PTRACE_SETREGSET};

        use crate::libseccomp_sys::{SCMP_ARCH_PPC, SCMP_ARCH_PPC64, SCMP_ARCH_PPC64LE};

        // Define the pt_regs structure for PowerPC.
        #[repr(C)]
        struct pt_regs {
            gpr: [u64; 32],   // General-purpose registers
            nip: u64,         // Next instruction pointer
            msr: u64,         // Machine state register
            orig_gpr3: u64,   // Original GPR3 for syscall restart
            ctr: u64,         // Count register
            link: u64,        // Link register
            xer: u64,         // Fixed-point exception register
            ccr: u64,         // Condition register
            softe_or_mq: u64, // Soft enabled/disabled (64-bit) or MQ (32-bit)
            trap: u64,        // Trap reason
            dar: u64,         // Data address register
            dsisr: u64,       // Data storage interrupt status register
            result: u64,      // System call result
        }

        // SAFETY: Allocate the registers structure.
        let mut regs: pt_regs = unsafe { std::mem::zeroed() };

        // Define the IOVEC structure for the register set.
        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: std::mem::size_of::<pt_regs>(),
        };

        // SAFETY: Retrieve the current register state.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        // Modify the return value in GPR3.
        match arch {
            SCMP_ARCH_PPC | SCMP_ARCH_PPC64 | SCMP_ARCH_PPC64LE => {
                #[allow(clippy::arithmetic_side_effects)]
                #[allow(clippy::cast_sign_loss)]
                let rval = if let Some(e) = errno {
                    if (regs.trap & 0xfff0) == 0x3000 {
                        // SCV case: Error value is negated.
                        -(e as i64)
                    } else {
                        // Non-SCV case: Positive error value.
                        regs.ccr |= 0x10000000; // Set condition register.
                        e as i64
                    }
                } else {
                    // Success case
                    if (regs.trap & 0xfff0) == 0x3000 {
                        regs.gpr[3] = 0; // Clear GPR3 for SCV.
                    } else {
                        regs.ccr &= !0x10000000; // Clear condition register.
                    }
                    0
                };

                #[allow(clippy::cast_sign_loss)]
                {
                    // Set the return value in GPR3.
                    regs.gpr[3] = rval as u64;
                }
            }
            _ => return Err(Errno::EINVAL),
        }

        // SAFETY: Write the modified register state back
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &io)
        })
        .map(drop)
    }

    #[cfg(target_arch = "loongarch64")]
    {
        use libc::{c_void, iovec, user_regs_struct, PTRACE_GETREGSET, PTRACE_SETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_LOONGARCH64;

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_LOONGARCH64 {
            return Err(Errno::EINVAL);
        }

        // SAFETY: Allocate the register structure.
        let mut regs: user_regs_struct = unsafe { mem::zeroed() };

        // IOVEC for PTRACE_GETREGSET and PTRACE_SETREGSET.
        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: std::mem::size_of::<user_regs_struct>(),
        };

        // SAFETY: Retrieve the current register state
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        #[allow(clippy::arithmetic_side_effects)]
        let rval = if let Some(e) = errno {
            -(e as i64) // Error case
        } else {
            0 // Success case
        };

        #[allow(clippy::cast_sign_loss)]
        {
            // Modify the return value in regs[4].
            regs.regs[4] = rval as u64;
        }

        // SAFETY: Write the modified register state back.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })
        .map(drop)
    }

    #[cfg(not(any(
        target_arch = "x86_64",
        target_arch = "x86",
        target_arch = "aarch64",
        target_arch = "arm",
        target_arch = "s390x",
        target_arch = "riscv64",
        target_arch = "powerpc",
        target_arch = "powerpc64",
        target_arch = "mips",
        target_arch = "mips32r6",
        target_arch = "mips64",
        target_arch = "mips64r6",
        target_arch = "loongarch64",
    )))]
    {
        compile_error!("BUG: ptrace_set_return is not implemented for this architecture!");
    }
}

/// Retrieve the system call return code from the tracee and determine
/// if it indicates an error or success.
#[allow(unused_variables)]
pub fn ptrace_get_error(pid: Pid, arch: u32) -> Result<Option<Errno>, Errno> {
    #[cfg(target_arch = "x86_64")]
    {
        use nix::sys::ptrace;

        use crate::libseccomp_sys::{SCMP_ARCH_X32, SCMP_ARCH_X86, SCMP_ARCH_X86_64};

        // RAX offset in the user area on x86_64-based kernels.
        const RAX_OFFSET: u64 = 10 * 8;

        // Read the raw value in RAX. This is a 64-bit read.
        let raw_rax = ptrace::read_user(pid, RAX_OFFSET as ptrace::AddressType)?;
        #[allow(clippy::cast_sign_loss)]
        let raw_rax = raw_rax as u64;

        #[allow(clippy::cast_possible_truncation)]
        #[allow(clippy::cast_possible_wrap)]
        let val = match arch {
            // 64-bit
            SCMP_ARCH_X86_64 => {
                // Keep it as 64 bits, interpret as signed.
                raw_rax as i64
            }
            // x32 or pure i386 personality within x86_64
            SCMP_ARCH_X86 | SCMP_ARCH_X32 => {
                // Sign-extend the lower 32 bits.
                let lo = raw_rax as u32;
                i64::from(lo as i32)
            }
            _ => return Err(Errno::EINVAL),
        };

        // Check if it's a negated errno:
        if let Some(e) = check_negated_errno(val) {
            Ok(Some(e))
        } else {
            Ok(None)
        }
    }

    #[cfg(target_arch = "x86")]
    {
        use nix::sys::ptrace;

        use crate::libseccomp_sys::SCMP_ARCH_X86;

        // EAX offset in the user area on 32-bit x86.
        const EAX_OFFSET: u64 = 6 * 4;

        // Ensure the architecture matches.
        if arch != SCMP_ARCH_X86 {
            return Err(Errno::EINVAL);
        }

        // Read the raw EAX.
        let raw_eax = ptrace::read_user(pid, EAX_OFFSET as ptrace::AddressType)? as i32;
        let val = raw_eax as i64;

        if let Some(e) = check_negated_errno(val) {
            Ok(Some(e))
        } else {
            Ok(None)
        }
    }

    #[cfg(target_arch = "aarch64")]
    {
        use libc::{c_void, iovec, PTRACE_GETREGSET};

        use crate::libseccomp_sys::{SCMP_ARCH_AARCH64, SCMP_ARCH_ARM};

        // AArch64 user regs struct
        #[repr(C)]
        #[derive(Copy, Clone)]
        struct Aarch64UserRegsStruct {
            regs: [u64; 31],
            sp: u64,
            pc: u64,
            pstate: u64,
        }

        // ARM user regs struct
        #[repr(C)]
        #[derive(Copy, Clone)]
        struct ArmPtRegs {
            uregs: [u32; 18],
        }

        // Union for retrieving either aarch64 or arm regs
        #[repr(C)]
        union ArmRegsUnion {
            aarch64: Aarch64UserRegsStruct,
            arm: ArmPtRegs,
        }

        let mut regs = ArmRegsUnion {
            aarch64: unsafe { mem::zeroed() },
        };

        let mut io = iovec {
            iov_base: (&mut regs) as *mut _ as *mut c_void,
            iov_len: mem::size_of::<ArmRegsUnion>(),
        };

        // Get registers
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        let val: i64 = match arch {
            // SCMP_ARCH_AARCH64 => 64-bit read from X0
            SCMP_ARCH_AARCH64 => {
                let a64 = unsafe { regs.aarch64 };
                a64.regs[0] as i64
            }
            // SCMP_ARCH_ARM => 32-bit read from R0
            SCMP_ARCH_ARM => {
                let arm = unsafe { regs.arm };
                // Sign-extend
                (arm.uregs[0] as i32) as i64
            }
            _ => return Err(Errno::EINVAL),
        };

        if let Some(e) = check_negated_errno(val) {
            Ok(Some(e))
        } else {
            Ok(None)
        }
    }

    #[cfg(target_arch = "arm")]
    {
        use libc::{c_void, iovec, PTRACE_GETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_ARM;

        #[repr(C)]
        struct ArmPtRegs {
            uregs: [u32; 18],
        }

        if arch != SCMP_ARCH_ARM {
            return Err(Errno::EINVAL);
        }

        let mut regs: ArmPtRegs = unsafe { mem::zeroed() };

        let mut io = iovec {
            iov_base: (&mut regs) as *mut _ as *mut c_void,
            iov_len: mem::size_of::<ArmPtRegs>(),
        };

        // Get registers
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        let val = (regs.uregs[0] as i32) as i64;
        if let Some(e) = check_negated_errno(val) {
            Ok(Some(e))
        } else {
            Ok(None)
        }
    }

    #[cfg(any(
        target_arch = "mips",
        target_arch = "mips32r6",
        target_arch = "mips64",
        target_arch = "mips64r6"
    ))]
    {
        use libc::{c_void, PTRACE_GETREGS};
        use nix::errno::Errno;

        #[repr(C)]
        #[derive(Copy, Clone)]
        struct MipsPtRegs {
            uregs: [u64; 38],
        }

        let mut regs = mem::MaybeUninit::<MipsPtRegs>::uninit();

        // SAFETY: Retrieve the current register state.
        Errno::result(unsafe {
            libc::ptrace(
                PTRACE_GETREGS,
                pid.as_raw(),
                std::ptr::null_mut::<c_void>(),
                regs.as_mut_ptr(),
            )
        })?;

        // SAFETY: PTRACE_GETREGS returned success.
        let regs = unsafe { regs.assume_init() };

        const REG_V0: usize = 2;
        const REG_A0: usize = 4;
        const REG_A3: usize = REG_A0 + 3;

        if regs.uregs[REG_A3] != 0 {
            Ok(Some(Errno::from_raw(regs.uregs[REG_V0] as i32)))
        } else {
            Ok(None)
        }
    }

    #[cfg(target_arch = "riscv64")]
    {
        use libc::{c_void, iovec, user_regs_struct, PTRACE_GETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_RISCV64;

        if arch != SCMP_ARCH_RISCV64 {
            return Err(Errno::EINVAL);
        }

        let mut regs: user_regs_struct = unsafe { mem::zeroed() };
        let mut io = iovec {
            iov_base: (&mut regs) as *mut _ as *mut c_void,
            iov_len: mem::size_of::<user_regs_struct>(),
        };

        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        let val = regs.a0 as i64;
        if let Some(e) = check_negated_errno(val) {
            Ok(Some(e))
        } else {
            Ok(None)
        }
    }

    #[cfg(target_arch = "s390x")]
    {
        use libc::{c_void, iovec, PTRACE_GETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_S390X;

        #[repr(C, align(8))]
        struct psw_t {
            mask: u64,
            addr: u64,
        }

        #[repr(C)]
        struct s390_regs {
            psw: psw_t,
            gprs: [u64; 16],
            acrs: [u32; 16],
            orig_gpr2: u64,
        }

        if arch != SCMP_ARCH_S390X {
            return Err(Errno::EINVAL);
        }

        let mut regs: s390_regs = unsafe { mem::zeroed() };
        let mut io = iovec {
            iov_base: (&mut regs) as *mut _ as *mut c_void,
            iov_len: mem::size_of::<s390_regs>(),
        };

        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        // Syscall return value is in gprs[2]
        let val = regs.gprs[2] as i64;
        if let Some(e) = check_negated_errno(val) {
            Ok(Some(e))
        } else {
            Ok(None)
        }
    }

    #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
    {
        use libc::{c_void, iovec, PTRACE_GETREGSET};

        use crate::libseccomp_sys::{SCMP_ARCH_PPC, SCMP_ARCH_PPC64, SCMP_ARCH_PPC64LE};

        #[repr(C)]
        struct pt_regs {
            gpr: [u64; 32],
            nip: u64,
            msr: u64,
            orig_gpr3: u64,
            ctr: u64,
            link: u64,
            xer: u64,
            ccr: u64,
            softe_or_mq: u64,
            trap: u64,
            dar: u64,
            dsisr: u64,
            result: u64,
        }

        let mut regs: pt_regs = unsafe { mem::zeroed() };
        let mut io = iovec {
            iov_base: (&mut regs) as *mut _ as *mut c_void,
            iov_len: mem::size_of::<pt_regs>(),
        };

        // Get registers
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        // Confirm architecture
        match arch {
            SCMP_ARCH_PPC | SCMP_ARCH_PPC64 | SCMP_ARCH_PPC64LE => {
                // On PPC, the return value is always in gpr[3].
                let r3 = regs.gpr[3] as i64;

                // SCV syscalls have a signature: if (regs.trap & 0xfff0) == 0x3000 => SCV
                let scv = (regs.trap & 0xfff0) == 0x3000;

                if scv {
                    // If SCV, negative => error
                    if let Some(e) = check_negated_errno(r3) {
                        Ok(Some(e))
                    } else {
                        Ok(None)
                    }
                } else {
                    // If not SCV, check CCR bit 0x10000000 for error
                    // If set => error is positive in gpr[3]
                    // If not set => success
                    if (regs.ccr & 0x10000000) != 0 {
                        // gpr[3] is the error code, not negated.
                        let err = r3 as i32;
                        Ok(Some(Errno::from_raw(err)))
                    } else {
                        // success
                        Ok(None)
                    }
                }
            }
            _ => Err(Errno::EINVAL),
        }
    }

    #[cfg(target_arch = "loongarch64")]
    {
        use libc::{c_void, iovec, user_regs_struct, PTRACE_GETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_LOONGARCH64;

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_LOONGARCH64 {
            return Err(Errno::EINVAL);
        }

        let mut regs: user_regs_struct = unsafe { mem::zeroed() };
        let mut io = iovec {
            iov_base: (&mut regs) as *mut _ as *mut c_void,
            iov_len: mem::size_of::<user_regs_struct>(),
        };

        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        let val = regs.regs[4] as i64;
        if let Some(e) = check_negated_errno(val) {
            Ok(Some(e))
        } else {
            Ok(None)
        }
    }

    #[cfg(not(any(
        target_arch = "x86_64",
        target_arch = "x86",
        target_arch = "aarch64",
        target_arch = "arm",
        target_arch = "s390x",
        target_arch = "riscv64",
        target_arch = "powerpc",
        target_arch = "powerpc64",
        target_arch = "mips",
        target_arch = "mips32r6",
        target_arch = "mips64",
        target_arch = "mips64r6",
        target_arch = "loongarch64",
    )))]
    {
        compile_error!("BUG: ptrace_get_error is not implemented for this architecture!");
    }
}

/// Set the syscall number for the specified process.
///
/// This function modifies the architecture-specific register that holds
/// the syscall number.
#[allow(clippy::cast_possible_wrap)]
#[allow(unused_variables)]
pub fn ptrace_set_syscall(pid: Pid, arch: u32, sysno: u64) -> Result<(), Errno> {
    #[cfg(target_arch = "x86_64")]
    {
        use nix::sys::ptrace;

        // ORIG_RAX is at offset 15 * 8 bytes in the user area for x86_64.
        //
        // This works the same for x32 and x86.
        const ORIG_RAX_OFFSET: u64 = 15 * 8;

        // x86_64 architecture (64-bit)
        ptrace::write_user(
            pid,
            ORIG_RAX_OFFSET as ptrace::AddressType,
            sysno as libc::c_long,
        )
    }

    #[cfg(target_arch = "x86")]
    {
        use nix::sys::ptrace;

        // ORIG_EAX is at offset 11 * 4 bytes in the user area for x86.
        const ORIG_EAX_OFFSET: u64 = 11 * 4;

        // Write the syscall number into the ORIG_EAX register of the target process.
        ptrace::write_user(
            pid,
            ORIG_EAX_OFFSET as ptrace::AddressType,
            sysno as libc::c_long,
        )
    }

    #[cfg(target_arch = "aarch64")]
    {
        use libc::{c_void, iovec, PTRACE_SETREGSET};

        // Create an iovec structure to pass the syscall number.
        let io = iovec {
            iov_base: std::ptr::addr_of!(sysno) as *mut c_void,
            iov_len: mem::size_of::<u64>(),
        };

        // NT_ARM_SYSTEM_CALL is 0x404.
        // SAFETY: Use libc::ptrace to set the register set.
        Errno::result(unsafe { libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), 0x404, &io) }).map(drop)
    }

    #[cfg(target_arch = "arm")]
    {
        // PTRACE_SET_SYSCALL constant on ARM is 23.
        // SAFETY: Use libc::ptrace to set the syscall.
        Errno::result(unsafe { libc::ptrace(23, pid.as_raw(), 0, sysno as libc::c_uint) }).map(drop)
    }

    #[cfg(any(
        target_arch = "mips",
        target_arch = "mips32r6",
        target_arch = "mips64",
        target_arch = "mips64r6"
    ))]
    {
        use libc::{c_void, PTRACE_GETREGS, PTRACE_SETREGS};
        use nix::errno::Errno;

        #[repr(C)]
        #[derive(Copy, Clone)]
        struct MipsPtRegs {
            uregs: [u64; 38],
        }

        let mut regs = mem::MaybeUninit::<MipsPtRegs>::uninit();

        // SAFETY: Retrieve the current register state.
        Errno::result(unsafe {
            libc::ptrace(
                PTRACE_GETREGS,
                pid.as_raw(),
                std::ptr::null_mut::<c_void>(),
                regs.as_mut_ptr(),
            )
        })?;

        // SAFETY: PTRACE_GETREGS returned success.
        let mut regs = unsafe { regs.assume_init() };

        // Modify the syscall number.
        const REG_V0: usize = 2;
        regs.uregs[REG_V0] = sysno;

        // SAFETY: Write the modified register state back.
        Errno::result(unsafe {
            libc::ptrace(
                PTRACE_SETREGS,
                pid.as_raw(),
                std::ptr::null_mut::<c_void>(),
                &regs as *const MipsPtRegs as *const c_void,
            )
        })
        .map(drop)
    }

    #[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
    {
        use nix::sys::ptrace;

        // PT_R0 is at offset 0 in the user area.
        const PT_R0_OFFSET: u64 = 0;

        // Write the syscall number into the R0 register of the target process
        ptrace::write_user(
            pid,
            PT_R0_OFFSET as ptrace::AddressType,
            sysno as libc::c_long,
        )
    }

    #[cfg(target_arch = "riscv64")]
    {
        use libc::{c_void, iovec, user_regs_struct, PTRACE_GETREGSET, PTRACE_SETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_RISCV64;

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_RISCV64 {
            return Err(Errno::EINVAL);
        }

        // Define the user_regs_struct for the tracee.
        // SAFETY: Zero-initialize the struct.
        let mut regs: user_regs_struct = unsafe { mem::zeroed() };

        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: mem::size_of::<user_regs_struct>(),
        };

        // SAFETY: Retrieve the current register state.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        // Modify the syscall number (a7 holds the syscall number on RISC-V)
        regs.a7 = sysno;

        // RISC-V requires to set return value for system call number tampering.
        regs.a0 = (-(Errno::ENOSYS as i64)) as u64;

        // SAFETY: Set the modified register state.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &io)
        })
        .map(drop)
    }

    #[cfg(target_arch = "s390x")]
    {
        use libc::{c_void, iovec, PTRACE_GETREGSET, PTRACE_SETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_S390X;

        #[repr(C, align(8))]
        struct psw_t {
            mask: u64,
            addr: u64,
        }

        #[repr(C)]
        struct s390_regs {
            psw: psw_t,
            gprs: [u64; 16],
            acrs: [u32; 16],
            orig_gpr2: u64,
        }

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_S390X {
            return Err(Errno::EINVAL);
        }

        // SAFETY: Zero-initialize the struct.
        let mut regs: s390_regs = unsafe { mem::zeroed() };

        // Define the IOVEC structure for the register set.
        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: mem::size_of::<s390_regs>(),
        };

        // SAFETY: Retrieve the current registers.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        // Set the syscall number in GPR2.
        regs.gprs[2] = sysno;

        // SAFETY: Update the registers with the new syscall number.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &io)
        })
        .map(drop)
    }

    #[cfg(target_arch = "loongarch64")]
    {
        use libc::{c_void, iovec, user_regs_struct, PTRACE_GETREGSET, PTRACE_SETREGSET};

        use crate::libseccomp_sys::SCMP_ARCH_LOONGARCH64;

        // Ensure we're working with the correct architecture.
        if arch != SCMP_ARCH_LOONGARCH64 {
            return Err(Errno::EINVAL);
        }

        // Define the user_regs_struct for the tracee.
        // SAFETY: Zero-initialize the struct.
        let mut regs: user_regs_struct = unsafe { mem::zeroed() };

        let mut io = iovec {
            iov_base: std::ptr::addr_of_mut!(regs) as *mut c_void,
            iov_len: mem::size_of::<user_regs_struct>(),
        };

        // SAFETY: Retrieve the current register state.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_GETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &mut io)
        })?;

        // Modify the syscall number (regs[11] holds the syscall number on LOONGARCH64)
        regs.regs[11] = sysno;

        // SAFETY: Set the modified register state.
        Errno::result(unsafe {
            libc::ptrace(PTRACE_SETREGSET, pid.as_raw(), libc::NT_PRSTATUS, &io)
        })
        .map(drop)
    }

    #[cfg(not(any(
        target_arch = "x86_64",
        target_arch = "x86",
        target_arch = "aarch64",
        target_arch = "arm",
        target_arch = "s390x",
        target_arch = "riscv64",
        target_arch = "powerpc",
        target_arch = "powerpc64",
        target_arch = "mips",
        target_arch = "mips32r6",
        target_arch = "mips64",
        target_arch = "mips64r6",
        target_arch = "loongarch64",
    )))]
    {
        compile_error!("BUG: ptrace_set_syscall is not implemented for this architecture!");
    }
}

/// Retrieve information about the system call that caused a process to stop.
///
/// This function wraps the `PTRACE_GET_SYSCALL_INFO` ptrace request and returns
/// a `ptrace_syscall_info` structure containing the syscall information.
pub fn ptrace_get_syscall_info(pid: Pid) -> Result<ptrace_syscall_info, Errno> {
    let mut info = mem::MaybeUninit::<ptrace_syscall_info>::uninit();
    let info_size = mem::size_of::<ptrace_syscall_info>();

    // SAFETY: The ptrace call is inherently unsafe and must be
    // handled with care. We ensure `info` is properly initialized
    // before use and the size is correct.
    Errno::result(unsafe {
        libc::ptrace(
            0x420e, // PTRACE_GET_SYSCALL_INFO
            pid.as_raw(),
            info_size,
            info.as_mut_ptr() as *mut libc::c_void,
        )
    })?;

    // SAFETY: `info` is initialized by the ptrace call on success.
    Ok(unsafe { info.assume_init() })
}

// A small helper closure to check if a 64-bit value looks like -ERRNO.
// Specifically, if -4095 <= val < 0, we interpret it as an errno.
#[inline]
fn check_negated_errno(val: i64) -> Option<Errno> {
    // The largest possible negated errno we expect is -4095
    // (somewhat standard across Linux).
    // If val is in the range -4095..=-1, it's an error code.
    const MIN_ERRNO: i64 = -4095;
    #[allow(clippy::arithmetic_side_effects)]
    #[allow(clippy::cast_possible_truncation)]
    if (MIN_ERRNO..0).contains(&val) {
        // We flip the sign to get the positive errno.
        Some(Errno::from_raw((-val) as i32))
    } else {
        None
    }
}

/// Represents no entry.
///
/// You may get this e.g. when you don't set
/// PTRACE_O_TRACESYSGOOD in ptrace options.
pub const PTRACE_SYSCALL_INFO_NONE: u8 = 0;

/// Represents ptrace syscall entry stop.
pub const PTRACE_SYSCALL_INFO_ENTRY: u8 = 1;

/// Represents ptrace syscall exit stop.
pub const PTRACE_SYSCALL_INFO_EXIT: u8 = 2;

/// Represents ptrace seccomp stop.
pub const PTRACE_SYSCALL_INFO_SECCOMP: u8 = 3;

/// Representation of the `struct ptrace_syscall_info` for syscall information.
#[repr(C)]
#[derive(Copy, Clone)]
pub struct ptrace_syscall_info {
    /// Type of system call stop
    pub op: u8,
    /// AUDIT_ARCH_* value; see seccomp(2)
    pub arch: u32,
    /// CPU instruction pointer
    pub instruction_pointer: u64,
    /// CPU stack pointer
    pub stack_pointer: u64,
    /// Holds ptrace syscall information data
    ///
    /// SAFETY: check `op` before accessing the union!
    pub data: ptrace_syscall_info_data,
}

/// This union holds ptrace syscall information data.
#[repr(C)]
#[derive(Copy, Clone)]
pub union ptrace_syscall_info_data {
    /// op == PTRACE_SYSCALL_INFO_ENTRY
    pub entry: ptrace_syscall_info_entry,
    /// op == PTRACE_SYSCALL_INFO_EXIT
    pub exit: ptrace_syscall_info_exit,
    /// op == PTRACE_SYSCALL_INFO_SECCOMP
    pub seccomp: ptrace_syscall_info_seccomp,
}

/// op == PTRACE_SYSCALL_INFO_ENTRY
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct ptrace_syscall_info_entry {
    /// System call number
    pub nr: u64,
    /// System call arguments
    pub args: [u64; 6],
}

/// op == PTRACE_SYSCALL_INFO_EXIT
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct ptrace_syscall_info_exit {
    /// System call return value
    pub rval: i64,
    /// System call error flag;
    /// Boolean: does rval contain an error value (-ERRCODE),
    /// or a nonerror return value?
    pub is_error: u8,
}

/// op == PTRACE_SYSCALL_INFO_SECCOMP
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct ptrace_syscall_info_seccomp {
    /// System call number
    pub nr: u64,
    /// System call arguments
    pub args: [u64; 6],
    /// SECCOMP_RET_DATA portion of SECCOMP_RET_TRACE return value
    pub ret_data: u32,
}

impl std::fmt::Debug for ptrace_syscall_info {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ptrace_syscall_info")
            .field("op", &self.op)
            .field("arch", &self.arch)
            .field("ip", &self.instruction_pointer)
            .field("sp", &self.stack_pointer)
            // SAFETY: `op` is checked before union access.
            .field("data", unsafe {
                match self.op {
                    // Interpret the union based on the op field
                    PTRACE_SYSCALL_INFO_ENTRY => &self.data.entry,
                    PTRACE_SYSCALL_INFO_EXIT => &self.data.exit,
                    PTRACE_SYSCALL_INFO_SECCOMP => &self.data.seccomp,
                    _ => &"Unknown op",
                }
            })
            .finish()
    }
}

impl ptrace_syscall_info {
    /// Returns true if this `op` has no information on event.
    ///
    ///
    /// You may get this e.g. when you don't set
    /// PTRACE_O_TRACESYSGOOD in ptrace options.
    pub fn is_none(&self) -> bool {
        self.op == PTRACE_SYSCALL_INFO_NONE
    }

    /// Returns entry info if this is a system call entry.
    pub fn entry(&self) -> Option<ptrace_syscall_info_entry> {
        if self.op != PTRACE_SYSCALL_INFO_ENTRY {
            return None;
        }

        // SAFETY: The `op` check above asserts
        // the `entry` member of the union
        // is valid.
        Some(unsafe { self.data.entry })
    }

    /// Returns exit info if this is a system call exit.
    pub fn exit(&self) -> Option<ptrace_syscall_info_exit> {
        if self.op != PTRACE_SYSCALL_INFO_EXIT {
            return None;
        }

        // SAFETY: The `op` check above asserts
        // the `exit` member of the union
        // is valid.
        Some(unsafe { self.data.exit })
    }

    /// Returns seccomp info if this is a system call seccomp event.
    pub fn seccomp(&self) -> Option<ptrace_syscall_info_seccomp> {
        if self.op != PTRACE_SYSCALL_INFO_SECCOMP {
            return None;
        }

        // SAFETY: The `op` check above asserts
        // the `seccomp` member of the union
        // is valid.
        Some(unsafe { self.data.seccomp })
    }

    /// Returns the system call name if available.
    pub fn syscall(&self) -> Option<&'static XPath> {
        let nr = if let Some(info) = self.entry() {
            info.nr
        } else if let Some(info) = self.seccomp() {
            info.nr
        } else {
            return None;
        };

        // SAFETY: In libseccomp we trust.
        #[allow(clippy::cast_possible_truncation)]
        let ptr = unsafe { seccomp_syscall_resolve_num_arch(self.arch, nr as i32) };

        // Check for NULL.
        if ptr.is_null() {
            return None;
        }

        // SAFETY: libseccomp returned success, pointer is valid.
        Some(XPath::from_bytes(unsafe { CStr::from_ptr(ptr) }.to_bytes()))
    }
}

#[cfg(test)]
mod tests {
    use std::{ffi::CString, process::exit};

    use nix::{
        sys::{
            ptrace,
            ptrace::Options,
            signal::{kill, raise, Signal},
            wait::{waitpid, WaitStatus},
        },
        unistd::{chdir, fork, getcwd, ForkResult, Uid},
    };

    use super::*;
    use crate::{
        fs::randint,
        path::{XPath, XPathBuf},
    };

    #[test]
    fn test_ptrace_set_syscall_chdir_noop() {
        ptrace_test_set_syscall_chdir(None);
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_eperm() {
        ptrace_test_set_syscall_chdir(Some(Errno::EPERM));
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_enoent() {
        ptrace_test_set_syscall_chdir(Some(Errno::ENOENT));
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_esrch() {
        ptrace_test_set_syscall_chdir(Some(Errno::ESRCH));
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_eintr() {
        ptrace_test_set_syscall_chdir(Some(Errno::EINTR));
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_eio() {
        ptrace_test_set_syscall_chdir(Some(Errno::EIO));
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_enxio() {
        ptrace_test_set_syscall_chdir(Some(Errno::ENXIO));
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_e2big() {
        ptrace_test_set_syscall_chdir(Some(Errno::E2BIG));
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_enoexec() {
        ptrace_test_set_syscall_chdir(Some(Errno::ENOEXEC));
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_ebadf() {
        ptrace_test_set_syscall_chdir(Some(Errno::EBADF));
    }

    #[test]
    fn test_ptrace_set_syscall_chdir_echild() {
        ptrace_test_set_syscall_chdir(Some(Errno::ECHILD));
    }

    fn ptrace_test_set_syscall_chdir(error: Option<Errno>) {
        chdir("/tmp").expect("Failed to cd to /tmp");

        match unsafe { fork() } {
            Ok(ForkResult::Child) => {
                // Child process: Request to be traced
                ptrace::traceme().expect("Failed to set traceme");

                // Stop the child to allow the parent to attach.
                raise(Signal::SIGSTOP).expect("Failed to stop child");

                // Attempt to change directory to `/`.
                let ret = match chdir("/") {
                    Ok(_) => 0,
                    Err(errno) => errno as i32,
                };

                if ret == 0 {
                    // Assert that the directory has not changed.
                    let p = getcwd().map(XPathBuf::from).expect("Failed to get cwd");
                    assert!(
                        *p == *XPath::from_bytes(b"/tmp"),
                        "Dir changed unexpectedly to {p}!"
                    );
                }

                // Exit with errno.
                exit(ret);
            }
            Ok(ForkResult::Parent { child }) => {
                // Parent process: Wait for the child to stop.
                if let WaitStatus::Stopped(pid, Signal::SIGSTOP) =
                    waitpid(child, None).expect("Failed to wait for child")
                {
                    assert_eq!(pid, child, "Unexpected PID from wait");
                } else {
                    panic!("Child did not stop as expected");
                }

                // Set ptrace options.
                ptrace::setoptions(child, Options::PTRACE_O_TRACESYSGOOD)
                    .expect("Failed to set ptrace options");

                // Step to the syscall entry.
                ptrace::syscall(child, None).expect("Failed to continue to syscall entry");

                // Loop until we get to the expected system call.
                let mut expecting_entry = true;
                let arch = loop {
                    let pid = match waitpid(child, None).expect("Failed to wait") {
                        WaitStatus::PtraceSyscall(pid) => pid,
                        status => panic!("Unexpected wait status: {status:?}"),
                    };
                    assert_eq!(pid, child, "Unexpected PID from wait");

                    // Retrieve syscall info.
                    let info = ptrace_get_syscall_info(child).expect("Failed to get syscall info");

                    if expecting_entry {
                        assert_eq!(
                            info.op, PTRACE_SYSCALL_INFO_ENTRY,
                            "Expected syscall entry stop"
                        );

                        // Check the syscall number.
                        let data = unsafe { info.data.entry };
                        if data.nr == libc::SYS_chdir as u64 {
                            break info.arch;
                        }

                        expecting_entry = false;
                    } else {
                        assert_eq!(
                            info.op, PTRACE_SYSCALL_INFO_EXIT,
                            "Expected syscall entry exit"
                        );

                        expecting_entry = true;
                    }

                    // Step to the next syscall entry/exit.
                    ptrace::syscall(child, None).expect("Failed to continue to syscall");
                };

                // Set system call number to an invalid number.
                ptrace_set_syscall(child, arch, u64::MAX).expect("Failed to set syscall");

                // Step to the syscall exit.
                ptrace::syscall(child, None).expect("Failed to continue to syscall");

                // Wait for the process to stop.
                let pid = match waitpid(child, None).expect("Failed to wait") {
                    WaitStatus::PtraceSyscall(pid) => pid,
                    status => panic!("Unexpected wait status: {status:?}"),
                };
                assert_eq!(pid, child, "Unexpected PID from wait");

                // Set return value to success.
                ptrace_set_return(pid, arch, error).expect("Failed to set return value");

                // Resume child and cleanup.
                ptrace::cont(child, None).expect("Failed to resume child");

                // Wait for the process to exit.
                let expected_exit = error.map(|err| err as i32).unwrap_or(0);
                let pid = match waitpid(child, None).expect("Failed to wait") {
                    WaitStatus::Exited(pid, exit_code) if exit_code == expected_exit => pid,
                    status => panic!(
                        "Unexpected wait status: {status:?} (expected exit_code:{expected_exit})"
                    ),
                };
                assert_eq!(pid, child, "Unexpected PID from wait");
            }
            Err(_) => panic!("Fork failed"),
        }
    }

    #[test]
    fn test_ptrace_get_syscall_info_random_args() {
        // Generate random arguments (shared between parent and child)
        // We generate in u32 range so tests work on 32-bit too.
        let args: Vec<u32> = (0..6)
            .map(|_| randint(1..=u32::MAX.into()).expect("Failed to generate random number") as u32)
            .collect();

        match unsafe { fork() } {
            Ok(ForkResult::Child) => {
                // Child process: Request to be traced
                ptrace::traceme().expect("Failed to set traceme");

                // Stop the child to allow the parent to attach.
                raise(Signal::SIGSTOP).expect("Failed to stop child");

                // Trigger an invalid syscall with the shared arguments.
                unsafe {
                    if cfg!(target_arch = "x86") {
                        libc::syscall(
                            0xdead,
                            (args[1] as u64) << 32 | (args[0] as u64),
                            (args[3] as u64) << 32 | (args[2] as u64),
                            (args[5] as u64) << 32 | (args[4] as u64),
                        )
                    } else {
                        libc::syscall(0xdead, args[0], args[1], args[2], args[3], args[4], args[5])
                    }
                };

                // Exit after invalid syscall.
                exit(0);
            }
            Ok(ForkResult::Parent { child }) => {
                // Parent process: Wait for the child to stop.
                if let WaitStatus::Stopped(pid, Signal::SIGSTOP) =
                    waitpid(child, None).expect("Failed to wait for child")
                {
                    assert_eq!(pid, child, "Unexpected PID from wait");
                } else {
                    panic!("Child did not stop as expected");
                }

                // Set ptrace options.
                ptrace::setoptions(child, Options::PTRACE_O_TRACESYSGOOD)
                    .expect("Failed to set ptrace options");

                // Step to the syscall entry.
                ptrace::syscall(child, None).expect("Failed to continue to syscall entry");

                // Loop until we get to the expected system call.
                let mut expecting_entry = true;
                loop {
                    let pid = match waitpid(child, None).expect("Failed to wait") {
                        WaitStatus::PtraceSyscall(pid) => pid,
                        status => panic!("Unexpected wait status: {status:?}"),
                    };
                    assert_eq!(pid, child, "Unexpected PID from wait");

                    // Retrieve syscall info.
                    let info = ptrace_get_syscall_info(child).expect("Failed to get syscall info");

                    if expecting_entry {
                        assert_eq!(
                            info.op, PTRACE_SYSCALL_INFO_ENTRY,
                            "Expected syscall entry stop"
                        );

                        // Check the syscall number.
                        let data = unsafe { info.data.entry };
                        if data.nr == 0xdead {
                            // Check syscall arguments.
                            for i in 0..6 {
                                assert_eq!(
                                    data.args[i] as u32, args[i],
                                    "Argument mismatch at index {i}: {:?} != {:?}",
                                    data.args, args
                                );
                            }

                            break;
                        }

                        expecting_entry = false;
                    } else {
                        assert_eq!(
                            info.op, PTRACE_SYSCALL_INFO_EXIT,
                            "Expected syscall entry exit"
                        );

                        expecting_entry = true;
                    }

                    // Step to the next syscall entry/exit.
                    ptrace::syscall(child, None).expect("Failed to continue to syscall");
                }

                // Step to the syscall exit.
                ptrace::syscall(child, None).expect("Failed to continue to syscall");

                // Wait for the process to stop.
                let pid = match waitpid(child, None).expect("Failed to wait") {
                    WaitStatus::PtraceSyscall(pid) => pid,
                    status => panic!("Unexpected wait status: {status:?}"),
                };
                assert_eq!(pid, child, "Unexpected PID from wait");

                // Retrieve syscall info at exit.
                let exit_info =
                    ptrace_get_syscall_info(child).expect("Failed to get syscall info at exit");

                assert_eq!(
                    exit_info.op, PTRACE_SYSCALL_INFO_EXIT,
                    "Expected syscall exit stop"
                );

                let exit_data = unsafe { exit_info.data.exit };
                assert!(exit_data.is_error != 0, "Expected syscall error");
                assert_eq!(
                    exit_data.rval,
                    -libc::ENOSYS as i64,
                    "Expected ENOSYS return value"
                );

                // Terminate child and cleanup.
                kill(child, Signal::SIGKILL).expect("Failed to terminate child");
                waitpid(child, None).expect("Failed to wait for child");
            }
            Err(_) => panic!("Fork failed"),
        }
    }

    #[test]
    fn test_ptrace_get_error_chdir_success() {
        ptrace_test_get_error_chdir(None);
    }

    #[test]
    fn test_ptrace_get_error_chdir_enoent() {
        ptrace_test_get_error_chdir(Some(Errno::ENOENT));
    }

    #[test]
    fn test_ptrace_get_error_chdir_eacces() {
        ptrace_test_get_error_chdir(Some(Errno::EACCES));
    }

    #[test]
    fn test_ptrace_get_error_chdir_enotdir() {
        ptrace_test_get_error_chdir(Some(Errno::ENOTDIR));
    }

    #[test]
    fn test_ptrace_get_error_chdir_efault() {
        ptrace_test_get_error_chdir(Some(Errno::EFAULT));
    }

    fn ptrace_test_get_error_chdir(mut error: Option<Errno>) {
        // We may test 5 calls:
        //   1) `chdir("/tmp")` => expected success.
        //   2) `chdir("/this/path/does_not_exist")` => expected ENOENT.
        //   3) `chdir("/root")` => expected EACCES.
        //   4) `chdir("/etc/passwd")` => expected ENOTDIR.
        //   5) `chdir(NULL)` => expected EFAULT.
        //
        // We'll store each path as an Option<&str>. If it's None, we interpret that
        // as a NULL pointer in the child. Then we track the expected Errno.

        if Uid::effective().as_raw() == 0 && error == Some(Errno::EACCES) {
            // We cannot test EACCES if running as root.
            error = None;
        }

        let maybe_path = match error {
            None => Some("/tmp"),
            Some(Errno::ENOENT) => Some("/var/empty/lol"),
            Some(Errno::EACCES) => Some("/root"),
            Some(Errno::ENOTDIR) => Some("/etc/passwd"),
            Some(Errno::EFAULT) => None,
            error => unreachable!("BUG: {error:?} is not supported!"),
        };

        match unsafe { fork() } {
            Ok(ForkResult::Child) => {
                // Child: request tracing
                ptrace::traceme().expect("Failed to enable traceme");
                // Stop ourselves so parent can attach
                raise(Signal::SIGSTOP).expect("Child failed to raise SIGSTOP");

                let result = if let Some(path_str) = maybe_path {
                    // Normal path
                    let c_path = CString::new(path_str.as_bytes()).expect("CString::new failed");
                    Errno::result(unsafe { libc::chdir(c_path.as_ptr()) })
                } else {
                    // None => call chdir(NULL)
                    Errno::result(unsafe { libc::chdir(std::ptr::null()) })
                };

                // Exit with errno.
                exit(match result {
                    Ok(_) => 0,
                    Err(errno) => errno as i32,
                });
            }
            Ok(ForkResult::Parent { child }) => {
                // Parent process: Wait for the child to stop.
                if let WaitStatus::Stopped(pid, Signal::SIGSTOP) =
                    waitpid(child, None).expect("Failed to wait for child")
                {
                    assert_eq!(pid, child, "Unexpected PID from wait");
                } else {
                    panic!("Child did not stop as expected");
                }

                // Set ptrace options.
                ptrace::setoptions(child, Options::PTRACE_O_TRACESYSGOOD)
                    .expect("Failed to set ptrace options");

                // Step to the syscall entry.
                ptrace::syscall(child, None).expect("Failed to continue to syscall entry");

                // Loop until we get to the expected system call.
                let mut expecting_entry = true;
                let arch = loop {
                    let pid = match waitpid(child, None).expect("Failed to wait") {
                        WaitStatus::PtraceSyscall(pid) => pid,
                        status => panic!("Unexpected wait status: {status:?}"),
                    };
                    assert_eq!(pid, child, "Unexpected PID from wait");

                    // Retrieve syscall info.
                    let info = ptrace_get_syscall_info(child).expect("Failed to get syscall info");

                    if expecting_entry {
                        assert_eq!(
                            info.op, PTRACE_SYSCALL_INFO_ENTRY,
                            "Expected syscall entry stop"
                        );

                        // Check the syscall number.
                        let data = unsafe { info.data.entry };
                        if data.nr == libc::SYS_chdir as u64 {
                            break info.arch;
                        }

                        expecting_entry = false;
                    } else {
                        assert_eq!(
                            info.op, PTRACE_SYSCALL_INFO_EXIT,
                            "Expected syscall entry exit"
                        );

                        expecting_entry = true;
                    }

                    // Step to the next syscall entry/exit.
                    ptrace::syscall(child, None).expect("Failed to continue to syscall");
                };

                // Step to the syscall exit.
                ptrace::syscall(child, None).expect("Failed to continue to syscall");

                // Wait for the process to stop.
                let pid = match waitpid(child, None).expect("Failed to wait") {
                    WaitStatus::PtraceSyscall(pid) => pid,
                    status => panic!("Unexpected wait status: {status:?}"),
                };
                assert_eq!(pid, child, "Unexpected PID from wait");

                // Now call ptrace_get_error to see if there's an error
                let got = ptrace_get_error(child, arch).expect("Failed to get error code");
                let expected = error;
                assert_eq!(
                    got, expected,
                    "Mismatch: expected {:?}, got {:?}",
                    expected, got
                );

                // Resume child and cleanup.
                ptrace::cont(child, None).expect("Failed to resume child");

                // Wait for the process to exit.
                let expected_exit = error.map(|err| err as i32).unwrap_or(0);
                let pid = match waitpid(child, None).expect("Failed to wait") {
                    WaitStatus::Exited(pid, exit_val) if exit_val == expected_exit => pid,
                    status => panic!(
                        "Unexpected wait status: {status:?} expected exit code:{expected_exit}"
                    ),
                };
                assert_eq!(pid, child, "Unexpected PID from wait");
            }
            Err(_) => panic!("fork() failed"),
        }
    }
}
