Normalize inactive State Anchor work storage
This commit is contained in:
parent
7cd8a90af0
commit
42e197510e
6 changed files with 133 additions and 11 deletions
|
|
@ -33,10 +33,10 @@ use rustix::fs::{
|
|||
use rustix::io::dup;
|
||||
use rustix::thread::{ThreadNameSpaceType, move_into_thread_name_spaces};
|
||||
use storage::{
|
||||
atomic_replace_at, create_directory, create_regular_file, descriptor_child_path,
|
||||
descriptor_path, host_path, inspect_directory, move_all_entries, move_selected_entries,
|
||||
open_directory, open_exact_directory_path, open_mounted_directory, open_or_create_quarantine,
|
||||
open_regular_file, path_from_host, remove_directory_contents,
|
||||
atomic_replace_at, clear_work_directory, create_directory, create_regular_file,
|
||||
descriptor_child_path, descriptor_path, host_path, inspect_directory, move_all_entries,
|
||||
move_selected_entries, open_directory, open_exact_directory_path, open_mounted_directory,
|
||||
open_or_create_quarantine, open_regular_file, path_from_host, remove_directory_contents,
|
||||
require_directory_descriptor_identity, require_maintenance_work_shape, require_same_descriptor,
|
||||
validate_relative_path,
|
||||
};
|
||||
|
|
@ -272,6 +272,15 @@ impl<'configuration> StateAnchorLayout<'configuration> {
|
|||
self.revalidate()
|
||||
}
|
||||
|
||||
/// Clears the private fuse-overlayfs work directory after proving the anchor inactive.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses changed authority, a remaining mount, or descriptor-confined cleanup failure.
|
||||
pub fn normalize_inactive_work_directory(&self) -> Result<(), StateAnchorError> {
|
||||
self.require_unmounted()?;
|
||||
clear_work_directory(&self.work_descriptor)
|
||||
}
|
||||
|
||||
/// Atomically replaces sandbox runtime files in host-private transient storage.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use super::storage::{
|
||||
descriptor_child_path, descriptor_path, inherit_for_exec, open_directory,
|
||||
clear_work_directory, descriptor_child_path, descriptor_path, inherit_for_exec, open_directory,
|
||||
open_exact_directory_path, open_mounted_directory, overlay_options, path_from_host,
|
||||
reopen_inherited_directory, require_directory_descriptor_identity, rootless_unmount_path,
|
||||
reopen_inherited_directory, require_directory_descriptor_identity, require_same_descriptor,
|
||||
rootless_unmount_path,
|
||||
};
|
||||
use super::{
|
||||
AsFd, AsRawFd, FileType, HostProcessOwnershipSlot, Instant, MOUNT_POLL_INTERVAL, MOUNT_TIMEOUT,
|
||||
|
|
@ -161,6 +162,7 @@ pub struct RuntimeStateAnchorAuthority {
|
|||
lower: OwnedFd,
|
||||
persistent_anchor: OwnedFd,
|
||||
upper: OwnedFd,
|
||||
work: OwnedFd,
|
||||
runtime_anchor: OwnedFd,
|
||||
merged_label: PathBuf,
|
||||
merged_descriptor: OwnedFd,
|
||||
|
|
@ -233,13 +235,14 @@ impl RuntimeStateAnchorAuthority {
|
|||
inherited.runtime_anchor_label,
|
||||
(retained.st_dev, retained.st_ino),
|
||||
)?;
|
||||
drop(open_directory(&persistent_anchor, Path::new("work"))?);
|
||||
let work = open_directory(&persistent_anchor, Path::new("work"))?;
|
||||
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
||||
let merged_label = inherited.runtime_anchor_label.join("merged");
|
||||
Ok(Self {
|
||||
lower,
|
||||
persistent_anchor,
|
||||
upper,
|
||||
work,
|
||||
runtime_anchor,
|
||||
merged_label,
|
||||
merged_descriptor,
|
||||
|
|
@ -495,6 +498,9 @@ impl RuntimeStateAnchorMount {
|
|||
return Err(StateAnchorError::OwnershipMismatch);
|
||||
}
|
||||
child_broker().reap(process)?;
|
||||
let observed_work = open_directory(&authority.persistent_anchor, Path::new("work"))?;
|
||||
require_same_descriptor(&observed_work, &authority.work)?;
|
||||
clear_work_directory(&authority.work)?;
|
||||
Ok(RuntimeStateAnchorAbsenceAuthority {
|
||||
runtime_anchor: authority.runtime_anchor,
|
||||
mount,
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ impl<'layout, 'configuration> FuseOverlayfsMount<'layout, 'configuration> {
|
|||
}
|
||||
self.layout.require_unmounted()?;
|
||||
child_broker().reap(self.process)?;
|
||||
self.layout.normalize_inactive_work_directory()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +166,7 @@ impl<'layout, 'configuration> FuseOverlayfsMount<'layout, 'configuration> {
|
|||
}
|
||||
self.layout.require_unmounted()?;
|
||||
child_broker().reap(self.process)?;
|
||||
self.layout.normalize_inactive_work_directory()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use super::{
|
|||
mount_identity, openat2, renameat, renameat_with, statat, unlinkat,
|
||||
};
|
||||
|
||||
const FUSE_OVERLAYFS_NESTED_WORK: &str = "work";
|
||||
|
||||
/// Independently verifies that no mount is attached at the exact merged path.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -240,18 +242,34 @@ pub(super) fn require_maintenance_work_shape(directory: impl AsFd) -> Result<(),
|
|||
if names.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if names.as_slice() != [OsString::from("work")] {
|
||||
if names.as_slice() != [OsString::from(FUSE_OVERLAYFS_NESTED_WORK)] {
|
||||
return Err(StateAnchorError::InvalidLayout(
|
||||
"State Anchor work directory has unsupported entries",
|
||||
));
|
||||
}
|
||||
let nested = open_directory(&directory, Path::new("work"))?;
|
||||
let nested = open_directory(&directory, Path::new(FUSE_OVERLAYFS_NESTED_WORK))?;
|
||||
require_nested_work_authority(&directory, &nested)?;
|
||||
if directory_has_entries(&nested)? {
|
||||
return Err(StateAnchorError::InvalidLayout(
|
||||
"State Anchor work directory has unsupported entries",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn clear_work_directory(directory: impl AsFd) -> Result<(), StateAnchorError> {
|
||||
remove_directory_contents(directory)
|
||||
}
|
||||
|
||||
fn require_nested_work_authority(
|
||||
directory: impl AsFd,
|
||||
nested: impl AsFd,
|
||||
) -> Result<(), StateAnchorError> {
|
||||
let parent = rustix::fs::fstat(&directory).map_err(io::Error::from)?;
|
||||
let child = rustix::fs::fstat(&nested).map_err(io::Error::from)?;
|
||||
if child.st_mode & 0o7777 != 0o700
|
||||
|| child.st_uid != parent.st_uid
|
||||
|| child.st_gid != parent.st_gid
|
||||
|| directory_has_entries(&nested)?
|
||||
{
|
||||
return Err(StateAnchorError::InvalidLayout(
|
||||
"State Anchor work directory has unsupported entries",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use lazaret_state_anchor::{
|
|||
ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, RuntimeStateAnchorInherited,
|
||||
StateAnchorLaunchAuthority, StateAnchorLayout, StateAnchorResetScope,
|
||||
};
|
||||
use rustix::fs::{XattrFlags, fsetxattr};
|
||||
|
||||
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
|
|
@ -33,6 +34,40 @@ fn maintenance_accepts_exact_empty_fuse_overlayfs_work_shape() {
|
|||
layout.inspect_state().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inactive_normalization_removes_exact_opaque_fuse_overlayfs_residue() {
|
||||
let tree = TestTree::new();
|
||||
let configuration = configuration(&tree);
|
||||
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
||||
let nested = domain_path(&configuration).join("state-anchor/work/work");
|
||||
fs::create_dir(&nested).unwrap();
|
||||
fs::set_permissions(&nested, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
create_opaque_work_residue(&nested, "4");
|
||||
create_opaque_work_residue(&nested, "12");
|
||||
|
||||
assert!(layout.inspect_state().is_err());
|
||||
layout.normalize_inactive_work_directory().unwrap();
|
||||
layout.inspect_state().unwrap();
|
||||
let work = domain_path(&configuration).join("state-anchor/work");
|
||||
assert!(work.read_dir().unwrap().next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inactive_normalization_unlinks_work_symlinks_without_following_them() {
|
||||
let tree = TestTree::new();
|
||||
let configuration = configuration(&tree);
|
||||
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
||||
let work = domain_path(&configuration).join("state-anchor/work");
|
||||
let foreign = tree.path.join("foreign-work");
|
||||
fs::create_dir(&foreign).unwrap();
|
||||
fs::write(foreign.join("value"), b"foreign").unwrap();
|
||||
std::os::unix::fs::symlink(&foreign, work.join("residue")).unwrap();
|
||||
|
||||
layout.normalize_inactive_work_directory().unwrap();
|
||||
assert!(!work.join("residue").exists());
|
||||
assert_eq!(fs::read(foreign.join("value")).unwrap(), b"foreign");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maintenance_refuses_mutated_fuse_overlayfs_work_shapes() {
|
||||
let tree = TestTree::new();
|
||||
|
|
@ -60,6 +95,26 @@ fn maintenance_refuses_mutated_fuse_overlayfs_work_shapes() {
|
|||
assert!(layout.inspect_state().is_err());
|
||||
}
|
||||
|
||||
fn create_opaque_work_residue(nested: &Path, name: &str) {
|
||||
let residue = nested.join(name);
|
||||
fs::create_dir(&residue).unwrap();
|
||||
fs::set_permissions(&residue, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
fs::write(residue.join(".wh..wh..opq"), b"").unwrap();
|
||||
fs::set_permissions(
|
||||
residue.join(".wh..wh..opq"),
|
||||
fs::Permissions::from_mode(0o700),
|
||||
)
|
||||
.unwrap();
|
||||
let residue = File::open(residue).unwrap();
|
||||
fsetxattr(
|
||||
&residue,
|
||||
"user.fuseoverlayfs.opaque",
|
||||
b"y",
|
||||
XattrFlags::empty(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_reset_removes_exact_state_and_whiteout_without_touching_siblings() {
|
||||
let tree = TestTree::new();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
|
@ -157,10 +157,26 @@ fn run() -> io::Result<()> {
|
|||
"upper runtime-file entry survived transient override preflight",
|
||||
));
|
||||
}
|
||||
let held_directory = layout.merged().join("root/open-lock");
|
||||
fs::create_dir_all(&held_directory)?;
|
||||
let held = File::open(&held_directory)?;
|
||||
fs::remove_dir(&held_directory)?;
|
||||
drop(held);
|
||||
if layout.work().join("work").read_dir()?.next().is_none() {
|
||||
return Err(io::Error::other(
|
||||
"open-directory removal did not create fuse-overlayfs work residue",
|
||||
));
|
||||
}
|
||||
runtime_mount.unmount().map_err(io::Error::other)?;
|
||||
layout.inspect_state().map_err(io::Error::other)?;
|
||||
fs::remove_file(layout.upper().join("etc/hosts"))?;
|
||||
|
||||
let first = acquire(&layout, &artifact)?;
|
||||
if layout.merged().join("root/open-lock").exists() {
|
||||
return Err(io::Error::other(
|
||||
"removed open directory returned after normalized remount",
|
||||
));
|
||||
}
|
||||
fs::write(
|
||||
layout.merged().join("usr/bin/installed-tool"),
|
||||
b"persistent",
|
||||
|
|
|
|||
Loading…
Reference in a new issue