diff --git a/platform/sandbox/src/workspace/directory.rs b/platform/sandbox/src/workspace/directory.rs index 061e3156e9..2802191444 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,70 @@ 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] + #[ignore = "requires a fresh lower-layer overlayfs directory in MAKA_TEST_OVERLAYFS_DIR"] + fn captured_grant_survives_linux_overlayfs_copy_up() { + let (path, directory) = overlayfs_directory(); + + 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()); + } + + #[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 8a3e1e9538..00aa9a7110 100644 --- a/platform/sandbox/src/workspace/directory/linux.rs +++ b/platform/sandbox/src/workspace/directory/linux.rs @@ -20,7 +20,12 @@ use cap_std::fs::Dir; use std::{io, os::fd::AsRawFd}; -pub(super) fn volume(directory: &Dir) -> io::Result { +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()) }; @@ -37,12 +42,18 @@ pub(super) fn volume(directory: &Dir) -> io::Result { 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: true, + }) } - 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,15 +70,23 @@ 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 - // 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]); - 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)?)?, + // Copy-up replaces the lower inode's birth time with the upper's. + stable_birth: filesystem.f_type != libc::OVERLAYFS_SUPER_MAGIC, + }) } } }