Refuse inexact production selector authorities

This commit is contained in:
ms2k 2026-07-16 21:06:02 +09:00
commit 4cf7a5e595
4 changed files with 230 additions and 34 deletions

View file

@ -13,6 +13,7 @@ use lazaret_core::identity::{
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation};
use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact};
use lazaret_linux::filesystem::namespace_identity;
use lazaret_linux::platform::{PlatformObservation, check_platform};
use lazaret_prepared_lower::{
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedLowerStore, RegistryCredentials,
};
@ -48,6 +49,7 @@ pub struct InstalledProductPaths<'a> {
}
pub struct ProductionSelection {
store: Arc<HostCoordinatorDomainStore>,
domain_id: DomainId,
configuration_fingerprint: ConfigurationFingerprint,
catalog: Catalog,
@ -68,7 +70,6 @@ impl ProductionSelection {
/// Refuses a configuration other than the exact one used for selection.
pub fn cold_start_inputs<'a>(
&'a self,
store: Arc<HostCoordinatorDomainStore>,
configuration: &'a ResolvedDomainConfiguration,
) -> Result<ProductionColdStartInputs<'a>, ProductionSelectionError> {
if configuration.domain_id() != self.domain_id
@ -77,7 +78,7 @@ impl ProductionSelection {
return Err(failure("selected cold start does not match configuration"));
}
Ok(ProductionColdStartInputs {
store,
store: Arc::clone(&self.store),
catalog: &self.catalog,
configuration,
generation: self.generation.clone(),
@ -105,9 +106,30 @@ impl ProductionSelection {
/// changed Storage Root authority, exhausted identities, and configuration-policy mismatch.
pub async fn select_production_cold_start(
configuration: &ResolvedDomainConfiguration,
store: &HostCoordinatorDomainStore,
store: Arc<HostCoordinatorDomainStore>,
product_paths: InstalledProductPaths<'_>,
) -> Result<ProductionSelection, ProductionSelectionError> {
let kernel_release = std::fs::read_to_string("/proc/sys/kernel/osrelease").map_err(failure)?;
select_production_cold_start_for_platform(
configuration,
store,
product_paths,
&PlatformObservation {
operating_system: std::env::consts::OS,
architecture: std::env::consts::ARCH,
kernel_release: kernel_release.trim(),
},
)
.await
}
async fn select_production_cold_start_for_platform(
configuration: &ResolvedDomainConfiguration,
store: Arc<HostCoordinatorDomainStore>,
product_paths: InstalledProductPaths<'_>,
observation: &PlatformObservation<'_>,
) -> Result<ProductionSelection, ProductionSelectionError> {
check_platform(observation).map_err(failure)?;
let escalation_client = open_installed_executable(product_paths.escalation_client)?;
let sandbox_init = open_installed_executable(product_paths.sandbox_init)?;
let catalog = Catalog::under_storage_root(configuration.storage_root()).map_err(failure)?;
@ -122,7 +144,7 @@ pub async fn select_production_cold_start(
.pull_confined(
&configuration.fingerprint_record().image,
&RegistryCredentials::Anonymous,
&host_platform()?,
&Platform::linux("amd64"),
configuration.storage_root(),
random_id()?,
)
@ -137,16 +159,7 @@ pub async fn select_production_cold_start(
.map_err(failure)?;
image.cleanup().map_err(failure)?;
let next_generation = store
.snapshot()
.map_err(failure)?
.generations
.iter()
.map(|generation| generation.generation_id.get())
.max()
.unwrap_or(0)
.checked_add(1)
.ok_or_else(|| failure("generation identity exhausted"))?;
let next_generation = next_generation(&store)?;
let (coordinator_id, network_sidecar_id, sandbox_id) = distinct_generation_ids()?;
let generation = Generation {
domain_id: configuration.domain_id(),
@ -161,6 +174,7 @@ pub async fn select_production_cold_start(
let (resolver_addresses, network_probe, reachability) =
select_network_readiness(configuration, &generation, namespace);
Ok(ProductionSelection {
store,
domain_id: configuration.domain_id(),
configuration_fingerprint: configuration.configuration_fingerprint(),
catalog,
@ -175,6 +189,19 @@ pub async fn select_production_cold_start(
})
}
fn next_generation(store: &HostCoordinatorDomainStore) -> Result<u64, ProductionSelectionError> {
store
.snapshot()
.map_err(failure)?
.generations
.iter()
.map(|generation| generation.generation_id.get())
.max()
.unwrap_or(0)
.checked_add(1)
.ok_or_else(|| failure("generation identity exhausted"))
}
fn select_network_readiness(
configuration: &ResolvedDomainConfiguration,
generation: &Generation,
@ -270,16 +297,6 @@ fn rootless_id_map() -> IdMap {
}
}
fn host_platform() -> Result<Platform, ProductionSelectionError> {
match std::env::consts::ARCH {
"x86_64" => Ok(Platform::linux("amd64")),
"aarch64" => Ok(Platform::linux("arm64")),
architecture => Err(failure(format!(
"host architecture {architecture} has no selected OCI platform mapping"
))),
}
}
fn distinct_generation_ids()
-> Result<(CoordinatorId, NetworkSidecarId, SandboxId), ProductionSelectionError> {
for _ in 0..8 {
@ -307,12 +324,20 @@ fn random_id() -> Result<[u8; 32], ProductionSelectionError> {
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::fs;
use std::fs::{self, OpenOptions};
use std::future::Future;
use std::os::unix::ffi::OsStringExt;
use std::path::PathBuf;
use std::task::{Context, Poll, Waker};
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
use lazaret_core::identity::{DeviceId, InodeId, NamespaceIdentity};
use lazaret_core::records::Domain;
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_runtime_supervisor::sandbox::production_sandbox_layout;
use crate::domain_store::DomainStoreMutation;
use super::*;
#[test]
@ -370,7 +395,108 @@ mod tests {
assert_ne!(coordinator.as_bytes(), network.as_bytes());
assert_ne!(coordinator.as_bytes(), sandbox.as_bytes());
assert_ne!(network.as_bytes(), sandbox.as_bytes());
assert!(host_platform().is_ok());
}
#[test]
fn unsupported_platforms_refuse_before_installed_or_storage_side_effects() {
for observation in [
PlatformObservation {
operating_system: "linux",
architecture: "x86_64",
kernel_release: "6.8.14",
},
PlatformObservation {
operating_system: "linux",
architecture: "aarch64",
kernel_release: "6.9.0",
},
] {
let tree = tempfile::tempdir().unwrap();
let configuration = configuration(tree.path());
let store = domain_store(&configuration);
let mut future = Box::pin(select_production_cold_start_for_platform(
&configuration,
store,
InstalledProductPaths {
escalation_client: Path::new("/missing-escalation-client"),
sandbox_init: Path::new("/missing-sandbox-init"),
},
&observation,
));
let mut context = Context::from_waker(Waker::noop());
let Poll::Ready(result) = future.as_mut().poll(&mut context) else {
panic!("platform refusal performed asynchronous work");
};
assert!(result.is_err());
let storage = tree.path().join("data");
assert!(!storage.join("storage-catalog.lock").exists());
assert!(!storage.join("catalog.redb").exists());
assert!(!storage.join("prepared-lower").exists());
assert!(!storage.join("image-transports").exists());
}
}
#[test]
fn generation_selection_and_cold_start_retain_one_exact_domain_store() {
let tree = tempfile::tempdir().unwrap();
let configuration = configuration(tree.path());
let first_root = tree.path().join("store-a");
let second_root = tree.path().join("store-b");
fs::create_dir(&first_root).unwrap();
fs::create_dir(&second_root).unwrap();
let first = domain_store_at(&configuration, &first_root);
let second = domain_store_at(&configuration, &second_root);
second
.apply(&[DomainStoreMutation::PutGeneration(Generation {
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(1),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id: CoordinatorId::new([1; 32]),
network_sidecar_id: NetworkSidecarId::new([2; 32]),
sandbox_id: SandboxId::new([3; 32]),
durable_decision: DurableDecision::None,
})])
.unwrap();
assert_eq!(next_generation(&first).unwrap(), 1);
assert_eq!(next_generation(&second).unwrap(), 2);
assert!(!Arc::ptr_eq(&first, &second));
}
fn domain_store(
configuration: &ResolvedDomainConfiguration,
) -> Arc<HostCoordinatorDomainStore> {
let root = PathBuf::from(std::ffi::OsString::from_vec(
configuration
.persistent_domain_root()
.canonical_label()
.as_bytes()
.to_vec(),
));
domain_store_at(configuration, &root)
}
fn domain_store_at(
configuration: &ResolvedDomainConfiguration,
root: &Path,
) -> Arc<HostCoordinatorDomainStore> {
let lock = OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(root.join("domain.lock"))
.unwrap();
Arc::new(
HostCoordinatorDomainStore::open(
try_exclusive_flock(lock).unwrap(),
&root.join("domain.redb"),
&Domain {
domain_id: configuration.domain_id(),
profile_name: configuration.selected_profile().to_owned(),
canonical_workspace: configuration.workspace().canonical_label().clone(),
},
)
.unwrap(),
)
}
fn configuration(root: &Path) -> ResolvedDomainConfiguration {

View file

@ -87,6 +87,7 @@ impl PreparedLowerStore {
Err(error) => return Err(error.into()),
}
let root = open_directory_at(&storage_root, Path::new("prepared-lower"))?;
require_owned_directory(&root)?;
Self::initialize_retained(root, catalog)
}
@ -101,7 +102,8 @@ impl PreparedLowerStore {
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
Err(error) => return Err(error.into()),
}
open_directory_at(&store.root, Path::new(name))?;
let directory = open_directory_at(&store.root, Path::new(name))?;
require_owned_directory(&directory)?;
}
open_lock_at(&store.root)?;
Ok(store)
@ -472,6 +474,14 @@ impl PreparedLowerStore {
}
}
fn require_owned_directory(directory: &File) -> Result<()> {
let stat = rustix::fs::fstat(directory)?;
if stat.st_uid != rustix::process::getuid().as_raw() || stat.st_mode & 0o7777 != 0o700 {
bail!("fixed prepared-lower directory must be owned by the invoking user with mode 0700");
}
Ok(())
}
struct Identities {
artifact_id: PreparedArtifactId,
source: SourceIdentity,
@ -685,3 +695,20 @@ fn hex(bytes: &[u8]) -> String {
fn secure_resolve() -> ResolveFlags {
ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS
}
#[cfg(test)]
mod fixed_directory_tests {
use std::os::unix::fs::PermissionsExt;
use super::*;
#[test]
fn prepared_lower_root_requires_invoking_owner_and_exact_private_mode() {
let directory = tempfile::tempdir().unwrap();
std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o777)).unwrap();
let root = File::open(directory.path()).unwrap();
assert!(require_owned_directory(&root).is_err());
std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(require_owned_directory(&root).is_ok());
}
}

View file

@ -355,15 +355,14 @@ impl LayoutWriter {
staging_id,
} => {
let storage = File::from(storage_root.open_authority()?);
create_directory(&storage, "image-transports")?;
let parent = Arc::new(open_directory(&storage, Path::new("image-transports"))?);
let parent = Arc::new(create_directory(&storage, "image-transports")?);
let name = hex(&staging_id);
mkdirat(&parent, name.as_str(), Mode::from_raw_mode(0o700))?;
let root = Arc::new(open_directory(&parent, Path::new(&name))?);
require_owned_directory(&root)?;
let stat = fstat(&root)?;
create_directory(&root, "blobs")?;
let blobs = open_directory(&root, Path::new("blobs"))?;
create_directory(&blobs, "sha256")?;
let blobs = create_directory(&root, "blobs")?;
drop(create_directory(&blobs, "sha256")?);
write_relative_atomic(
&root,
Path::new("oci-layout"),
@ -522,12 +521,21 @@ impl LayoutWriter {
}
}
fn create_directory(parent: &File, path: &str) -> Result<()> {
fn create_directory(parent: &File, path: &str) -> Result<File> {
match mkdirat(parent, path, Mode::from_raw_mode(0o700)) {
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
Err(error) => return Err(error.into()),
}
drop(open_directory(parent, Path::new(path))?);
let directory = open_directory(parent, Path::new(path))?;
require_owned_directory(&directory)?;
Ok(directory)
}
fn require_owned_directory(directory: &File) -> Result<()> {
let stat = fstat(directory)?;
if stat.st_uid != rustix::process::getuid().as_raw() || stat.st_mode & 0o7777 != 0o700 {
bail!("fixed image-transport directory must be owned by the invoking user with mode 0700");
}
Ok(())
}
@ -695,11 +703,30 @@ fn hex(bytes: &[u8]) -> String {
mod tests {
use std::io::{Read, Write};
use std::net::TcpListener;
use std::os::unix::fs::PermissionsExt;
use oci_client::client::ClientProtocol;
use super::*;
#[test]
fn preexisting_transport_parents_require_exact_private_mode() {
for name in ["image-transports", "blobs", "sha256"] {
let tree = tempfile::tempdir().unwrap();
std::fs::create_dir(tree.path().join(name)).unwrap();
std::fs::set_permissions(
tree.path().join(name),
std::fs::Permissions::from_mode(0o777),
)
.unwrap();
let parent = File::open(tree.path()).unwrap();
assert!(
create_directory(&parent, name).is_err(),
"accepted wrong mode for {name}"
);
}
}
#[test]
fn refuses_mismatched_and_incomplete_platforms() {
assert!(

View file

@ -67,6 +67,22 @@ impl Fixture {
}
}
#[test]
fn preexisting_fixed_children_require_exact_private_mode() {
for name in ["artifacts", "stages", "quarantine"] {
let fixture = Fixture::new();
std::fs::set_permissions(
fixture.root.join(name),
std::fs::Permissions::from_mode(0o777),
)
.unwrap();
assert!(
PreparedLowerStore::initialize(&fixture.root, fixture.catalog.clone()).is_err(),
"accepted wrong mode for {name}"
);
}
}
#[derive(Clone)]
struct Entry<'a> {
path: &'a str,