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

use std::{
    borrow::Cow,
    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6},
    ops::Deref,
    os::{
        fd::{AsRawFd, RawFd},
        unix::ffi::OsStrExt,
    },
};

use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    fcntl::OFlag,
    sys::socket::{AddressFamily, SockaddrLike, SockaddrStorage, UnixAddr},
    NixPath,
};

use crate::{
    compat::{
        addr_family, MsgFlags, PF_ALG, PF_INET, PF_INET6, PF_MAX, PF_NETLINK, PF_UNIX, PF_UNSPEC,
    },
    config::HOOK_SCKCALLS,
    confine::{op2errno, op2name, scmp_arch_bits},
    fd::fd_status_flags,
    kernel::net::{
        accept::handle_accept,
        bind::handle_bind,
        connect::handle_connect,
        getpeername::handle_getpeername,
        getsockname::handle_getsockname,
        getsockopt::handle_getsockopt,
        recvfrom::handle_recvfrom,
        recvmsg::{handle_recvmmsg, handle_recvmmsg64, handle_recvmsg},
        sendmsg::{handle_sendmmsg, handle_sendmsg},
        sendto::handle_sendto,
        socket::{handle_socket, handle_socketpair},
    },
    lookup::{file_type, safe_canonicalize, CanonicalPath, FileType, FsFlags},
    path::{XPath, XPathBuf},
    req::UNotifyEventRequest,
    sandbox::{Action, Capability, SandboxGuard},
    warn,
};

pub(crate) mod accept;
pub(crate) mod bind;
pub(crate) mod connect;
pub(crate) mod getpeername;
pub(crate) mod getsockname;
pub(crate) mod getsockopt;
pub(crate) mod recvfrom;
pub(crate) mod recvmsg;
pub(crate) mod sendmsg;
pub(crate) mod sendto;
pub(crate) mod socket;

const UNIX_PATH_MAX: usize = 108;

pub(crate) fn sys_socketcall(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Determine socket subcall.
    let op: u8 = match req.data.args[0].try_into() {
        Ok(op) => op,
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };
    if HOOK_SCKCALLS.binary_search(&op).is_err() {
        // SAFETY: No pointer dereference in access check.
        return unsafe { request.continue_syscall() };
    }

    // Determine system call arguments.
    // On x86 unsigned long is 4 bytes, and on s390x 8 bytes.
    let is32 = scmp_arch_bits(req.data.arch) == 32;
    let sizeof_ulong: usize = if is32 { 4 } else { 8 };
    const ARGLEN: usize = 6;
    let mut args = [0u64; ARGLEN];
    #[expect(clippy::arithmetic_side_effects)]
    let bufsiz = sizeof_ulong * ARGLEN;
    match request.read_vec(req.data.args[1], bufsiz) {
        Ok(buf) if buf.len() != bufsiz => {
            // Short read.
            return request.fail_syscall(Errno::EFAULT);
        }
        Ok(buf) => {
            for (i, chunk) in buf.chunks_exact(sizeof_ulong).enumerate() {
                match sizeof_ulong {
                    4 => match chunk.try_into() {
                        Ok(bytes) => args[i] = u64::from(u32::from_ne_bytes(bytes)),
                        Err(_) => return request.fail_syscall(Errno::EFAULT),
                    },
                    8 => match chunk.try_into() {
                        Ok(bytes) => args[i] = u64::from_ne_bytes(bytes),
                        Err(_) => return request.fail_syscall(Errno::EFAULT),
                    },
                    _ => {
                        // SAFETY: The is32 check above
                        // ensures this branch is never reached.
                        unreachable!("BUG: Invalid sizeof unsigned long: {sizeof_ulong}!");
                    }
                }
            }
        }
        Err(errno) => return request.fail_syscall(errno),
    }

    syscall_network_handler(request, &args, op)
}

pub(crate) fn sys_socket(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x1)
}

pub(crate) fn sys_socketpair(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x8)
}

pub(crate) fn sys_bind(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x2)
}

pub(crate) fn sys_accept(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x5)
}

pub(crate) fn sys_accept4(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x12)
}

pub(crate) fn sys_getpeername(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x7)
}

pub(crate) fn sys_getsockname(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x6)
}

pub(crate) fn sys_getsockopt(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0xf)
}

pub(crate) fn sys_connect(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x3)
}

pub(crate) fn sys_recvfrom(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0xc)
}

pub(crate) fn sys_recvmsg(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x11)
}

pub(crate) fn sys_recvmmsg(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x13)
}

pub(crate) fn sys_recvmmsg64(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    // recvmmsg_time64 does not have a socketcall(2) equivalent,
    // therefore we use the dummy op `u8::MAX`.
    syscall_network_handler(request, &req.data.args, u8::MAX)
}

pub(crate) fn sys_sendto(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0xb)
}

pub(crate) fn sys_sendmsg(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x10)
}

pub(crate) fn sys_sendmmsg(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;
    syscall_network_handler(request, &req.data.args, 0x14)
}

// A helper function to handle network-related syscalls.
#[expect(clippy::cognitive_complexity)]
fn syscall_network_handler(request: UNotifyEventRequest, args: &[u64; 6], op: u8) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let sandbox = request.get_sandbox();
        let flags = *sandbox.flags;
        let allow_safe_bind = flags.allow_safe_bind();
        let allow_safe_kcapi = flags.allow_safe_kcapi();
        let allow_unsupp_socket = flags.allow_unsupp_socket();
        let restrict_oob = !flags.allow_unsafe_oob();

        let cap = match op {
            0x1 => {
                // a. socket system call.
                // b. socketcall -> socket indirection.
                let nlfam = sandbox.netlink_families;
                drop(sandbox); // drop read-lock before emulation.
                return handle_socket(&request, args, flags, nlfam);
            }
            0x8 => {
                // a. socketpair system call.
                // b. socketcall -> socketpair indirection.
                // As of version 3.42.0, this is checked for bind
                // access with the dummy `!unnamed' path.
                return handle_socketpair(&request, sandbox, args, 0x8);
            }
            0x5 | 0x6 | 0x7 | 0xc | 0x11 | 0x12 | 0x13 | u8::MAX => {
                // accept, accept4, getsockname, getpeername:
                //
                // accept{,4} are IP blocklist only.
                // get{peer,sock}name are informational.
                // recvfrom is informational.
                // recvmsg, recvmmsg, and recvmmsg_time64 are informational.
                Capability::empty()
            }
            0x2 /* bind */ => Capability::CAP_NET_BIND,
            _ /* connect, send{,to,{m,}msg} */ => Capability::CAP_NET_CONNECT,
        };
        drop(sandbox); // release the read-lock before get-fd.

        // SAFETY: Get the file descriptor before access check
        // as it may change after which is a TOCTOU vector.
        // This also allows us to early return on invalid file
        // descriptors without having to resort to access()'ing
        // /proc/$pid/fd/$fd which will return ENOENT with
        // /proc mounted as hidepid=2.
        #[expect(clippy::cast_possible_truncation)]
        let fd = request.get_fd(args[0] as RawFd)?;

        // SAFETY:
        // 1. Check if fd has O_PATH in status flags and return EBADF.
        // 2. Check if fd points to a socket or return ENOTSOCK.
        if fd_status_flags(&fd)?.contains(OFlag::O_PATH) {
            return Err(Errno::EBADF);
        } else if file_type(&fd, None, false)? != FileType::Sock {
            return Err(Errno::ENOTSOCK);
        }

        match op {
            0x5 | 0x12 => {
                // accept{,4} uses a different data structure,
                // so we handle it in its own branch.
                return handle_accept(fd, &request, args, op);
            }
            0x6 => {
                // getsockname is used for informational purposes only.
                return handle_getsockname(fd, &request, args);
            }
            0x7 => {
                // getpeername is used for informational purposes only.
                return handle_getpeername(fd, &request, args);
            }
            0x9 => {
                // send: Connection mode socket.
                // This only happens via socketcall(2) multiplexer.
                // On 64-bit we do not hook into send(2).
                return handle_sendto(fd, args, &request, None, restrict_oob);
            }
            0xc => {
                // recvfrom is used for informational purposes only.
                return handle_recvfrom(fd, args, &request, restrict_oob);
            }
            0x11 => {
                // recvmsg is used for informational purposes only.
                return handle_recvmsg(fd, &request, args, flags);
            }
            0x13 => {
                // recvmsg is used for informational purposes only.
                return handle_recvmmsg(fd, &request, args, flags);
            }
            u8::MAX => {
                // recvmmsg_time64 is used for informational purposes only.
                return handle_recvmmsg64(fd, &request, args, flags);
            }
            0xf => {
                // getsockopt is used for informational purposes only.
                return handle_getsockopt(fd, &request, args);
            }
            0x10 => {
                // sendmsg uses a different data structure, so we handle it in its own branch.
                return handle_sendmsg(fd, &request, args, flags);
            }
            0x14 => {
                // sendmmsg uses a different data structure, so we handle it in its own branch.
                return handle_sendmmsg(fd, &request, args, flags);
            }
            _ => {} // fall through.
        }

        let idx = if op == 0xb /* sendto */ { 4 } else { 1 };
        let addr_remote = args[idx];
        #[expect(clippy::arithmetic_side_effects)]
        #[expect(clippy::cast_possible_truncation)]
        let addr_len = args[idx + 1] as libc::socklen_t;
        if addr_remote == 0 && addr_len == 0 {
            if op == 0xb {
                // sendto: Connection mode socket.
                return handle_sendto(fd, args, &request, None, restrict_oob);
            } else {
                return Err(Errno::EFAULT);
            }
        } else if addr_remote == 0 || addr_len == 0 {
            return Err(Errno::EFAULT);
        } // else we have a valid address to check for access.

        let sandbox = request.get_sandbox();
        let anyaddr = sandbox.flags.allow_unsafe_any_addr();
        let local_net = sandbox.flags.force_local_net();
        let argaddr = if op == 0x2 && addr_len as usize == size_of::<libc::sa_family_t>() {
            // unix(7): "If a bind(2) call specifies addrlen as `sizeof(sa_family_t)`,
            // or [...], then the socket is autobound to an abstract address".
            let addr = UnixAddr::new_unnamed();
            // SAFETY: `addr` is constructed using new_unnamed.
            #[expect(clippy::disallowed_methods)]
            unsafe { SockaddrStorage::from_raw(addr.as_ptr().cast(), Some(addr.len())) }.unwrap()
        } else {
            get_addr(&request, addr_remote, addr_len)?
        };
        let (mut addr, root) = canon_addr(&request, &sandbox, &argaddr, cap)?;
        match addr_family(&addr) {
            PF_UNIX => {
                // Check for access.
                sandbox_addr(&request, &sandbox, &addr, &root, op, cap)?;
            }
            PF_INET => {
                // Turn 0.0.0.0 into 127.0.0.1 unless trace/allow_unsafe_any_addr:1.
                // Turn all addresses to 127.0.0.1 if trace/force_local_net:1.
                if !anyaddr {
                    make_loaddr(&mut addr, local_net);
                }
                // Check for access.
                sandbox_addr(&request, &sandbox, &addr, &root, op, cap).map_err(|errno| {
                    if op == 0x2 /*bind*/ && has_privileged_port_v4(&addr) {
                        Errno::EACCES
                    } else {
                        errno
                    }
                })?;
            }
            PF_INET6 => {
                // Turn :: into :1 unless trace/allow_unsafe_any_addr:1.
                // Turn all addresses to ::1 if trace/force_local_net:1.
                if !anyaddr {
                    make_lo6addr(&mut addr, local_net);
                }
                // Check for access.
                sandbox_addr(&request, &sandbox, &addr, &root, op, cap).map_err(|errno| {
                    if op == 0x2 /*bind*/ && has_privileged_port_v6(&addr) {
                        Errno::EACCES
                    } else {
                        errno
                    }
                })?;
            }
            PF_UNSPEC => {
                // SAFETY: We do not check address for AF_UNSPEC:
                //
                // Some  protocol sockets (e.g., TCP sockets as well as datagram sockets in the
                // UNIX and Internet domains) may dissolve the association by connecting to an
                // address with the sa_family member of sockaddr set to AF_UNSPEC; thereafter, the
                // socket can be connected to another address. (AF_UNSPEC is supported since
                // Linux 2.2.)
            }
            PF_NETLINK => {
                // SAFETY: We do not check Netlink address for access.
                // We apply filtering on netlink families at socket level.
            }
            PF_ALG if allow_safe_kcapi && op == 0x2 /*bind*/ => {
                // SAFETY: Admin requested access to KCAPI.
            }
            PF_ALG => {
                // a. SAFETY: Access to KCAPI is disabled by default.
                // b. Non-bind(2) call is not supported for AF_ALG socket.
                return Err(Errno::EOPNOTSUPP);
            }
            n if n >= PF_MAX => return Err(Errno::EAFNOSUPPORT),
            _ if !allow_unsupp_socket => return Err(Errno::EAFNOSUPPORT),
            _ => {}, // fall-through to emulate, continue here is unsafe.
        };
        drop(sandbox); // release the read-lock.

        // Emulate syscall.
        match op {
            0x2 => handle_bind(fd, (addr, argaddr), root, &request, allow_safe_bind),
            0x3 => handle_connect(fd, (addr, argaddr), &request, allow_safe_bind),
            0xb => handle_sendto(fd, args, &request, Some(addr), restrict_oob),
            _ => unreachable!(),
        }
    })
}

