Make State Anchor acquisition resumable and descriptor-confined
This commit is contained in:
parent
e6d6406c64
commit
48c10e9a7a
2 changed files with 1030 additions and 197 deletions
109
crates/lazaret-state-anchor/tests/authority.rs
Normal file
109
crates/lazaret-state-anchor/tests/authority.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use lazaret_core::configuration::{
|
||||
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
|
||||
};
|
||||
use lazaret_state_anchor::{ProtectedRuntimeFiles, StateAnchorLayout};
|
||||
|
||||
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[test]
|
||||
fn replaced_runtime_root_is_refused_before_runtime_file_write() {
|
||||
let tree = TestTree::new();
|
||||
let configuration = configuration(&tree);
|
||||
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
||||
let runtime_root = PathBuf::from(OsString::from_vec(
|
||||
configuration
|
||||
.runtime_domain_root()
|
||||
.canonical_label()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
));
|
||||
let moved_root = runtime_root.with_extension("retained");
|
||||
fs::rename(&runtime_root, &moved_root).unwrap();
|
||||
let replacement_files = runtime_root.join("state-anchor/runtime-files");
|
||||
fs::create_dir_all(&replacement_files).unwrap();
|
||||
fs::write(replacement_files.join("hostname"), b"replacement\n").unwrap();
|
||||
|
||||
assert!(layout.revalidate().is_err());
|
||||
assert!(
|
||||
layout
|
||||
.replace_protected_runtime_files(&ProtectedRuntimeFiles {
|
||||
resolv_conf: b"nameserver 192.0.2.1\n",
|
||||
hosts: b"127.0.0.1 localhost\n",
|
||||
hostname: b"changed\n",
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(replacement_files.join("hostname")).unwrap(),
|
||||
b"replacement\n"
|
||||
);
|
||||
assert!(!replacement_files.join("resolv.conf").exists());
|
||||
assert!(!replacement_files.join("hosts").exists());
|
||||
}
|
||||
|
||||
struct TestTree {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestTree {
|
||||
fn new() -> Self {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"lazaret-state-anchor-authority-{}-{}",
|
||||
std::process::id(),
|
||||
SEQUENCE.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&path).unwrap();
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestTree {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn configuration(tree: &TestTree) -> 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).unwrap();
|
||||
fs::create_dir(&workspace).unwrap();
|
||||
fs::create_dir(&runtime).unwrap();
|
||||
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\"]\n",
|
||||
tree.path.join("data").display()
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let layout = SandboxLayoutV1::from_production_layout([
|
||||
"/workspace".to_owned(),
|
||||
"/proc".to_owned(),
|
||||
"/sys".to_owned(),
|
||||
"/dev".to_owned(),
|
||||
"/run/lazaret".to_owned(),
|
||||
])
|
||||
.unwrap();
|
||||
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,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
|
@ -19,8 +19,8 @@ use lazaret_linux::process::{
|
|||
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
|
||||
};
|
||||
use lazaret_state_anchor::{
|
||||
FuseOverlayfsMount, ProtectedRuntimeFiles, StateAnchorLayout, StateAnchorSandboxView,
|
||||
unmount_after_coordinator_loss,
|
||||
FuseOverlayfsMount, ProtectedRuntimeFiles, StateAnchorAcquisition, StateAnchorLayout,
|
||||
StateAnchorSandboxView, unmount_after_coordinator_loss,
|
||||
};
|
||||
use rustix::process::Signal;
|
||||
|
||||
|
|
@ -121,8 +121,9 @@ fn run() -> io::Result<()> {
|
|||
"runtime-file replacement escaped transient storage",
|
||||
));
|
||||
}
|
||||
drop(view);
|
||||
let wrong = lazaret_linux::filesystem::current_mount_identity(layout.upper())?;
|
||||
if unmount_after_coordinator_loss(layout.merged(), wrong).is_ok() {
|
||||
if unmount_after_coordinator_loss(&layout, wrong).is_ok() {
|
||||
return Err(io::Error::other(
|
||||
"ownership mismatch unmounted foreign identity",
|
||||
));
|
||||
|
|
@ -142,7 +143,7 @@ fn run() -> io::Result<()> {
|
|||
while !second.process().has_exited()? {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
unmount_after_coordinator_loss(layout.merged(), identity).map_err(io::Error::other)?;
|
||||
unmount_after_coordinator_loss(&layout, identity).map_err(io::Error::other)?;
|
||||
second
|
||||
.finish_after_external_unmount()
|
||||
.map_err(io::Error::other)?;
|
||||
|
|
@ -150,21 +151,23 @@ fn run() -> io::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn acquire(
|
||||
layout: &StateAnchorLayout,
|
||||
fn acquire<'layout, 'configuration>(
|
||||
layout: &'layout StateAnchorLayout<'configuration>,
|
||||
artifact: &PreparedArtifact,
|
||||
) -> io::Result<FuseOverlayfsMount> {
|
||||
) -> io::Result<FuseOverlayfsMount<'layout, 'configuration>> {
|
||||
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,
|
||||
StateAnchorAcquisition {
|
||||
lower: Path::new(std::ffi::OsStr::from_bytes(artifact.path.as_bytes())),
|
||||
expected_lower: (artifact.device.get(), artifact.inode.get()),
|
||||
expected_parent: current_process_identity()?,
|
||||
ownership: 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));
|
||||
|
|
@ -176,6 +179,19 @@ fn acquire(
|
|||
}
|
||||
Ok(ProcessOwnershipCommitAck::v1(staged))
|
||||
},
|
||||
|readiness| {
|
||||
if expected.get() != Some(readiness.staged()) {
|
||||
return Err(io::Error::other("ready process identity changed"));
|
||||
}
|
||||
if lazaret_linux::filesystem::current_mount_identity(layout.merged())?
|
||||
!= readiness.mount()
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"readiness preceded exact mount verification",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue