AkurAI Build
Menu

AkurAI-Build

public

Latest change fe39ce51fc9c202d5411f39fd9a8a2feeca4a85e - Harden test suite: 16→179 hermetic tests, race-free integration harness by Ólafur Búi Ólafsson

//! Shared hardened filesystem helpers.
//!
//! One implementation of symlink-free opens and descriptor/path identity
//! pinning, used by the artifact, database, and server layers so future
//! fixes to these security-critical checks cannot diverge (architecture
//! review 10, finding 6).

use std::{
    fs::{self, File, OpenOptions},
    path::Path,
};

use anyhow::{Context, Result, ensure};

/// Open an existing file without following symlinks and require it to be
/// a regular file. `write` additionally requests write access.
pub(crate) fn open_nofollow(path: &Path, write: bool) -> Result<File> {
    let mut options = OpenOptions::new();
    options.read(true).write(write);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        options.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
    }
    let file = options
        .open(path)
        .with_context(|| format!("open {} without following symlinks", path.display()))?;
    ensure!(
        file.metadata()?.is_file(),
        "{} is not a regular file",
        path.display()
    );
    Ok(file)
}

/// Re-check that `path` still names exactly the open descriptor `file`
/// (same device and inode, still a regular non-symlink file), closing the
/// verify-then-use window against path swaps.
pub(crate) fn ensure_path_matches_file(path: &Path, file: &File, label: &str) -> Result<()> {
    let path_metadata = fs::symlink_metadata(path)
        .with_context(|| format!("inspect {label} {}", path.display()))?;
    ensure!(
        path_metadata.is_file() && !path_metadata.file_type().is_symlink(),
        "{label} path changed while in use: {}",
        path.display()
    );
    let file_metadata = file.metadata()?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::MetadataExt;
        ensure!(
            path_metadata.dev() == file_metadata.dev()
                && path_metadata.ino() == file_metadata.ino(),
            "{label} path changed while in use: {}",
            path.display()
        );
    }
    #[cfg(not(unix))]
    let _ = file_metadata;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::os::unix::fs::symlink;

    #[test]
    fn opens_regular_file() -> Result<()> {
        let dir = tempfile::tempdir()?;
        let path = dir.path().join("data.txt");
        fs::write(&path, b"hello")?;
        let file = open_nofollow(&path, false)?;
        assert!(file.metadata()?.is_file());
        Ok(())
    }

    #[test]
    fn opens_file_for_writing() -> Result<()> {
        let dir = tempfile::tempdir()?;
        let path = dir.path().join("data.txt");
        fs::write(&path, b"hello")?;
        let _file = open_nofollow(&path, true)?;
        Ok(())
    }

    #[test]
    fn rejects_symlink() -> Result<()> {
        let dir = tempfile::tempdir()?;
        let real = dir.path().join("real.txt");
        let link = dir.path().join("link.txt");
        fs::write(&real, b"hello")?;
        symlink(&real, &link)?;
        assert!(open_nofollow(&link, false).is_err());
        Ok(())
    }

    #[test]
    fn rejects_directory() -> Result<()> {
        let dir = tempfile::tempdir()?;
        assert!(open_nofollow(dir.path(), false).is_err());
        Ok(())
    }

    #[test]
    fn rejects_nonexistent_file() {
        let path = std::path::Path::new("/nonexistent/path/file.txt");
        assert!(open_nofollow(path, false).is_err());
    }

    #[test]
    fn ensure_path_matches_file_succeeds_for_unchanged() -> Result<()> {
        let dir = tempfile::tempdir()?;
        let path = dir.path().join("data.txt");
        fs::write(&path, b"hello")?;
        let file = open_nofollow(&path, false)?;
        ensure_path_matches_file(&path, &file, "test")?;
        Ok(())
    }

    #[test]
    fn ensure_path_matches_file_rejects_swapped_symlink() -> Result<()> {
        let dir = tempfile::tempdir()?;
        let original = dir.path().join("original.txt");
        let outside = std::env::temp_dir().join("fsguard-outside-target.txt");
        let _cleanup = std::fs::File::create(&outside)?;

        // Write to the real file first, then open it
        fs::write(&original, b"original")?;
        let file = open_nofollow(&original, false)?;

        // Replace the original with a symlink pointing outside
        fs::remove_file(&original)?;
        symlink(&outside, &original)?;

        // The path now points to outside content — should be rejected
        assert!(ensure_path_matches_file(&original, &file, "test").is_err());

        // Cleanup
        let _ = fs::remove_file(&outside);
        Ok(())
    }
}