Implement exact State Anchor mount and lease lifecycle
This commit is contained in:
parent
27bc3d835e
commit
e6d6406c64
1 changed files with 1852 additions and 0 deletions
242
crates/lazaret-state-anchor/tests/state-anchor-lifecycle.rs
Normal file
242
crates/lazaret-state-anchor/tests/state-anchor-lifecycle.rs
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
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 lazaret_core::configuration::{
|
||||
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
|
||||
};
|
||||
use lazaret_core::identity::{CoordinatorId, DomainId, GenerationId};
|
||||
use lazaret_core::records::{
|
||||
ArtifactState, HostProcessOwnershipSlot, PreparedArtifact, ProcessOwnershipCommitAck,
|
||||
};
|
||||
use lazaret_linux::process::{
|
||||
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
|
||||
};
|
||||
use lazaret_state_anchor::{
|
||||
FuseOverlayfsMount, ProtectedRuntimeFiles, StateAnchorLayout, StateAnchorSandboxView,
|
||||
unmount_after_coordinator_loss,
|
||||
};
|
||||
use rustix::process::Signal;
|
||||
|
||||
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
|
||||
if let Some(result) = dispatch_private_process_role(&arguments) {
|
||||
return exit(result);
|
||||
}
|
||||
exit(run())
|
||||
}
|
||||
|
||||
fn exit(result: io::Result<()>) -> ExitCode {
|
||||
match result {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("State Anchor lifecycle fixture failed: {error}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn run() -> io::Result<()> {
|
||||
match fs::metadata("/dev/fuse") {
|
||||
Ok(_) => {}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {
|
||||
eprintln!("skipped: /dev/fuse is unavailable");
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
let tree = TestTree::new()?;
|
||||
let configuration = configuration(&tree)?;
|
||||
let lower = tree.path.join("prepared-lower");
|
||||
fs::create_dir_all(lower.join("usr/bin"))?;
|
||||
fs::create_dir_all(lower.join("etc"))?;
|
||||
fs::write(lower.join("usr/bin/base-tool"), b"base")?;
|
||||
for name in ["resolv.conf", "hosts", "hostname"] {
|
||||
fs::write(lower.join("etc").join(name), b"image")?;
|
||||
}
|
||||
let metadata = lower.metadata()?;
|
||||
let artifact = PreparedArtifact {
|
||||
prepared_artifact_id: lazaret_core::identity::PreparedArtifactId::new([8; 32]),
|
||||
source_identity: lazaret_core::identity::SourceIdentity::new(b"fixture".to_vec()),
|
||||
selected_manifest_digest: lazaret_core::identity::SelectedManifestDigest::new([9; 32]),
|
||||
policy_id: lazaret_core::identity::PolicyId::new([10; 32]),
|
||||
id_map_digest: lazaret_core::identity::IdMapDigest::new([11; 32]),
|
||||
path: lazaret_core::identity::HostPath::new(lower.as_os_str().as_encoded_bytes().to_vec()),
|
||||
device: lazaret_core::identity::DeviceId::new(metadata.dev()),
|
||||
inode: lazaret_core::identity::InodeId::new(metadata.ino()),
|
||||
state: ArtifactState::Available,
|
||||
};
|
||||
let layout = StateAnchorLayout::create(&configuration).map_err(io::Error::other)?;
|
||||
|
||||
let first = acquire(&layout, &artifact)?;
|
||||
fs::write(
|
||||
layout.merged().join("usr/bin/installed-tool"),
|
||||
b"persistent",
|
||||
)?;
|
||||
fs::write(layout.merged().join("etc/developer.conf"), b"persistent")?;
|
||||
if lower.join("usr/bin/installed-tool").exists()
|
||||
|| lower.join("etc/developer.conf").exists()
|
||||
|| !layout.upper().join("usr/bin/installed-tool").exists()
|
||||
|| !layout.upper().join("etc/developer.conf").exists()
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"writes did not remain upper-only over the read-only lower",
|
||||
));
|
||||
}
|
||||
let view = StateAnchorSandboxView::from_mounted(&layout, &artifact, first.mount_identity())
|
||||
.map_err(io::Error::other)?;
|
||||
view.revalidate().map_err(io::Error::other)?;
|
||||
layout
|
||||
.replace_protected_runtime_files(&ProtectedRuntimeFiles {
|
||||
resolv_conf: b"nameserver 192.0.2.53\n",
|
||||
hosts: b"127.0.0.1 localhost\n",
|
||||
hostname: b"lazaret\n",
|
||||
})
|
||||
.map_err(io::Error::other)?;
|
||||
let runtime_sources = view.protected_runtime_mounts().collect::<Vec<_>>();
|
||||
if runtime_sources.len() != 3
|
||||
|| runtime_sources
|
||||
.iter()
|
||||
.any(|(source, _)| source.starts_with(&lower) || source.starts_with(layout.upper()))
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"protected runtime files are not host-private runtime material",
|
||||
));
|
||||
}
|
||||
if lower.join("etc/resolv.conf").read_link().is_ok()
|
||||
|| fs::read(lower.join("etc/resolv.conf"))? != b"image"
|
||||
|| layout.upper().join("etc/resolv.conf").exists()
|
||||
|| fs::read(&runtime_sources[0].0)? != b"nameserver 192.0.2.53\n"
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"runtime-file replacement escaped transient storage",
|
||||
));
|
||||
}
|
||||
let wrong = lazaret_linux::filesystem::current_mount_identity(layout.upper())?;
|
||||
if unmount_after_coordinator_loss(layout.merged(), wrong).is_ok() {
|
||||
return Err(io::Error::other(
|
||||
"ownership mismatch unmounted foreign identity",
|
||||
));
|
||||
}
|
||||
first.unmount().map_err(io::Error::other)?;
|
||||
|
||||
let second = acquire(&layout, &artifact)?;
|
||||
if fs::read(layout.merged().join("usr/bin/installed-tool"))? != b"persistent"
|
||||
|| fs::read(layout.merged().join("etc/developer.conf"))? != b"persistent"
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"upper state did not persist across remount",
|
||||
));
|
||||
}
|
||||
let identity = second.mount_identity();
|
||||
second.process().send_signal(Signal::KILL)?;
|
||||
while !second.process().has_exited()? {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
unmount_after_coordinator_loss(layout.merged(), identity).map_err(io::Error::other)?;
|
||||
second
|
||||
.finish_after_external_unmount()
|
||||
.map_err(io::Error::other)?;
|
||||
drop(tree);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn acquire(
|
||||
layout: &StateAnchorLayout,
|
||||
artifact: &PreparedArtifact,
|
||||
) -> io::Result<FuseOverlayfsMount> {
|
||||
let expected = std::cell::Cell::new(None);
|
||||
FuseOverlayfsMount::acquire(
|
||||
layout,
|
||||
Path::new(std::ffi::OsStr::from_bytes(artifact.path.as_bytes())),
|
||||
(artifact.device.get(), artifact.inode.get()),
|
||||
current_process_identity()?,
|
||||
ProcessOwnershipContext {
|
||||
domain_id: DomainId::new([1; 32]),
|
||||
generation_id: GenerationId::new(2),
|
||||
coordinator_id: CoordinatorId::new([3; 32]),
|
||||
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
||||
},
|
||||
|staged| {
|
||||
expected.set(Some(staged));
|
||||
Ok(())
|
||||
},
|
||||
|staged| {
|
||||
if expected.get() != Some(staged) {
|
||||
return Err(io::Error::other("staged process identity changed"));
|
||||
}
|
||||
Ok(ProcessOwnershipCommitAck::v1(staged))
|
||||
},
|
||||
)
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
|
||||
struct TestTree {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestTree {
|
||||
fn new() -> io::Result<Self> {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"lazaret state-anchor,:{}-{}",
|
||||
std::process::id(),
|
||||
SEQUENCE.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&path)?;
|
||||
Ok(Self { path })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestTree {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
fn configuration(
|
||||
tree: &TestTree,
|
||||
) -> io::Result<lazaret_core::configuration::ResolvedDomainConfiguration> {
|
||||
let configuration_directory = tree.path.join("configuration");
|
||||
let workspace = tree.path.join("workspace");
|
||||
let runtime = tree.path.join("runtime");
|
||||
fs::create_dir(&configuration_directory)?;
|
||||
fs::create_dir(&workspace)?;
|
||||
fs::create_dir(&runtime)?;
|
||||
let configuration_path = configuration_directory.join("lazaret.toml");
|
||||
fs::write(
|
||||
&configuration_path,
|
||||
format!(
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\", \"/etc\"]\n",
|
||||
tree.path.join("data").display()
|
||||
),
|
||||
)?;
|
||||
let layout = SandboxLayoutV1::from_production_layout([
|
||||
"/workspace".to_owned(),
|
||||
"/proc".to_owned(),
|
||||
"/sys".to_owned(),
|
||||
"/dev".to_owned(),
|
||||
"/run/lazaret".to_owned(),
|
||||
])
|
||||
.map_err(io::Error::other)?;
|
||||
load_trusted_configuration(ConfigurationLoadRequest {
|
||||
explicit_configuration_path: Some(configuration_path),
|
||||
selected_profile: "default".to_owned(),
|
||||
workspace,
|
||||
home: tree.path.join("home"),
|
||||
xdg_config_home: None,
|
||||
xdg_data_home: None,
|
||||
xdg_runtime_dir: runtime,
|
||||
environment: BTreeMap::<String, OsString>::new(),
|
||||
sandbox_layout: layout,
|
||||
})
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
Loading…
Reference in a new issue