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

use std::{
    io::IoSlice,
    os::{
        fd::{AsRawFd, RawFd},
        unix::ffi::OsStrExt,
    },
};

use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    sys::socket::{sendmsg, ControlMessage, SockaddrStorage, UnixCredentials},
    unistd::Pid,
};
use zeroize::Zeroizing;

use crate::{
    cache::UnixVal,
    compat::{
        addr_family, cmsg_len_32, cmsg_space_32, getsockdomain, msghdr, msghdr32, MsgFlags, PF_ALG,
        PF_INET, PF_INET6, PF_NETLINK, PF_UNIX, UIO_MAXIOV,
    },
    config::MAX_RW_COUNT,
    confine::scmp_arch_bits,
    fd::{get_nonblock, has_recv_timeout, SafeOwnedFd},
    fs::is_same_vm,
    kernel::{
        net::{canon_addr, get_addr, make_lo6addr, make_loaddr, sandbox_addr, to_msgflags},
        sandbox_path,
    },
    path::XPath,
    req::UNotifyEventRequest,
    sandbox::{Capability, Flags, Options},
};

#[expect(clippy::cognitive_complexity)]
pub(crate) fn handle_sendmsg(
    fd: SafeOwnedFd,
    request: &UNotifyEventRequest,
    args: &[u64; 6],
    flags: Flags,
    options: Options,
) -> Result<ScmpNotifResp, Errno> {
    // Truncate flags to 32-bit keeping unknown flags.
    let msgflags = to_msgflags(args[2]);

    // Reject MSG_OOB as necessary.
    if !options.allow_unsafe_oob() && msgflags.contains(MsgFlags::MSG_OOB) {
        // Signal no support to let the sandbox process handle the error
        // gracefully. This is consistent with the Linux kernel.
        return Err(Errno::EOPNOTSUPP);
    }

    let req = request.scmpreq;
    let is32 = scmp_arch_bits(req.data.arch) == 32;
    let size = if is32 {
        // socketcall(2) is the same number on all:
        // x86, mips, mipsel, ppc, ppc64, ppc64le, s390 and s390x.
        size_of::<msghdr32>()
    } else {
        size_of::<msghdr>()
    };
    let buf = request.read_vec_zeroed(args[1], size)?;

    let msg = if is32 {
        // SAFETY: Unaligned read of msghdr32 buffer.
        let msg: msghdr32 = unsafe { std::ptr::read_unaligned(buf.as_ptr() as *const _) };
        crate::compat::msghdr::from(msg)
    } else {
        // SAFETY: Unaligned read of msghdr buffer.
        unsafe { std::ptr::read_unaligned(buf.as_ptr() as *const _) }
    };

    let addr_remote = msg.msg_name;
    let addr_len = msg.msg_namelen;

    // Step 1: Handle the address.
    let sandbox = request.get_sandbox();
    let any_addr = sandbox.flags.allow_unsafe_any_addr();
    let local_net = sandbox.flags.force_local_net();
    let log_scmp = sandbox.log_scmp();
    let mut addr_unix = false;
    let addr = if !addr_remote.is_null() && addr_len > 0 {
        let argaddr = get_addr(request, addr_remote as u64, addr_len)?;
        let (mut addr, root) =
            canon_addr(request, &sandbox, &argaddr, Capability::CAP_NET_CONNECT)?;
        match addr_family(&addr) {
            PF_UNIX => {
                // Check for access.
                sandbox_addr(
                    request,
                    &sandbox,
                    &addr,
                    &root,
                    0x10,
                    Capability::CAP_NET_CONNECT,
                )?;
                addr_unix = true;
            }
            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 !any_addr {
                    make_loaddr(&mut addr, local_net, 0x10 /*sendmsg*/)?;
                }

                // Check for access.
                sandbox_addr(
                    request,
                    &sandbox,
                    &addr,
                    &root,
                    0x10,
                    Capability::CAP_NET_CONNECT,
                )?;
            }
            PF_INET6 => {
                // Turn :: into :1 unless trace/allow_unsafe_any_addr:1.
                // Turn all addresses to ::1 if trace/force_local_net:1.
                if !any_addr {
                    make_lo6addr(&mut addr, local_net, 0x10 /*sendmsg*/)?;
                }
                // Check for access.
                sandbox_addr(
                    request,
                    &sandbox,
                    &addr,
                    &root,
                    0x10,
                    Capability::CAP_NET_CONNECT,
                )?;
            }
            PF_ALG | PF_NETLINK => {
                // We do not check AF_ALG or AF_NETLINK for access.
            }
            _ if options.allow_unsupp_socket() => {
                // We do not check unsupported sockets for access.
            }
            _ => return Err(Errno::EAFNOSUPPORT),
        };

        Some((addr, root, argaddr))
    } else {
        // Connection-mode socket.
        // We cannot continue here due to the added level of pointer indirection.
        // We need to check whether this is an AF_UNIX socket.
        addr_unix = getsockdomain(&fd)
            .map(|domain| domain == libc::AF_UNIX)
            .unwrap_or(false);

        None
    };

    // Step 2: Handle control messages.
    let mut control_messages = Vec::new();
    let control_data = if !msg.msg_control.is_null() && msg.msg_controllen > 0 {
        #[expect(clippy::useless_conversion)]
        let cmsg_len = usize::try_from(msg.msg_controllen)
            .or(Err(Errno::EINVAL))?
            .min(*MAX_RW_COUNT); // Cap at MAX_RW_COUNT.
        let cmsg_buf = request.read_vec(msg.msg_control as u64, cmsg_len)?;
        Some(parse_control_messages(request, &cmsg_buf)?)
    } else {
        None
    };

    #[allow(unused)]
    if let Some((
        ref control_fds,
        ref control_creds,
        ref control_ivs,
        ref control_ops,
        ref control_aead_assoclens,
        ref control_udp_gso_segments,
        ref control_ipv4_packet_infos,
        ref control_ipv6_packet_infos,
        ref control_rxq_ovfls,
        ref control_tx_times,
        ref control_ipv4_toses,
        ref control_ipv6_tclasses,
    )) = control_data
    {
        // Check for sendfd access as necessary.
        if !control_fds.is_empty() {
            if let Some((ref addr, ref root, _)) = addr {
                sandbox_addr(
                    request,
                    &sandbox,
                    addr,
                    root,
                    0x10,
                    Capability::CAP_NET_SENDFD,
                )?;
            } else {
                // For cases where address is not available, we perform
                // an access check with a dummy path so as to enable
                // user to practically confine this case.
                sandbox_path(
                    Some(request),
                    &sandbox,
                    request.scmpreq.pid(), // Unused when request.is_some()
                    XPath::from_bytes(b"!unnamed"),
                    Capability::CAP_NET_SENDFD,
                    "sendmsg",
                )?;
            }
        }

        for fds in control_fds {
            // Check safe file types for SCM_RIGHTS.
            for fd in fds {
                request.check_scm_rights(fd, flags, 0x10, log_scmp)?;
            }

            // SAFETY: OwnedFd is repr(transparent) over RawFd, so
            // the pointer cast & the slice length are correct.
            let raw_fds: &[RawFd] =
                unsafe { std::slice::from_raw_parts(fds.as_ptr() as *const RawFd, fds.len()) };

            control_messages.push(ControlMessage::ScmRights(raw_fds));
        }

        for creds in control_creds {
            control_messages.push(ControlMessage::ScmCredentials(creds));
        }

        for iv_data in control_ivs {
            control_messages.push(ControlMessage::AlgSetIv(iv_data.as_slice()));
        }

        for op in control_ops {
            control_messages.push(ControlMessage::AlgSetOp(op));
        }

        for assoclen in control_aead_assoclens {
            control_messages.push(ControlMessage::AlgSetAeadAssoclen(assoclen));
        }

        for gso_segments in control_udp_gso_segments {
            control_messages.push(ControlMessage::UdpGsoSegments(gso_segments));
        }

        for pktinfo in control_ipv4_packet_infos {
            control_messages.push(ControlMessage::Ipv4PacketInfo(pktinfo));
        }

        for pktinfo in control_ipv6_packet_infos {
            control_messages.push(ControlMessage::Ipv6PacketInfo(pktinfo));
        }

        for rxq_ovfl in control_rxq_ovfls {
            control_messages.push(ControlMessage::RxqOvfl(rxq_ovfl));
        }

        #[cfg(not(target_os = "android"))]
        for tx_time in control_tx_times {
            control_messages.push(ControlMessage::TxTime(tx_time));
        }

        for tos in control_ipv4_toses {
            control_messages.push(ControlMessage::Ipv4Tos(tos));
        }

        for tclass in control_ipv6_tclasses {
            control_messages.push(ControlMessage::Ipv6TClass(tclass));
        }
    }
    drop(sandbox); // release the read-lock.

    // Step 3: Handle the payload which is an array of struct iovecs.
    let mut io_buffers: Vec<Vec<u8>> = Vec::new();
    let mut io_slices: Vec<IoSlice> = Vec::new();
    if !(msg.msg_iov.is_null() || msg.msg_iovlen == 0) {
        // The msg_iovlen member of the msghdr struct must not be fully
        // trusted, it can be overly large, and allocating a Vector of
        // that capacity may overflow. Here we cap it to UIO_MAXIOV for
        // safety.
        #[expect(clippy::useless_conversion)]
        let len = usize::try_from(msg.msg_iovlen)
            .or(Err(Errno::EMSGSIZE))?
            .min(UIO_MAXIOV);

        let size = if is32 {
            len.checked_mul(size_of::<crate::compat::iovec32>())
        } else {
            len.checked_mul(size_of::<libc::iovec>())
        }
        .ok_or(Errno::EMSGSIZE)?;

        let buf = request.read_vec_zeroed(msg.msg_iov as u64, size)?;
        let mut iovecs: Vec<libc::iovec> = Vec::new();
        if is32 {
            for chunk in buf.chunks(size_of::<crate::compat::iovec32>()) {
                // SAFETY: See above.
                let iov32: crate::compat::iovec32 =
                    unsafe { std::ptr::read_unaligned(chunk.as_ptr() as *const _) };
                iovecs.push(iov32.into());
            }
        } else {
            for chunk in buf.chunks(size_of::<libc::iovec>()) {
                // SAFETY: See above.
                iovecs.push(unsafe { std::ptr::read_unaligned(chunk.as_ptr() as *const _) });
            }
        };

        for iov in iovecs {
            if iov.iov_base.is_null() || iov.iov_len == 0 {
                continue;
            }

            // Validate length to avoid large allocations.
            #[expect(clippy::useless_conversion)]
            let iov_len = usize::try_from(iov.iov_len).or(Err(Errno::EMSGSIZE))?;
            if iov_len > *MAX_RW_COUNT {
                // Invalid iovec count: Too large.
                return Err(Errno::EMSGSIZE);
            }

            // Read the memory from the remote process into our buffer.
            let data_buf = request.read_vec(iov.iov_base as u64, iov_len)?;

            // Keep the pointer accessible, IoSlice needs a valid reference.
            io_buffers.try_reserve(1).or(Err(Errno::ENOMEM))?;
            io_buffers.push(data_buf);
        }
        io_slices
            .try_reserve(io_buffers.len())
            .or(Err(Errno::ENOMEM))?;
        for buffer in &io_buffers {
            io_slices.push(IoSlice::new(buffer));
        }
    }

    // Record blocking call so it can get invalidated.
    let is_blocking = if !msgflags.contains(MsgFlags::MSG_DONTWAIT) && !get_nonblock(&fd)? {
        let req = request.scmpreq;
        let ignore_restart = has_recv_timeout(&fd)?;

        // Record the blocking call.
        request.cache.add_sys_block(req, ignore_restart)?;

        true
    } else {
        false
    };

    let result = if let Some((addr, _, _)) = addr {
        // UNIX domain/abstract socket.
        sendmsg(
            fd.as_raw_fd(),
            &io_slices,
            &control_messages,
            msgflags.into(),
            Some(&addr),
        )
    } else {
        // Connection-mode socket.
        sendmsg::<SockaddrStorage>(
            fd.as_raw_fd(),
            &io_slices,
            &control_messages,
            msgflags.into(),
            None,
        )
    };

    // Remove invalidation record unless interrupted.
    if is_blocking {
        request
            .cache
            .del_sys_block(req.id, matches!(result, Err(Errno::EINTR)))?;
    }

    // Send SIGPIPE for EPIPE unless MSG_NOSIGNAL is set.
    #[expect(clippy::cast_possible_wrap)]
    Ok(match result {
        Ok(n) => {
            // Record sender PID for SCM_PIDFD/SO_PASSCRED fixup at recvmsg(2).
            if addr_unix {
                // Look up destination's device and inode to disambiguate at recv(2).
                let (ddev, dino) = addr
                    .as_ref()
                    .and_then(|(_, _, argaddr)| argaddr.as_unix_addr())
                    .and_then(|unix| unix.path())
                    .map(|path| XPath::from_bytes(path.as_os_str().as_bytes()))
                    .and_then(|path| request.lookup_unix_vfs_id(path).ok())
                    .map_or((None, None), |(dev, ino)| (Some(dev), Some(ino)));
                let mut val = UnixVal::default();
                if let (Some(dev), Some(ino)) = (ddev, dino) {
                    if val.dest.try_reserve(1).is_ok() {
                        val.dest.push((dev, ino));
                    }
                }
                let _ = request.add_unix(&fd, req.pid(), val);
            }
            request.return_syscall(n as i64)
        }
        Err(Errno::EPIPE) if !msgflags.contains(MsgFlags::MSG_NOSIGNAL) => {
            request.pidfd_kill(libc::SIGPIPE)?;
            request.fail_syscall(Errno::EPIPE)
        }
        Err(errno) => request.fail_syscall(errno),
    })
}

