Implement exact bootstrap recovery and artifact GC
This commit is contained in:
parent
0f4845d4f8
commit
d25942596f
1 changed files with 1631 additions and 48 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue