Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHECKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ Inspect how the in-process executor assigns a session identifier to each check's

Every check runs against a copy-on-write clone of the working tree, so a misbehaving agent cannot mutate the user's real files. Both checks below must pass for this requirement to be satisfied.

## Check Sandbox Exists and Is Platform-Gated
## Check Sandbox Is Cross-Platform and Platform-Gated

Inspect the sandboxing code. Confirm there is a sandbox abstraction (a trait such as `Sandbox`) with a macOS copy-on-write implementation built on APFS `clonefile`, and that operating-system-specific code is selected with `cfg` attributes so the crate still compiles on non-macOS targets. The check fails if sandboxing is compiled unconditionally for a single operating system in a way that would break the build on other targets.
Inspect the sandboxing code. Confirm there is a sandbox abstraction (a trait such as `Sandbox`) with a working copy-on-write implementation on **both** macOS and Linux: macOS built on APFS `clonefile`, and Linux built on reflinks (the `FICLONE` ioctl) with a plain-copy fallback for filesystems that lack reflink support. Operating-system-specific code must be selected with `cfg` attributes so the crate still compiles on every target. The check fails if a real sandbox is created on only one of macOS or Linux (for example, if Linux falls through to a stub that returns an "unsupported platform" error instead of cloning the working tree), or if sandboxing is compiled unconditionally for a single operating system in a way that would break the build on other targets.

## Check Execution Uses the Sandbox

Expand Down
134 changes: 134 additions & 0 deletions src/checks/sandbox/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//! Linux copy-on-write sandbox via reflinks (the `FICLONE` ioctl).
//!
//! Linux has no whole-tree clone syscall like macOS's `clonefile`, so we
//! recreate the directory structure under a fresh temp directory and clone each
//! regular file with `FICLONE` — a reflink, i.e. a metadata-only copy-on-write
//! share of the file's extents. `FICLONE` is only supported on copy-on-write
//! filesystems (Btrfs, XFS with `reflink=1`, bcachefs, ...); on others (ext4,
//! tmpfs) it fails and we fall back to a plain byte copy, so the sandbox is
//! still an independent clone of the tree — just without the CoW space savings.
//! The clone lives under a temp directory and is removed when the
//! [`SandboxHandle`] is dropped.

use std::fs::{self, File, OpenOptions};
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::os::unix::io::AsRawFd;
use std::path::Path;

use async_trait::async_trait;
use miette::{IntoDiagnostic, Result, miette};

use super::{Sandbox, SandboxHandle};

/// Reflink (`FICLONE`)-backed sandbox with a plain-copy fallback.
#[derive(Default)]
pub struct ReflinkSandbox;

impl ReflinkSandbox {
pub fn new() -> Self {
Self
}
}

#[async_trait]
impl Sandbox for ReflinkSandbox {
async fn create(&self, source: &Path) -> Result<SandboxHandle> {
let source = source.to_path_buf();
// Cloning a tree is blocking filesystem work — keep it off the runtime.
tokio::task::spawn_blocking(move || clone_tree(&source))
.await
.into_diagnostic()?
}
}

fn clone_tree(source: &Path) -> Result<SandboxHandle> {
let temp = tempfile::Builder::new()
.prefix("multi-check-")
.tempdir()
.into_diagnostic()?;
// Mirror the macOS layout: the clone root is a `sandbox` directory under the
// temp directory, which owns teardown.
let dest = temp.path().join("sandbox");

copy_dir(source, &dest)?;

Ok(SandboxHandle {
root: dest,
_temp: Some(temp),
})
}

/// Recursively recreate `source` at `dest`, reflinking regular files where the
/// filesystem supports it and copying them otherwise. Directories are recreated
/// and symlinks are copied verbatim (the link, not its target).
fn copy_dir(source: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest).into_diagnostic()?;
// Carry the directory's permission bits across.
let perms = fs::metadata(source).into_diagnostic()?.permissions();
fs::set_permissions(dest, perms).into_diagnostic()?;

