Normalize inactive State Anchor work storage
This commit is contained in:
parent
7cd8a90af0
commit
42e197510e
9 changed files with 133 additions and 11 deletions
|
|
@ -167,6 +167,9 @@ impl<'a> ColdStart<'a> {
|
||||||
);
|
);
|
||||||
let state_anchor_layout = StateAnchorLayout::create(inputs.configuration)
|
let state_anchor_layout = StateAnchorLayout::create(inputs.configuration)
|
||||||
.map_err(|error| failure(format!("State Anchor layout: {error}")))?;
|
.map_err(|error| failure(format!("State Anchor layout: {error}")))?;
|
||||||
|
state_anchor_layout
|
||||||
|
.normalize_inactive_work_directory()
|
||||||
|
.map_err(|error| failure(format!("State Anchor work recovery: {error}")))?;
|
||||||
let staging = RuntimeStaging::create(
|
let staging = RuntimeStaging::create(
|
||||||
inputs.configuration.runtime_domain_root(),
|
inputs.configuration.runtime_domain_root(),
|
||||||
inputs.container_id,
|
inputs.container_id,
|
||||||
|
|
|
||||||
|
|
@ -599,6 +599,9 @@ fn recover_state_anchor(
|
||||||
) && owners.ownership.state_anchor_mount.is_some();
|
) && owners.ownership.state_anchor_mount.is_some();
|
||||||
if !full_mount {
|
if !full_mount {
|
||||||
let _ = owners.prove_absent().map_err(|error| error.to_string())?;
|
let _ = owners.prove_absent().map_err(|error| error.to_string())?;
|
||||||
|
StateAnchorLayout::create(configuration)
|
||||||
|
.and_then(|layout| layout.normalize_inactive_work_directory())
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
return clean_role_after_absence(
|
return clean_role_after_absence(
|
||||||
store,
|
store,
|
||||||
generation.generation_id,
|
generation.generation_id,
|
||||||
|
|
@ -652,6 +655,9 @@ fn recover_state_anchor(
|
||||||
) {
|
) {
|
||||||
return Err("completed recovery witness changed State Anchor authority".into());
|
return Err("completed recovery witness changed State Anchor authority".into());
|
||||||
}
|
}
|
||||||
|
layout
|
||||||
|
.normalize_inactive_work_directory()
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
coordinator
|
coordinator
|
||||||
.complete_release_after_unmount(&authority, &layout, first, || {
|
.complete_release_after_unmount(&authority, &layout, first, || {
|
||||||
fresh_absence(store, generation.generation_id, &layout)
|
fresh_absence(store, generation.generation_id, &layout)
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,11 @@ fn complete_boot_mismatch_inventory_proves_only_after_every_holder_and_mount() {
|
||||||
#[test]
|
#[test]
|
||||||
fn recovery_preflight_recovers_full_runtime_then_is_idempotent() {
|
fn recovery_preflight_recovers_full_runtime_then_is_idempotent() {
|
||||||
let fixture = RecoveryFixture::new();
|
let fixture = RecoveryFixture::new();
|
||||||
|
let layout = StateAnchorLayout::create(&fixture.configuration).unwrap();
|
||||||
|
let residue = layout.work().join("work/4");
|
||||||
|
fs::create_dir_all(&residue).unwrap();
|
||||||
|
fs::write(residue.join(".wh..wh..opq"), b"").unwrap();
|
||||||
|
drop(layout);
|
||||||
let mut boundaries = Vec::new();
|
let mut boundaries = Vec::new();
|
||||||
fixture
|
fixture
|
||||||
.recover_with(|boundary, generation| {
|
.recover_with(|boundary, generation| {
|
||||||
|
|
@ -251,6 +256,8 @@ fn recovery_preflight_recovers_full_runtime_then_is_idempotent() {
|
||||||
.join(hex_lower(&[6; 32]))
|
.join(hex_lower(&[6; 32]))
|
||||||
.exists()
|
.exists()
|
||||||
);
|
);
|
||||||
|
let layout = StateAnchorLayout::create(&fixture.configuration).unwrap();
|
||||||
|
assert!(layout.work().read_dir().unwrap().next().is_none());
|
||||||
|
|
||||||
recover_before_cold_start(
|
recover_before_cold_start(
|
||||||
&fixture.configuration,
|
&fixture.configuration,
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,10 @@ use rustix::fs::{
|
||||||
use rustix::io::dup;
|
use rustix::io::dup;
|
||||||
use rustix::thread::{ThreadNameSpaceType, move_into_thread_name_spaces};
|
use rustix::thread::{ThreadNameSpaceType, move_into_thread_name_spaces};
|
||||||
use storage::{
|
use storage::{
|
||||||
atomic_replace_at, create_directory, create_regular_file, descriptor_child_path,
|
atomic_replace_at, clear_work_directory, create_directory, create_regular_file,
|
||||||
descriptor_path, host_path, inspect_directory, move_all_entries, move_selected_entries,
|
descriptor_child_path, descriptor_path, host_path, inspect_directory, move_all_entries,
|
||||||
open_directory, open_exact_directory_path, open_mounted_directory, open_or_create_quarantine,
|
move_selected_entries, open_directory, open_exact_directory_path, open_mounted_directory,
|
||||||
open_regular_file, path_from_host, remove_directory_contents,
|
open_or_create_quarantine, open_regular_file, path_from_host, remove_directory_contents,
|
||||||
require_directory_descriptor_identity, require_maintenance_work_shape, require_same_descriptor,
|
require_directory_descriptor_identity, require_maintenance_work_shape, require_same_descriptor,
|
||||||
validate_relative_path,
|
validate_relative_path,
|
||||||
};
|
};
|
||||||
|
|
@ -272,6 +272,15 @@ impl<'configuration> StateAnchorLayout<'configuration> {
|
||||||
self.revalidate()
|
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.
|
/// Atomically replaces sandbox runtime files in host-private transient storage.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use super::storage::{
|
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,
|
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::{
|
use super::{
|
||||||
AsFd, AsRawFd, FileType, HostProcessOwnershipSlot, Instant, MOUNT_POLL_INTERVAL, MOUNT_TIMEOUT,
|
AsFd, AsRawFd, FileType, HostProcessOwnershipSlot, Instant, MOUNT_POLL_INTERVAL, MOUNT_TIMEOUT,
|
||||||
|
|
@ -161,6 +162,7 @@ pub struct RuntimeStateAnchorAuthority {
|
||||||
lower: OwnedFd,
|
lower: OwnedFd,
|
||||||
persistent_anchor: OwnedFd,
|
persistent_anchor: OwnedFd,
|
||||||
upper: OwnedFd,
|
upper: OwnedFd,
|
||||||
|
work: OwnedFd,
|
||||||
runtime_anchor: OwnedFd,
|
runtime_anchor: OwnedFd,
|
||||||
merged_label: PathBuf,
|
merged_label: PathBuf,
|
||||||
merged_descriptor: OwnedFd,
|
merged_descriptor: OwnedFd,
|
||||||
|
|
@ -233,13 +235,14 @@ impl RuntimeStateAnchorAuthority {
|
||||||
inherited.runtime_anchor_label,
|
inherited.runtime_anchor_label,
|
||||||
(retained.st_dev, retained.st_ino),
|
(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_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
||||||
let merged_label = inherited.runtime_anchor_label.join("merged");
|
let merged_label = inherited.runtime_anchor_label.join("merged");
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
lower,
|
lower,
|
||||||
persistent_anchor,
|
persistent_anchor,
|
||||||
upper,
|
upper,
|
||||||
|
work,
|
||||||
runtime_anchor,
|
runtime_anchor,
|
||||||
merged_label,
|
merged_label,
|
||||||
merged_descriptor,
|
merged_descriptor,
|
||||||
|
|
@ -495,6 +498,9 @@ impl RuntimeStateAnchorMount {
|
||||||
return Err(StateAnchorError::OwnershipMismatch);
|
return Err(StateAnchorError::OwnershipMismatch);
|
||||||
}
|
}
|
||||||
child_broker().reap(process)?;
|
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 {
|
Ok(RuntimeStateAnchorAbsenceAuthority {
|
||||||
runtime_anchor: authority.runtime_anchor,
|
runtime_anchor: authority.runtime_anchor,
|
||||||
mount,
|
mount,
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ impl<'layout, 'configuration> FuseOverlayfsMount<'layout, 'configuration> {
|
||||||
}
|
}
|
||||||
self.layout.require_unmounted()?;
|
self.layout.require_unmounted()?;
|
||||||
child_broker().reap(self.process)?;
|
child_broker().reap(self.process)?;
|
||||||
|
self.layout.normalize_inactive_work_directory()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,6 +166,7 @@ impl<'layout, 'configuration> FuseOverlayfsMount<'layout, 'configuration> {
|
||||||
}
|
}
|
||||||
self.layout.require_unmounted()?;
|
self.layout.require_unmounted()?;
|
||||||
child_broker().reap(self.process)?;
|
child_broker().reap(self.process)?;
|
||||||
|
self.layout.normalize_inactive_work_directory()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ use super::{
|
||||||
mount_identity, openat2, renameat, renameat_with, statat, unlinkat,
|
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.
|
/// Independently verifies that no mount is attached at the exact merged path.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
@ -240,18 +242,34 @@ pub(super) fn require_maintenance_work_shape(directory: impl AsFd) -> Result<(),
|
||||||
if names.is_empty() {
|
if names.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if names.as_slice() != [OsString::from("work")] {
|
if names.as_slice() != [OsString::from(FUSE_OVERLAYFS_NESTED_WORK)] {
|
||||||
return Err(StateAnchorError::InvalidLayout(
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
"State Anchor work directory has unsupported entries",
|
"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 parent = rustix::fs::fstat(&directory).map_err(io::Error::from)?;
|
||||||
let child = rustix::fs::fstat(&nested).map_err(io::Error::from)?;
|
let child = rustix::fs::fstat(&nested).map_err(io::Error::from)?;
|
||||||
if child.st_mode & 0o7777 != 0o700
|
if child.st_mode & 0o7777 != 0o700
|
||||||
|| child.st_uid != parent.st_uid
|
|| child.st_uid != parent.st_uid
|
||||||
|| child.st_gid != parent.st_gid
|
|| child.st_gid != parent.st_gid
|
||||||
|| directory_has_entries(&nested)?
|
|
||||||
{
|
{
|
||||||
return Err(StateAnchorError::InvalidLayout(
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
"State Anchor work directory has unsupported entries",
|
"State Anchor work directory has unsupported entries",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ use lazaret_state_anchor::{
|
||||||
ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, RuntimeStateAnchorInherited,
|
ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, RuntimeStateAnchorInherited,
|
||||||
StateAnchorLaunchAuthority, StateAnchorLayout, StateAnchorResetScope,
|
StateAnchorLaunchAuthority, StateAnchorLayout, StateAnchorResetScope,
|
||||||
};
|
};
|
||||||
|
use rustix::fs::{XattrFlags, fsetxattr};
|
||||||
|
|
||||||
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
|
@ -33,6 +34,40 @@ fn maintenance_accepts_exact_empty_fuse_overlayfs_work_shape() {
|
||||||
layout.inspect_state().unwrap();
|
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]
|
#[test]
|
||||||
fn maintenance_refuses_mutated_fuse_overlayfs_work_shapes() {
|
fn maintenance_refuses_mutated_fuse_overlayfs_work_shapes() {
|
||||||
let tree = TestTree::new();
|
let tree = TestTree::new();
|
||||||
|
|
@ -60,6 +95,26 @@ fn maintenance_refuses_mutated_fuse_overlayfs_work_shapes() {
|
||||||
assert!(layout.inspect_state().is_err());
|
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]
|
#[test]
|
||||||
fn selected_reset_removes_exact_state_and_whiteout_without_touching_siblings() {
|
fn selected_reset_removes_exact_state_and_whiteout_without_touching_siblings() {
|
||||||
let tree = TestTree::new();
|
let tree = TestTree::new();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs;
|
use std::fs::{self, File};
|
||||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
@ -157,10 +157,26 @@ fn run() -> io::Result<()> {
|
||||||
"upper runtime-file entry survived transient override preflight",
|
"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)?;
|
runtime_mount.unmount().map_err(io::Error::other)?;
|
||||||
|
layout.inspect_state().map_err(io::Error::other)?;
|
||||||
fs::remove_file(layout.upper().join("etc/hosts"))?;
|
fs::remove_file(layout.upper().join("etc/hosts"))?;
|
||||||
|
|
||||||
let first = acquire(&layout, &artifact)?;
|
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(
|
fs::write(
|
||||||
layout.merged().join("usr/bin/installed-tool"),
|
layout.merged().join("usr/bin/installed-tool"),
|
||||||
b"persistent",
|
b"persistent",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue