syd 3.54.1

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, unistd::Pid};

use crate::{
    cookie::safe_kill,
    error,
    fd::PROC_FILE,
    kernel::sandbox_path,
    lookup::{CanonicalPath, FileInfo, FsFlags},
    path::XPathBuf,
    ptrace::{ptrace_get_error, ptrace_syscall_info, ptrace_syscall_info_seccomp},
    req::{RemoteProcess, SysArg},
    sandbox::{Capability, 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<FileInfo, 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() {
        if !path.abs().is_root() {
            return Err(Errno::ENOENT);
        }

        // Record dir information to check at exit.
        return FileInfo::from_fd(&path.dir);
    }

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

    // Record dir information to check at exit.
    FileInfo::from_fd(&path.dir)
}

// 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<FileInfo, 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() {
        if !path.abs().is_root() {
            return Err(Errno::ENOENT);
        }

        // Record dir information to check at exit.
        return FileInfo::from_fd(&path.dir);
    }

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

    // Record dir information to check at exit.
    FileInfo::from_fd(&path.dir)
}

pub(crate) fn sysexit_chdir(
    pid: Pid,
    info: ptrace_syscall_info,
    file_info: FileInfo,
) -> 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 _ = safe_kill(pid, libc::SIGKILL);
            return Err(Errno::ESRCH);
        }
    };

    // Validate /proc/$pid/cwd against TOCTTOU!
    let cwd_sym = XPathBuf::from_cwd(pid)?;
    let file_info_new = FileInfo::from_path(PROC_FILE(), &cwd_sym, true)?;

    if file_info_new != file_info {
        // TOCTOU detected, terminate process.
        error!("ctx": "chdir", "op": "dir_mismatch",
            "msg": "dir mismatch detected: assume TOCTTOU!",
            "pid": pid.as_raw(),
            "mnt": [file_info.mnt, file_info_new.mnt],
            "ino": [file_info.ino, file_info_new.ino]);
        let _ = safe_kill(pid, libc::SIGKILL);
        Err(Errno::ESRCH)
    } else {
        // Continue process.
        Ok(())
    }
}

fn sandbox_chdir(
    sandbox: &SandboxGuard,
    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(())
}