fn get_addr(
    request: &UNotifyEventRequest,
    addr_remote: u64,
    addr_len: libc::socklen_t,
) -> Result<SockaddrStorage, Errno> {
    // SAFETY:
    // 1. Do not fully trust addr_len.
    // 2. Return EINVAL on negative or zero addr_len.
    let addr_len: usize = addr_len.try_into().or(Err(Errno::EINVAL))?;
    if addr_len < 3 {
        return Err(Errno::EINVAL);
    }
    #[expect(clippy::arithmetic_side_effects)]
    let addr_len = addr_len.min(size_of::<libc::sockaddr_un>() + UNIX_PATH_MAX);

    let addr = request.read_vec(addr_remote, addr_len)?;
    let addr_len = addr.len().try_into().or(Err(Errno::EINVAL))?;
    let addr = addr.as_ptr().cast();

    // SAFETY: Invoking `SockaddrStorage::from_raw` is safe because:
    // 1. The memory location of `sockaddr_ptr` is valid, correctly aligned.
    // 2. The memory is allocated based on a valid `sockaddr` structure.
    // 3. There are no concurrent writes to the memory location while reading.
    match unsafe { SockaddrStorage::from_raw(addr, Some(addr_len)) } {
        Some(addr)
            if addr.as_sockaddr_in().is_some()
                && (addr_len as usize) < size_of::<libc::sockaddr_in>() =>
        {
            Err(Errno::EINVAL)
        }
        Some(addr)
            if addr.as_sockaddr_in6().is_some()
                && (addr_len as usize) < size_of::<libc::sockaddr_in6>() =>
        {
            Err(Errno::EINVAL)
        }
        Some(addr) => Ok(addr),
        None => {
            // Invalid socket address.
            Err(Errno::EINVAL)
        }
    }
}

// Canonicalizes UNIX domain socket names.
// Returns address and directory.
// Directory is None for non-UNIX addresses.
fn canon_addr(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    addr: &SockaddrStorage,
    cap: Capability,
) -> Result<(SockaddrStorage, Option<CanonicalPath>), Errno> {
    #[expect(clippy::cast_possible_truncation)]
    if let Some(path) = addr.as_unix_addr().and_then(|a| a.path()) {
        // Check for chroot.
        if sandbox.is_chroot() {
            return Err(Errno::ENOENT);
        }

        // SAFETY: Path may have trailing nul-bytes.
        // Truncate the path at the first occurrence of a null byte
        // Note this is _not_ an abstract UNIX socket so it's safe.
        let path = path.as_os_str().as_bytes();
        let null = memchr::memchr(0, path).unwrap_or(path.len());
        let path = XPathBuf::from(&path[..null]);

        // For bind(2), the path must be missing or we return EADDRINUSE.
        // For connect family, the path must exist or we return ENOENT.
        let fsflags = if cap == Capability::CAP_NET_BIND {
            FsFlags::MISS_LAST
        } else {
            FsFlags::MUST_PATH
        };

        // SAFETY:
        //
        // 1. Always resolve symlinks.
        // 2. Ensure relative UNIX socket paths match process CWD.
        let pid = request.scmpreq.pid();
        let path = safe_canonicalize(pid, None, &path, fsflags, Some(sandbox.deref()))?;

        let sun_path = if path.base().is_empty() {
            // SAFETY: We open a FD to the path and then use the
            // proc path /proc/thread-self/fd/$fd in address' path
            // argument to avoid symlink TOCTOU because connect and
            // sendto follow symlinks in basename unlike bind.
            #[expect(clippy::disallowed_methods)]
            let fd = path.dir.as_ref().unwrap();
            let mut pfd = XPathBuf::from("/proc/thread-self/fd");
            pfd.push_fd(fd.as_raw_fd());
            pfd.append_byte(0);
            pfd
        } else {
            // SAFETY:
            // 1. We split the address into directory and basename
            //    regardless of UNIX_PATH_MAX as we are later going to use
            //    the handler thread to mitigate the TOCTOU vector in the
            //    basename of the UNIX socket address. This is only used
            //    for bind() which does not resolve symbolic links in
            //    basename.
            // 2. We add "./" for easier identification at recvfrom(2)
            //    boundary which is for informational purposes only.
            let mut base = XPathBuf::from("./");
            base.append_bytes(path.base().as_os_str().as_bytes());
            base.append_byte(0);
            base
        };

        // Create sockaddr_un struct.
        let mut sockaddr = libc::sockaddr_un {
            sun_family: libc::AF_UNIX as libc::sa_family_t,
            sun_path: [0; UNIX_PATH_MAX],
        };
        let socklen = sun_path.len();
        if socklen > UNIX_PATH_MAX {
            return Err(Errno::ENAMETOOLONG);
        }

        // SAFETY: Copy the bytes without overlapping regions.
        unsafe {
            std::ptr::copy_nonoverlapping(
                sun_path.as_ptr(),
                sockaddr.sun_path.as_mut_ptr().cast(),
                socklen,
            )
        };

        // Calculate the correct size of the sockaddr_un struct,
        // including the family and the path. The size is the offset of
        // the sun_path field plus the length of the path (including the
        // null terminator).
        #[expect(clippy::arithmetic_side_effects)]
        let size = size_of::<libc::sa_family_t>() + socklen;

        // SAFETY: We are converting a sockaddr_un to a
        // SockaddrStorage using a raw pointer. The sockaddr_un
        // is valid for the duration of this operation, ensuring
        // the safety of the pointer. However, this operation is
        // inherently unsafe due to direct pointer manipulation.
        let addr = unsafe {
            SockaddrStorage::from_raw(
                std::ptr::addr_of!(sockaddr) as *const _,
                Some(size as libc::socklen_t),
            )
        }
        .ok_or(Errno::EINVAL)?;

        Ok((addr, Some(path)))
    } else {
        // No need to canonicalize.
        Ok((*addr, None))
    }
}

/// Processes the address family of a `SockaddrStorage` object and performs logging or other
/// required operations specific to the syscall being handled.
///
/// This helper function isolates the logic involved in dealing with different address families
/// and reduces code duplication across different syscall handler functions.
///
/// # Parameters
///
/// - `addr`: Reference to a `SockaddrStorage`, representing the socket address involved in the syscall.
/// - `syscall_name`: A string slice holding the name of the syscall being handled, used for logging purposes.
///
/// # Safety
///
/// The function contains unsafe blocks due to potential TOCTOU (Time-of-Check Time-of-Use)
/// vulnerabilities. Each unsafe block within this function has been annotated with a detailed
/// safety comment to ensure that unsafe operations are used correctly and securely.
///
/// # Errors
///
/// The function returns an `io::Error` in cases where:
/// - The conversion from `SockaddrStorage` to a specific address family representation fails.
/// - Any other unexpected error condition occurs during the processing of the address family.
///
/// # Returns
///
/// Returns an `Result<(), Errno>`:
/// - `Ok(())` if the processing is successful.
/// - `Err(Errno)` containing a description of the error, if any error occurs during processing.
pub(crate) fn sandbox_addr(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    addr: &SockaddrStorage,
    root: &Option<CanonicalPath>,
    op: u8,
    caps: Capability,
) -> Result<(), Errno> {
    // bind, connect, accept, sendto, sendmsg, accept4, sendmmsg
    assert!(
        matches!(op, 0x2 | 0x3 | 0x5 | 0xb | 0x10 | 0x12 | 0x14),
        "BUG: sandbox_addr called with invalid op:{op:#x}, report a bug!"
    );

    match addr.family() {
        Some(AddressFamily::Unix) => sandbox_addr_unix(request, sandbox, addr, root, op, caps),
        Some(AddressFamily::Inet | AddressFamily::Inet6) => {
            sandbox_addr_inet(request, sandbox, addr, op, caps)
        }
        Some(_) | None => sandbox_addr_notsup(sandbox),
    }
}

/// Process a `AddressFamily::Unix` socket address.
#[expect(clippy::cognitive_complexity)]
pub(crate) fn sandbox_addr_unix(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    addr: &SockaddrStorage,
    root: &Option<CanonicalPath>,
    op: u8,
    caps: Capability,
) -> Result<(), Errno> {
    // bind, connect, sendto, sendmsg, sendmmsg
    assert!(
        matches!(op, 0x2 | 0x3 | 0xb | 0x10 | 0x14),
        "BUG: sandbox_addr_unix called with invalid op:{op:#x}, report a bug!"
    );

    if sandbox.getcaps(caps).is_empty() {
        // Sandboxing is off.
        return Ok(());
    }

    let addr = addr.as_unix_addr().ok_or(Errno::EINVAL)?;
    let (path, abs) = match (addr.path(), addr.as_abstract()) {
        (Some(path), _) => match root {
            Some(path) => (Cow::Borrowed(path.abs()), false),
            None => {
                // Check for chroot.
                if sandbox.is_chroot() {
                    return Err(Errno::ENOENT);
                }

                let path = path.as_os_str().as_bytes();
                let null = memchr::memchr(0, path).unwrap_or(path.len());
                let p = XPathBuf::from(&path[..null]);
                (Cow::Owned(p), false)
            }
        },
        (_, Some(path)) => {
            // SAFETY: Prefix UNIX abstract sockets with `@' before access check.
            let mut unix = XPathBuf::from("@");
            let null = memchr::memchr(0, path).unwrap_or(path.len());
            unix.append_bytes(&path[..null]);
            (Cow::Owned(unix), true)
        }
        _ => {
            // SAFETY: Use dummy path `!unnamed' for unnamed UNIX sockets.
            (Cow::Borrowed(XPath::from_bytes(b"!unnamed")), true)
        }
    };

    // Check for access.
    let (action, filter) = sandbox.check_unix(caps, &path);

    if !filter {
        let sys = op2name(op);
        if sandbox.log_scmp() {
            warn!("ctx": "access", "cap": caps, "act": action,
                "sys": sys, "unix": &path, "abs": abs,
                "tip": format!("configure `allow/{caps}+{path}'"),
                "req": request);
        } else {
            warn!("ctx": "access", "cap": caps, "act": action,
                "sys": sys, "unix": &path, "abs": abs,
                "tip": format!("configure `allow/{caps}+{path}'"),
                "pid": request.scmpreq.pid);
        }
    }

    match action {
        Action::Allow | Action::Warn => Ok(()),
        Action::Deny | Action::Filter => Err(op2errno(op)),
        Action::Panic => panic!(),
        Action::Exit => std::process::exit(op2errno(op) as i32),
        action => {
            // Stop|Kill
            let _ = request.kill(action);
            Err(op2errno(op))
        }
    }
}

/// Process an IPv4 or IPv6 address.
#[expect(clippy::cognitive_complexity)]
pub(crate) fn sandbox_addr_inet(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    addr: &SockaddrStorage,
    op: u8,
    caps: Capability,
) -> Result<(), Errno> {
    // accept(2) and accept4(2) are treated specially:
    // No ACL is done, only IP blocklist check.
    //
    // First branch is accept, accept4.
    // Second branch is bind, connect, send{to,{m,}msg}.
    if matches!(op, 0x5 | 0x12) {
        assert!(
            caps.is_empty(),
            "BUG: sandbox_addr_inet called with op:{op:#x} and {caps}, report a bug!"
        );
    } else if matches!(op, 0x2 | 0x3 | 0xb | 0x10 | 0x14) {
        assert!(
            !caps.is_empty(),
            "BUG: sandbox_addr_inet called with op:{op:#x} and without caps, report a bug!"
        );
    } else {
        unreachable!("BUG: sandbox_addr_inet called with op:{op:#x}, report a bug!");
    }

    if !caps.is_empty() && sandbox.getcaps(caps).is_empty() {
        // Sandboxing is off.
        return Ok(());
    }

    let (addr, port) = if let Some(sin) = addr.as_sockaddr_in() {
        (IpAddr::V4(sin.ip()), sin.port())
    } else if let Some(sa6) = addr.as_sockaddr_in6() {
        (sa6.ip().to_canonical(), sa6.port())
    } else {
        return Err(Errno::EINVAL);
    };

    // Check for access and IP blocklist as necessary.
    // caps.is_empty() implies accept{,4}(2) here.
    let (action, filter) = sandbox.check_ip(caps, addr, port);

    if caps.is_empty() && !filter {
        // accept{,4}
        let ipv = if addr.is_ipv6() { 6 } else { 4 };
        let sys = op2name(op);
        if sandbox.log_scmp() {
            warn!("ctx": "block", "act": action,
                "sys": sys, "addr": format!("{addr}!{port}"), "ipv": ipv,
                "tip": format!("configure `block-{addr}'"),
                "req": request);
        } else {
            warn!("ctx": "block", "act": action,
                "sys": sys, "addr": format!("{addr}!{port}"), "ipv": ipv,
                "tip": format!("configure `block-{addr}'"),
                "pid": request.scmpreq.pid);
        }
    } else if !filter {
        // connect, sendto, send{m,}msg
        let ipv = if addr.is_ipv6() { 6 } else { 4 };
        let sys = op2name(op);
        if sandbox.log_scmp() {
            warn!("ctx": "access", "cap": caps, "act": action,
                "sys": sys, "addr": format!("{addr}!{port}"), "ipv": ipv,
                "tip": format!("configure `allow/{caps}+{addr}!{port}'"),
                "req": request);
        } else {
            warn!("ctx": "access", "cap": caps, "act": action,
                "sys": sys, "addr": format!("{addr}!{port}"), "ipv": ipv,
                "tip": format!("configure `allow/{caps}+{addr}!{port}'"),
                "pid": request.scmpreq.pid);
        }
    }

    match action {
        Action::Allow | Action::Warn => Ok(()),
        Action::Deny | Action::Filter => Err(op2errno(op)),
        Action::Panic => panic!(),
        Action::Exit => std::process::exit(op2errno(op) as i32),
        action => {
            // Stop|Kill
            let _ = request.kill(action);
            Err(op2errno(op))
        }
    }
}

/// Process a socket address of an unsupported socket family.
pub(crate) fn sandbox_addr_notsup(sandbox: &SandboxGuard) -> Result<(), Errno> {
    if sandbox.flags.allow_unsupp_socket() {
        Ok(())
    } else {
        Err(Errno::EAFNOSUPPORT)
    }
}

