Decompose oversized Rust files by responsibility

This commit is contained in:
ms2k 2026-07-19 03:02:44 +09:00
commit ee9201b057
7 changed files with 18648 additions and 18513 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,767 @@
fn validate_host_base(path: &Path, name: &str) -> Result<(), ConfigurationError> {
if !path.is_absolute()
|| path.as_os_str().is_empty()
|| path.as_os_str().as_bytes().contains(&0)
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidHostInput,
format!("{name} must be a NUL-free absolute path"),
));
}
Ok(())
}
fn config_home(request: &ConfigurationLoadRequest) -> PathBuf {
request
.xdg_config_home
.as_ref()
.filter(|path| path.is_absolute() && !path.as_os_str().is_empty())
.cloned()
.unwrap_or_else(|| request.home.join(".config"))
}
fn data_home(request: &ConfigurationLoadRequest) -> PathBuf {
request
.xdg_data_home
.as_ref()
.filter(|path| path.is_absolute() && !path.as_os_str().is_empty())
.cloned()
.unwrap_or_else(|| request.home.join(".local/share"))
}
fn expand_host_path(
declaration: &str,
home: &Path,
profile: Option<&Path>,
) -> Result<PathBuf, ConfigurationError> {
validate_declared_host_path(declaration, profile.is_some())?;
if let Some(suffix) = declaration.strip_prefix("~/") {
return Ok(home.join(suffix));
}
if let Some(suffix) = declaration.strip_prefix("{profile_dir}/") {
return profile.map(|root| root.join(suffix)).ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"profile-relative path is invalid in this field",
)
});
}
Ok(PathBuf::from(declaration))
}
fn read_stable_configuration(plan: &ConfinedPathPlan) -> Result<Vec<u8>, ConfigurationError> {
let path_descriptor = plan.open_authority()?;
let before = rustix::fs::fstat(&path_descriptor).map_err(io_error)?;
validate_configuration_stat(&before)?;
let proc_path = format!("/proc/self/fd/{}", path_descriptor.as_raw_fd());
let read_descriptor = rustix::fs::open(
proc_path,
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NONBLOCK,
Mode::empty(),
)
.map_err(io_error)?;
let opened = rustix::fs::fstat(&read_descriptor).map_err(io_error)?;
if !same_file_snapshot(&before, &opened) {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidConfigurationSource,
"configuration reopen selected a different object",
));
}
let size = usize::try_from(before.st_size).map_err(|_| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidConfigurationSource,
"configuration size is negative or unsupported",
)
})?;
if size > MAX_CONFIGURATION_BYTES {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidConfigurationSource,
"configuration exceeds 1,048,576 bytes",
));
}
let mut file = File::from(read_descriptor);
let first = read_exact_snapshot(&mut file, size)?;
file.seek(SeekFrom::Start(0)).map_err(io_error)?;
let second = read_exact_snapshot(&mut file, size)?;
let after = rustix::fs::fstat(&file).map_err(io_error)?;
if first != second || !same_file_snapshot(&before, &after) {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidConfigurationSource,
"configuration changed during its single load attempt",
));
}
Ok(first)
}
fn validate_configuration_stat(stat: &Stat) -> Result<(), ConfigurationError> {
let uid = rustix::process::getuid().as_raw();
if !FileType::from_raw_mode(stat.st_mode).is_file()
|| stat.st_nlink != 1
|| !matches!(stat.st_uid, 0) && stat.st_uid != uid
|| stat.st_mode & 0o022 != 0
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidConfigurationSource,
"configuration is not a safe regular file",
));
}
Ok(())
}
fn read_exact_snapshot(file: &mut File, size: usize) -> Result<Vec<u8>, ConfigurationError> {
let mut bytes = vec![0; size];
file.read_exact(&mut bytes).map_err(io_error)?;
let mut trailing = [0];
match file.read(&mut trailing) {
Ok(0) => Ok(bytes),
Ok(_) => Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidConfigurationSource,
"configuration grew during read",
)),
Err(error) => Err(io_error(error)),
}
}
fn same_file_snapshot(left: &Stat, right: &Stat) -> bool {
left.st_dev == right.st_dev
&& left.st_ino == right.st_ino
&& left.st_size == right.st_size
&& left.st_ctime == right.st_ctime
&& left.st_ctime_nsec == right.st_ctime_nsec
}
fn confined_existing(
canonical: &Path,
expected: ConfinedPathType,
executable: bool,
) -> Result<ConfinedPathPlan, ConfigurationError> {
let plan = ConfinedPathPlan::for_canonical_path(canonical, expected)?;
let descriptor = plan.open_authority()?;
let stat = rustix::fs::fstat(&descriptor).map_err(io_error)?;
if executable && stat.st_mode & 0o111 == 0 {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"resolved source has the wrong type or mode",
));
}
Ok(plan)
}
fn open_absolute_directory(path: &Path) -> Result<OwnedFd, ConfigurationError> {
openat2(
rustix::fs::CWD,
path,
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
)
.map_err(io_error)
}
fn ensure_owned_directory(path: &Path, uid: u32) -> Result<PathBuf, ConfigurationError> {
validate_host_base(path, "Lazaret root")?;
if path.exists() {
let canonical = std::fs::canonicalize(path).map_err(io_error)?;
if canonical.as_os_str().as_bytes() != path.as_os_str().as_bytes() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"Lazaret root contains a symlink",
));
}
validate_owned_directory(&open_absolute_directory(&canonical)?, uid)?;
return Ok(canonical);
}
let mut existing = path;
let mut missing = Vec::new();
while !existing.exists() {
let name = existing.file_name().ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"cannot find an existing ancestor for Lazaret root",
)
})?;
missing.push(name.to_os_string());
existing = existing.parent().ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"cannot find an existing ancestor for Lazaret root",
)
})?;
}
let existing_canonical = std::fs::canonicalize(existing).map_err(io_error)?;
if existing_canonical.as_os_str().as_bytes() != existing.as_os_str().as_bytes() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"Lazaret root ambient ancestor contains a symlink",
));
}
let mut current = open_absolute_directory(&existing_canonical)?;
let mut canonical = existing_canonical;
for component in missing.into_iter().rev() {
match rustix::fs::mkdirat(&current, &component, Mode::RWXU) {
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
Err(error) => return Err(io_error(error)),
}
let next = openat2(
&current,
&component,
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
ResolveFlags::BENEATH
| ResolveFlags::NO_MAGICLINKS
| ResolveFlags::NO_SYMLINKS
| ResolveFlags::NO_XDEV,
)
.map_err(io_error)?;
validate_owned_directory(&next, uid)?;
canonical.push(&component);
current = next;
}
Ok(canonical)
}
fn validate_owned_directory(descriptor: &OwnedFd, uid: u32) -> Result<(), ConfigurationError> {
let stat = rustix::fs::fstat(descriptor).map_err(io_error)?;
if !FileType::from_raw_mode(stat.st_mode).is_dir()
|| stat.st_uid != uid
|| stat.st_mode & 0o7777 != 0o700
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"Lazaret-owned directory is not invoking-user-owned mode 0700",
));
}
Ok(())
}
fn validate_persistent_destinations(
destinations: &[String],
protected: &BTreeSet<String>,
) -> Result<Vec<String>, ConfigurationError> {
require_pairwise_non_ancestral(
destinations.iter().map(String::as_str),
ConfigurationErrorKind::InvalidProfile,
)?;
for destination in destinations {
validate_sandbox_path(destination)?;
require_clear_of_protected(destination, protected)?;
}
Ok(destinations.to_vec())
}
fn require_clear_of_protected(
destination: &str,
protected: &BTreeSet<String>,
) -> Result<(), ConfigurationError> {
if protected
.iter()
.any(|protected| sandbox_paths_intersect(destination, protected))
{
Err(ConfigurationError::new(
ConfigurationErrorKind::PathConflict,
"configured sandbox destination intersects a protected destination",
))
} else {
Ok(())
}
}
fn require_strict_child_of_one(
destination: &str,
parents: &[String],
) -> Result<(), ConfigurationError> {
let count = parents
.iter()
.filter(|parent| sandbox_is_strict_descendant(destination, parent))
.count();
if count == 1 {
Ok(())
} else {
Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidVolume,
"volume destination is not a strict child of exactly one persistent path",
))
}
}
fn require_pairwise_non_ancestral<'a>(
paths: impl IntoIterator<Item = &'a str>,
kind: ConfigurationErrorKind,
) -> Result<(), ConfigurationError> {
let paths = paths.into_iter().collect::<Vec<_>>();
for (index, left) in paths.iter().enumerate() {
for right in &paths[index + 1..] {
if sandbox_paths_intersect(left, right) {
return Err(ConfigurationError::new(
kind,
"paths are duplicate or ancestral",
));
}
}
}
Ok(())
}
fn sandbox_paths_intersect(left: &str, right: &str) -> bool {
left == right
|| left
.strip_prefix(right)
.is_some_and(|suffix| suffix.starts_with('/'))
|| right
.strip_prefix(left)
.is_some_and(|suffix| suffix.starts_with('/'))
}
fn sandbox_is_strict_descendant(child: &str, parent: &str) -> bool {
child
.strip_prefix(parent)
.is_some_and(|suffix| suffix.starts_with('/'))
}
fn require_pairwise_disjoint(
paths: &[&Path],
authorities: &[&ConfinedPathPlan],
kind: ConfigurationErrorKind,
) -> Result<(), ConfigurationError> {
if paths.len() != authorities.len() {
return Err(ConfigurationError::new(
kind,
"host path and authority sets differ",
));
}
for (index, left) in paths.iter().enumerate() {
for (right, right_authority) in paths[index + 1..].iter().zip(&authorities[index + 1..]) {
if paths_intersect(left, right) || authorities[index].aliases(right_authority) {
return Err(ConfigurationError::new(
kind,
"host authority trees overlap",
));
}
}
}
Ok(())
}
fn paths_intersect(left: &Path, right: &Path) -> bool {
left == right || left.starts_with(right) || right.starts_with(left)
}
fn is_strict_descendant(child: &Path, parent: &Path) -> bool {
child != parent && child.starts_with(parent)
}
fn resolve_network(
configuration: &TrustedHostConfigurationV1,
selected: &ProfileConfigurationV1,
) -> Result<(Vec<Ipv4Prefix>, Vec<Ipv4Prefix>), ConfigurationError> {
let mut allow = BTreeSet::new();
let mut deny = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
.into_iter()
.map(parse_ipv4_prefix)
.collect::<Result<BTreeSet<_>, _>>()?;
for prefix in configuration
.network()
.default_allow
.iter()
.chain(&selected.allow_egress)
{
allow.insert(parse_ipv4_prefix(prefix)?);
}
for prefix in configuration
.network()
.default_deny
.iter()
.chain(&selected.deny_egress)
{
deny.insert(parse_ipv4_prefix(prefix)?);
}
if allow.iter().any(|prefix| deny.contains(prefix)) {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"equal normalized prefix is both allowed and denied",
));
}
Ok((allow.into_iter().collect(), deny.into_iter().collect()))
}
const MAX_READINESS_TARGETS: usize = 32;
fn resolve_network_readiness(
readiness: &NetworkReadinessConfigurationV1,
allow: &[Ipv4Prefix],
deny: &[Ipv4Prefix],
) -> Result<ResolvedNetworkReadinessV1, ConfigurationError> {
if readiness.denied.is_empty() || readiness.permitted.is_empty() || readiness.dns.is_empty() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"profile readiness requires denied, permitted, and DNS targets",
));
}
if readiness.denied.len() > MAX_READINESS_TARGETS
|| readiness.permitted.len() > MAX_READINESS_TARGETS
|| readiness.dns.len() > MAX_READINESS_TARGETS
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"profile readiness target count exceeds 32",
));
}
let denied = resolve_tcp_readiness_targets(&readiness.denied, allow, deny, false)?;
let permitted = resolve_tcp_readiness_targets(&readiness.permitted, allow, deny, true)?;
let dns = readiness
.dns
.iter()
.map(|target| {
Ok(ResolvedDnsProbeTargetV1 {
resolver: parse_probe_address(&target.resolver)?,
query: normalize_dns_query(&target.query)?,
})
})
.collect::<Result<BTreeSet<_>, ConfigurationError>>()?;
if dns.len() != readiness.dns.len() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"profile readiness contains duplicate DNS targets",
));
}
Ok(ResolvedNetworkReadinessV1 {
denied: denied.into_iter().collect(),
permitted: permitted.into_iter().collect(),
dns: dns.into_iter().collect(),
})
}
fn resolve_tcp_readiness_targets(
targets: &[NetworkProbeTargetConfigurationV1],
allow: &[Ipv4Prefix],
deny: &[Ipv4Prefix],
expected_allowed: bool,
) -> Result<BTreeSet<ResolvedNetworkProbeTargetV1>, ConfigurationError> {
let resolved = targets
.iter()
.map(|target| {
if target.port == 0 {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"readiness TCP target has port zero",
));
}
let address = parse_probe_address(&target.address)?;
if is_effectively_allowed(address, allow, deny) != expected_allowed {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"readiness TCP target disagrees with effective profile policy",
));
}
Ok(ResolvedNetworkProbeTargetV1 {
address,
port: target.port,
})
})
.collect::<Result<BTreeSet<_>, _>>()?;
if resolved.len() != targets.len() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"profile readiness contains duplicate TCP targets",
));
}
if resolved
.iter()
.map(|target| target.address)
.collect::<BTreeSet<_>>()
.len()
!= resolved.len()
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"readiness TCP targets repeat an observation address",
));
}
Ok(resolved)
}
fn parse_probe_address(value: &str) -> Result<[u8; 4], ConfigurationError> {
let address = value.parse::<std::net::Ipv4Addr>().map_err(|_| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"readiness target address is not IPv4",
)
})?;
if address.is_unspecified() || address.is_multicast() || address.is_broadcast() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"readiness target address is not unicast",
));
}
Ok(address.octets())
}
fn normalize_dns_query(value: &str) -> Result<String, ConfigurationError> {
let query = value.strip_suffix('.').unwrap_or(value);
if query.is_empty() || query.len() > 253 || !query.is_ascii() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"DNS readiness query name is invalid",
));
}
for label in query.split('.') {
if label.is_empty()
|| label.len() > 63
|| label.starts_with('-')
|| label.ends_with('-')
|| !label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"DNS readiness query name is invalid",
));
}
}
Ok(query.to_ascii_lowercase())
}
fn is_effectively_allowed(address: [u8; 4], allow: &[Ipv4Prefix], deny: &[Ipv4Prefix]) -> bool {
let best_allow = allow
.iter()
.filter(|prefix| prefix_contains(**prefix, address))
.map(|prefix| prefix.prefix_length)
.max();
let best_deny = deny
.iter()
.filter(|prefix| prefix_contains(**prefix, address))
.map(|prefix| prefix.prefix_length)
.max();
match (best_allow, best_deny) {
(Some(allowed), Some(denied)) => allowed > denied,
(Some(_) | None, None) => true,
(None, Some(_)) => false,
}
}
fn prefix_contains(prefix: Ipv4Prefix, address: [u8; 4]) -> bool {
let mask = if prefix.prefix_length == 0 {
0
} else {
u32::MAX << (32 - prefix.prefix_length)
};
u32::from_be_bytes(address) & mask == u32::from_be_bytes(prefix.address)
}
fn parse_ipv4_prefix(value: &str) -> Result<Ipv4Prefix, ConfigurationError> {
let (address, prefix) = value.split_once('/').ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"IPv4 prefix has no prefix length",
)
})?;
if prefix.contains('/') {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"IPv4 prefix contains multiple separators",
));
}
let address = address.parse::<std::net::Ipv4Addr>().map_err(|_| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"network prefix is not IPv4",
)
})?;
let prefix_length = prefix.parse::<u8>().map_err(|_| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"IPv4 prefix length is invalid",
)
})?;
if prefix_length > 32 {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidNetwork,
"IPv4 prefix length exceeds 32",
));
}
let raw = u32::from(address);
let mask = if prefix_length == 0 {
0
} else {
u32::MAX << (32 - prefix_length)
};
Ok(Ipv4Prefix {
address: (raw & mask).to_be_bytes(),
prefix_length,
})
}
fn resolve_environment_references(
selected: &ProfileConfigurationV1,
) -> Result<Vec<EnvironmentReference>, ConfigurationError> {
selected
.env
.iter()
.map(|(sandbox_name, declaration)| {
let host_name = declaration.strip_prefix("env:").ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidProfile,
"environment value is not an env: reference",
)
})?;
validate_environment_name(sandbox_name)?;
validate_environment_name(host_name)?;
Ok(EnvironmentReference {
sandbox_name: sandbox_name.clone(),
host_name: host_name.to_owned(),
})
})
.collect()
}
fn resolve_environment_values(
references: &[EnvironmentReference],
environment: &BTreeMap<String, OsString>,
) -> Result<BTreeMap<String, EnvironmentValue>, ConfigurationError> {
references
.iter()
.map(|reference| {
let value = environment.get(&reference.host_name).ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::MissingEnvironment,
format!("{} is unset", reference.host_name),
)
})?;
if value.as_bytes().contains(&0) || value.to_str().is_none() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::MissingEnvironment,
format!("{} is not NUL-free UTF-8", reference.host_name),
));
}
Ok((
reference.sandbox_name.clone(),
EnvironmentValue(value.clone()),
))
})
.collect()
}
fn validate_executable(descriptor: &OwnedFd, uid: u32) -> Result<(), ConfigurationError> {
let stat = rustix::fs::fstat(descriptor).map_err(io_error)?;
let executable_by_invoking_user = if stat.st_uid == uid {
stat.st_mode & 0o100 != 0
} else {
let primary_gid = rustix::process::getgid().as_raw();
let supplementary_gids = rustix::process::getgroups().map_err(io_error)?;
stat.st_mode & 0o001 != 0
|| stat.st_mode & 0o010 != 0
&& (stat.st_gid == primary_gid
|| supplementary_gids
.iter()
.any(|group| group.as_raw() == stat.st_gid))
};
if !FileType::from_raw_mode(stat.st_mode).is_file()
|| stat.st_nlink != 1
|| !matches!(stat.st_uid, 0) && stat.st_uid != uid
|| stat.st_mode & 0o022 != 0
|| !executable_by_invoking_user
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidAutoApproval,
"executable is not a safe single-link regular executable",
));
}
Ok(())
}
fn encode_volume(volume: &ResolvedVolume) -> Result<Vec<u8>, ConfigurationError> {
let mut encoded = Vec::new();
push_bytes(&mut encoded, volume.canonical_host_source.as_bytes())?;
push_bytes(&mut encoded, volume.container_path.as_bytes())?;
encoded.push(match volume.kind {
VolumeKind::File => 0,
VolumeKind::Directory => 1,
});
encoded.push(match volume.access {
VolumeAccess::ReadOnly => 0,
VolumeAccess::ReadWrite => 1,
});
push_bool(&mut encoded, volume.create);
Ok(encoded)
}
fn encode_auto_approval(rule: &ResolvedAutoApprovalRule) -> Result<Vec<u8>, ConfigurationError> {
let mut encoded = Vec::new();
push_bytes(&mut encoded, rule.canonical_executable.as_bytes())?;
push_declared_list(
&mut encoded,
rule.args.iter().map(|argument| {
let mut item = Vec::new();
push_bytes(&mut item, argument.as_bytes())?;
Ok(item)
}),
)?;
push_bool(&mut encoded, rule.allow_extra_args);
Ok(encoded)
}
fn push_bytes(target: &mut Vec<u8>, bytes: &[u8]) -> Result<(), ConfigurationError> {
let length = u32::try_from(bytes.len()).map_err(|_| fingerprint_overflow())?;
target.extend_from_slice(&length.to_be_bytes());
target.extend_from_slice(bytes);
Ok(())
}
fn push_bool(target: &mut Vec<u8>, value: bool) {
target.push(u8::from(value));
}
fn push_sorted_list(
target: &mut Vec<u8>,
items: impl IntoIterator<Item = Result<Vec<u8>, ConfigurationError>>,
) -> Result<(), ConfigurationError> {
let mut items = items.into_iter().collect::<Result<Vec<_>, _>>()?;
items.sort_unstable();
push_framed_items(target, &items)
}
fn push_declared_list(
target: &mut Vec<u8>,
items: impl IntoIterator<Item = Result<Vec<u8>, ConfigurationError>>,
) -> Result<(), ConfigurationError> {
let items = items.into_iter().collect::<Result<Vec<_>, _>>()?;
push_framed_items(target, &items)
}
fn push_framed_items(target: &mut Vec<u8>, items: &[Vec<u8>]) -> Result<(), ConfigurationError> {
let count = u32::try_from(items.len()).map_err(|_| fingerprint_overflow())?;
target.extend_from_slice(&count.to_be_bytes());
for item in items {
push_bytes(target, item)?;
}
Ok(())
}
fn fingerprint_overflow() -> ConfigurationError {
ConfigurationError::new(
ConfigurationErrorKind::FingerprintOverflow,
"field or collection does not fit v1 u32 framing",
)
}
fn host_path(path: &Path) -> HostPath {
HostPath::new(path.as_os_str().as_bytes().to_vec())
}
fn hex_lower(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
output.push(char::from(HEX[usize::from(byte >> 4)]));
output.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
output
}
fn io_error(error: impl fmt::Display) -> ConfigurationError {
ConfigurationError::new(ConfigurationErrorKind::Io, error.to_string())
}

View file

@ -0,0 +1,372 @@
/// Parses the closed trusted-host configuration document without resolving host authority.
///
/// # Errors
/// Unknown, duplicate, aliased, malformed, and unknown-version input is rejected.
pub fn parse_trusted_configuration(
bytes: &[u8],
) -> Result<TrustedHostConfigurationV1, ConfigurationError> {
if bytes.len() > MAX_CONFIGURATION_BYTES {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidConfigurationSource,
"configuration exceeds 1,048,576 bytes",
));
}
let text = std::str::from_utf8(bytes).map_err(|_| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidToml,
"configuration is not UTF-8",
)
})?;
let configuration: TrustedHostConfigurationV1 = toml::from_str(text).map_err(|error| {
ConfigurationError::new(ConfigurationErrorKind::InvalidToml, error.to_string())
})?;
validate_structural_configuration(&configuration)?;
Ok(configuration)
}
/// Resolves one selected profile into move-owned host authority.
///
/// # Errors
/// Loading fails atomically when source stability, strict syntax, confinement, environment,
/// network, volume, executable, or fingerprint validation fails.
#[allow(clippy::too_many_lines)]
pub fn load_trusted_configuration(
request: ConfigurationLoadRequest,
) -> Result<ResolvedDomainConfiguration, ConfigurationError> {
validate_host_base(&request.home, "HOME")?;
validate_host_base(&request.xdg_runtime_dir, "XDG_RUNTIME_DIR")?;
validate_profile_name(&request.selected_profile)?;
let configuration_path = if let Some(explicit) = &request.explicit_configuration_path {
let declaration = explicit.as_os_str().to_str().ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidHostInput,
"configuration path is not UTF-8",
)
})?;
expand_host_path(declaration, &request.home, None)?
} else {
config_home(&request).join("lazaret/config.toml")
};
if request.explicit_configuration_path.is_none() {
validate_host_base(&configuration_path, "configuration path")?;
}
let configuration_canonical = std::fs::canonicalize(&configuration_path).map_err(io_error)?;
let configuration_source = ConfinedPathPlan::for_canonical_path(
&configuration_canonical,
ConfinedPathType::RegularFile,
)?;
let configuration_bytes = read_stable_configuration(&configuration_source)?;
let configuration = parse_trusted_configuration(&configuration_bytes)?;
let selected = configuration
.profiles()
.get(&request.selected_profile)
.ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::UnknownProfile,
request.selected_profile.clone(),
)
})?;
let workspace_canonical = std::fs::canonicalize(&request.workspace).map_err(io_error)?;
let workspace = confined_existing(&workspace_canonical, ConfinedPathType::Directory, false)?;
let workspace_bytes = workspace_canonical.as_os_str().as_bytes();
let domain_id = derive_domain_id(&request.selected_profile, workspace_bytes)?;
let data_root_declaration = configuration.storage().data_root.as_deref();
let storage_path = match data_root_declaration {
Some(declaration) => expand_host_path(declaration, &request.home, None)?,
None => data_home(&request).join("lazaret"),
};
let runtime_path = request.xdg_runtime_dir.join("lazaret");
let invoking_uid = rustix::process::getuid().as_raw();
let storage_canonical = ensure_owned_directory(&storage_path, invoking_uid)?;
let runtime_canonical = ensure_owned_directory(&runtime_path, invoking_uid)?;
let profile_path = storage_canonical
.join("profiles")
.join(&request.selected_profile);
let profile_canonical = ensure_owned_directory(&profile_path, invoking_uid)?;
let domain_hex = hex_lower(domain_id.as_bytes());
let persistent_domain_canonical = ensure_owned_directory(
&storage_canonical.join("domains").join(&domain_hex),
invoking_uid,
)?;
let runtime_domain_canonical = ensure_owned_directory(
&runtime_canonical.join("domains").join(&domain_hex),
invoking_uid,
)?;
let storage_root = confined_existing(&storage_canonical, ConfinedPathType::Directory, false)?;
let runtime_root = confined_existing(&runtime_canonical, ConfinedPathType::Directory, false)?;
let profile_root = confined_existing(&profile_canonical, ConfinedPathType::Directory, false)?;
let persistent_domain_root = confined_existing(
&persistent_domain_canonical,
ConfinedPathType::Directory,
false,
)?;
let runtime_domain_root = confined_existing(
&runtime_domain_canonical,
ConfinedPathType::Directory,
false,
)?;
let core_paths = [
configuration_canonical.as_path(),
workspace_canonical.as_path(),
storage_canonical.as_path(),
runtime_canonical.as_path(),
];
let core_authorities = [
&configuration_source,
&workspace,
&storage_root,
&runtime_root,
];
require_pairwise_disjoint(
&core_paths,
&core_authorities,
ConfigurationErrorKind::PathConflict,
)?;
let persistent_dirs = validate_persistent_destinations(
&selected.persistent_dirs,
request.sandbox_layout.protected_destinations(),
)?;
let (effective_allow_prefixes, effective_deny_prefixes) =
resolve_network(&configuration, selected)?;
let network_readiness = resolve_network_readiness(
&selected.readiness,
&effective_allow_prefixes,
&effective_deny_prefixes,
)?;
let environment_references = resolve_environment_references(selected)?;
let mut volumes = Vec::with_capacity(selected.volumes.len());
let mut resolved_volume_records = Vec::with_capacity(selected.volumes.len());
let mut volume_paths: Vec<PathBuf> = Vec::with_capacity(selected.volumes.len());
for volume in &selected.volumes {
validate_sandbox_path(&volume.container)?;
require_strict_child_of_one(&volume.container, &persistent_dirs)?;
require_clear_of_protected(
&volume.container,
request.sandbox_layout.protected_destinations(),
)?;
let under_profile = volume.host.starts_with("{profile_dir}/");
if volume.create && (volume.kind != VolumeKind::Directory || !under_profile) {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidVolume,
"create=true requires a directory strictly beneath {profile_dir}",
));
}
let source_path = expand_host_path(&volume.host, &request.home, Some(&profile_canonical))?;
let canonical = if volume.create {
ensure_owned_directory(&source_path, invoking_uid)?
} else {
std::fs::canonicalize(&source_path).map_err(io_error)?
};
if !volume.create && canonical.as_os_str().as_bytes() != source_path.as_os_str().as_bytes()
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidVolume,
"volume source contains a symlink",
));
}
let expected = match volume.kind {
VolumeKind::File => ConfinedPathType::RegularFile,
VolumeKind::Directory => ConfinedPathType::Directory,
};
let source_plan = confined_existing(&canonical, expected, false)?;
for (forbidden, forbidden_authority) in core_paths.iter().zip(core_authorities) {
if paths_intersect(&canonical, forbidden) {
let allowed_profile_child = under_profile
&& *forbidden == storage_canonical.as_path()
&& is_strict_descendant(&canonical, &profile_canonical);
if !allowed_profile_child {
return Err(ConfigurationError::new(
ConfigurationErrorKind::PathConflict,
"volume source intersects a protected host tree",
));
}
}
if source_plan.aliases(forbidden_authority) {
return Err(ConfigurationError::new(
ConfigurationErrorKind::PathConflict,
"volume source aliases a protected host authority",
));
}
}
if volume_paths
.iter()
.any(|other| paths_intersect(&canonical, other))
|| volumes
.iter()
.any(|other: &ResolvedVolumeAuthority| source_plan.aliases(&other.source_plan))
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::PathConflict,
"volume sources overlap",
));
}
let record = ResolvedVolume {
canonical_host_source: host_path(&canonical),
container_path: volume.container.clone(),
kind: volume.kind,
access: volume.access,
create: volume.create,
};
volume_paths.push(canonical);
resolved_volume_records.push(record.clone());
volumes.push(ResolvedVolumeAuthority {
record,
source_plan,
});
}
require_pairwise_non_ancestral(
selected
.volumes
.iter()
.map(|volume| volume.container.as_str()),
ConfigurationErrorKind::InvalidVolume,
)?;
let mut auto_approval_rules = Vec::new();
let mut resolved_auto_approval_records = Vec::new();
if !selected.escalation.enabled && !selected.escalation.auto_approve.is_empty() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidAutoApproval,
"disabled escalation profile contains auto-approval rules",
));
}
for rule in &selected.escalation.auto_approve {
validate_text(&rule.executable, "auto-approval executable")?;
for argument in &rule.args {
validate_text(argument, "auto-approval argument")?;
}
if rule.allow_extra_args && rule.args.is_empty() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidAutoApproval,
"allow_extra_args requires a nonempty configured prefix",
));
}
let executable_path = expand_host_path(&rule.executable, &request.home, None)?;
let canonical = std::fs::canonicalize(&executable_path).map_err(io_error)?;
if core_paths
.iter()
.copied()
.chain(volume_paths.iter().map(PathBuf::as_path))
.chain([
profile_canonical.as_path(),
persistent_domain_canonical.as_path(),
runtime_domain_canonical.as_path(),
])
.any(|forbidden| paths_intersect(&canonical, forbidden))
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidAutoApproval,
"auto-approval executable intersects a writable or control tree",
));
}
let executable_plan = confined_existing(&canonical, ConfinedPathType::RegularFile, true)?;
if core_authorities
.iter()
.copied()
.chain([&profile_root, &persistent_domain_root, &runtime_domain_root])
.any(|forbidden| executable_plan.aliases(forbidden))
|| volumes
.iter()
.any(|volume| executable_plan.aliases(&volume.source_plan))
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidAutoApproval,
"auto-approval executable aliases a writable or control authority",
));
}
let executable = executable_plan.reopen_exact(OFlags::RDONLY | OFlags::NONBLOCK)?;
validate_executable(&executable, invoking_uid)?;
let record = ResolvedAutoApprovalRule {
canonical_executable: host_path(&canonical),
args: rule.args.clone(),
allow_extra_args: rule.allow_extra_args,
};
if resolved_auto_approval_records.contains(&record) {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidAutoApproval,
"duplicate normalized auto-approval rule",
));
}
resolved_auto_approval_records.push(record.clone());
auto_approval_rules.push(ResolvedAutoApproval {
record,
executable_plan,
invoking_uid,
});
}
let configuration_descriptor = configuration_source.open_authority()?;
validate_configuration_stat(&rustix::fs::fstat(&configuration_descriptor).map_err(io_error)?)?;
for authority in [
&workspace,
&storage_root,
&runtime_root,
&profile_root,
&persistent_domain_root,
&runtime_domain_root,
] {
drop(authority.open_authority()?);
}
for volume in &volumes {
drop(volume.open_source()?);
}
for rule in &auto_approval_rules {
let executable = rule
.executable_plan
.reopen_exact(OFlags::RDONLY | OFlags::NONBLOCK)?;
validate_executable(&executable, invoking_uid)?;
}
let fingerprint_record = ConfigurationFingerprintRecordV1 {
selected_profile: request.selected_profile.clone(),
storage_root: host_path(&storage_canonical),
runtime_root: host_path(&runtime_canonical),
image: selected.image.clone(),
isolate_gitignored: selected.isolate_gitignored,
persistent_dirs,
environment_references,
volumes: resolved_volume_records,
effective_allow_prefixes: effective_allow_prefixes
.iter()
.map(|prefix| (prefix.address, prefix.prefix_length))
.collect(),
effective_deny_prefixes: effective_deny_prefixes
.iter()
.map(|prefix| (prefix.address, prefix.prefix_length))
.collect(),
network_readiness,
escalation_enabled: selected.escalation.enabled,
auto_approval_rules: resolved_auto_approval_records,
};
let configuration_fingerprint = derive_configuration_fingerprint(&fingerprint_record)?;
let ephemeral_environment = resolve_environment_values(
&fingerprint_record.environment_references,
&request.environment,
)?;
Ok(ResolvedDomainConfiguration {
selected_profile: request.selected_profile,
domain_id,
configuration_fingerprint,
fingerprint_record,
effective_allow_prefixes,
effective_deny_prefixes,
ephemeral_environment,
configuration_source,
workspace,
storage_root,
runtime_root,
profile_root,
persistent_domain_root,
runtime_domain_root,
volumes,
auto_approval_rules,
})
}

View file

@ -0,0 +1,37 @@
#[cfg(test)]
mod tests {
use super::{DescriptorIdentity, descriptor_identities_alias, validate_path_suffix};
#[test]
fn bind_mount_identity_aliases_across_distinct_mount_ids() {
let original = DescriptorIdentity {
device: 7,
inode: 11,
mount_id: 13,
};
let bind_alias = DescriptorIdentity {
mount_id: 17,
..original
};
let distinct = DescriptorIdentity {
inode: 19,
..original
};
assert!(descriptor_identities_alias(original, bind_alias));
assert!(!descriptor_identities_alias(original, distinct));
}
#[test]
fn path_suffix_rejects_every_expansion_and_shell_metacharacter() {
for metacharacter in [
'*', '?', '[', ']', '{', '}', '$', '~', '\\', '`', '\'', '"', ';', '|', '&', '<', '>',
'(', ')', '!', '\n', '\r', '\t',
] {
assert!(
validate_path_suffix(&format!("safe{metacharacter}suffix")).is_err(),
"{metacharacter:?} must be rejected"
);
}
assert!(validate_path_suffix("safe path/with+literal@characters").is_ok());
}
}

View file

@ -0,0 +1,308 @@
/// Derives the specification-framed v1 Domain ID.
///
/// # Errors
/// A field longer than `u32::MAX` cannot be framed.
pub fn derive_domain_id(
selected_profile: &str,
canonical_workspace: &[u8],
) -> Result<DomainId, ConfigurationError> {
let mut preimage = b"lazaret-domain-id-v1\0".to_vec();
push_bytes(&mut preimage, selected_profile.as_bytes())?;
push_bytes(&mut preimage, canonical_workspace)?;
Ok(DomainId::new(Sha256::digest(preimage).into()))
}
/// Derives the specification-framed v1 Configuration Fingerprint.
///
/// # Errors
/// A field or collection longer than `u32::MAX` cannot be framed.
pub fn derive_configuration_fingerprint(
record: &ConfigurationFingerprintRecordV1,
) -> Result<ConfigurationFingerprint, ConfigurationError> {
let mut preimage = b"lazaret-configuration-fingerprint-v1\0".to_vec();
push_bytes(&mut preimage, record.selected_profile.as_bytes())?;
push_bytes(&mut preimage, record.storage_root.as_bytes())?;
push_bytes(&mut preimage, record.runtime_root.as_bytes())?;
push_bytes(&mut preimage, record.image.as_bytes())?;
push_bool(&mut preimage, record.isolate_gitignored);
push_sorted_list(
&mut preimage,
record.persistent_dirs.iter().map(|path| {
let mut item = Vec::new();
push_bytes(&mut item, path.as_bytes())?;
Ok(item)
}),
)?;
push_sorted_list(
&mut preimage,
record.environment_references.iter().map(|reference| {
let mut item = Vec::new();
push_bytes(&mut item, reference.sandbox_name.as_bytes())?;
push_bytes(&mut item, reference.host_name.as_bytes())?;
Ok(item)
}),
)?;
push_sorted_list(&mut preimage, record.volumes.iter().map(encode_volume))?;
push_sorted_list(
&mut preimage,
record
.effective_allow_prefixes
.iter()
.map(|prefix| Ok([prefix.0.as_slice(), &[prefix.1]].concat())),
)?;
push_sorted_list(
&mut preimage,
record
.effective_deny_prefixes
.iter()
.map(|prefix| Ok([prefix.0.as_slice(), &[prefix.1]].concat())),
)?;
push_sorted_list(
&mut preimage,
record.network_readiness.denied.iter().map(|target| {
Ok([
target.address.as_slice(),
target.port.to_be_bytes().as_slice(),
]
.concat())
}),
)?;
push_sorted_list(
&mut preimage,
record.network_readiness.permitted.iter().map(|target| {
Ok([
target.address.as_slice(),
target.port.to_be_bytes().as_slice(),
]
.concat())
}),
)?;
push_sorted_list(
&mut preimage,
record.network_readiness.dns.iter().map(|target| {
let mut item = target.resolver.to_vec();
push_bytes(&mut item, target.query.as_bytes())?;
Ok(item)
}),
)?;
push_bool(&mut preimage, record.escalation_enabled);
push_sorted_list(
&mut preimage,
record.auto_approval_rules.iter().map(encode_auto_approval),
)?;
Ok(ConfigurationFingerprint::new(
Sha256::digest(preimage).into(),
))
}
fn validate_structural_configuration(
configuration: &TrustedHostConfigurationV1,
) -> Result<(), ConfigurationError> {
if configuration.schema_id() != TRUSTED_HOST_CONFIGURATION_SCHEMA_ID {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidToml,
"unknown configuration schema",
));
}
for (name, profile) in configuration.profiles() {
validate_profile_name(name)?;
validate_text(&profile.image, "profile image")?;
validate_image_reference(&profile.image)?;
require_pairwise_non_ancestral(
profile.persistent_dirs.iter().map(String::as_str),
ConfigurationErrorKind::InvalidProfile,
)?;
for persistent in &profile.persistent_dirs {
validate_sandbox_path(persistent)?;
}
for (sandbox_name, reference) in &profile.env {
validate_environment_name(sandbox_name)?;
let host_name = reference.strip_prefix("env:").ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidProfile,
"environment value is not an env: reference",
)
})?;
validate_environment_name(host_name)?;
}
for volume in &profile.volumes {
validate_declared_host_path(&volume.host, true)?;
validate_sandbox_path(&volume.container)?;
}
if !profile.escalation.enabled && !profile.escalation.auto_approve.is_empty() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidAutoApproval,
"disabled escalation profile contains auto-approval rules",
));
}
for rule in &profile.escalation.auto_approve {
validate_declared_host_path(&rule.executable, false)?;
for argument in &rule.args {
validate_text(argument, "auto-approval argument")?;
}
if rule.allow_extra_args && rule.args.is_empty() {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidAutoApproval,
"allow_extra_args requires a nonempty configured prefix",
));
}
}
for prefix in configuration
.network()
.default_allow
.iter()
.chain(configuration.network().default_deny.iter())
.chain(profile.allow_egress.iter())
.chain(profile.deny_egress.iter())
{
parse_ipv4_prefix(prefix)?;
}
}
if let Some(data_root) = &configuration.storage().data_root {
validate_declared_host_path(data_root, false)?;
}
Ok(())
}
fn validate_image_reference(reference: &str) -> Result<(), ConfigurationError> {
let valid = !reference.is_empty()
&& reference.is_ascii()
&& !reference
.bytes()
.any(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control())
&& !reference.contains("://")
&& !reference.starts_with(['/', ':', '@'])
&& !reference.ends_with(['/', ':', '@'])
&& !reference.contains("//")
&& reference.split('@').count() <= 2;
if valid {
Ok(())
} else {
Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidProfile,
"profile image is not a valid v1 OCI reference",
))
}
}
fn validate_profile_name(name: &str) -> Result<(), ConfigurationError> {
let bytes = name.as_bytes();
let valid = (1..=64).contains(&bytes.len())
&& bytes[0].is_ascii_alphanumeric()
&& bytes
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
&& !matches!(name, "." | "..");
if valid {
Ok(())
} else {
Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidProfile,
"profile name violates the v1 grammar",
))
}
}
fn validate_environment_name(name: &str) -> Result<(), ConfigurationError> {
let mut bytes = name.bytes();
let valid = bytes
.next()
.is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_');
if valid {
Ok(())
} else {
Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidProfile,
"environment name violates the v1 grammar",
))
}
}
fn validate_text(value: &str, field: &str) -> Result<(), ConfigurationError> {
if value.contains('\0') {
Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidProfile,
format!("{field} contains NUL"),
))
} else {
Ok(())
}
}
fn validate_declared_host_path(
declaration: &str,
allow_profile: bool,
) -> Result<(), ConfigurationError> {
validate_text(declaration, "host path")?;
let suffix = if let Some(suffix) = declaration.strip_prefix('/') {
suffix
} else if let Some(suffix) = declaration.strip_prefix("~/") {
suffix
} else if allow_profile {
declaration.strip_prefix("{profile_dir}/").ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"host path is not absolute or an allowed anchored form",
)
})?
} else {
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"host path is not absolute or ~/ anchored",
));
};
validate_path_suffix(suffix)
}
fn validate_sandbox_path(path: &str) -> Result<(), ConfigurationError> {
validate_text(path, "sandbox path")?;
let suffix = path.strip_prefix('/').ok_or_else(|| {
ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"sandbox path is not absolute",
)
})?;
validate_path_suffix(suffix)
}
fn validate_path_suffix(suffix: &str) -> Result<(), ConfigurationError> {
if suffix.is_empty()
|| suffix.ends_with('/')
|| suffix
.split('/')
.any(|component| component.is_empty() || matches!(component, "." | ".."))
|| suffix.bytes().any(|byte| {
byte.is_ascii_control()
|| matches!(
byte,
b'*' | b'?'
| b'['
| b']'
| b'{'
| b'}'
| b'$'
| b'~'
| b'\\'
| b'`'
| b'\''
| b'"'
| b';'
| b'|'
| b'&'
| b'<'
| b'>'
| b'('
| b')'
| b'!'
)
})
{
return Err(ConfigurationError::new(
ConfigurationErrorKind::InvalidPath,
"path is empty, non-normalized, or contains expansion syntax",
));
}
Ok(())
}

View file

@ -839,682 +839,4 @@ pub struct DomainStore {
pub session_terminals: Vec<SessionTerminal>,
pub mailboxes: Vec<Mailbox>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum ParticipantOwnership {
PreparedLower(PreparedLowerOwnership),
StateAnchor(StateAnchorOwnership),
GitRouting(GitRoutingOwnership),
Network(Box<NetworkOwnership>),
Helper(HelperOwnership),
Sandbox(SandboxOwnership),
Publication(PublicationOwnership),
}
impl ParticipantOwnership {
#[must_use]
pub const fn role(&self) -> ParticipantRole {
match self {
Self::PreparedLower(_) => ParticipantRole::PreparedLower,
Self::StateAnchor(_) => ParticipantRole::StateAnchor,
Self::GitRouting(_) => ParticipantRole::GitRouting,
Self::Network(_) => ParticipantRole::Network,
Self::Helper(_) => ParticipantRole::Helper,
Self::Sandbox(_) => ParticipantRole::Sandbox,
Self::Publication(_) => ParticipantRole::Publication,
}
}
#[must_use]
pub fn empty(role: ParticipantRole) -> Self {
match role {
ParticipantRole::PreparedLower => Self::PreparedLower(PreparedLowerOwnership::empty()),
ParticipantRole::StateAnchor => Self::StateAnchor(StateAnchorOwnership::empty()),
ParticipantRole::GitRouting => Self::GitRouting(GitRoutingOwnership::empty()),
ParticipantRole::Network => Self::Network(Box::new(NetworkOwnership::empty())),
ParticipantRole::Helper => Self::Helper(HelperOwnership::empty()),
ParticipantRole::Sandbox => Self::Sandbox(SandboxOwnership::empty()),
ParticipantRole::Publication => Self::Publication(PublicationOwnership::empty()),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match self {
Self::PreparedLower(ownership) => ownership.is_empty(),
Self::StateAnchor(ownership) => ownership.is_empty(),
Self::GitRouting(ownership) => ownership.is_empty(),
Self::Network(ownership) => ownership.is_empty(),
Self::Helper(ownership) => ownership.is_empty(),
Self::Sandbox(ownership) => ownership.is_empty(),
Self::Publication(ownership) => ownership.is_empty(),
}
}
fn is_intent(&self) -> bool {
match self {
Self::PreparedLower(ownership) => ownership
.lease_state
.is_none_or(|state| state == ParticipantState::Intent),
_ => true,
}
}
fn is_release_intent(&self) -> bool {
match self {
Self::PreparedLower(ownership) => ownership
.lease_state
.is_none_or(|state| state == ParticipantState::ReleaseIntent),
_ => true,
}
}
fn is_active_for(&self, state: ParticipantState, generation_id: GenerationId) -> bool {
match (self, state) {
(Self::PreparedLower(ownership), ParticipantState::LeaseHeld) => {
ownership.is_lease_held()
}
(Self::StateAnchor(ownership), ParticipantState::Prepared) => ownership.is_prepared(),
(Self::GitRouting(ownership), ParticipantState::Prepared) => ownership.is_prepared(),
(Self::Network(ownership), ParticipantState::Committed) => {
ownership.is_committed(generation_id)
}
(Self::Helper(ownership), ParticipantState::Committed) => ownership.is_committed(),
(Self::Sandbox(ownership), ParticipantState::Committed) => ownership.is_committed(),
(Self::Publication(ownership), ParticipantState::Committed) => ownership.is_committed(),
_ => false,
}
}
fn cleared(&self) -> Self {
Self::empty(self.role())
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
match (self, next) {
(Self::PreparedLower(old), Self::PreparedLower(new)) => old.validate_progress_to(new),
(Self::StateAnchor(old), Self::StateAnchor(new)) => old.validate_progress_to(new),
(Self::GitRouting(old), Self::GitRouting(new)) => old.validate_progress_to(new),
(Self::Network(old), Self::Network(new)) => old.validate_progress_to(new),
(Self::Helper(old), Self::Helper(new)) => old.validate_progress_to(new),
(Self::Sandbox(old), Self::Sandbox(new)) => old.validate_progress_to(new),
(Self::Publication(old), Self::Publication(new)) => old.validate_progress_to(new),
_ => Err(RecordTransitionError::RecordIdentityChanged),
}
}
fn is_initial_progress(&self) -> bool {
match self {
Self::PreparedLower(_) | Self::Publication(_) => true,
Self::StateAnchor(ownership) => ownership.daemon.is_initial_acquisition(),
Self::GitRouting(ownership) => ownership.daemon.is_initial_acquisition(),
Self::Network(ownership) => ownership
.runtime_supervisor
.is_initial_or_current_committed(),
Self::Helper(ownership) => {
ownership.connectivity_helper.is_initial_acquisition()
&& ownership
.runtime_supervisor
.is_initial_or_current_committed()
}
Self::Sandbox(ownership) => {
ownership.host_coordinator.is_initial_or_current_committed()
&& ownership.runtime_supervisor.is_initial_acquisition()
&& ownership.attached_runsc.is_initial_acquisition()
}
}
}
fn validate_release_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
match (self, next) {
(Self::PreparedLower(old), Self::PreparedLower(new)) => old.validate_release_to(new),
_ if self == next => Ok(()),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PreparedLowerOwnership {
pub prepared_artifact_id: Option<PreparedArtifactId>,
pub source_identity: Option<SourceIdentity>,
pub lease_state: Option<ParticipantState>,
pub path: Option<HostPath>,
pub device: Option<DeviceId>,
pub inode: Option<InodeId>,
pub released_state_anchor: Option<StateAnchorReleaseEvidence>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct StateAnchorReleaseEvidence {
pub daemon: PidfsProcessIdentityV1,
pub user_namespace: NamespaceIdentity,
pub mount_namespace: NamespaceIdentity,
pub mount: MountIdentity,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct StateAnchorOwnership {
pub lower_artifact_id: Option<PreparedArtifactId>,
pub upper_path: Option<HostPath>,
pub work_path: Option<HostPath>,
pub merged_path: Option<HostPath>,
pub daemon: PersistedHostProcessOwnership,
pub user_namespace: Option<NamespaceIdentity>,
pub mount_namespace: Option<NamespaceIdentity>,
pub mount: Option<MountIdentity>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct GitRoutingOwnership {
pub mountpoint: Option<HostPath>,
pub host_root: Option<HostPath>,
pub isolated_root: Option<HostPath>,
pub daemon: PersistedHostProcessOwnership,
pub mount: Option<MountIdentity>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NetworkOwnership {
pub derived_policy_digest: Option<DerivedPolicyDigest>,
pub readiness_fingerprint: Option<ReadinessFingerprint>,
pub readiness_evidence: Option<NetworkReadinessEvidence>,
pub namespace: Option<NamespaceIdentity>,
pub runtime_supervisor: PersistedHostProcessOwnership,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct HelperOwnership {
pub network_sidecar_id: Option<NetworkSidecarId>,
pub connectivity_helper: PersistedHostProcessOwnership,
pub runtime_supervisor: PersistedHostProcessOwnership,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SandboxOwnership {
pub container_id: Option<ContainerId>,
pub runtime_root: Option<HostPath>,
pub runtime_root_identity: Option<MountIdentity>,
pub bundle_digest: Option<BundleDigest>,
pub host_coordinator: PersistedHostProcessOwnership,
pub runtime_supervisor: PersistedHostProcessOwnership,
pub attached_runsc: PersistedHostProcessOwnership,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PublicationOwnership {
pub staged_socket_path: Option<HostPath>,
pub staged_identity_path: Option<HostPath>,
pub socket_path: Option<HostPath>,
pub identity_path: Option<HostPath>,
pub socket_identity: Option<MountIdentity>,
pub identity_file_identity: Option<MountIdentity>,
}
impl PreparedLowerOwnership {
fn empty() -> Self {
Self {
prepared_artifact_id: None,
source_identity: None,
lease_state: None,
path: None,
device: None,
inode: None,
released_state_anchor: None,
}
}
fn is_empty(&self) -> bool {
self.prepared_artifact_id.is_none()
&& self.source_identity.is_none()
&& self.lease_state.is_none()
&& self.path.is_none()
&& self.device.is_none()
&& self.inode.is_none()
&& self.released_state_anchor.is_none()
}
fn is_lease_held(&self) -> bool {
self.prepared_artifact_id.is_some()
&& self.source_identity.is_some()
&& self.lease_state == Some(ParticipantState::LeaseHeld)
&& self.path.is_some()
&& self.device.is_some()
&& self.inode.is_some()
&& self.released_state_anchor.is_none()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.prepared_artifact_id.as_ref(),
next.prepared_artifact_id.as_ref(),
)?;
validate_option_progress(self.source_identity.as_ref(), next.source_identity.as_ref())?;
validate_prepared_lower_lease_progress(self.lease_state, next.lease_state)?;
validate_option_progress(self.path.as_ref(), next.path.as_ref())?;
validate_option_progress(self.device.as_ref(), next.device.as_ref())?;
validate_option_progress(self.inode.as_ref(), next.inode.as_ref())?;
validate_option_progress(
self.released_state_anchor.as_ref(),
next.released_state_anchor.as_ref(),
)
}
fn validate_release_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
if self.prepared_artifact_id != next.prepared_artifact_id
|| self.source_identity != next.source_identity
|| self.path != next.path
|| self.device != next.device
|| self.inode != next.inode
{
return Err(RecordTransitionError::EvidenceChanged);
}
let release_evidence_valid = match self.lease_state {
Some(ParticipantState::ReleaseIntent) => {
self.released_state_anchor == next.released_state_anchor
}
_ => next.released_state_anchor.is_some() || self.released_state_anchor.is_none(),
};
if !release_evidence_valid {
return Err(RecordTransitionError::EvidenceChanged);
}
match (self.lease_state, next.lease_state) {
(None, None)
| (Some(ParticipantState::ReleaseIntent), Some(ParticipantState::ReleaseIntent)) => {
Ok(())
}
(Some(_), Some(ParticipantState::ReleaseIntent)) => Ok(()),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
}
impl StateAnchorOwnership {
fn empty() -> Self {
Self {
lower_artifact_id: None,
upper_path: None,
work_path: None,
merged_path: None,
daemon: PersistedHostProcessOwnership::unassigned(),
user_namespace: None,
mount_namespace: None,
mount: None,
}
}
fn is_empty(&self) -> bool {
self.lower_artifact_id.is_none()
&& self.upper_path.is_none()
&& self.work_path.is_none()
&& self.merged_path.is_none()
&& self.daemon.is_unassigned()
&& self.user_namespace.is_none()
&& self.mount_namespace.is_none()
&& self.mount.is_none()
}
fn is_prepared(&self) -> bool {
self.lower_artifact_id.is_some()
&& self.upper_path.is_some()
&& self.work_path.is_some()
&& self.merged_path.is_some()
&& self.daemon.is_current_committed()
&& self.user_namespace.is_some()
&& self.mount_namespace.is_some()
&& self.mount.is_some()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.lower_artifact_id.as_ref(),
next.lower_artifact_id.as_ref(),
)?;
validate_option_progress(self.upper_path.as_ref(), next.upper_path.as_ref())?;
validate_option_progress(self.work_path.as_ref(), next.work_path.as_ref())?;
validate_option_progress(self.merged_path.as_ref(), next.merged_path.as_ref())?;
validate_process_progress(&self.daemon, &next.daemon)?;
validate_option_progress(self.user_namespace.as_ref(), next.user_namespace.as_ref())?;
validate_option_progress(self.mount_namespace.as_ref(), next.mount_namespace.as_ref())?;
validate_option_progress(self.mount.as_ref(), next.mount.as_ref())
}
}
impl GitRoutingOwnership {
fn empty() -> Self {
Self {
mountpoint: None,
host_root: None,
isolated_root: None,
daemon: PersistedHostProcessOwnership::unassigned(),
mount: None,
}
}
fn is_empty(&self) -> bool {
self.mountpoint.is_none()
&& self.host_root.is_none()
&& self.isolated_root.is_none()
&& self.daemon.is_unassigned()
&& self.mount.is_none()
}
fn is_prepared(&self) -> bool {
self.mountpoint.is_some()
&& self.host_root.is_some()
&& self.isolated_root.is_some()
&& self.daemon.is_current_committed()
&& self.mount.is_some()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(self.mountpoint.as_ref(), next.mountpoint.as_ref())?;
validate_option_progress(self.host_root.as_ref(), next.host_root.as_ref())?;
validate_option_progress(self.isolated_root.as_ref(), next.isolated_root.as_ref())?;
validate_process_progress(&self.daemon, &next.daemon)?;
validate_option_progress(self.mount.as_ref(), next.mount.as_ref())
}
}
impl NetworkOwnership {
fn empty() -> Self {
Self {
derived_policy_digest: None,
readiness_fingerprint: None,
readiness_evidence: None,
namespace: None,
runtime_supervisor: PersistedHostProcessOwnership::unassigned(),
}
}
fn is_empty(&self) -> bool {
self.derived_policy_digest.is_none()
&& self.readiness_fingerprint.is_none()
&& self.readiness_evidence.is_none()
&& self.namespace.is_none()
&& self.runtime_supervisor.is_unassigned()
}
fn is_committed(&self, generation_id: GenerationId) -> bool {
let (
Some(derived_policy_digest),
Some(readiness_fingerprint),
Some(readiness_evidence),
Some(namespace),
Ok(runtime_supervisor),
) = (
self.derived_policy_digest,
self.readiness_fingerprint,
self.readiness_evidence.as_ref(),
self.namespace,
self.runtime_supervisor.serviceable_pidfs(),
)
else {
return false;
};
readiness_evidence.matches(
generation_id,
derived_policy_digest,
readiness_fingerprint,
namespace,
runtime_supervisor,
)
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.derived_policy_digest.as_ref(),
next.derived_policy_digest.as_ref(),
)?;
validate_option_progress(
self.readiness_fingerprint.as_ref(),
next.readiness_fingerprint.as_ref(),
)?;
validate_option_progress(
self.readiness_evidence.as_ref(),
next.readiness_evidence.as_ref(),
)?;
validate_option_progress(self.namespace.as_ref(), next.namespace.as_ref())?;
validate_existing_process_progress(&self.runtime_supervisor, &next.runtime_supervisor)
}
}
impl HelperOwnership {
fn empty() -> Self {
Self {
network_sidecar_id: None,
connectivity_helper: PersistedHostProcessOwnership::unassigned(),
runtime_supervisor: PersistedHostProcessOwnership::unassigned(),
}
}
fn is_empty(&self) -> bool {
self.network_sidecar_id.is_none()
&& self.connectivity_helper.is_unassigned()
&& self.runtime_supervisor.is_unassigned()
}
fn is_committed(&self) -> bool {
self.network_sidecar_id.is_some()
&& self.connectivity_helper.is_current_committed()
&& self.runtime_supervisor.is_current_committed()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.network_sidecar_id.as_ref(),
next.network_sidecar_id.as_ref(),
)?;
validate_process_progress(&self.connectivity_helper, &next.connectivity_helper)?;
validate_existing_process_progress(&self.runtime_supervisor, &next.runtime_supervisor)
}
}
impl SandboxOwnership {
fn empty() -> Self {
Self {
container_id: None,
runtime_root: None,
runtime_root_identity: None,
bundle_digest: None,
host_coordinator: PersistedHostProcessOwnership::unassigned(),
runtime_supervisor: PersistedHostProcessOwnership::unassigned(),
attached_runsc: PersistedHostProcessOwnership::unassigned(),
}
}
fn is_empty(&self) -> bool {
self.container_id.is_none()
&& self.runtime_root.is_none()
&& self.runtime_root_identity.is_none()
&& self.bundle_digest.is_none()
&& self.host_coordinator.is_unassigned()
&& self.runtime_supervisor.is_unassigned()
&& self.attached_runsc.is_unassigned()
}
fn is_committed(&self) -> bool {
self.container_id.is_some()
&& self.runtime_root.is_some()
&& self.runtime_root_identity.is_some()
&& self.bundle_digest.is_some()
&& self.host_coordinator.is_current_committed()
&& self.runtime_supervisor.is_current_committed()
&& self.attached_runsc.is_current_committed()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(self.container_id.as_ref(), next.container_id.as_ref())?;
validate_option_progress(self.runtime_root.as_ref(), next.runtime_root.as_ref())?;
validate_option_progress(
self.runtime_root_identity.as_ref(),
next.runtime_root_identity.as_ref(),
)?;
validate_option_progress(self.bundle_digest.as_ref(), next.bundle_digest.as_ref())?;
validate_existing_process_progress(&self.host_coordinator, &next.host_coordinator)?;
validate_process_progress(&self.runtime_supervisor, &next.runtime_supervisor)?;
validate_process_progress(&self.attached_runsc, &next.attached_runsc)
}
}
impl PublicationOwnership {
fn empty() -> Self {
Self {
staged_socket_path: None,
staged_identity_path: None,
socket_path: None,
identity_path: None,
socket_identity: None,
identity_file_identity: None,
}
}
fn is_empty(&self) -> bool {
self.staged_socket_path.is_none()
&& self.staged_identity_path.is_none()
&& self.socket_path.is_none()
&& self.identity_path.is_none()
&& self.socket_identity.is_none()
&& self.identity_file_identity.is_none()
}
fn is_committed(&self) -> bool {
self.staged_socket_path.is_some()
&& self.staged_identity_path.is_some()
&& self.socket_path.is_some()
&& self.identity_path.is_some()
&& self.socket_identity.is_some()
&& self.identity_file_identity.is_some()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.staged_socket_path.as_ref(),
next.staged_socket_path.as_ref(),
)?;
validate_option_progress(
self.staged_identity_path.as_ref(),
next.staged_identity_path.as_ref(),
)?;
validate_option_progress(self.socket_path.as_ref(), next.socket_path.as_ref())?;
validate_option_progress(self.identity_path.as_ref(), next.identity_path.as_ref())?;
validate_option_progress(self.socket_identity.as_ref(), next.socket_identity.as_ref())?;
validate_option_progress(
self.identity_file_identity.as_ref(),
next.identity_file_identity.as_ref(),
)
}
}
fn validate_participant_progress(
old: Option<&ParticipantOwnership>,
next: Option<&ParticipantOwnership>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(None, None) => Ok(()),
(None, Some(next)) if next.is_initial_progress() => Ok(()),
(None, Some(_)) => Err(RecordTransitionError::ProcessStageSkipped),
(Some(_), None) => Err(RecordTransitionError::EvidenceLost),
(Some(old), Some(next)) => old.validate_progress_to(next),
}
}
fn validate_participant_release(
old: Option<&ParticipantOwnership>,
next: Option<&ParticipantOwnership>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(Some(old), Some(next)) => old.validate_release_to(next),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
fn validate_participant_activation(
old: Option<&ParticipantOwnership>,
next: Option<&ParticipantOwnership>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(Some(old), Some(next)) => old.validate_progress_to(next),
_ => Err(RecordTransitionError::PriorAcquisitionSnapshotRequired),
}
}
fn validate_prepared_lower_lease_progress(
old: Option<ParticipantState>,
next: Option<ParticipantState>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(None, None | Some(ParticipantState::Intent))
| (
Some(ParticipantState::Intent),
Some(ParticipantState::Intent | ParticipantState::LeaseHeld),
)
| (Some(ParticipantState::LeaseHeld), Some(ParticipantState::LeaseHeld)) => Ok(()),
(Some(_), None) => Err(RecordTransitionError::EvidenceLost),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
fn validate_option_progress<T: Eq>(
old: Option<&T>,
next: Option<&T>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(None, None | Some(_)) => Ok(()),
(Some(_), None) => Err(RecordTransitionError::EvidenceLost),
(Some(old), Some(next)) if old == next => Ok(()),
(Some(_), Some(_)) => Err(RecordTransitionError::EvidenceChanged),
}
}
fn validate_process_progress(
old: &PersistedHostProcessOwnership,
next: &PersistedHostProcessOwnership,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(
PersistedHostProcessOwnership::Unassigned,
PersistedHostProcessOwnership::Unassigned | PersistedHostProcessOwnership::Staged(_),
) => Ok(()),
(
PersistedHostProcessOwnership::Unassigned,
PersistedHostProcessOwnership::Committed(_),
) => Err(RecordTransitionError::ProcessStageSkipped),
(
PersistedHostProcessOwnership::Staged(_) | PersistedHostProcessOwnership::Committed(_),
PersistedHostProcessOwnership::Unassigned,
) => Err(RecordTransitionError::EvidenceLost),
(PersistedHostProcessOwnership::Committed(_), PersistedHostProcessOwnership::Staged(_)) => {
Err(RecordTransitionError::EvidenceLost)
}
(
PersistedHostProcessOwnership::Staged(old),
PersistedHostProcessOwnership::Staged(next),
) if old == next => Ok(()),
(
PersistedHostProcessOwnership::Staged(old),
PersistedHostProcessOwnership::Committed(next),
) if old == next.pidfs() => Ok(()),
(
PersistedHostProcessOwnership::Committed(old),
PersistedHostProcessOwnership::Committed(next),
) if old == next => Ok(()),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
fn validate_existing_process_progress(
old: &PersistedHostProcessOwnership,
next: &PersistedHostProcessOwnership,
) -> Result<(), RecordTransitionError> {
if matches!(
(old, next),
(
PersistedHostProcessOwnership::Unassigned,
PersistedHostProcessOwnership::Committed(_)
)
) {
Ok(())
} else {
validate_process_progress(old, next)
}
}
include!("lifecycle/ownership.rs");

View file

@ -0,0 +1,679 @@
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum ParticipantOwnership {
PreparedLower(PreparedLowerOwnership),
StateAnchor(StateAnchorOwnership),
GitRouting(GitRoutingOwnership),
Network(Box<NetworkOwnership>),
Helper(HelperOwnership),
Sandbox(SandboxOwnership),
Publication(PublicationOwnership),
}
impl ParticipantOwnership {
#[must_use]
pub const fn role(&self) -> ParticipantRole {
match self {
Self::PreparedLower(_) => ParticipantRole::PreparedLower,
Self::StateAnchor(_) => ParticipantRole::StateAnchor,
Self::GitRouting(_) => ParticipantRole::GitRouting,
Self::Network(_) => ParticipantRole::Network,
Self::Helper(_) => ParticipantRole::Helper,
Self::Sandbox(_) => ParticipantRole::Sandbox,
Self::Publication(_) => ParticipantRole::Publication,
}
}
#[must_use]
pub fn empty(role: ParticipantRole) -> Self {
match role {
ParticipantRole::PreparedLower => Self::PreparedLower(PreparedLowerOwnership::empty()),
ParticipantRole::StateAnchor => Self::StateAnchor(StateAnchorOwnership::empty()),
ParticipantRole::GitRouting => Self::GitRouting(GitRoutingOwnership::empty()),
ParticipantRole::Network => Self::Network(Box::new(NetworkOwnership::empty())),
ParticipantRole::Helper => Self::Helper(HelperOwnership::empty()),
ParticipantRole::Sandbox => Self::Sandbox(SandboxOwnership::empty()),
ParticipantRole::Publication => Self::Publication(PublicationOwnership::empty()),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match self {
Self::PreparedLower(ownership) => ownership.is_empty(),
Self::StateAnchor(ownership) => ownership.is_empty(),
Self::GitRouting(ownership) => ownership.is_empty(),
Self::Network(ownership) => ownership.is_empty(),
Self::Helper(ownership) => ownership.is_empty(),
Self::Sandbox(ownership) => ownership.is_empty(),
Self::Publication(ownership) => ownership.is_empty(),
}
}
fn is_intent(&self) -> bool {
match self {
Self::PreparedLower(ownership) => ownership
.lease_state
.is_none_or(|state| state == ParticipantState::Intent),
_ => true,
}
}
fn is_release_intent(&self) -> bool {
match self {
Self::PreparedLower(ownership) => ownership
.lease_state
.is_none_or(|state| state == ParticipantState::ReleaseIntent),
_ => true,
}
}
fn is_active_for(&self, state: ParticipantState, generation_id: GenerationId) -> bool {
match (self, state) {
(Self::PreparedLower(ownership), ParticipantState::LeaseHeld) => {
ownership.is_lease_held()
}
(Self::StateAnchor(ownership), ParticipantState::Prepared) => ownership.is_prepared(),
(Self::GitRouting(ownership), ParticipantState::Prepared) => ownership.is_prepared(),
(Self::Network(ownership), ParticipantState::Committed) => {
ownership.is_committed(generation_id)
}
(Self::Helper(ownership), ParticipantState::Committed) => ownership.is_committed(),
(Self::Sandbox(ownership), ParticipantState::Committed) => ownership.is_committed(),
(Self::Publication(ownership), ParticipantState::Committed) => ownership.is_committed(),
_ => false,
}
}
fn cleared(&self) -> Self {
Self::empty(self.role())
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
match (self, next) {
(Self::PreparedLower(old), Self::PreparedLower(new)) => old.validate_progress_to(new),
(Self::StateAnchor(old), Self::StateAnchor(new)) => old.validate_progress_to(new),
(Self::GitRouting(old), Self::GitRouting(new)) => old.validate_progress_to(new),
(Self::Network(old), Self::Network(new)) => old.validate_progress_to(new),
(Self::Helper(old), Self::Helper(new)) => old.validate_progress_to(new),
(Self::Sandbox(old), Self::Sandbox(new)) => old.validate_progress_to(new),
(Self::Publication(old), Self::Publication(new)) => old.validate_progress_to(new),
_ => Err(RecordTransitionError::RecordIdentityChanged),
}
}
fn is_initial_progress(&self) -> bool {
match self {
Self::PreparedLower(_) | Self::Publication(_) => true,
Self::StateAnchor(ownership) => ownership.daemon.is_initial_acquisition(),
Self::GitRouting(ownership) => ownership.daemon.is_initial_acquisition(),
Self::Network(ownership) => ownership
.runtime_supervisor
.is_initial_or_current_committed(),
Self::Helper(ownership) => {
ownership.connectivity_helper.is_initial_acquisition()
&& ownership
.runtime_supervisor
.is_initial_or_current_committed()
}
Self::Sandbox(ownership) => {
ownership.host_coordinator.is_initial_or_current_committed()
&& ownership.runtime_supervisor.is_initial_acquisition()
&& ownership.attached_runsc.is_initial_acquisition()
}
}
}
fn validate_release_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
match (self, next) {
(Self::PreparedLower(old), Self::PreparedLower(new)) => old.validate_release_to(new),
_ if self == next => Ok(()),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PreparedLowerOwnership {
pub prepared_artifact_id: Option<PreparedArtifactId>,
pub source_identity: Option<SourceIdentity>,
pub lease_state: Option<ParticipantState>,
pub path: Option<HostPath>,
pub device: Option<DeviceId>,
pub inode: Option<InodeId>,
pub released_state_anchor: Option<StateAnchorReleaseEvidence>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct StateAnchorReleaseEvidence {
pub daemon: PidfsProcessIdentityV1,
pub user_namespace: NamespaceIdentity,
pub mount_namespace: NamespaceIdentity,
pub mount: MountIdentity,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct StateAnchorOwnership {
pub lower_artifact_id: Option<PreparedArtifactId>,
pub upper_path: Option<HostPath>,
pub work_path: Option<HostPath>,
pub merged_path: Option<HostPath>,
pub daemon: PersistedHostProcessOwnership,
pub user_namespace: Option<NamespaceIdentity>,
pub mount_namespace: Option<NamespaceIdentity>,
pub mount: Option<MountIdentity>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct GitRoutingOwnership {
pub mountpoint: Option<HostPath>,
pub host_root: Option<HostPath>,
pub isolated_root: Option<HostPath>,
pub daemon: PersistedHostProcessOwnership,
pub mount: Option<MountIdentity>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NetworkOwnership {
pub derived_policy_digest: Option<DerivedPolicyDigest>,
pub readiness_fingerprint: Option<ReadinessFingerprint>,
pub readiness_evidence: Option<NetworkReadinessEvidence>,
pub namespace: Option<NamespaceIdentity>,
pub runtime_supervisor: PersistedHostProcessOwnership,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct HelperOwnership {
pub network_sidecar_id: Option<NetworkSidecarId>,
pub connectivity_helper: PersistedHostProcessOwnership,
pub runtime_supervisor: PersistedHostProcessOwnership,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SandboxOwnership {
pub container_id: Option<ContainerId>,
pub runtime_root: Option<HostPath>,
pub runtime_root_identity: Option<MountIdentity>,
pub bundle_digest: Option<BundleDigest>,
pub host_coordinator: PersistedHostProcessOwnership,
pub runtime_supervisor: PersistedHostProcessOwnership,
pub attached_runsc: PersistedHostProcessOwnership,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PublicationOwnership {
pub staged_socket_path: Option<HostPath>,
pub staged_identity_path: Option<HostPath>,
pub socket_path: Option<HostPath>,
pub identity_path: Option<HostPath>,
pub socket_identity: Option<MountIdentity>,
pub identity_file_identity: Option<MountIdentity>,
}
impl PreparedLowerOwnership {
fn empty() -> Self {
Self {
prepared_artifact_id: None,
source_identity: None,
lease_state: None,
path: None,
device: None,
inode: None,
released_state_anchor: None,
}
}
fn is_empty(&self) -> bool {
self.prepared_artifact_id.is_none()
&& self.source_identity.is_none()
&& self.lease_state.is_none()
&& self.path.is_none()
&& self.device.is_none()
&& self.inode.is_none()
&& self.released_state_anchor.is_none()
}
fn is_lease_held(&self) -> bool {
self.prepared_artifact_id.is_some()
&& self.source_identity.is_some()
&& self.lease_state == Some(ParticipantState::LeaseHeld)
&& self.path.is_some()
&& self.device.is_some()
&& self.inode.is_some()
&& self.released_state_anchor.is_none()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.prepared_artifact_id.as_ref(),
next.prepared_artifact_id.as_ref(),
)?;
validate_option_progress(self.source_identity.as_ref(), next.source_identity.as_ref())?;
validate_prepared_lower_lease_progress(self.lease_state, next.lease_state)?;
validate_option_progress(self.path.as_ref(), next.path.as_ref())?;
validate_option_progress(self.device.as_ref(), next.device.as_ref())?;
validate_option_progress(self.inode.as_ref(), next.inode.as_ref())?;
validate_option_progress(
self.released_state_anchor.as_ref(),
next.released_state_anchor.as_ref(),
)
}
fn validate_release_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
if self.prepared_artifact_id != next.prepared_artifact_id
|| self.source_identity != next.source_identity
|| self.path != next.path
|| self.device != next.device
|| self.inode != next.inode
{
return Err(RecordTransitionError::EvidenceChanged);
}
let release_evidence_valid = match self.lease_state {
Some(ParticipantState::ReleaseIntent) => {
self.released_state_anchor == next.released_state_anchor
}
_ => next.released_state_anchor.is_some() || self.released_state_anchor.is_none(),
};
if !release_evidence_valid {
return Err(RecordTransitionError::EvidenceChanged);
}
match (self.lease_state, next.lease_state) {
(None, None)
| (Some(ParticipantState::ReleaseIntent), Some(ParticipantState::ReleaseIntent)) => {
Ok(())
}
(Some(_), Some(ParticipantState::ReleaseIntent)) => Ok(()),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
}
impl StateAnchorOwnership {
fn empty() -> Self {
Self {
lower_artifact_id: None,
upper_path: None,
work_path: None,
merged_path: None,
daemon: PersistedHostProcessOwnership::unassigned(),
user_namespace: None,
mount_namespace: None,
mount: None,
}
}
fn is_empty(&self) -> bool {
self.lower_artifact_id.is_none()
&& self.upper_path.is_none()
&& self.work_path.is_none()
&& self.merged_path.is_none()
&& self.daemon.is_unassigned()
&& self.user_namespace.is_none()
&& self.mount_namespace.is_none()
&& self.mount.is_none()
}
fn is_prepared(&self) -> bool {
self.lower_artifact_id.is_some()
&& self.upper_path.is_some()
&& self.work_path.is_some()
&& self.merged_path.is_some()
&& self.daemon.is_current_committed()
&& self.user_namespace.is_some()
&& self.mount_namespace.is_some()
&& self.mount.is_some()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.lower_artifact_id.as_ref(),
next.lower_artifact_id.as_ref(),
)?;
validate_option_progress(self.upper_path.as_ref(), next.upper_path.as_ref())?;
validate_option_progress(self.work_path.as_ref(), next.work_path.as_ref())?;
validate_option_progress(self.merged_path.as_ref(), next.merged_path.as_ref())?;
validate_process_progress(&self.daemon, &next.daemon)?;
validate_option_progress(self.user_namespace.as_ref(), next.user_namespace.as_ref())?;
validate_option_progress(self.mount_namespace.as_ref(), next.mount_namespace.as_ref())?;
validate_option_progress(self.mount.as_ref(), next.mount.as_ref())
}
}
impl GitRoutingOwnership {
fn empty() -> Self {
Self {
mountpoint: None,
host_root: None,
isolated_root: None,
daemon: PersistedHostProcessOwnership::unassigned(),
mount: None,
}
}
fn is_empty(&self) -> bool {
self.mountpoint.is_none()
&& self.host_root.is_none()
&& self.isolated_root.is_none()
&& self.daemon.is_unassigned()
&& self.mount.is_none()
}
fn is_prepared(&self) -> bool {
self.mountpoint.is_some()
&& self.host_root.is_some()
&& self.isolated_root.is_some()
&& self.daemon.is_current_committed()
&& self.mount.is_some()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(self.mountpoint.as_ref(), next.mountpoint.as_ref())?;
validate_option_progress(self.host_root.as_ref(), next.host_root.as_ref())?;
validate_option_progress(self.isolated_root.as_ref(), next.isolated_root.as_ref())?;
validate_process_progress(&self.daemon, &next.daemon)?;
validate_option_progress(self.mount.as_ref(), next.mount.as_ref())
}
}
impl NetworkOwnership {
fn empty() -> Self {
Self {
derived_policy_digest: None,
readiness_fingerprint: None,
readiness_evidence: None,
namespace: None,
runtime_supervisor: PersistedHostProcessOwnership::unassigned(),
}
}
fn is_empty(&self) -> bool {
self.derived_policy_digest.is_none()
&& self.readiness_fingerprint.is_none()
&& self.readiness_evidence.is_none()
&& self.namespace.is_none()
&& self.runtime_supervisor.is_unassigned()
}
fn is_committed(&self, generation_id: GenerationId) -> bool {
let (
Some(derived_policy_digest),
Some(readiness_fingerprint),
Some(readiness_evidence),
Some(namespace),
Ok(runtime_supervisor),
) = (
self.derived_policy_digest,
self.readiness_fingerprint,
self.readiness_evidence.as_ref(),
self.namespace,
self.runtime_supervisor.serviceable_pidfs(),
)
else {
return false;
};
readiness_evidence.matches(
generation_id,
derived_policy_digest,
readiness_fingerprint,
namespace,
runtime_supervisor,
)
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.derived_policy_digest.as_ref(),
next.derived_policy_digest.as_ref(),
)?;
validate_option_progress(
self.readiness_fingerprint.as_ref(),
next.readiness_fingerprint.as_ref(),
)?;
validate_option_progress(
self.readiness_evidence.as_ref(),
next.readiness_evidence.as_ref(),
)?;
validate_option_progress(self.namespace.as_ref(), next.namespace.as_ref())?;
validate_existing_process_progress(&self.runtime_supervisor, &next.runtime_supervisor)
}
}
impl HelperOwnership {
fn empty() -> Self {
Self {
network_sidecar_id: None,
connectivity_helper: PersistedHostProcessOwnership::unassigned(),
runtime_supervisor: PersistedHostProcessOwnership::unassigned(),
}
}
fn is_empty(&self) -> bool {
self.network_sidecar_id.is_none()
&& self.connectivity_helper.is_unassigned()
&& self.runtime_supervisor.is_unassigned()
}
fn is_committed(&self) -> bool {
self.network_sidecar_id.is_some()
&& self.connectivity_helper.is_current_committed()
&& self.runtime_supervisor.is_current_committed()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.network_sidecar_id.as_ref(),
next.network_sidecar_id.as_ref(),
)?;
validate_process_progress(&self.connectivity_helper, &next.connectivity_helper)?;
validate_existing_process_progress(&self.runtime_supervisor, &next.runtime_supervisor)
}
}
impl SandboxOwnership {
fn empty() -> Self {
Self {
container_id: None,
runtime_root: None,
runtime_root_identity: None,
bundle_digest: None,
host_coordinator: PersistedHostProcessOwnership::unassigned(),
runtime_supervisor: PersistedHostProcessOwnership::unassigned(),
attached_runsc: PersistedHostProcessOwnership::unassigned(),
}
}
fn is_empty(&self) -> bool {
self.container_id.is_none()
&& self.runtime_root.is_none()
&& self.runtime_root_identity.is_none()
&& self.bundle_digest.is_none()
&& self.host_coordinator.is_unassigned()
&& self.runtime_supervisor.is_unassigned()
&& self.attached_runsc.is_unassigned()
}
fn is_committed(&self) -> bool {
self.container_id.is_some()
&& self.runtime_root.is_some()
&& self.runtime_root_identity.is_some()
&& self.bundle_digest.is_some()
&& self.host_coordinator.is_current_committed()
&& self.runtime_supervisor.is_current_committed()
&& self.attached_runsc.is_current_committed()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(self.container_id.as_ref(), next.container_id.as_ref())?;
validate_option_progress(self.runtime_root.as_ref(), next.runtime_root.as_ref())?;
validate_option_progress(
self.runtime_root_identity.as_ref(),
next.runtime_root_identity.as_ref(),
)?;
validate_option_progress(self.bundle_digest.as_ref(), next.bundle_digest.as_ref())?;
validate_existing_process_progress(&self.host_coordinator, &next.host_coordinator)?;
validate_process_progress(&self.runtime_supervisor, &next.runtime_supervisor)?;
validate_process_progress(&self.attached_runsc, &next.attached_runsc)
}
}
impl PublicationOwnership {
fn empty() -> Self {
Self {
staged_socket_path: None,
staged_identity_path: None,
socket_path: None,
identity_path: None,
socket_identity: None,
identity_file_identity: None,
}
}
fn is_empty(&self) -> bool {
self.staged_socket_path.is_none()
&& self.staged_identity_path.is_none()
&& self.socket_path.is_none()
&& self.identity_path.is_none()
&& self.socket_identity.is_none()
&& self.identity_file_identity.is_none()
}
fn is_committed(&self) -> bool {
self.staged_socket_path.is_some()
&& self.staged_identity_path.is_some()
&& self.socket_path.is_some()
&& self.identity_path.is_some()
&& self.socket_identity.is_some()
&& self.identity_file_identity.is_some()
}
fn validate_progress_to(&self, next: &Self) -> Result<(), RecordTransitionError> {
validate_option_progress(
self.staged_socket_path.as_ref(),
next.staged_socket_path.as_ref(),
)?;
validate_option_progress(
self.staged_identity_path.as_ref(),
next.staged_identity_path.as_ref(),
)?;
validate_option_progress(self.socket_path.as_ref(), next.socket_path.as_ref())?;
validate_option_progress(self.identity_path.as_ref(), next.identity_path.as_ref())?;
validate_option_progress(self.socket_identity.as_ref(), next.socket_identity.as_ref())?;
validate_option_progress(
self.identity_file_identity.as_ref(),
next.identity_file_identity.as_ref(),
)
}
}
fn validate_participant_progress(
old: Option<&ParticipantOwnership>,
next: Option<&ParticipantOwnership>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(None, None) => Ok(()),
(None, Some(next)) if next.is_initial_progress() => Ok(()),
(None, Some(_)) => Err(RecordTransitionError::ProcessStageSkipped),
(Some(_), None) => Err(RecordTransitionError::EvidenceLost),
(Some(old), Some(next)) => old.validate_progress_to(next),
}
}
fn validate_participant_release(
old: Option<&ParticipantOwnership>,
next: Option<&ParticipantOwnership>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(Some(old), Some(next)) => old.validate_release_to(next),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
fn validate_participant_activation(
old: Option<&ParticipantOwnership>,
next: Option<&ParticipantOwnership>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(Some(old), Some(next)) => old.validate_progress_to(next),
_ => Err(RecordTransitionError::PriorAcquisitionSnapshotRequired),
}
}
fn validate_prepared_lower_lease_progress(
old: Option<ParticipantState>,
next: Option<ParticipantState>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(None, None | Some(ParticipantState::Intent))
| (
Some(ParticipantState::Intent),
Some(ParticipantState::Intent | ParticipantState::LeaseHeld),
)
| (Some(ParticipantState::LeaseHeld), Some(ParticipantState::LeaseHeld)) => Ok(()),
(Some(_), None) => Err(RecordTransitionError::EvidenceLost),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
fn validate_option_progress<T: Eq>(
old: Option<&T>,
next: Option<&T>,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(None, None | Some(_)) => Ok(()),
(Some(_), None) => Err(RecordTransitionError::EvidenceLost),
(Some(old), Some(next)) if old == next => Ok(()),
(Some(_), Some(_)) => Err(RecordTransitionError::EvidenceChanged),
}
}
fn validate_process_progress(
old: &PersistedHostProcessOwnership,
next: &PersistedHostProcessOwnership,
) -> Result<(), RecordTransitionError> {
match (old, next) {
(
PersistedHostProcessOwnership::Unassigned,
PersistedHostProcessOwnership::Unassigned | PersistedHostProcessOwnership::Staged(_),
) => Ok(()),
(
PersistedHostProcessOwnership::Unassigned,
PersistedHostProcessOwnership::Committed(_),
) => Err(RecordTransitionError::ProcessStageSkipped),
(
PersistedHostProcessOwnership::Staged(_) | PersistedHostProcessOwnership::Committed(_),
PersistedHostProcessOwnership::Unassigned,
) => Err(RecordTransitionError::EvidenceLost),
(PersistedHostProcessOwnership::Committed(_), PersistedHostProcessOwnership::Staged(_)) => {
Err(RecordTransitionError::EvidenceLost)
}
(
PersistedHostProcessOwnership::Staged(old),
PersistedHostProcessOwnership::Staged(next),
) if old == next => Ok(()),
(
PersistedHostProcessOwnership::Staged(old),
PersistedHostProcessOwnership::Committed(next),
) if old == next.pidfs() => Ok(()),
(
PersistedHostProcessOwnership::Committed(old),
PersistedHostProcessOwnership::Committed(next),
) if old == next => Ok(()),
_ => Err(RecordTransitionError::EvidenceChanged),
}
}
fn validate_existing_process_progress(
old: &PersistedHostProcessOwnership,
next: &PersistedHostProcessOwnership,
) -> Result<(), RecordTransitionError> {
if matches!(
(old, next),
(
PersistedHostProcessOwnership::Unassigned,
PersistedHostProcessOwnership::Committed(_)
)
) {
Ok(())
} else {
validate_process_progress(old, next)
}
}