Implement exact bootstrap recovery and artifact GC

This commit is contained in:
ms2k 2026-07-17 05:38:43 +09:00
commit d25942596f

View file

@ -1,12 +1,13 @@
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fs;
use std::io;
use std::io::{self, BufRead, BufReader, Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use lazaret_core::configuration::{
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
@ -16,12 +17,14 @@ use lazaret_core::records::{
ArtifactState, HostProcessOwnershipSlot, PreparedArtifact, ProcessOwnershipCommitAck,
};
use lazaret_linux::process::{
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
dispatch_private_process_role,
};
use lazaret_state_anchor::{
FuseOverlayfsMount, ProtectedRuntimeFiles, RuntimeStateAnchorAuthority,
RuntimeStateAnchorInherited, StateAnchorAcquisition, StateAnchorLaunchAuthority,
StateAnchorLayout, StateAnchorSandboxView, unmount_after_coordinator_loss,
StateAnchorLayout, StateAnchorSandboxView, dispatch_private_state_anchor_recovery,
recover_private_state_anchor_mount, unmount_after_coordinator_loss,
};
use rustix::process::Signal;
@ -29,6 +32,15 @@ static SEQUENCE: AtomicU64 = AtomicU64::new(0);
fn main() -> ExitCode {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
if arguments
.first()
.is_some_and(|value| value == "--private-recovery-holder")
{
return exit(run_private_recovery_holder(&arguments));
}
if let Some(result) = dispatch_private_state_anchor_recovery(&arguments) {
return exit(result);
}
if let Some(result) = dispatch_private_process_role(&arguments) {
return exit(result);
}
@ -55,8 +67,9 @@ fn run() -> io::Result<()> {
}
Err(error) => return Err(error),
}
let tree = TestTree::new()?;
let configuration = configuration(&tree)?;
let tree = TestTree::new().map_err(|error| io::Error::other(format!("test tree: {error}")))?;
let configuration = configuration(&tree)
.map_err(|error| io::Error::other(format!("configuration: {error}")))?;
let lower = tree.path.join("prepared-lower");
fs::create_dir_all(lower.join("usr/bin"))?;
fs::create_dir_all(lower.join("etc"))?;
@ -76,7 +89,10 @@ fn run() -> io::Result<()> {
inode: lazaret_core::identity::InodeId::new(metadata.ino()),
state: ArtifactState::Available,
};
let layout = StateAnchorLayout::create(&configuration).map_err(io::Error::other)?;
let layout = StateAnchorLayout::create(&configuration)
.map_err(|error| io::Error::other(format!("State Anchor layout: {error}")))?;
exercise_private_recovery(&layout)
.map_err(|error| io::Error::other(format!("private recovery exercise: {error}")))?;
let launch =
StateAnchorLaunchAuthority::capture(&layout, &artifact).map_err(io::Error::other)?;
@ -197,6 +213,139 @@ fn run() -> io::Result<()> {
Ok(())
}
#[allow(deprecated)]
fn run_private_recovery_holder(arguments: &[OsString]) -> io::Result<()> {
if arguments.len() != 2 {
return Err(io::Error::other("private holder arguments are malformed"));
}
let merged = PathBuf::from(&arguments[1]);
let uid = rustix::process::getuid().as_raw();
let gid = rustix::process::getgid().as_raw();
rustix::thread::unshare(rustix::thread::UnshareFlags::NEWUSER)
.map_err(|error| io::Error::other(format!("holder user namespace: {error}")))?;
fs::write("/proc/self/setgroups", "deny")
.map_err(|error| io::Error::other(format!("holder setgroups: {error}")))?;
fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))
.map_err(|error| io::Error::other(format!("holder uid map: {error}")))?;
fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))
.map_err(|error| io::Error::other(format!("holder gid map: {error}")))?;
rustix::thread::unshare(rustix::thread::UnshareFlags::NEWNS)
.map_err(|error| io::Error::other(format!("holder mount namespace: {error}")))?;
rustix::mount::mount_change(
"/",
rustix::mount::MountPropagationFlags::PRIVATE | rustix::mount::MountPropagationFlags::REC,
)
.map_err(|error| io::Error::other(format!("holder private propagation: {error}")))?;
rustix::mount::mount(
"tmpfs",
&merged,
"tmpfs",
rustix::mount::MountFlags::empty(),
c"size=1m",
)
.map_err(|error| io::Error::other(format!("holder synthetic mount: {error}")))?;
let user = lazaret_linux::filesystem::namespace_identity(Path::new("/proc/self/ns/user"))?;
let mount_namespace =
lazaret_linux::filesystem::namespace_identity(Path::new("/proc/self/ns/mnt"))?;
let mount = lazaret_linux::filesystem::current_mount_identity(&merged)?;
writeln!(
io::stdout(),
"{} {} {} {} {} {} {}",
user.device.get(),
user.inode.get(),
mount_namespace.device.get(),
mount_namespace.inode.get(),
mount.mount_id.get(),
mount.device.get(),
mount.inode.get(),
)?;
io::stdout().flush()?;
loop {
std::thread::park();
}
}
fn exercise_private_recovery(layout: &StateAnchorLayout<'_>) -> io::Result<()> {
let target = TargetCommand::new("/proc/self/exe")
.argument("--private-recovery-holder")
.argument(layout.merged_label());
let mut captured = child_broker()
.run_transient_captured(&target)
.map_err(|error| io::Error::other(format!("starting private holder: {error}")))?;
let values = {
let (stdout, stderr) = captured.streams();
let mut reader = BufReader::new(stdout);
let mut line = String::new();
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match reader.read_line(&mut line) {
Ok(0) if Instant::now() < deadline => std::thread::yield_now(),
Ok(0) => {
let mut detail = String::new();
match stderr.read_to_string(&mut detail) {
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {}
Err(error) => return Err(error),
}
return Err(io::Error::other(format!(
"private holder closed without evidence: {}",
detail.trim()
)));
}
Ok(_) => break,
Err(error)
if error.kind() == io::ErrorKind::WouldBlock && Instant::now() < deadline =>
{
std::thread::yield_now();
}
Err(error) => return Err(error),
}
}
line.split_whitespace()
.map(|value| {
value
.parse::<u64>()
.map_err(|_| io::Error::other("private holder output is malformed"))
})
.collect::<io::Result<Vec<_>>>()?
};
if values.len() != 7 {
return Err(io::Error::other("private holder evidence is incomplete"));
}
let holder = captured.into_process();
let user = lazaret_core::identity::NamespaceIdentity {
device: lazaret_core::identity::DeviceId::new(values[0]),
inode: lazaret_core::identity::InodeId::new(values[1]),
};
let mount_namespace = lazaret_core::identity::NamespaceIdentity {
device: lazaret_core::identity::DeviceId::new(values[2]),
inode: lazaret_core::identity::InodeId::new(values[3]),
};
let mount = lazaret_core::identity::MountIdentity {
mount_id: lazaret_core::identity::MountId::new(values[4]),
device: lazaret_core::identity::DeviceId::new(values[5]),
inode: lazaret_core::identity::InodeId::new(values[6]),
};
let wrong = lazaret_core::identity::MountIdentity {
mount_id: lazaret_core::identity::MountId::new(mount.mount_id.get() + 1),
..mount
};
if recover_private_state_anchor_mount(layout, &holder, user, mount_namespace, wrong).is_ok() {
return Err(io::Error::other("private recovery accepted wrong mount"));
}
let observed =
recover_private_state_anchor_mount(layout, &holder, user, mount_namespace, mount)
.map_err(io::Error::other)?;
if observed == mount {
return Err(io::Error::other("private recovery did not prove absence"));
}
holder.send_signal(Signal::KILL)?;
while !holder.has_exited()? {
std::thread::yield_now();
}
child_broker().reap(holder)
}
fn acquire<'layout, 'configuration>(
layout: &'layout StateAnchorLayout<'configuration>,
artifact: &PreparedArtifact,