/// Process an unnamed socket with the given socket family.
///
/// Used at socketpair(2) boundary for AF_UNIX sockets.
#[expect(clippy::cognitive_complexity)]
pub(crate) fn sandbox_addr_unnamed(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard,
    op: u8,
) -> Result<(), Errno> {
    // socketpair
    assert_eq!(
        op, 0x8,
        "BUG: sandbox_addr_unnamed called with invalid op:{op:#x}, report a bug!"
    );

    // Check for access.
    let caps = Capability::CAP_NET_BIND;
    let name = XPath::from_bytes(b"!unnamed");

    if sandbox.getcaps(caps).is_empty() {
        // Sandboxing is off.
        return Ok(());
    }
    let (action, filter) = sandbox.check_unix(caps, name);

    if !filter {
        let sys = op2name(op);
        if sandbox.log_scmp() {
            warn!("ctx": "access", "cap": caps, "act": action,
                "sys": sys, "unix": &name, "abs": true,
                "tip": format!("configure `allow/{caps}+{name}'"),
                "req": request);
        } else {
            warn!("ctx": "access", "cap": caps, "act": action,
                "sys": sys, "unix": &name, "abs": true,
                "tip": format!("configure `allow/{caps}+{name}'"),
                "pid": request.scmpreq.pid);
        }
    }

    match action {
        Action::Allow | Action::Warn => Ok(()),
        Action::Deny | Action::Filter => Err(op2errno(op)),
        Action::Panic => panic!(),
        Action::Exit => std::process::exit(op2errno(op) as i32),
        action => {
            // Stop|Kill
            let _ = request.kill(action);
            Err(op2errno(op))
        }
    }
}

pub(crate) fn to_msgflags(arg: u64) -> Result<MsgFlags, Errno> {
    let flags = arg.try_into().or(Err(Errno::EINVAL))?;
    MsgFlags::from_bits(flags).ok_or(Errno::EINVAL)
}

// Change 0.0.0.0 to 127.0.0.1 (IPv4)
// If `force` is true change all addresses.
pub(crate) fn make_loaddr(addr: &mut SockaddrStorage, force: bool) {
    if !force && !is_anyaddr(addr) {
        return;
    }
    if let Some(sin) = addr.as_sockaddr_in() {
        let port = sin.port();
        *addr = SockaddrStorage::from(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port));
    }
}

// Change :: to ::1 (IPv6)
// If `force` is true change all addresses.
pub(crate) fn make_lo6addr(addr: &mut SockaddrStorage, force: bool) {
    if !force && !is_any6addr(addr) {
        return;
    }
    if let Some(sin6) = addr.as_sockaddr_in6() {
        let port = sin6.port();
        let flowinfo = sin6.flowinfo();
        let scope_id = sin6.scope_id();
        *addr = SockaddrStorage::from(SocketAddrV6::new(
            Ipv6Addr::LOCALHOST,
            port,
            flowinfo,
            scope_id,
        ));
    }
}

// Return true if `addr` is 0.0.0.0 (Ipv4).
fn is_anyaddr(addr: &SockaddrStorage) -> bool {
    addr.as_sockaddr_in()
        .map(|sin| sin.ip() == Ipv4Addr::UNSPECIFIED)
        .unwrap_or(false)
}

// Return true if `addr` is :: (Ipv6).
fn is_any6addr(addr: &SockaddrStorage) -> bool {
    addr.as_sockaddr_in6()
        .map(|sin6| sin6.ip() == Ipv6Addr::UNSPECIFIED)
        .unwrap_or(false)
}

// Return true if `addr` has a privileged port (< 1024) (Ipv4).
fn has_privileged_port_v4(addr: &SockaddrStorage) -> bool {
    addr.as_sockaddr_in()
        .map(|sin| {
            let port = sin.port();
            port != 0 && port < 1024
        })
        .unwrap_or(false)
}

// Return true if `addr` has a privileged port (< 1024) (Ipv6).
fn has_privileged_port_v6(addr: &SockaddrStorage) -> bool {
    addr.as_sockaddr_in6()
        .map(|sin6| {
            let port = sin6.port();
            port != 0 && port < 1024
        })
        .unwrap_or(false)
}
