Normalize inactive State Anchor work storage

This commit is contained in:
ms2k 2026-07-23 21:44:16 +09:00
commit 42e197510e
2 changed files with 133 additions and 11 deletions

View file

@ -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();

View file

@ -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",