syd 3.58.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/kernel/pipe.rs: pipe(2) and pipe2(2) syscall handlers
//
// Copyright (c) 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{mem::size_of, os::fd::RawFd};

use libc::c_int;
use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, fcntl::OFlag};

use crate::{
    confine::scmp_arch_has_legacy_pipe,
    cookie::{safe_exit_group, safe_pipe2},
    fd::{get_pipe_size, set_pipe_size},
    log_enabled,
    path::XPath,
    req::UNotifyEventRequest,
    sandbox::{Action, Capability, SandboxGuard},
    syslog::LogLevel,
    warn, xfmt,
};

pub(crate) fn sys_pipe(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;
        let legacy = scmp_arch_has_legacy_pipe(req.data.arch);

        handle_pipe(&request, req.data.args[0], OFlag::empty(), legacy)
    })
}

pub(crate) fn sys_pipe2(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;
        let flags = to_pipe2_flags(req.data.args[1])?;
        handle_pipe(&request, req.data.args[0], flags, false /*legacy*/)
    })
}

fn handle_pipe(
    request: &UNotifyEventRequest,
    fdptr: u64,
    mut flags: OFlag,
    legacy: bool,
) -> Result<ScmpNotifResp, Errno> {
    let notification = flags.contains(O_NOTIFICATION_PIPE);

    // Check for sandbox access.
    let sandbox = request.get_sandbox();
    let force_cloexec = sandbox.flags.force_cloexec();
    let force_rand_fd = sandbox.flags.force_rand_fd();
    let pipe_max = sandbox.pipe_max;
    sandbox_pipe(request, &sandbox, notification)?;

    if legacy {
        // Legacy pipe(2) can't be emulated on MIPS.
        // SAFETY: Access check depends on syscall only.
        return Ok(unsafe { request.continue_syscall() });
    }
    drop(sandbox); // release the read lock.

    let cloexec = force_cloexec || flags.contains(OFlag::O_CLOEXEC);
    flags.insert(OFlag::O_CLOEXEC);
    let (fd0, fd1) = safe_pipe2(flags)?;

    // Confine pipe buffer size to pipe/max.
    // Notification pipes change buffer size with IOC_WATCH_QUEUE_SET_SIZE.
    if !notification && get_pipe_size(&fd0)? > pipe_max {
        set_pipe_size(&fd0, pipe_max)?;
    }

    // Ensure memory is writable before installing fds.
    //
    // 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);
    }

    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)?;

    // pipe(2) and pipe2(2) return 0 on success.
    Ok(request.return_syscall(0))
}

#[expect(clippy::cognitive_complexity)]
fn sandbox_pipe(
    request: &UNotifyEventRequest,
    sandbox: &SandboxGuard<'_>,
    notification: bool,
) -> Result<(), Errno> {
    let caps = Capability::CAP_CREATE;
    let name = if notification {
        XPath::from_bytes(b"!notification_pipe")
    } else {
        XPath::from_bytes(b"!pipe")
    };

    // Notification pipes are disabled at startup,
    // unless trace/allow_unsafe_pipe:1 is specified.
    let deny_notif = notification && !sandbox.options.allow_unsafe_pipe();

    if sandbox.getcaps(caps).is_empty() {
        // Sandboxing is off.
        return if deny_notif {
            Err(Errno::ENOPKG)
        } else {
            Ok(())
        };
    }
    let action = sandbox.check_name(caps, name);

    if action.is_logging() && log_enabled!(LogLevel::Warn) {
        if sandbox.log_scmp() {
            warn!("ctx": "access", "cap": caps, "act": action,
                "sys": request.syscall, "path": &name,
                "tip": xfmt!("configure `allow/{caps}+{name}'"),
                "req": request);
        } else {
            warn!("ctx": "access", "cap": caps, "act": action,
                "sys": request.syscall, "path": &name,
                "tip": xfmt!("configure `allow/{caps}+{name}'"),
                "pid": request.scmpreq.pid);
        }
    }

    let deny_errno = if notification {
        Errno::ENOPKG
    } else {
        Errno::ENOMEM
    };

    match action {
        Action::Allow | Action::Warn if deny_notif => Err(Errno::ENOPKG),
        Action::Allow | Action::Warn => Ok(()),
        Action::Deny | Action::Filter => Err(deny_errno),
        Action::Panic => panic!(),
        Action::Exit => safe_exit_group(deny_errno as i32),
        action => {
            // Stop|Kill
            let _ = request.kill(action);
            Err(deny_errno)
        }
    }
}

// O_NOTIFICATION_PIPE aliases O_EXCL.
const O_NOTIFICATION_PIPE: OFlag = OFlag::O_EXCL;

// Valid flags for pipe2(2).
const PIPE2_VALID: OFlag = OFlag::O_CLOEXEC
    .union(OFlag::O_NONBLOCK)
    .union(OFlag::O_DIRECT)
    .union(O_NOTIFICATION_PIPE);

fn to_pipe2_flags(arg: u64) -> Result<OFlag, Errno> {
    // Linux truncates upper bits.
    #[expect(clippy::cast_possible_truncation)]
    let flags = arg as c_int;

    // Reject invalid bits.
    let flags = OFlag::from_bits_retain(flags);
    if !flags.difference(PIPE2_VALID).is_empty() {
        return Err(Errno::EINVAL);
    }

    Ok(flags)
}