syd 3.58.0

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

// SAFETY: This module has been liberated from unsafe code!
#![forbid(unsafe_code)]

use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, sys::socket::SockaddrStorage};

use crate::{
    compat::{send, sendto, sockaddr_family, AddressFamily, MsgFlags, SockType},
    cookie::safe_getzerocopy,
    fd::has_send_timeout,
    ip::{SockInfo, SocketCall},
    kernel::net::{check_send_state, handle_safe_bind_send, max_send_len, to_msgflags, SockOpts},
    req::UNotifyEventRequest,
};

pub(crate) fn handle_sendto(
    request: &UNotifyEventRequest,
    mut sock: SockInfo,
    sock_opts: SockOpts,
    addr: Option<(SockaddrStorage, SockaddrStorage)>,
    args: &[u64; 6],
) -> Result<ScmpNotifResp, Errno> {
    let SockOpts {
        flags: _,
        options,
        is_nonblock,
    } = sock_opts;

    // Truncate flags to 32-bit keeping unknown flags.
    // Linux ignores MSG_CMSG_COMPAT for sendto(2).
    let flags = to_msgflags(args[3]).difference(MsgFlags::MSG_CMSG_COMPAT);

    // Reject MSG_ZEROCOPY which Syd can't support with current design.
    // Syd sends copies of messages therefore completions never arrive.
    // Linux ignores MSG_ZEROCOPY if SO_ZEROCOPY isn't set on socket.
    let flags = if flags.contains(MsgFlags::MSG_ZEROCOPY) {
        if safe_getzerocopy(sock.fd())? {
            return Err(Errno::ENOBUFS);
        }
        flags.difference(MsgFlags::MSG_ZEROCOPY)
    } else {
        flags
    };

    // Reject MSG_OOB as necessary.
    let restrict_oob = !options.allow_unsafe_oob();
    if restrict_oob && flags.contains(MsgFlags::MSG_OOB) {
        return Err(Errno::EOPNOTSUPP);
    }

    // Length argument to sendto(2) is not trusted.
    let count = usize::try_from(args[2]).or(Err(Errno::EINVAL))?;

    // Linux rejects oversized atomic sends before destination lookup.
    // Addressed sends are checked in syscall_network_handler.
    if addr.is_none() {
        sock.check_send_len(count)?;
    }

    // Cap count at MAX_RW_COUNT and maximum socket send buffer.
    let len = count.min(max_send_len());

    // Linux sends readable prefix for byte streams, messages are atomic.
    // Linux rejects oversized IPv4 datagrams before reading payload.
    // Linux returns EPIPE for unconnected streams before reading payload.
    // Linux reports socket errors before parse errors.
    let buf = match read_data(request, &mut sock, args[1], len) {
        Ok(buf) => buf,
        Err(Errno::EFAULT) => {
            if args[2] > 0xFFFF
                && sock.get_dom()? == AddressFamily::Inet
                && matches!(sock.get_stype()?, SockType::Datagram | SockType::Raw)
            {
                return Err(Errno::EMSGSIZE);
            }
            check_send_state(request, &mut sock, flags, addr.is_some(), is_nonblock)?;

            return Err(Errno::EFAULT);
        }
        Err(errno) => return Err(errno),
    };

    // Record sender PID for SCM_PIDFD/SO_PASSCRED fixup at recvmsg(2).
    //
    // To avoid races, this must be done before sendto(2) and on errors
    // the entry will be removed back again.
    let req = request.scmpreq;
    let addr_unix = match addr.as_ref() {
        Some((addr, _)) => sockaddr_family(addr) == AddressFamily::Unix,
        None => sock.get_dom()? == AddressFamily::Unix,
    };
    let unix_data = if addr_unix {
        let unix = addr
            .as_ref()
            .and_then(|(_, addr_arg)| addr_arg.as_unix_addr());
        // Ignore errors: UNIX socket diagnostics may not be supported.
        // `unix` is None for connection-mode sockets.
        request.add_send(sock.fd(), req.pid(), unix).ok()
    } else {
        None
    };

    // Record blocking call so it can get invalidated.
    let is_blocking = if !is_nonblock && !flags.contains(MsgFlags::MSG_DONTWAIT) {
        let ignore_restart = has_send_timeout(sock.fd())?;

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

        true
    } else {
        false
    };

    // Perform sendmsg(2).
    let result = if let Some((ref addr, _)) = addr {
        // Connection-less socket.
        sendto(sock.fd(), &buf, addr, flags)
    } else {
        // Connection mode socket, no address specified.
        send(sock.fd(), &buf, flags)
    };

    // Remove invalidation record.
    if is_blocking {
        request.cache.del_sys_block(req.id)?;
    }

    // Delete sender record on errors.
    if result.is_err() {
        if let Some((inode, dest)) = unix_data {
            let _ = request.del_send(inode, dest);
        }
    }

    // Handle allow_safe_bind.
    // Ignore errors as sendto has already succeeded.
    if result.is_ok() && options.allow_safe_bind() {
        if let Some((ref addr, _)) = addr {
            let _ = handle_safe_bind_send(request, SocketCall::SendTo, &mut sock, addr);
        }
    }

    // Send SIGPIPE for EPIPE unless MSG_NOSIGNAL is set.
    #[expect(clippy::cast_possible_wrap)]
    Ok(match result {
        Ok(n) => request.return_syscall(n as i64),
        Err(Errno::EPIPE) if !flags.contains(MsgFlags::MSG_NOSIGNAL) => {
            if sock.get_send_sigpipe()? {
                request.pidfd_kill(libc::SIGPIPE)?;
            }
            request.fail_syscall(Errno::EPIPE)
        }
        Err(errno) => request.fail_syscall(errno),
    })
}

// Read send data from sandbox process memory.
//
// Linux sends readable prefix for byte streams, messages are atomic.
fn read_data(
    request: &UNotifyEventRequest,
    sock: &mut SockInfo,
    addr_remote: u64,
    len: usize,
) -> Result<Vec<u8>, Errno> {
    if sock.get_stream_send()? {
        let buf = request.read_vec(addr_remote, len)?;
        if buf.is_empty() && len != 0 {
            return Err(Errno::EFAULT);
        }
        Ok(buf)
    } else {
        request.read_vec_all(addr_remote, len)
    }
}