syd 3.56.0

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

use std::{net::IpAddr, os::fd::AsRawFd};

use ipnet::IpNet;
use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    sys::{
        socket::{SockaddrLike, SockaddrStorage},
        stat::Mode,
    },
};

use crate::{
    cache::UnixVal,
    compat::{sockaddr_family, AddressFamily},
    cookie::{safe_bind, safe_fchdir, safe_getsockname, safe_umask},
    fd::SafeOwnedFd,
    info,
    ip::IpProto,
    kernel::net::{get_port, NetAddr},
    path::{XPath, XPathBuf, XPathCow, XPathGlob},
    proc::proc_umask,
    req::UNotifyEventRequest,
    sandbox::{Action, AddressPattern, Capability, CidrRule},
    wildmatch::MatchMethod,
    xfmt,
};

pub(crate) fn handle_bind(
    request: &UNotifyEventRequest,
    fd: SafeOwnedFd,
    addr: (SockaddrStorage, SockaddrStorage),
    target: (NetAddr, Option<IpProto>),
    allow_safe_bind: bool,
) -> Result<ScmpNotifResp, Errno> {
    let req = request.scmpreq;
    let (addr, argaddr) = addr;
    let (target, sock_proto) = target;

    // Prepare environment for UNIX domain sockets.
    let umask_set = if let NetAddr::UnixPath(ref root) = target {
        let mask = proc_umask(req.pid())?;

        // Honour directory for too long sockets.
        // Current working directory is per-thread here.
        // We cannot resolve symlinks in root or we risk TOCTOU!
        let dirfd = root.dir();
        safe_fchdir(dirfd)?;

        // Set per-thread umask(2) to honour process' umask.
        // POSIX ACLs may override this.
        safe_umask(mask);

        true
    } else {
        false
    };

    // All done, call underlying system call.
    // bind(2) doesn't follow symlinks in basename.
    let result = safe_bind(&fd, &addr);

    // Restore umask(2) as necessary.
    if umask_set {
        safe_umask(Mode::from_bits_retain(0o777));
    }

    // Check for bind(2) errors.
    result?;

    // Move domain on bind as necessary.
    // This happens before trace/allow_safe_bind, so the address is
    // going to be allowlisted in the new domain.
    match &target {
        NetAddr::Inet(ip, port) => request
            .get_sandbox()
            .move_on_bind_inet(*ip, *port, sock_proto),
        NetAddr::Unix(name) => request.get_sandbox().move_on_bind_unix(name),
        NetAddr::UnixPath(root) => request.get_sandbox().move_on_bind_unix(root.abs()),
        NetAddr::UnixUnnamed => request
            .get_sandbox()
            .move_on_bind_unix(XPath::from_bytes(b"!unnamed")),
        NetAddr::None => {}
    }

    // Handle trace/allow_safe_bind and bind_map.
    // Ignore errors as bind has already succeeded.
    //
    // Configure sandbox:
    // Remove and re-add the address so repeated binds to the same
    // address cannot overflow the vector.
    #[expect(clippy::cognitive_complexity)]
    let _result = (|fd: SafeOwnedFd, request: &UNotifyEventRequest| -> Result<(), Errno> {
        let (addr, port) = match sockaddr_family(&addr) {
            AddressFamily::Unix => {
                let unix = match target {
                    NetAddr::UnixPath(root) => {
                        // Case 1: UNIX domain socket
                        //
                        // Handle bind_map after successful bind(2) for UNIX sockets.
                        // We ignore errors because there's nothing we can do about them.
                        // We use original address structure for path for getsockname(2).
                        let _ = request.add_unix(
                            &fd,
                            request.scmpreq.pid(),
                            UnixVal {
                                addr: argaddr.as_unix_addr().copied(),
                                ..UnixVal::default()
                            },
                        );
                        drop(fd); // Close our copy of the socket.

                        if !allow_safe_bind {
                            return Ok(());
                        }

                        XPathCow::Owned(root.take())
                    }
                    NetAddr::Unix(name) => {
                        // Case 2: UNIX abstract socket
                        //
                        // Handle bind_map after successful bind for UNIX sockets.
                        // We ignore errors because there's nothing we can do
                        // about them.
                        // BindMap is only used for SO_PEERCRED for UNIX abstract sockets.
                        let _ = request.add_unix(&fd, request.scmpreq.pid(), UnixVal::default());
                        drop(fd); // Close our copy of the socket.

                        if !allow_safe_bind {
                            return Ok(());
                        }

                        XPathCow::Owned(name)
                    }
                    NetAddr::UnixUnnamed => {
                        // Case 3: unnamed UNIX socket.
                        let unix = if addr.as_unix_addr().map_or(0, |addr| addr.len()) as usize
                            == size_of::<libc::sa_family_t>()
                        {
                            // Autobind on abstract UNIX socket.
                            safe_getsockname::<SockaddrStorage>(fd.as_raw_fd())?
                                .as_unix_addr()
                                .ok_or(Errno::EINVAL)?
                                .as_abstract()
                                .map(|path| {
                                    // Prefix UNIX abstract sockets with `@' before access check.
                                    // Abstract socket names may contain embedded NUL bytes.
                                    let mut unix = XPathBuf::try_from("@")?;
                                    unix.try_append_bytes(path)?;
                                    Ok::<_, Errno>(unix)
                                })
                                .ok_or(Errno::EINVAL)?
                                .map(XPathCow::Owned)?
                        } else {
                            // Use dummy path `!unnamed' for unnamed UNIX sockets.
                            XPathCow::Borrowed(XPath::from_bytes(b"!unnamed"))
                        };

                        // Handle bind_map after successful bind for UNIX sockets.
                        // We ignore errors because there's nothing we can do
                        // about them.
                        // BindMap is only used for SO_PEERCRED for UNIX abstract sockets.
                        let _ = request.add_unix(&fd, request.scmpreq.pid(), UnixVal::default());
                        drop(fd); // Close our copy of the socket.

                        if !allow_safe_bind {
                            return Ok(());
                        }

                        unix
                    }
                    NetAddr::None | NetAddr::Inet(..) => {
                        unreachable!("BUG: non-UNIX target for AF_UNIX bind, report a bug!");
                    }
                };

                info!("ctx": "bind", "op": "allow_safe_bind",
                    "sys": "bind", "pid": request.scmpreq.pid().as_raw(), "unix": &unix,
                    "msg": xfmt!("add rule `allow/net/connect+{unix}' after bind"));

                let mut sandbox = request.get_mut_sandbox();
                if let Some(idx) = sandbox.glob_rules.iter().position(|(glob, act, cap)| {
                    glob.meth == MatchMethod::Literal
                        && *act == Action::Allow
                        && *cap == Capability::CAP_NET_CONNECT
                        && glob.glob.as_bytes() == unix.as_bytes()
                }) {
                    sandbox.glob_rules.remove(idx);
                }

                return sandbox.glob_rules.push_front((
                    XPathGlob::new(unix.try_into_owned()?, MatchMethod::Literal),
                    Action::Allow,
                    Capability::CAP_NET_CONNECT,
                ));
            }
            AddressFamily::Inet => {
                if !allow_safe_bind {
                    return Ok(());
                }

                let addr = addr.as_sockaddr_in().ok_or(Errno::EINVAL)?;
                let mut port = addr.port();
                let addr = IpNet::new_assert(IpAddr::V4(addr.ip()), 32);

                if port == 0 {
                    port = get_port(&fd)?;
                }
                drop(fd); // Close our copy of the socket.

                (addr, port)
            }
            AddressFamily::Inet6 => {
                if !allow_safe_bind {
                    return Ok(());
                }

                let addr = addr.as_sockaddr_in6().ok_or(Errno::EINVAL)?;
                let mut port = addr.port();
                let addr = addr.ip();
                let addr = if let Some(addr) = addr.to_ipv4_mapped() {
                    IpNet::new_assert(IpAddr::V4(addr), 32)
                } else {
                    IpNet::new_assert(IpAddr::V6(addr), 128)
                };

                if port == 0 {
                    port = get_port(&fd)?;
                }
                drop(fd); // Close our copy of the socket.

                (addr, port)
            }
            _ => return Ok(()),
        };

        // Configure sandbox:
        // Remove and re-add the address so repeated binds to the
        // same address cannot overflow the vector.
        let addr = AddressPattern {
            addr,
            port: port.into(),
            proto: None,
        };
        info!("ctx": "bind", "op": "allow_safe_bind",
            "sys": "bind", "pid": request.scmpreq.pid().as_raw(), "rule": &addr,
            "msg": xfmt!("add rule `allow/net/connect+{addr}' after bind"));

        let rule = CidrRule {
            act: Action::Allow,
            cap: Capability::CAP_NET_CONNECT,
            pat: addr,
        };

        let mut sandbox = request.get_mut_sandbox();
        match sandbox.cidr_rules.iter().position(|r| *r == rule) {
            Some(0) => {} // No need to re-add.
            Some(idx) => {
                sandbox.cidr_rules.remove(idx);
                sandbox.cidr_rules.push_front(rule)?;
            }
            None => sandbox.cidr_rules.push_front(rule)?,
        }

        // 1. The sandbox lock will be released on drop here.
        // 2. The socket fd will be closed on drop here.
        Ok(())
    })(fd, request);

    Ok(request.return_syscall(0))
}