#[expect(clippy::cognitive_complexity)]
pub(crate) fn handle_sendmmsg(
    fd: SafeOwnedFd,
    request: &UNotifyEventRequest,
    args: &[u64; 6],
    flags: Flags,
    options: Options,
) -> Result<ScmpNotifResp, Errno> {
    // Truncate flags to 32-bit keeping unknown flags.
    let msgflags = to_msgflags(args[3]);

    // Reject MSG_OOB as necessary.
    if !options.allow_unsafe_oob() && msgflags.contains(MsgFlags::MSG_OOB) {
        // Signal no support to let the sandbox process handle the error
        // gracefully. This is consistent with the Linux kernel.
        return Err(Errno::EOPNOTSUPP);
    }

    // NULL check was performed already.
    let addr = args[1];
    let vlen = usize::try_from(args[2]).or(Err(Errno::EINVAL))?;
    if vlen == 0 {
        return Ok(request.return_syscall(0));
    }
    let vlen = vlen.min(1024); // Cap at IOV_MAX

    let req = request.scmpreq;
    let is32 = scmp_arch_bits(req.data.arch) == 32;

    let size = if is32 {
        vlen.checked_mul(size_of::<crate::compat::mmsghdr32>())
    } else {
        vlen.checked_mul(size_of::<crate::compat::mmsghdr>())
    }
    .ok_or(Errno::EINVAL)?;

    // Read mmsghdr structures from remote process memory.
    let buf = request.read_vec_zeroed(addr, size)?;

    let mut mmsghdrs: Vec<crate::compat::mmsghdr> = Vec::new();
    if is32 {
        for chunk in buf.chunks(size_of::<crate::compat::mmsghdr32>()) {
            // SAFETY: See the relevant comment in handle_sendmsg.
            let mmsghdr: crate::compat::mmsghdr32 = unsafe {
                std::ptr::read_unaligned(chunk.as_ptr() as *const crate::compat::mmsghdr32)
            };
            mmsghdrs.try_reserve(1).or(Err(Errno::ENOMEM))?;
            mmsghdrs.push(mmsghdr.into());
        }
    } else {
        for chunk in buf.chunks(size_of::<crate::compat::mmsghdr>()) {
            mmsghdrs.try_reserve(1).or(Err(Errno::ENOMEM))?;
            // SAFETY: See the relevant comment in handle_sendmsg.
            mmsghdrs.push(unsafe {
                std::ptr::read_unaligned(chunk.as_ptr() as *const crate::compat::mmsghdr)
            });
        }
    };

    // Check if the call is a blocking call which we need to invalidate as necessary.
    let (is_blocking, ignore_restart) =
        if !msgflags.contains(MsgFlags::MSG_DONTWAIT) && !get_nonblock(&fd)? {
            (true, has_recv_timeout(&fd)?)
        } else {
            (false, false)
        };
    // Check if we want to send SIGPIPE on EPIPE.
    let must_signal = !msgflags.contains(MsgFlags::MSG_NOSIGNAL);

    // Prepare a series of sendmsg(2) calls.
    // Send messages in order, break on error,
    // return the number of successfully sent messages if > 0.
    let mut addr_unix = false;
    let mut datagrams: usize = 0;
    let mut send_dest: Vec<(u32, u32)> = Vec::new();
    let mut last_errno: Option<Errno> = None;
    for mmsg in &mut mmsghdrs {
        let msg = &mut mmsg.msg_hdr;

        // Step 1: Handle the address.
        let addr_remote = msg.msg_name;
        let addr_len = msg.msg_namelen;
        let sandbox = request.get_sandbox();
        let any_addr = sandbox.flags.allow_unsafe_any_addr();
        let local_net = sandbox.flags.force_local_net();
        let log_scmp = sandbox.log_scmp();
        let addr_root = if !addr_remote.is_null() && addr_len > 0 {
            let argaddr = get_addr(request, addr_remote as u64, addr_len)?;
            let (mut addr, root) =
                canon_addr(request, &sandbox, &argaddr, Capability::CAP_NET_CONNECT)?;
            match addr_family(&addr) {
                PF_UNIX => {
                    // Check for access.
                    sandbox_addr(
                        request,
                        &sandbox,
                        &addr,
                        &root,
                        0x14,
                        Capability::CAP_NET_CONNECT,
                    )?;
                    addr_unix = true;
                }
                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 !any_addr {
                        make_loaddr(&mut addr, local_net, 0x14 /*sendmmsg*/)?;
                    }
                    // Check for access.
                    sandbox_addr(
                        request,
                        &sandbox,
                        &addr,
                        &root,
                        0x14,
                        Capability::CAP_NET_CONNECT,
                    )?;
                }
                PF_INET6 => {
                    // Turn :: into :1 unless trace/allow_unsafe_any_addr:1.
                    // Turn all addresses to ::1 if trace/force_local_net:1.
                    if !any_addr {
                        make_lo6addr(&mut addr, local_net, 0x14 /*sendmmsg*/)?;
                    }
                    // Check for access.
                    sandbox_addr(
                        request,
                        &sandbox,
                        &addr,
                        &root,
                        0x14,
                        Capability::CAP_NET_CONNECT,
                    )?;
                }
                PF_ALG | PF_NETLINK => {
                    // We do not check AF_ALG and AF_NETLINK for access.
                }
                _ if options.allow_unsupp_socket() => {
                    // We do not check unsupported sockets for access.
                }
                _ => return Err(Errno::EAFNOSUPPORT),
            };

            Some((addr, root, argaddr))
        } else {
            // Connection-mode socket.
            // We cannot continue here due to the added level of pointer indirection.
            // We need to check whether this is an AF_UNIX socket.
            if !addr_unix {
                addr_unix = getsockdomain(&fd)
                    .map(|domain| domain == libc::AF_UNIX)
                    .unwrap_or(false);
            }

            None
        };

        // Step 2: Handle control messages.
        let mut control_data = Vec::new();
        let control_datum = if !msg.msg_control.is_null() && msg.msg_controllen > 0 {
            #[expect(clippy::useless_conversion)]
            let cmsg_len = usize::try_from(msg.msg_controllen)
                .or(Err(Errno::EINVAL))?
                .min(*MAX_RW_COUNT); // Cap at MAX_RW_COUNT.
            let cmsg_buf = request.read_vec(msg.msg_control as u64, cmsg_len)?;
            Some(parse_control_messages(request, &cmsg_buf)?)
        } else {
            None
        };
        control_data.try_reserve(1).or(Err(Errno::ENOMEM))?;
        control_data.push(control_datum); // Keep OwnedFd alive!

        let mut control_messages = Vec::new();
        for control_datum in &control_data {
            #[allow(unused)]
            if let Some((
                ref control_fds,
                ref control_creds,
                ref control_ivs,
                ref control_ops,
                ref control_aead_assoclens,
                ref control_udp_gso_segments,
                ref control_ipv4_packet_infos,
                ref control_ipv6_packet_infos,
                ref control_rxq_ovfls,
                ref control_tx_times,
                ref control_ipv4_toses,
                ref control_ipv6_tclasses,
            )) = &control_datum
            {
                // Check for sendfd access as necessary.
                if !control_fds.is_empty() {
                    if let Some((ref addr, ref root, _)) = addr_root {
                        sandbox_addr(
                            request,
                            &sandbox,
                            addr,
                            root,
                            0x14,
                            Capability::CAP_NET_SENDFD,
                        )?;
                    } else {
                        // For cases where address is not available, we
                        // perform an access check with a dummy path so
                        // as to enable user to practically confine this
                        // case.
                        sandbox_path(
                            Some(request),
                            &sandbox,
                            request.scmpreq.pid(), // Unused when request.is_some()
                            XPath::from_bytes(b"!unnamed"),
                            Capability::CAP_NET_SENDFD,
                            "sendmmsg",
                        )?;
                    }
                }

                for fds in control_fds {
                    // Check safe file types for SCM_RIGHTS.
                    for fd in fds {
                        request.check_scm_rights(fd, flags, 0x14, log_scmp)?;
                    }

                    // SAFETY: OwnedFd is repr(transparent) over RawFd, so
                    // the pointer cast & the slice length are correct.
                    let raw_fds: &[RawFd] = unsafe {
                        std::slice::from_raw_parts(fds.as_ptr() as *const RawFd, fds.len())
                    };

                    control_messages.push(ControlMessage::ScmRights(raw_fds));
                }

                for creds in control_creds {
                    control_messages.push(ControlMessage::ScmCredentials(creds));
                }

                for iv_data in control_ivs {
                    control_messages.push(ControlMessage::AlgSetIv(iv_data.as_slice()));
                }

                for op in control_ops {
                    control_messages.push(ControlMessage::AlgSetOp(op));
                }

                for assoclen in control_aead_assoclens {
                    control_messages.push(ControlMessage::AlgSetAeadAssoclen(assoclen));
                }

                for gso_segments in control_udp_gso_segments {
                    control_messages.push(ControlMessage::UdpGsoSegments(gso_segments));
                }

                for pktinfo in control_ipv4_packet_infos {
                    control_messages.push(ControlMessage::Ipv4PacketInfo(pktinfo));
                }

                for pktinfo in control_ipv6_packet_infos {
                    control_messages.push(ControlMessage::Ipv6PacketInfo(pktinfo));
                }

                for rxq_ovfl in control_rxq_ovfls {
                    control_messages.push(ControlMessage::RxqOvfl(rxq_ovfl));
                }

                #[cfg(not(target_os = "android"))]
                for tx_time in control_tx_times {
                    control_messages.push(ControlMessage::TxTime(tx_time));
                }

                for tos in control_ipv4_toses {
                    control_messages.push(ControlMessage::Ipv4Tos(tos));
                }

                for tclass in control_ipv6_tclasses {
                    control_messages.push(ControlMessage::Ipv6TClass(tclass));
                }
            }
        }
        drop(sandbox); // release the read-lock before emulation.

        // Step 3: Handle the payload which is an array of struct iovecs.
        let mut io_buffers = Vec::new();
        let mut io_slices: Vec<IoSlice> = Vec::new();
        if !(msg.msg_iov.is_null() || msg.msg_iovlen == 0) {
            // The msg_iovlen member of the msghdr struct must not be
            // fully trusted, it can be overly large, and allocating a
            // Vector of that capacity may overflow. Here we cap it to
            // UIO_MAXIOV for safety.
            #[expect(clippy::useless_conversion)]
            let len = usize::try_from(msg.msg_iovlen)
                .or(Err(Errno::EMSGSIZE))?
                .min(UIO_MAXIOV);

            let size = if is32 {
                len.checked_mul(size_of::<crate::compat::iovec32>())
            } else {
                len.checked_mul(size_of::<libc::iovec>())
            }
            .ok_or(Errno::EMSGSIZE)?;

            let buf = request.read_vec_zeroed(msg.msg_iov as u64, size)?;
            let mut iovecs: Vec<libc::iovec> = Vec::new();
            if is32 {
                for chunk in buf.chunks(size_of::<crate::compat::iovec32>()) {
                    // SAFETY: See the relevant comment in handle_sendmsg.
                    let iov32: crate::compat::iovec32 =
                        unsafe { std::ptr::read_unaligned(chunk.as_ptr() as *const _) };
                    iovecs.try_reserve(1).or(Err(Errno::ENOMEM))?;
                    iovecs.push(iov32.into());
                }
            } else {
                for chunk in buf.chunks(size_of::<libc::iovec>()) {
                    iovecs.try_reserve(1).or(Err(Errno::ENOMEM))?;
                    // SAFETY: See the relevant comment in handle_sendmsg.
                    iovecs.push(unsafe { std::ptr::read_unaligned(chunk.as_ptr() as *const _) });
                }
            };

            for iov in iovecs {
                if iov.iov_base.is_null() || iov.iov_len == 0 {
                    continue;
                }

                // Validate length to avoid large allocations.
                #[expect(clippy::useless_conversion)]
                let iov_len = usize::try_from(iov.iov_len).or(Err(Errno::EMSGSIZE))?;
                if iov_len > *MAX_RW_COUNT {
                    // Invalid iovec count: Too large.
                    return Err(Errno::EMSGSIZE);
                }

                // Read the memory from the remote process into our buffer.
                let data_buf = request.read_vec(iov.iov_base as u64, iov_len)?;

                // Keep the pointer accessible, IoSlice needs a valid reference.
                io_buffers.try_reserve(1).or(Err(Errno::ENOMEM))?;
                io_buffers.push(data_buf);
            }
            for buffer in &io_buffers {
                io_slices.try_reserve(1).or(Err(Errno::ENOMEM))?;
                io_slices.push(IoSlice::new(buffer));
            }
        }

        // Record blocking call so it can get invalidated.
        if is_blocking {
            request.cache.add_sys_block(req, ignore_restart)?;
        }

        // Make the sendmsg(2) call.
        let result = if let Some((addr, _, _)) = addr_root {
            // Connection-less socket.
            sendmsg(
                fd.as_raw_fd(),
                &io_slices,
                &control_messages,
                msgflags.into(),
                Some(&addr),
            )
        } else {
            // Connection-mode socket.
            sendmsg::<SockaddrStorage>(
                fd.as_raw_fd(),
                &io_slices,
                &control_messages,
                msgflags.into(),
                None,
            )
        };

        // Remove invalidation record unless interrupted.
        if is_blocking {
            request
                .cache
                .del_sys_block(req.id, matches!(result, Err(Errno::EINTR)))?;
        }

        // Send SIGPIPE for EPIPE unless MSG_NOSIGNAL is set.
        match result {
            Ok(n) => {
                mmsg.msg_len = n.try_into().or(Err(Errno::EINVAL))?;
                datagrams = datagrams.checked_add(1).ok_or(Errno::EOVERFLOW)?;
                if addr_unix {
                    // Look up and collect destination's device and inode
                    // to disambiguate at recv(2) boundary.
                    if let Some((dev, ino)) = addr_root
                        .as_ref()
                        .map(|(_, _, argaddr)| argaddr)
                        .and_then(|argaddr| argaddr.as_unix_addr())
                        .and_then(|unix| unix.path())
                        .map(|path| XPath::from_bytes(path.as_os_str().as_bytes()))
                        .and_then(|path| request.lookup_unix_vfs_id(path).ok())
                    {
                        if send_dest.try_reserve(1).is_ok() {
                            send_dest.push((dev, ino));
                        }
                    }
                }
            }
            Err(Errno::EPIPE) if must_signal => {
                let _ = request.pidfd_kill(libc::SIGPIPE);
                last_errno = Some(Errno::EPIPE);
                break;
            }
            Err(errno) => {
                last_errno = Some(errno);
                break;
            }
        }
    }

    if addr_unix {
        // Record sender PID and all destinations after the loop.
        // There's nothing to be done for errors.
        let _ = request.add_unix(
            &fd,
            req.pid(),
            UnixVal {
                dest: send_dest,
                ..UnixVal::default()
            },
        );
    }

    // Write back mmsghdr structures for successfully sent messages.
    let mut buf: Zeroizing<Vec<u8>> = Zeroizing::new(Vec::new());
    if is32 {
        for mmsghdr in &mmsghdrs[..datagrams] {
            let mmsghdr32: crate::compat::mmsghdr32 = (*mmsghdr).into();
            // SAFETY: Convert each mmsghdr (or mmsghdr32 within the
            // conversion logic) back to its byte representation.
            let bytes: [u8; size_of::<crate::compat::mmsghdr32>()] =
                unsafe { std::mem::transmute(mmsghdr32) };
            buf.try_reserve(bytes.len()).or(Err(Errno::ENOMEM))?;
            buf.extend_from_slice(&bytes);
        }
    } else {
        for mmsghdr in &mmsghdrs[..datagrams] {
            // SAFETY: See above.
            let bytes: [u8; size_of::<crate::compat::mmsghdr>()] =
                unsafe { std::mem::transmute(*mmsghdr) };
            buf.try_reserve(bytes.len()).or(Err(Errno::ENOMEM))?;
            buf.extend_from_slice(&bytes);
        }
    }
    if !buf.is_empty() {
        request.write_mem(&buf, addr)?;
    }

    // Return the number of successfully sent messages if at least one succeeded,
    // otherwise return the error from the first failed message.
    if datagrams > 0 {
        #[expect(clippy::cast_possible_wrap)]
        Ok(request.return_syscall(datagrams as i64))
    } else {
        Err(last_errno.unwrap_or(Errno::EIO))
    }
}

#[expect(clippy::type_complexity)]
fn parse_control_messages(
    request: &UNotifyEventRequest,
    cmsg_buf: &[u8],
) -> Result<
    (
        Vec<Vec<SafeOwnedFd>>,
        Vec<UnixCredentials>,
        Vec<Zeroizing<Vec<u8>>>, // ivs
        Vec<libc::c_int>,        // ops
        Vec<u32>,                // aead_assoclens
        Vec<u16>,                // udp_gso_segments
        Vec<libc::in_pktinfo>,
        Vec<libc::in6_pktinfo>,
        Vec<u32>, // rxq_ovfls
        Vec<u64>, // tx_times
        Vec<u8>,  // ipv4 tos
        Vec<i32>, // ipv6 tclass
    ),
    Errno,
> {
    let mut control_fds = Vec::new();
    let mut control_creds = Vec::new();
    let mut control_ivs = Vec::new();
    let mut control_ops = Vec::new();
    let mut control_aead_assoclens = Vec::new();
    let mut control_udp_gso_segments = Vec::new();
    let mut control_ipv4_packet_infos = Vec::new();
    let mut control_ipv6_packet_infos = Vec::new();
    let mut control_rxq_ovfls = Vec::new();
    #[allow(unused)]
    let mut control_tx_times = Vec::new();
    let mut control_ipv4_toses = Vec::new();
    let mut control_ipv6_tclasses = Vec::new();

    let mut offset = 0;
    let req = request.scmpreq;
    let is32 = scmp_arch_bits(req.data.arch) == 32;
    while offset < cmsg_buf.len() {
        let (cmsg_header, cmsg_len0): (crate::compat::cmsghdr, usize) = if is32 {
            // SAFETY: Unaligned read of cmsghdr32.
            let cmsg_header_32: crate::compat::cmsghdr32 = unsafe {
                std::ptr::read_unaligned(
                    cmsg_buf[offset..].as_ptr() as *const crate::compat::cmsghdr32
                )
            };
            (cmsg_header_32.into(), cmsg_len_32(0))
        } else {
            (
                // SAFETY: Unaligned read of cmsghdr.
                unsafe {
                    std::ptr::read_unaligned(
                        cmsg_buf[offset..].as_ptr() as *const crate::compat::cmsghdr
                    )
                },
                // SAFETY: CMSG_LEN with 0 is always safe.
                unsafe { libc::CMSG_LEN(0) } as usize,
            )
        };
        if cmsg_header.cmsg_len < cmsg_len0 {
            return Err(Errno::EINVAL); // Invalid header length
        }
        #[expect(clippy::useless_conversion)]
        let data_len: usize = cmsg_header.cmsg_len.try_into().or(Err(Errno::EINVAL))?;
        let data_len = data_len.checked_sub(cmsg_len0).ok_or(Errno::EINVAL)?;

        let data_off = offset.checked_add(cmsg_len0).ok_or(Errno::EINVAL)?;
        let data_end = data_off.checked_add(data_len).ok_or(Errno::EINVAL)?;
        if data_end > cmsg_buf.len() {
            return Err(Errno::EINVAL); // Data goes beyond buffer.
        }
        let data = &cmsg_buf[data_off..data_end];

        match (cmsg_header.cmsg_level, cmsg_header.cmsg_type) {
            (libc::SOL_SOCKET, libc::SCM_RIGHTS) => {
                let fd_count = data_len
                    .checked_div(size_of::<RawFd>())
                    .ok_or(Errno::EINVAL)?;

                let mut fds = Vec::new();
                fds.try_reserve(fd_count).or(Err(Errno::ENOMEM))?;

                for i in 0..fd_count {
                    #[expect(clippy::arithmetic_side_effects)]
                    let fd_offset = i * size_of::<RawFd>();
                    // SAFETY: Unaligned read of RawFd from u8 cmsg buffer.
                    let fd = unsafe {
                        std::ptr::read_unaligned(data[fd_offset..].as_ptr() as *const RawFd)
                    };
                    let fd = request.get_fd(fd)?;
                    fds.push(fd);
                }
                control_fds.push(fds);
            }
            (libc::SOL_SOCKET, libc::SCM_CREDENTIALS) => {
                if data_len != size_of::<libc::ucred>() {
                    return Err(Errno::EINVAL); // Data length mismatch
                }
                // SAFETY: Unaligned read of ucred, data length validated.
                let mut creds: libc::ucred =
                    unsafe { std::ptr::read_unaligned(data.as_ptr() as *const libc::ucred) };
                // The sender must specify its own pid (unless it has
                // the capability CAP_SYS_ADMIN, in which case the PID
                // of any existing process may be specified.)
                if creds.pid != request.scmpreq.pid().as_raw() {
                    // Quoting unix(7):
                    // EPERM: The sender passed invalid credentials in the struct ucred.
                    // `is_same_vm` check is necessary to make dbus tests of vala work, see: #78
                    if !is_same_vm(Pid::from_raw(creds.pid), request.scmpreq.pid()).unwrap_or(false)
                    {
                        return Err(Errno::EPERM);
                    }
                }
                creds.pid = Pid::this().as_raw();
                let unix_creds = UnixCredentials::from(creds);
                control_creds.push(unix_creds); // Keep a ref to the UnixCredentials.
            }
            (libc::SOL_ALG, libc::ALG_SET_IV) => {
                // IV data is directly contained in the data part of the control message
                // First four bytes represent the length of the IV.
                if data_len < 4 {
                    return Err(Errno::EINVAL); // Data length mismatch
                }
                let iv_size = u32::from_ne_bytes([data[0], data[1], data[2], data[3]]) as usize;
                let iv_size = iv_size.checked_add(4).ok_or(Errno::EINVAL)?;
                if iv_size <= data_len {
                    // Extract the IV while respecting the indicated size,
                    // if the size is valid.
                    let iv_data = Zeroizing::new(data[4..iv_size].to_vec());
                    control_ivs.push(iv_data); // Store the IV data.
                } else {
                    return Err(Errno::EINVAL); // Data length mismatch.
                }
            }
            (libc::SOL_ALG, libc::ALG_SET_OP) => {
                if data_len != size_of::<libc::c_int>() {
                    return Err(Errno::EINVAL); // Data length mismatch
                }
                // SAFETY: Unaligned read of c_int, data length validated.
                let op = unsafe { std::ptr::read_unaligned(data.as_ptr() as *const libc::c_int) };
                control_ops.push(op); // Store the operation code
            }
            (libc::SOL_ALG, libc::ALG_SET_AEAD_ASSOCLEN) => {
                if data_len != size_of::<u32>() {
                    return Err(Errno::EINVAL); // Data length mismatch
                }
                // SAFETY: Unaligned read of u32, data length validated.
                let assoclen = unsafe { std::ptr::read_unaligned(data.as_ptr() as *const u32) };
                control_aead_assoclens.push(assoclen); // Store the AEAD assoclen
            }
            (libc::SOL_UDP, libc::UDP_SEGMENT) => {
                if data_len != size_of::<u16>() {
                    return Err(Errno::EINVAL); // Data length mismatch
                }
                // SAFETY: Unaligned read of u16, data length validated.
                let gso_segments = unsafe { std::ptr::read_unaligned(data.as_ptr() as *const u16) };
                control_udp_gso_segments.push(gso_segments); // Store the GSO segment count
            }
            (libc::IPPROTO_IP, libc::IP_PKTINFO) => {
                if data_len != size_of::<libc::in_pktinfo>() {
                    return Err(Errno::EINVAL); // Data length mismatch
                }
                // SAFETY: Unaligned read of in_pktinfo, data length validated.
                let pktinfo: libc::in_pktinfo =
                    unsafe { std::ptr::read_unaligned(data.as_ptr() as *const libc::in_pktinfo) };
                control_ipv4_packet_infos.push(pktinfo); // Store the IPv4 packet info
            }
            (libc::IPPROTO_IPV6, libc::IPV6_PKTINFO) => {
                if data_len != size_of::<libc::in6_pktinfo>() {
                    return Err(Errno::EINVAL); // Data length mismatch
                }
                // SAFETY: Unaligned read of in6_pktinfo, data length validated.
                let pktinfo: libc::in6_pktinfo =
                    unsafe { std::ptr::read_unaligned(data.as_ptr() as *const libc::in6_pktinfo) };
                control_ipv6_packet_infos.push(pktinfo); // Store the IPv6 packet info
            }
            (libc::SOL_SOCKET, libc::SO_RXQ_OVFL) => {
                if data_len != size_of::<u32>() {
                    return Err(Errno::EINVAL); // Data length mismatch
                }
                // SAFETY: Unaligned read of u32, data length validated.
                let rxq_ovfl = unsafe { std::ptr::read_unaligned(data.as_ptr() as *const u32) };
                control_rxq_ovfls.push(rxq_ovfl); // Store the Rx queue overflow count
            }
            #[cfg(not(target_os = "android"))]
            (libc::SOL_SOCKET, libc::SCM_TXTIME) => {
                if data_len != size_of::<u64>() {
                    return Err(Errno::EINVAL); // Data length mismatch
                }
                // SAFETY: Unaligned read of u64, data length validated.
                let tx_time = unsafe { std::ptr::read_unaligned(data.as_ptr() as *const u64) };
                control_tx_times.push(tx_time); // Store the Tx time
            }
            (libc::IPPROTO_IP, libc::IP_TOS) => {
                // IP_TOS ancillary data is a u8.
                if data_len != size_of::<u8>() {
                    return Err(Errno::EINVAL);
                }
                // SAFETY: Exact u8-sized payload validated.
                let val = unsafe { *data.as_ptr() };
                control_ipv4_toses.push(val);
            }
            (libc::IPPROTO_IPV6, libc::IPV6_TCLASS) => {
                if data_len != size_of::<i32>() {
                    return Err(Errno::EINVAL);
                }
                // SAFETY: Unaligned read of i32, data length validated.
                let val = unsafe { std::ptr::read_unaligned(data.as_ptr() as *const i32) };
                control_ipv6_tclasses.push(val);
            }
            _ => return Err(Errno::EINVAL),
        }

        #[expect(clippy::arithmetic_side_effects)]
        #[expect(clippy::cast_possible_truncation)]
        if !is32 {
            // SAFETY: CMSG_SPACE is always safe.
            offset += unsafe { libc::CMSG_SPACE(data_len as u32) } as usize;
        } else {
            offset += cmsg_space_32(data_len as u32);
        }
    }

    Ok((
        control_fds,
        control_creds,
        control_ivs,
        control_ops,
        control_aead_assoclens,
        control_udp_gso_segments,
        control_ipv4_packet_infos,
        control_ipv6_packet_infos,
        control_rxq_ovfls,
        control_tx_times,
        control_ipv4_toses,
        control_ipv6_tclasses,
    ))
}
