syd 3.52.0

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

use libseccomp::ScmpArch;
use nix::{
    errno::Errno,
    sys::signal::{kill, Signal},
    unistd::Pid,
};

use crate::{
    error,
    kernel::sandbox_path,
    lookup::{CanonicalPath, FileMapEntry, FileType, FsFlags},
    magic::ProcMagic,
    ptrace::{ptrace_get_error, ptrace_syscall_info, ptrace_syscall_info_seccomp},
    req::{RemoteProcess, SysArg},
    sandbox::{Capability, Sandbox, SandboxGuard},
};

// chdir(2) is a ptrace(2) hook, not a seccomp hook!
// seccomp(2) hook is only used with trace/allow_unsafe_ptrace:1.
pub(crate) fn sysenter_chdir(
    pid: Pid,
    sandbox: &SandboxGuard,
    arch: ScmpArch,
    data: ptrace_syscall_info_seccomp,
) -> Result<(), Errno> {
    let mut arg = SysArg {
        path: Some(0),
        ..Default::default()
    };

    if sandbox.flags.deny_dotdot() {
        // Apply trace/deny_dotdot for chdir(2).
        arg.fsflags.insert(FsFlags::NO_RESOLVE_DOTDOT);
    }

    // Read remote path.
    let process = RemoteProcess::new(pid);

    // This is a ptrace(2) hook, the PID cannot be validated.
    let (path, _, _) = process.read_path(sandbox, arch, data.args, arg, None)?;

    // Check for chroot, allow for the common `cd /` use case.
    if sandbox.is_chroot() {
        return if path.abs().is_root() {
            Ok(())
        } else {
            Err(Errno::ENOENT)
        };
    }

    sandbox_chdir(sandbox, pid, &path, "chdir")?;

    Ok(())
}

// fchdir is a ptrace(2) hook, not a seccomp hook!
// seccomp(2) hook is only used with trace/allow_unsafe_ptrace:1.
pub(crate) fn sysenter_fchdir(
    pid: Pid,
    sandbox: &SandboxGuard,
    arch: ScmpArch,
    data: ptrace_syscall_info_seccomp,
) -> Result<(), Errno> {
    let arg = SysArg {
        dirfd: Some(0),
        ..Default::default()
    };

    // Read remote path.
    let process = RemoteProcess::new(pid);

    // This is a ptrace(2) hook, the PID cannot be validated.
    let (path, _, _) = process.read_path(sandbox, arch, data.args, arg, None /*request*/)?;

    // Check for chroot, allow for the common `cd /` use case.
    if sandbox.is_chroot() {
        return if path.abs().is_root() {
            Ok(())
        } else {
            Err(Errno::ENOENT)
        };
    }

    sandbox_chdir(sandbox, pid, &path, "fchdir")?;

    Ok(())
}

pub(crate) fn sysexit_chdir(
    pid: Pid,
    info: ptrace_syscall_info,
    sandbox: &Sandbox,
) -> Result<(), Errno> {
    // Check for successful chdir exit.
    match ptrace_get_error(pid, info.arch) {
        Ok(None) => {
            // Successful chdir call, validate CWD magiclink.
        }
        Ok(Some(_)) => {
            // Unsuccessful chdir call, continue process.
            return Ok(());
        }
        Err(Errno::ESRCH) => return Err(Errno::ESRCH),
        Err(_) => {
            // Failed to get return value, terminate the process.
            let _ = kill(pid, Some(Signal::SIGKILL));
            return Err(Errno::ESRCH);
        }
    };

    // Validate /proc/$pid/cwd against TOCTTOU!
    if let Err(errno) = sandbox_chdir_atexit(sandbox, pid) {
        // CWD outside sandbox, which indicates successful TOCTTOU
        // attempt: Terminate the process.
        error!("ctx": "chdir", "op": "dir_mismatch",
            "msg": "dir mismatch detected: assume TOCTTOU!",
            "pid": pid.as_raw(), "err": errno as i32);
        let _ = kill(pid, Some(Signal::SIGKILL));
        Err(Errno::ESRCH)
    } else {
        // Continue process.
        Ok(())
    }
}

fn sandbox_chdir_atexit(sandbox: &Sandbox, pid: Pid) -> Result<(), Errno> {
    let magic = ProcMagic::Cwd { pid };
    let (mut entry, _) = FileMapEntry::from_magic_link(magic, true, None, Some(sandbox))?;

    let path = CanonicalPath {
        abs: entry.target.take().ok_or(Errno::ENOENT)??,
        base_offset: 0,
        dir: Some(entry.fd),
        typ: Some(FileType::Dir),
    };
    sandbox_chdir(sandbox, pid, &path, "chdir")
}

fn sandbox_chdir(
    sandbox: &Sandbox,
    pid: Pid,
    path: &CanonicalPath,
    sysname: &str,
) -> Result<(), Errno> {
    let mut caps = Capability::empty();
    if let Some(typ) = path.typ.as_ref() {
        if typ.is_dir() {
            caps.insert(Capability::CAP_CHDIR);
        }
    } else {
        return Err(Errno::ENOENT);
    }

    sandbox_path(None, sandbox, pid, path.abs(), caps, sysname)?;

    if !caps.contains(Capability::CAP_CHDIR) {
        // Return this after sandboxing to honour hidden paths.
        return Err(Errno::ENOTDIR);
    }

    Ok(())
}