syd 3.58.0

rock-solid application kernel
Documentation
// Syd: rock-solid application kernel
// src/kernel/net/socket.rs: socket(2) and socketpair(2) handlers
//
// Copyright (c) 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 std::os::fd::RawFd;

use libc::c_int;
use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, sys::socket::SockFlag};

use crate::{
    cache::UnixVal,
    caps::{util::capable, Capabilities},
    compat::{AddressFamily, SockType, AF_MAX, IPPROTO_MAX, SOCK_TYPE_MASK},
    confine::is_valid_ptr,
    cookie::{safe_socket, safe_socketpair},
    fd::fd_inode,
    ip::{NetlinkFamily, SocketCall},
    kernel::net::sandbox_addr_unnamed,
    proc::util::proc_tgid,
    req::UNotifyEventRequest,
    sandbox::{Flags, Options, SandboxGuard},
};

pub(crate) fn handle_socket(
    request: &UNotifyEventRequest,
    args: &[u64; 6],
    flags: Flags,
    options: Options,
    netlink_families: NetlinkFamily,
) -> Result<ScmpNotifResp, Errno> {
    let allow_unsupp_socket = options.allow_unsupp_socket();
    let allow_unsafe_kcapi = options.allow_unsafe_kcapi();
    let allow_unsafe_socket = options.allow_unsafe_socket();
    let force_cloexec = flags.force_cloexec();
    let force_rand_fd = flags.force_rand_fd();

    // Linux truncates upper bits and rejects unknown flags.
    #[expect(clippy::cast_possible_truncation)]
    let stype = args[1] as c_int;
    let sflag = SockFlag::from_bits(stype & !SOCK_TYPE_MASK).ok_or(Errno::EINVAL)?;

    #[expect(clippy::cast_possible_truncation)]
    let domain = AddressFamily::from_raw(args[0] as c_int);

    // Linux validates address family first, socket type next.
    if !(0..AF_MAX).contains(&domain.as_raw()) {
        return Err(Errno::EAFNOSUPPORT);
    }

    #[expect(clippy::cast_possible_truncation)]
    let proto = args[2] as c_int;

    let stype = match SockType::try_from(stype) {
        Err(Errno::EINVAL) => return Err(Errno::EINVAL),
        _ if domain == AddressFamily::Unspec => return Err(Errno::EAFNOSUPPORT),
        Ok(stype) => stype,
        Err(_) if domain == AddressFamily::Unix && !matches!(proto, 0 | libc::AF_UNIX) => {
            return Err(Errno::EPROTONOSUPPORT)
        }
        Err(_)
            if matches!(domain, AddressFamily::Inet | AddressFamily::Inet6)
                && !(0..IPPROTO_MAX).contains(&proto) =>
        {
            return Err(Errno::EINVAL);
        }
        Err(errno) if domain == AddressFamily::Packet => {
            return Err(
                if allow_unsafe_socket && capable(Capabilities::CAP_NET_RAW) {
                    errno
                } else {
                    Errno::EPERM
                },
            );
        }
        Err(_) if domain == AddressFamily::Alg && !allow_unsafe_kcapi => {
            return Err(Errno::EAFNOSUPPORT)
        }
        Err(_)
            if !matches!(
                domain,
                AddressFamily::Inet
                    | AddressFamily::Inet6
                    | AddressFamily::Unix
                    | AddressFamily::Netlink
                    | AddressFamily::Alg
                    | AddressFamily::Tipc
            ) =>
        {
            return Err(Errno::EAFNOSUPPORT)
        }
        Err(errno) => return Err(errno),
    };

    // Linux converts SOCK_RAW to SOCK_DGRAM for AF_UNIX sockets.
    let stype = if domain == AddressFamily::Unix && stype == SockType::Raw {
        SockType::Datagram
    } else {
        stype
    };

    // Linux checks AF_UNIX protocol before socket type.
    if domain == AddressFamily::Unix {
        if !matches!(proto, 0 | libc::AF_UNIX) {
            return Err(Errno::EPROTONOSUPPORT);
        }
        if !matches!(
            stype,
            SockType::Stream | SockType::Datagram | SockType::SeqPacket
        ) {
            return Err(Errno::ESOCKTNOSUPPORT);
        }
    }

    // Linux checks protocol for range for AF_INET{,6} before type lookup.
    if matches!(domain, AddressFamily::Inet | AddressFamily::Inet6) && stype == SockType::Raw {
        if !(0..IPPROTO_MAX).contains(&proto) {
            return Err(Errno::EINVAL);
        }

        // SOCK_RAW with proto=0 has no handler.
        if proto == 0 {
            return Err(Errno::EPROTONOSUPPORT);
        }
    }

    // Linux limits the SOCK_PACKET compat rewrite to AF_INET.
    // Linux checks protocol range before type lookup.
    if domain == AddressFamily::Inet6 && stype == SockType::Packet {
        return Err(if !(0..IPPROTO_MAX).contains(&proto) {
            Errno::EINVAL
        } else {
            Errno::ESOCKTNOSUPPORT
        });
    }

    // Validate socket domain.
    // 1. Limit available domains based on sandbox flags.
    // 2. Deny access to Raw and Packet sockets unless Syd holds
    //    CAP_NET_RAW. Raw and Packet sockets require CAP_NET_RAW and
    //    use of SOCK_PACKET is strongly discouraged.
    match domain {
        AddressFamily::Inet | AddressFamily::Inet6
            if stype.is_unsafe()
                && !(allow_unsafe_socket && capable(Capabilities::CAP_NET_RAW)) =>
        {
            return Err(Errno::EPERM);
        }
        AddressFamily::Inet | AddressFamily::Inet6 | AddressFamily::Unix => {}
        AddressFamily::Alg if allow_unsafe_kcapi => {}
        AddressFamily::Alg => return Err(Errno::EAFNOSUPPORT),
        AddressFamily::Netlink => {
            // Linux checks socket type before protocol.
            if !matches!(stype, SockType::Raw | SockType::Datagram) {
                return Err(Errno::ESOCKTNOSUPPORT);
            }
            // Restrict AF_NETLINK to the allowlisted families.
            // Linux returns EPROTONOSUPPORT for denied/invalid netlink protocols.
            #[expect(clippy::cast_possible_truncation)]
            let nlfam = args[2] as i32;
            if !(0..=NetlinkFamily::max()).contains(&nlfam) {
                return Err(Errno::EPROTONOSUPPORT);
            }
            let nlfam = NetlinkFamily::from_bits(1 << nlfam).ok_or(Errno::EPROTONOSUPPORT)?;
            if !netlink_families.contains(nlfam) {
                // Unsafe netlink family, deny.
                return Err(Errno::EPROTONOSUPPORT);
            }
        }
        AddressFamily::Packet if allow_unsafe_socket && capable(Capabilities::CAP_NET_RAW) => {}
        AddressFamily::Packet => return Err(Errno::EPERM),
        _ if !allow_unsupp_socket => return Err(Errno::EAFNOSUPPORT),
        _ if stype.is_unsafe() && !(allow_unsafe_socket && capable(Capabilities::CAP_NET_RAW)) => {
            return Err(Errno::EPERM);
        }
        _ => {} // trace/allow_unsupp_socket:1
    }

    let cloexec = force_cloexec || sflag.contains(SockFlag::SOCK_CLOEXEC);
    let sflag = sflag | SockFlag::SOCK_CLOEXEC;

    // All done, call underlying system call.
    let fd = safe_socket(domain, stype, sflag, proto)?;

    request.send_fd(fd, cloexec, force_rand_fd)
}

pub(crate) fn handle_socketpair(
    request: &UNotifyEventRequest,
    sandbox: SandboxGuard,
    subcall: SocketCall,
    args: &[u64; 6],
) -> Result<ScmpNotifResp, Errno> {
    let flags = *sandbox.flags;
    let options = *sandbox.options;
    let force_cloexec = flags.force_cloexec();
    let force_rand_fd = flags.force_rand_fd();
    let allow_unsupp_socket = options.allow_unsupp_socket();
    let allow_unsafe_kcapi = options.allow_unsafe_kcapi();

    // Linux truncates upper bits and rejects unknown flags.
    #[expect(clippy::cast_possible_truncation)]
    let stype = args[1] as c_int;
    let sflag = SockFlag::from_bits(stype & !SOCK_TYPE_MASK).ok_or(Errno::EINVAL)?;

    // Linux checks pointer before family/type validation.
    let fdptr = args[3];
    if !is_valid_ptr(fdptr, request.scmpreq.data.arch) {
        return Err(Errno::EFAULT);
    }

    // Ensure memory is writable.
    //
    // This is best effort, we can still leak fds if page protections
    // change after this call but before the next write memory call.
    if request.get_fault(fdptr, size_of::<[RawFd; 2]>())? > 0 {
        return Err(Errno::EFAULT);
    }

    #[expect(clippy::cast_possible_truncation)]
    let domain = AddressFamily::from_raw(args[0] as c_int);

    // Linux validates address family first, socket type next.
    if !(0..AF_MAX).contains(&domain.as_raw()) {
        return Err(Errno::EAFNOSUPPORT);
    }

    #[expect(clippy::cast_possible_truncation)]
    let proto = args[2] as c_int;

    let stype = match SockType::try_from(stype) {
        Err(Errno::EINVAL) => return Err(Errno::EINVAL),
        _ if domain == AddressFamily::Unspec => return Err(Errno::EAFNOSUPPORT),
        Ok(stype) => stype,
        Err(_) if domain == AddressFamily::Unix && !matches!(proto, 0 | libc::AF_UNIX) => {
            return Err(Errno::EPROTONOSUPPORT)
        }
        Err(_)
            if matches!(domain, AddressFamily::Inet | AddressFamily::Inet6)
                && !(0..IPPROTO_MAX).contains(&proto) =>
        {
            return Err(Errno::EINVAL);
        }
        Err(_) if domain == AddressFamily::Packet => return Err(Errno::EPERM),
        Err(_) if domain == AddressFamily::Alg && !allow_unsafe_kcapi => {
            return Err(Errno::EAFNOSUPPORT)
        }
        Err(_)
            if !allow_unsupp_socket
                && !matches!(
                    domain,
                    AddressFamily::Unix
                        | AddressFamily::Inet
                        | AddressFamily::Inet6
                        | AddressFamily::Netlink
                        | AddressFamily::Alg
                ) =>
        {
            return Err(Errno::EAFNOSUPPORT)
        }
        Err(errno) => return Err(errno),
    };

    // On Linux, the only supported domains for this call are AF_UNIX (or
    // synonymously, AF_LOCAL) and AF_TIPC (since Linux 4.12).
    //
    // Linux accepts SOCK_RAW for AF_UNIX and converts it to SOCK_DGRAM.
    let stype = if domain == AddressFamily::Unix && stype == SockType::Raw {
        SockType::Datagram
    } else {
        stype
    };

    // Linux supports socketpair(2) only for AF_UNIX and AF_TIPC.
    // AF_UNIX requires access check for bind access.
    let check_access = match domain {
        AddressFamily::Unix if !matches!(proto, 0 | libc::AF_UNIX) => {
            return Err(Errno::EPROTONOSUPPORT)
        }
        AddressFamily::Unix => true,
        AddressFamily::Alg if !allow_unsafe_kcapi => return Err(Errno::EAFNOSUPPORT),
        AddressFamily::Tipc if allow_unsupp_socket => false,
        AddressFamily::Inet | AddressFamily::Inet6 => {
            if !(0..IPPROTO_MAX).contains(&proto) {
                return Err(Errno::EINVAL);
            }
            let is_icmp = proto
                == if domain == AddressFamily::Inet {
                    libc::IPPROTO_ICMP
                } else {
                    libc::IPPROTO_ICMPV6
                };
            match stype {
                SockType::Datagram if is_icmp => {
                    return Err(Errno::EACCES);
                }
                SockType::Raw if proto == 0 => return Err(Errno::EPROTONOSUPPORT),
                SockType::Raw => return Err(Errno::EPERM),
                SockType::Packet if domain == AddressFamily::Inet => return Err(Errno::EPERM),
                _ if allow_unsupp_socket => false,
                SockType::Stream
                    if matches!(proto, 0 | libc::IPPROTO_TCP | libc::IPPROTO_MPTCP) =>
                {
                    return Err(Errno::EOPNOTSUPP)
                }
                SockType::Datagram
                    if matches!(proto, 0 | libc::IPPROTO_UDP | libc::IPPROTO_UDPLITE) =>
                {
                    return Err(Errno::EOPNOTSUPP)
                }
                SockType::Stream | SockType::Datagram => return Err(Errno::EPROTONOSUPPORT),
                _ => return Err(Errno::ESOCKTNOSUPPORT),
            }
        }
        _ => {
            match domain {
                AddressFamily::Netlink => {
                    if !matches!(stype, SockType::Raw | SockType::Datagram) {
                        return Err(Errno::ESOCKTNOSUPPORT);
                    }
                    if !(0..=NetlinkFamily::max()).contains(&proto) {
                        return Err(Errno::EPROTONOSUPPORT);
                    }
                    if NetlinkFamily::from_bits(1 << proto).is_none() {
                        return Err(Errno::EPROTONOSUPPORT);
                    }
                }
                AddressFamily::Alg => {
                    // Linux checks socket type before protocol.
                    if stype != SockType::SeqPacket {
                        return Err(Errno::ESOCKTNOSUPPORT);
                    }
                    if proto != 0 {
                        return Err(Errno::EPROTONOSUPPORT);
                    }
                }
                AddressFamily::Packet => return Err(Errno::EPERM),
                _ if !allow_unsupp_socket => return Err(Errno::EAFNOSUPPORT),
                _ => {} // trace/allow_unsupp_socket:1
            }
            return Err(Errno::EOPNOTSUPP);
        }
    };

    // Check AF_UNIX sockets for bind access to dummy `!unnamed' path.
    if check_access {
        sandbox_addr_unnamed(request, &sandbox, subcall)?;
    }
    drop(sandbox); // release read lock.

    let cloexec = force_cloexec || sflag.contains(SockFlag::SOCK_CLOEXEC);
    let sflag = sflag | SockFlag::SOCK_CLOEXEC;

    // All done, call underlying system call.
    let (fd0, fd1) = safe_socketpair(domain, stype, proto, sflag)?;

    // Handle UNIX map after successful socketpair(2) for UNIX sockets.
    // We ignore errors because there's nothing we can do about them.
    if domain == AddressFamily::Unix {
        // Record TGID as peer for each socket.
        // Record inode->PID mappings to UNIX map.
        if let Ok(pid) = proc_tgid(request.scmpreq.pid()) {
            // Validate request after proc(5) read.
            if request.is_valid() {
                if let Ok(ino0) = fd_inode(&fd0) {
                    let _ = request.add_unix2(ino0, pid, UnixVal::default());
                    let _ = request.set_unix_peer(ino0, pid, pid);
                }
                if let Ok(ino1) = fd_inode(&fd1) {
                    let _ = request.add_unix2(ino1, pid, UnixVal::default());
                    let _ = request.set_unix_peer(ino1, pid, pid);
                }
            }
        }
    }

    // Install both fds into the sandbox process.
    // Move fds into the function and close on return.
    let newfd0 = request.add_fd(fd0, cloexec, force_rand_fd)?;
    let newfd1 = request.add_fd(fd1, cloexec, force_rand_fd)?;

    // Write installed fds back to sandbox process memory.
    let a = newfd0.to_ne_bytes();
    let b = newfd1.to_ne_bytes();
    let out = [a[0], a[1], a[2], a[3], b[0], b[1], b[2], b[3]];
    request.write_mem_all(&out, fdptr)?;

    // socketpair(2) returns 0 on success.
    Ok(request.return_syscall(0))
}