From 224a9312b60e9d33dc0c3eb496e465f3cbe8a231 Mon Sep 17 00:00:00 2001 From: colafornia Date: Sat, 14 Nov 2026 21:57:06 +0800 Subject: [PATCH 1/2] fix(sandbox): preserve Linux overlayfs directory identity across copy-up Generated-by: Google Antigravity --- platform/sandbox/src/workspace/directory.rs | 63 +++++++++++++++--- .../sandbox/src/workspace/directory/linux.rs | 66 ++++++++++++++++--- 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/platform/sandbox/src/workspace/directory.rs b/platform/sandbox/src/workspace/directory.rs index 061e3156e9..d65968fb2f 100644 --- a/platform/sandbox/src/workspace/directory.rs +++ b/platform/sandbox/src/workspace/directory.rs @@ -30,16 +30,23 @@ mod linux; fn basis(directory: &Dir) -> io::Result { let metadata = directory.dir_metadata()?; #[cfg(target_os = "macos")] - let volume = volume_uuid(directory)?; + let (volume, stable_birth) = (volume_uuid(directory)?, true); #[cfg(target_os = "linux")] - let volume = linux::volume(directory)?; + let linux::Volume { + id: volume, + stable_birth, + } = linux::volume(directory)?; #[cfg(windows)] - let volume = metadata.dev(); // cap-fs-ext returns the filesystem's volume serial number. - let birth = metadata - .created() - .ok() - .and_then(|time| time.into_std().duration_since(UNIX_EPOCH).ok()) - .map(|duration| (duration.as_secs(), duration.subsec_nanos())); + let (volume, stable_birth) = (metadata.dev(), true); // cap-fs-ext returns the filesystem's volume serial number. + let birth = if stable_birth { + metadata + .created() + .ok() + .and_then(|time| time.into_std().duration_since(UNIX_EPOCH).ok()) + .map(|duration| (duration.as_secs(), duration.subsec_nanos())) + } else { + None + }; Ok((volume, metadata.ino(), birth)) } @@ -117,10 +124,50 @@ mod tests { std::fs::create_dir(&path).unwrap(); let directory = Dir::open_ambient_dir(&path, cap_std::ambient_authority()).unwrap(); let grant = DirectoryIdentity::capture(&directory).unwrap(); + + let probe = path.join(".probe"); + std::fs::write(&probe, b"ok").unwrap(); + std::fs::remove_file(&probe).unwrap(); + let same_reopened = Dir::open_ambient_dir(&path, cap_std::ambient_authority()).unwrap(); + assert!(grant.matches(&same_reopened).unwrap()); + std::fs::rename(&path, temp.path().join("original")).unwrap(); std::fs::create_dir(&path).unwrap(); let replacement = Dir::open_ambient_dir(&path, cap_std::ambient_authority()).unwrap(); assert!(!grant.matches(&replacement).unwrap()); assert!(grant.matches(&directory).unwrap()); } + + #[cfg(target_os = "linux")] + #[test] + fn captured_grant_survives_linux_overlayfs_copy_up() { + let Some(target) = std::env::var_os("MAKA_TEST_OVERLAYFS_DIR") else { + return; + }; + let path = std::path::PathBuf::from(target); + let directory = Dir::open_ambient_dir(&path, cap_std::ambient_authority()).unwrap(); + + use std::os::fd::AsRawFd; + let mut statfs = std::mem::MaybeUninit::::uninit(); + assert_eq!( + unsafe { libc::fstatfs(directory.as_raw_fd(), statfs.as_mut_ptr()) }, + 0 + ); + let statfs = unsafe { statfs.assume_init() }; + assert_eq!(statfs.f_type, libc::OVERLAYFS_SUPER_MAGIC); + + let before_birth = directory.dir_metadata().unwrap().created().ok(); + let grant = DirectoryIdentity::capture(&directory).unwrap(); + + let probe = tempfile::NamedTempFile::new_in(&path).unwrap(); + drop(probe); + + let after_dir = Dir::open_ambient_dir(&path, cap_std::ambient_authority()).unwrap(); + let after_birth = after_dir.dir_metadata().unwrap().created().ok(); + assert_ne!( + before_birth, after_birth, + "expected overlayfs copy-up to update directory birth time" + ); + assert!(grant.matches(&after_dir).unwrap()); + } } diff --git a/platform/sandbox/src/workspace/directory/linux.rs b/platform/sandbox/src/workspace/directory/linux.rs index 8a3e1e9538..a0e10d86d3 100644 --- a/platform/sandbox/src/workspace/directory/linux.rs +++ b/platform/sandbox/src/workspace/directory/linux.rs @@ -20,7 +20,13 @@ use cap_std::fs::Dir; use std::{io, os::fd::AsRawFd}; -pub(super) fn volume(directory: &Dir) -> io::Result { +#[derive(Debug)] +pub(super) struct Volume { + pub(super) id: String, + pub(super) stable_birth: bool, +} + +pub(super) fn volume(directory: &Dir) -> io::Result { // SAFETY: fstatfs initializes the struct on success, and the descriptor is held. let mut filesystem = std::mem::MaybeUninit::::uninit(); let result = unsafe { libc::fstatfs(directory.as_raw_fd(), filesystem.as_mut_ptr()) }; @@ -28,21 +34,34 @@ pub(super) fn volume(directory: &Dir) -> io::Result { return Err(io::Error::last_os_error()); } let filesystem = unsafe { filesystem.assume_init() }; + volume_from_statfs(directory, &filesystem) +} + +fn volume_from_statfs(directory: &Dir, filesystem: &libc::statfs) -> io::Result { // Cap-std may hold an O_PATH directory. Ioctls use a read handle to its `.`. match filesystem.f_type { - libc::EXT4_SUPER_MAGIC | libc::TMPFS_MAGIC => { - // These filesystems derive f_fsid from their superblock UUID. + libc::EXT4_SUPER_MAGIC | libc::TMPFS_MAGIC | libc::OVERLAYFS_SUPER_MAGIC => { + // ext4/tmpfs derive f_fsid from their superblock UUID; overlayfs keeps a + // stable superblock f_fsid across copy-up, whereas ovl_getattr switches + // stx_btime from the lower inode to the newly created upper inode on first + // directory copy-up (making birth timestamps unstable on overlayfs). // SAFETY: Linux fsid_t has two C int fields and both bit patterns are valid. let [first, second]: [u32; 2] = unsafe { std::mem::transmute(filesystem.f_fsid) }; if [first, second] == [0, 0] { return Err(no_identity()); } - Ok(format!( - "linux-fsid:{:08x}:{first:08x}:{second:08x}", - filesystem.f_type - )) + Ok(Volume { + id: format!( + "linux-fsid:{:08x}:{first:08x}:{second:08x}", + filesystem.f_type + ), + stable_birth: filesystem.f_type != libc::OVERLAYFS_SUPER_MAGIC, + }) } - libc::BTRFS_SUPER_MAGIC => btrfs_volume(directory), + libc::BTRFS_SUPER_MAGIC => Ok(Volume { + id: btrfs_volume(directory)?, + stable_birth: true, + }), libc::XFS_SUPER_MAGIC => { // XFS f_fsid is a device number. Read its on-disk UUID via XFS_IOC_FSGEOMETRY_V1. #[repr(C)] @@ -59,7 +78,10 @@ pub(super) fn volume(directory: &Dir) -> io::Result { flags: [0; 7], }; ioctl_read(&directory.open(".")?, b'X', 100, &mut geometry)?; - uuid(&geometry.uuid) + Ok(Volume { + id: uuid(&geometry.uuid)?, + stable_birth: true, + }) } _ => { // FS_IOC_GETFSUUID supplies the filesystem UUID on other filesystems @@ -67,7 +89,10 @@ pub(super) fn volume(directory: &Dir) -> io::Result { let mut fsuuid = [0_u8; 17]; ioctl_read(&directory.open(".")?, 0x15, 0, &mut fsuuid)?; let size = usize::from(fsuuid[0]); - uuid(fsuuid.get(1..1 + size).ok_or_else(no_identity)?) + Ok(Volume { + id: uuid(fsuuid.get(1..1 + size).ok_or_else(no_identity)?)?, + stable_birth: true, + }) } } } @@ -152,3 +177,24 @@ fn no_identity() -> io::Error { "filesystem has no stable volume identity", ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn overlayfs_statfs_requires_nonzero_fsid_and_marks_birth_unstable() { + let dir = tempfile::tempdir().unwrap(); + let opened = Dir::open_ambient_dir(dir.path(), cap_std::ambient_authority()).unwrap(); + let mut stat: libc::statfs = unsafe { std::mem::zeroed() }; + stat.f_type = libc::OVERLAYFS_SUPER_MAGIC; + + let zero_err = volume_from_statfs(&opened, &stat).unwrap_err(); + assert_eq!(zero_err.kind(), io::ErrorKind::Unsupported); + + stat.f_fsid = unsafe { std::mem::transmute([0x1234_5678_u32, 0x9abc_def0_u32]) }; + let parsed = volume_from_statfs(&opened, &stat).unwrap(); + assert_eq!(parsed.id, "linux-fsid:794c7630:12345678:9abcdef0"); + assert!(!parsed.stable_birth); + } +} From 0daa2ef6dc612300b893f82d27de368144c60509 Mon Sep 17 00:00:00 2001 From: colafornia Date: Sat, 14 Nov 2026 23:56:44 +0800 Subject: [PATCH 2/2] fix(sandbox): require an overlayfs instance UUID for directory grants Reuse FS_IOC_GETFSUUID instead of accepting a shared backing fsid. Reject overlays without an instance UUID while preserving copy-up stability. Replace the synthetic fsid test with an explicit live-mount rejection test. Generated-by: OpenAI Codex --- platform/sandbox/src/workspace/directory.rs | 48 +++++++++++++------ .../sandbox/src/workspace/directory/linux.rs | 41 +++------------- 2 files changed, 41 insertions(+), 48 deletions(-) diff --git a/platform/sandbox/src/workspace/directory.rs b/platform/sandbox/src/workspace/directory.rs index d65968fb2f..2802191444 100644 --- a/platform/sandbox/src/workspace/directory.rs +++ b/platform/sandbox/src/workspace/directory.rs @@ -140,21 +140,9 @@ mod tests { #[cfg(target_os = "linux")] #[test] + #[ignore = "requires a fresh lower-layer overlayfs directory in MAKA_TEST_OVERLAYFS_DIR"] fn captured_grant_survives_linux_overlayfs_copy_up() { - let Some(target) = std::env::var_os("MAKA_TEST_OVERLAYFS_DIR") else { - return; - }; - let path = std::path::PathBuf::from(target); - let directory = Dir::open_ambient_dir(&path, cap_std::ambient_authority()).unwrap(); - - use std::os::fd::AsRawFd; - let mut statfs = std::mem::MaybeUninit::::uninit(); - assert_eq!( - unsafe { libc::fstatfs(directory.as_raw_fd(), statfs.as_mut_ptr()) }, - 0 - ); - let statfs = unsafe { statfs.assume_init() }; - assert_eq!(statfs.f_type, libc::OVERLAYFS_SUPER_MAGIC); + let (path, directory) = overlayfs_directory(); let before_birth = directory.dir_metadata().unwrap().created().ok(); let grant = DirectoryIdentity::capture(&directory).unwrap(); @@ -170,4 +158,36 @@ mod tests { ); assert!(grant.matches(&after_dir).unwrap()); } + + #[cfg(target_os = "linux")] + #[test] + #[ignore = "requires an overlayfs uuid=off/null mount in MAKA_TEST_OVERLAYFS_DIR"] + fn captured_grant_rejects_linux_overlayfs_without_uuid() { + let (_, directory) = overlayfs_directory(); + assert_eq!( + DirectoryIdentity::capture(&directory).unwrap_err().kind(), + io::ErrorKind::Unsupported + ); + } + + #[cfg(target_os = "linux")] + fn overlayfs_directory() -> (std::path::PathBuf, Dir) { + use std::os::fd::AsRawFd; + + let path = std::path::PathBuf::from( + std::env::var_os("MAKA_TEST_OVERLAYFS_DIR").expect("overlayfs fixture directory"), + ); + let directory = Dir::open_ambient_dir(&path, cap_std::ambient_authority()).unwrap(); + let mut statfs = std::mem::MaybeUninit::::uninit(); + // SAFETY: fstatfs initializes the struct on success, and directory holds the fd. + let result = unsafe { libc::fstatfs(directory.as_raw_fd(), statfs.as_mut_ptr()) }; + assert_eq!(result, 0); + // SAFETY: fstatfs succeeded and initialized the struct. + let statfs = unsafe { statfs.assume_init() }; + assert_eq!(statfs.f_type, libc::OVERLAYFS_SUPER_MAGIC); + // SAFETY: Linux fsid_t contains two C int fields. + let fsid: [u32; 2] = unsafe { std::mem::transmute(statfs.f_fsid) }; + assert_ne!(fsid, [0, 0], "fixture must have a nonzero backing fsid"); + (path, directory) + } } diff --git a/platform/sandbox/src/workspace/directory/linux.rs b/platform/sandbox/src/workspace/directory/linux.rs index a0e10d86d3..00aa9a7110 100644 --- a/platform/sandbox/src/workspace/directory/linux.rs +++ b/platform/sandbox/src/workspace/directory/linux.rs @@ -20,7 +20,6 @@ use cap_std::fs::Dir; use std::{io, os::fd::AsRawFd}; -#[derive(Debug)] pub(super) struct Volume { pub(super) id: String, pub(super) stable_birth: bool, @@ -34,17 +33,10 @@ pub(super) fn volume(directory: &Dir) -> io::Result { return Err(io::Error::last_os_error()); } let filesystem = unsafe { filesystem.assume_init() }; - volume_from_statfs(directory, &filesystem) -} - -fn volume_from_statfs(directory: &Dir, filesystem: &libc::statfs) -> io::Result { // Cap-std may hold an O_PATH directory. Ioctls use a read handle to its `.`. match filesystem.f_type { - libc::EXT4_SUPER_MAGIC | libc::TMPFS_MAGIC | libc::OVERLAYFS_SUPER_MAGIC => { - // ext4/tmpfs derive f_fsid from their superblock UUID; overlayfs keeps a - // stable superblock f_fsid across copy-up, whereas ovl_getattr switches - // stx_btime from the lower inode to the newly created upper inode on first - // directory copy-up (making birth timestamps unstable on overlayfs). + libc::EXT4_SUPER_MAGIC | libc::TMPFS_MAGIC => { + // These filesystems derive f_fsid from their superblock UUID. // SAFETY: Linux fsid_t has two C int fields and both bit patterns are valid. let [first, second]: [u32; 2] = unsafe { std::mem::transmute(filesystem.f_fsid) }; if [first, second] == [0, 0] { @@ -55,7 +47,7 @@ fn volume_from_statfs(directory: &Dir, filesystem: &libc::statfs) -> io::Result< "linux-fsid:{:08x}:{first:08x}:{second:08x}", filesystem.f_type ), - stable_birth: filesystem.f_type != libc::OVERLAYFS_SUPER_MAGIC, + stable_birth: true, }) } libc::BTRFS_SUPER_MAGIC => Ok(Volume { @@ -85,13 +77,15 @@ fn volume_from_statfs(directory: &Dir, filesystem: &libc::statfs) -> io::Result< } _ => { // FS_IOC_GETFSUUID supplies the filesystem UUID on other filesystems - // that expose one. A missing UUID cannot authorize a durable grant. + // that expose one. Overlayfs needs its instance UUID: with uuid=off/null, + // f_fsid identifies the backing filesystem shared by independent overlays. let mut fsuuid = [0_u8; 17]; ioctl_read(&directory.open(".")?, 0x15, 0, &mut fsuuid)?; let size = usize::from(fsuuid[0]); Ok(Volume { id: uuid(fsuuid.get(1..1 + size).ok_or_else(no_identity)?)?, - stable_birth: true, + // Copy-up replaces the lower inode's birth time with the upper's. + stable_birth: filesystem.f_type != libc::OVERLAYFS_SUPER_MAGIC, }) } } @@ -177,24 +171,3 @@ fn no_identity() -> io::Error { "filesystem has no stable volume identity", ) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn overlayfs_statfs_requires_nonzero_fsid_and_marks_birth_unstable() { - let dir = tempfile::tempdir().unwrap(); - let opened = Dir::open_ambient_dir(dir.path(), cap_std::ambient_authority()).unwrap(); - let mut stat: libc::statfs = unsafe { std::mem::zeroed() }; - stat.f_type = libc::OVERLAYFS_SUPER_MAGIC; - - let zero_err = volume_from_statfs(&opened, &stat).unwrap_err(); - assert_eq!(zero_err.kind(), io::ErrorKind::Unsupported); - - stat.f_fsid = unsafe { std::mem::transmute([0x1234_5678_u32, 0x9abc_def0_u32]) }; - let parsed = volume_from_statfs(&opened, &stat).unwrap(); - assert_eq!(parsed.id, "linux-fsid:794c7630:12345678:9abcdef0"); - assert!(!parsed.stable_birth); - } -}