for entry in fs::read_dir(source).into_diagnostic()? {
let entry = entry.into_diagnostic()?;
let file_type = entry.file_type().into_diagnostic()?;
let src_path = entry.path();
let dst_path = dest.join(entry.file_name());

if file_type.is_dir() {
copy_dir(&src_path, &dst_path)?;
} else if file_type.is_symlink() {
let target = fs::read_link(&src_path).into_diagnostic()?;
std::os::unix::fs::symlink(target, &dst_path).into_diagnostic()?;
} else {
reflink_or_copy(&src_path, &dst_path)?;
}
}
Ok(())
}

/// Clone one regular file with a reflink, falling back to a byte copy when the
/// filesystem does not support `FICLONE`.
fn reflink_or_copy(src: &Path, dst: &Path) -> Result<()> {
if reflink(src, dst).is_ok() {
return Ok(());
}
// Fallback: a plain copy still yields an independent file (just no CoW).
// `fs::copy` carries the permission bits across as well.
fs::copy(src, dst).map_err(|err| {
miette!(
"copying {} to {} failed: {err}",
src.display(),
dst.display()
)
})?;
Ok(())
}

/// Attempt a `FICLONE` reflink of `src` into a freshly created `dst`. Returns an
/// error (leaving no destination file behind) if the filesystem or targets do
/// not support reflinking.
fn reflink(src: &Path, dst: &Path) -> std::io::Result<()> {
let src_file = File::open(src)?;
let perms = src_file.metadata()?.permissions();
let dst_file = OpenOptions::new()
.write(true)
.create_new(true)
.mode(perms.mode())
.open(dst)?;

// SAFETY: both descriptors are valid and owned for the duration of the call.
// `FICLONE` links the source fd's extents into the (empty) destination fd and
// retains neither descriptor.
let ret = unsafe { libc::ioctl(dst_file.as_raw_fd(), libc::FICLONE, src_file.as_raw_fd()) };
if ret != 0 {
let err = std::io::Error::last_os_error();
// Discard the empty destination we created so the copy fallback can
// recreate it with `create_new`.
drop(dst_file);
let _ = fs::remove_file(dst);
return Err(err);
}
// The open above applied the umask to `mode`; restore the source's exact
// permission bits so the clone matches (as `fs::copy` does on the fallback).
dst_file.set_permissions(perms)?;
Ok(())
}
24 changes: 16 additions & 8 deletions src/checks/sandbox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
//! and modify files freely without corrupting the real working directory. The
//! abstraction is a boxed trait object (mirroring `BoxedIngress` etc.); the
//! concrete implementation is selected per platform via `cfg`. macOS ships an
//! APFS `clonefile` implementation; other targets get a stub that errors, so the
//! crate still builds everywhere.
//! APFS `clonefile` implementation and Linux a reflink (`FICLONE`)
//! implementation; any remaining target gets a stub that errors, so the crate
//! still builds everywhere.

use std::path::{Path, PathBuf};

Expand All @@ -15,7 +16,10 @@ use miette::Result;
#[cfg(target_os = "macos")]
mod macos;

#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
mod linux;

#[cfg(not(any(target_os = "macos", target_os = "linux")))]
mod fallback;

/// A copy-on-write sandbox factory.
Expand Down Expand Up @@ -47,15 +51,19 @@ impl SandboxHandle {

/// Select the platform sandbox implementation.
///
/// macOS → the real APFS CoW sandbox. Other platforms → an unsupported stub
/// that fails with a clear diagnostic (CoW support for Linux/Windows is tracked
/// under *Future work*).
/// macOS → the APFS `clonefile` CoW sandbox. Linux → the reflink (`FICLONE`) CoW
/// sandbox. Any other platform → an unsupported stub that fails with a clear
/// diagnostic (Windows CoW support is tracked under *Future work*).
pub fn select_sandbox() -> BoxedSandbox {
#[cfg(target_os = "macos")]
{
Box::new(macos::ApfsSandbox::new())
}
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
{
Box::new(linux::ReflinkSandbox::new())
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Box::new(fallback::UnsupportedSandbox)
}
Expand Down Expand Up @@ -85,7 +93,7 @@ mod tests {
assert_obj_safe!(Sandbox);

#[tokio::test]
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
async fn clone_is_independent_of_source() {
use std::fs;
let src = tempfile::TempDir::new().unwrap();
Expand Down