Refuse orphan state without Domain Store authority
This commit is contained in:
parent
24d7d5b6e0
commit
e97ab6471c
5 changed files with 103 additions and 36 deletions
|
|
@ -6,7 +6,8 @@ use std::path::PathBuf;
|
||||||
use lazaret_core::records::Domain;
|
use lazaret_core::records::Domain;
|
||||||
use lazaret_host_coordinator::domain_store::{HostCoordinatorDomainStore, StateResetScopeV1};
|
use lazaret_host_coordinator::domain_store::{HostCoordinatorDomainStore, StateResetScopeV1};
|
||||||
use lazaret_host_coordinator::state_maintenance::{
|
use lazaret_host_coordinator::state_maintenance::{
|
||||||
DomainStateReport, StateAnchorEntryKind, inspect_domain_state, reset_domain_state,
|
DomainStateReport, StateAnchorEntryKind, inspect_absent_domain_state, inspect_domain_state,
|
||||||
|
reset_domain_state,
|
||||||
};
|
};
|
||||||
use lazaret_linux::filesystem::try_exclusive_flock;
|
use lazaret_linux::filesystem::try_exclusive_flock;
|
||||||
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
|
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
|
||||||
|
|
@ -64,11 +65,9 @@ pub fn run_command(arguments: &[OsString]) -> Result<i32, Box<dyn std::error::Er
|
||||||
) {
|
) {
|
||||||
Ok(database) => database,
|
Ok(database) => database,
|
||||||
Err(rustix::io::Errno::NOENT) => {
|
Err(rustix::io::Errno::NOENT) => {
|
||||||
return Ok(write_absent(
|
let report = inspect_absent_domain_state(&configuration)?;
|
||||||
&configuration,
|
write_report(&report, command.operation, command.machine);
|
||||||
command.operation,
|
return Ok(0);
|
||||||
command.machine,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Err(error) => return Err(io::Error::from(error).into()),
|
Err(error) => return Err(io::Error::from(error).into()),
|
||||||
};
|
};
|
||||||
|
|
@ -212,35 +211,6 @@ fn confirm(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_absent(
|
|
||||||
configuration: &lazaret_core::configuration::ResolvedDomainConfiguration,
|
|
||||||
operation: Operation,
|
|
||||||
machine: bool,
|
|
||||||
) -> i32 {
|
|
||||||
let report = DomainStateReport {
|
|
||||||
profile: configuration.selected_profile().to_owned(),
|
|
||||||
workspace: configuration
|
|
||||||
.workspace()
|
|
||||||
.canonical_label()
|
|
||||||
.as_bytes()
|
|
||||||
.to_vec(),
|
|
||||||
domain_id: *configuration.domain_id().as_bytes(),
|
|
||||||
anchors: configuration
|
|
||||||
.fingerprint_record()
|
|
||||||
.persistent_dirs
|
|
||||||
.iter()
|
|
||||||
.map(
|
|
||||||
|path| lazaret_host_coordinator::state_maintenance::StateAnchorReport {
|
|
||||||
container_path: path.clone(),
|
|
||||||
changes: Vec::new(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
write_report(&report, operation, machine);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_report(report: &DomainStateReport, operation: Operation, machine: bool) {
|
fn write_report(report: &DomainStateReport, operation: Operation, machine: bool) {
|
||||||
if machine {
|
if machine {
|
||||||
let anchors = report
|
let anchors = report
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,52 @@ fn reset_refuses_an_arbitrary_container_or_host_path_before_state_access() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
#[test]
|
||||||
|
fn missing_store_succeeds_only_when_the_complete_state_anchor_is_absent() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
for operation in ["list", "inspect", "verify", "reset"] {
|
||||||
|
let mut command = fixture.command(operation);
|
||||||
|
if operation == "reset" {
|
||||||
|
command.args(["--all", "--yes"]);
|
||||||
|
}
|
||||||
|
let absent = command.arg("--json").output().unwrap();
|
||||||
|
assert!(
|
||||||
|
absent.status.success(),
|
||||||
|
"{operation}: {}",
|
||||||
|
String::from_utf8_lossy(&absent.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain = fixture.domain_root();
|
||||||
|
assert!(!domain.join("domain.redb").exists());
|
||||||
|
fs::create_dir_all(domain.join("state-anchor/reset-quarantine")).unwrap();
|
||||||
|
fs::create_dir_all(domain.join("state-anchor/upper/usr")).unwrap();
|
||||||
|
fs::write(domain.join("state-anchor/upper/usr/value"), b"persisted").unwrap();
|
||||||
|
|
||||||
|
for operation in ["list", "inspect", "verify", "reset"] {
|
||||||
|
let mut command = fixture.command(operation);
|
||||||
|
if operation == "reset" {
|
||||||
|
command.args(["--all", "--yes"]);
|
||||||
|
}
|
||||||
|
let output = command.output().unwrap();
|
||||||
|
assert!(
|
||||||
|
!output.status.success(),
|
||||||
|
"{operation} accepted orphan state"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
.contains("State Anchor exists without Domain Store authority")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fs::read(domain.join("state-anchor/upper/usr/value")).unwrap(),
|
||||||
|
b"persisted"
|
||||||
|
);
|
||||||
|
assert!(domain.join("state-anchor/reset-quarantine").is_dir());
|
||||||
|
assert!(!domain.join("domain.redb").exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Fixture {
|
struct Fixture {
|
||||||
root: tempfile::TempDir,
|
root: tempfile::TempDir,
|
||||||
}
|
}
|
||||||
|
|
@ -77,4 +123,11 @@ impl Fixture {
|
||||||
.env("XDG_RUNTIME_DIR", self.root.path().join("runtime"));
|
.env("XDG_RUNTIME_DIR", self.root.path().join("runtime"));
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn domain_root(&self) -> std::path::PathBuf {
|
||||||
|
let mut domains = fs::read_dir(self.root.path().join("data/domains")).unwrap();
|
||||||
|
let domain = domains.next().unwrap().unwrap().path();
|
||||||
|
assert!(domains.next().is_none());
|
||||||
|
domain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use lazaret_core::records::{
|
||||||
pub use lazaret_state_anchor::StateAnchorEntryKind;
|
pub use lazaret_state_anchor::StateAnchorEntryKind;
|
||||||
use lazaret_state_anchor::{
|
use lazaret_state_anchor::{
|
||||||
StateAnchorEntry, StateAnchorInspection, StateAnchorLayout, StateAnchorResetScope,
|
StateAnchorEntry, StateAnchorInspection, StateAnchorLayout, StateAnchorResetScope,
|
||||||
require_no_orphan_reset_quarantine,
|
require_no_orphan_reset_quarantine, require_state_anchor_absent,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::domain_store::{HostCoordinatorDomainStore, StateResetIntentV1, StateResetScopeV1};
|
use crate::domain_store::{HostCoordinatorDomainStore, StateResetIntentV1, StateResetScopeV1};
|
||||||
|
|
@ -44,6 +44,22 @@ pub struct DomainStateReport {
|
||||||
pub anchors: Vec<StateAnchorReport>,
|
pub anchors: Vec<StateAnchorReport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an empty report only after proving that no physical State Anchor exists.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses changed authority, links, inaccessible paths, or any persistent or runtime anchor.
|
||||||
|
pub fn inspect_absent_domain_state(
|
||||||
|
configuration: &ResolvedDomainConfiguration,
|
||||||
|
) -> Result<DomainStateReport, StateMaintenanceError> {
|
||||||
|
require_state_anchor_absent(configuration).map_err(failure)?;
|
||||||
|
Ok(report(
|
||||||
|
configuration,
|
||||||
|
&StateAnchorInspection {
|
||||||
|
entries: Vec::new(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// Recovers a durable reset before any maintenance observer or runtime mount can expose state.
|
/// Recovers a durable reset before any maintenance observer or runtime mount can expose state.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,33 @@ pub enum StateAnchorResetScope<'a> {
|
||||||
Selected(&'a Path),
|
Selected(&'a Path),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Proves that no physical State Anchor exists under either retained Domain authority.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses changed authority, links, inaccessible paths, or any persistent or runtime anchor.
|
||||||
|
pub fn require_state_anchor_absent(
|
||||||
|
configuration: &ResolvedDomainConfiguration,
|
||||||
|
) -> Result<(), StateAnchorError> {
|
||||||
|
for plan in [
|
||||||
|
configuration.persistent_domain_root(),
|
||||||
|
configuration.runtime_domain_root(),
|
||||||
|
] {
|
||||||
|
let root = plan
|
||||||
|
.open_authority()
|
||||||
|
.map_err(|_| StateAnchorError::InvalidLayout("Domain authority changed"))?;
|
||||||
|
match open_directory(&root, Path::new("state-anchor")) {
|
||||||
|
Ok(_) => {
|
||||||
|
return Err(StateAnchorError::InvalidLayout(
|
||||||
|
"State Anchor exists without Domain Store authority",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(StateAnchorError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Refuses an orphan reset quarantine without opening transient mount paths.
|
/// Refuses an orphan reset quarantine without opening transient mount paths.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ PROCESS_LINT_ALLOWANCES = {
|
||||||
"crates/lazaret-host-coordinator/tests/runtime-composition.rs": {"run_benchmark_guest"},
|
"crates/lazaret-host-coordinator/tests/runtime-composition.rs": {"run_benchmark_guest"},
|
||||||
"crates/lazaret-client/tests/state_cli.rs": {
|
"crates/lazaret-client/tests/state_cli.rs": {
|
||||||
"machine_and_human_discovery_are_versioned_and_content_free",
|
"machine_and_human_discovery_are_versioned_and_content_free",
|
||||||
|
"missing_store_succeeds_only_when_the_complete_state_anchor_is_absent",
|
||||||
"reset_refuses_an_arbitrary_container_or_host_path_before_state_access",
|
"reset_refuses_an_arbitrary_container_or_host_path_before_state_access",
|
||||||
"command",
|
"command",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue