Refuse inexact production selector authorities
This commit is contained in:
parent
bc04112c77
commit
4cf7a5e595
4 changed files with 230 additions and 34 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue