Idunn/src/drivers.rs
Meta Vens 76cec0b540 Tests: the sealed-mode helper no longer redefines the module's existing mode_of
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 19:54:35 +02:00

15891 lines
647 KiB
Rust

use std::collections::{BTreeMap, BTreeSet};
use std::ffi::{OsStr, OsString};
use std::fs::{self, OpenOptions};
use std::io::{BufRead, BufReader, ErrorKind, Read, Write};
use std::net::{SocketAddr, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use anyhow::{Context, Result, anyhow, bail, ensure};
use cultcache_rs::{
CacheBackingStore, CultCacheEnvelope, CultCacheExpectedEnvelope, DatabaseEntry,
SingleFileMessagePackBackingStore, TryCompareExchangeSnapshotOutcome,
};
use cultmesh_rs::{CultMeshRudpSnapshotOptions, request_raw_snapshot_from_rudp_catalog};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::control_plane::SequenceAdmittedWarming;
use crate::deployment::{
ArtifactOutput, ArtifactSource, DockerRunnerBinding, GitlinkBinding,
IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT, IDUNN_RUNTIME_BUNDLE_ENVIRONMENT,
IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT, IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT,
LaunchArgument, OperatorBinding,
RUNTIME_PRESENCE_IDENTITY_BINDING, RUNTIME_PRESENCE_IDENTITY_FD_NAME, RouteBinding,
RouteDriver, SourceSelectionPolicy, TargetDeclaration, WorkloadNetwork,
};
use crate::deployment_plan::{
ArtifactReceipt, CompiledDeploymentPlan, ExternalInputMaterializationReceipt, GitlinkTreeFact,
SOURCE_SELECTION_FACTS_SCHEMA, SealedRelease, SourceSelection, SourceSelectionFacts,
};
use cultnet_rs::{
CultNetMessage, CultNetRawPayloadEncoding, CultNetWireContract,
GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA, GAMECULT_RUNTIME_PRESENCE_HEALTH_SIGNING_PURPOSE,
GAMECULT_SERVICE_TRUST_ANCHOR_SCHEMA, GameCultProviderHealthIdentity,
GameCultServiceTrustAnchorRecord, IDUNN_EXPECTED_INCARNATION_SCHEMA,
IDUNN_PROCESS_WRITE_LEASE_SCHEMA, IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME,
IDUNN_RUNTIME_ACTIVATION_SCHEMA, IdunnExpectedIncarnationRecord, IdunnProcessWriteLeaseRecord,
IdunnRuntimeActivationLaunch, IdunnRuntimeActivationRecord, IdunnRuntimeActivationSigner,
ODIN_RUNTIME_TOPOLOGY_CORRELATION_SCHEMA, OdinRuntimeTopologyCorrelationRecord,
ServiceIdentityProfile, ServiceIdentityTrustAnchor, decode_cultnet_message_from_slice,
derive_service_identity_id, encode_cultnet_message_to_vec, encode_frame,
open_service_identity_credential_reader,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ProcessIdentity {
pub uid: u32,
pub gid: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct GitTreeEntry {
mode: String,
kind: String,
object: String,
path: PathBuf,
}
/// S4-6 (Self's ruling, fourth Cut 1 fix batch): the largest symlink target
/// `materialize_tree_raw` will ever buffer in memory, matching the
/// platform's own path length limit (`PATH_MAX` on Linux). A blob recorded
/// with mode `120000` is refused outright once it is bigger than any real
/// symlink target could ever be, instead of being read whole into memory
/// first and only failing once written -- the previous shape let a 64 MiB
/// blob with a symlink mode take peak RSS past 200 MB before `ENAMETOOLONG`
/// finally closed it.
#[cfg(unix)]
const FROZEN_SOURCE_SYMLINK_TARGET_LIMIT: usize = libc::PATH_MAX as usize;
/// Windows' long-path limit, in UTF-16 units; the same bound on a symlink blob's size.
#[cfg(not(unix))]
const FROZEN_SOURCE_SYMLINK_TARGET_LIMIT: usize = 32_767;
/// F2/F3 layer (b): the only way `materialize_tree_raw` is allowed to obtain
/// a directory to write into. `path` must already be a directory this
/// function created earlier in the same freeze (tracked in `created_dirs`,
/// seeded with `root` by the caller), or must not exist yet at all. Anything
/// else already at `path` — a symlink, a file, or a directory this freeze did
/// not itself create — is refused outright rather than traversed, which is
/// what a plain `fs::create_dir_all` would silently do through a symlink.
fn ensure_frozen_directory(
root: &Path,
created_dirs: &mut std::collections::HashSet<PathBuf>,
path: &Path,
) -> Result<()> {
if created_dirs.contains(path) {
return Ok(());
}
ensure!(
path.starts_with(root),
"frozen source path {} escapes its root",
path.display()
);
let parent = path
.parent()
.context("frozen source path has no parent")?;
ensure_frozen_directory(root, created_dirs, parent)?;
match fs::symlink_metadata(path) {
Ok(metadata) => bail!(
"frozen source writer refuses to write through an existing {} at {}",
if metadata.file_type().is_symlink() {
"symlink"
} else if metadata.is_dir() {
"directory this freeze did not create"
} else {
"file"
},
path.display()
),
Err(ref error) if error.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir(path).with_context(|| format!("creating {}", path.display()))?;
}
Err(error) => return Err(error).with_context(|| format!("inspecting {}", path.display())),
}
created_dirs.insert(path.to_path_buf());
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedSource {
pub facts: SourceSelectionFacts,
pub recipe_bytes: Vec<u8>,
}
impl ResolvedSource {
pub fn validate_against(&self, binding: &OperatorBinding) -> Result<()> {
self.facts.validate_against(binding)?;
ensure!(!self.recipe_bytes.is_empty(), "resolved recipe is empty");
ensure!(
sha256_id(&self.recipe_bytes) == self.facts.recipe_blob_sha256,
"resolved recipe bytes differ from the selected Git object"
);
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FrozenSource {
receipt: FrozenSourceReceipt,
facts: SourceSelectionFacts,
recipe_bytes: Vec<u8>,
root: PathBuf,
}
impl FrozenSource {
pub fn receipt(&self) -> &FrozenSourceReceipt {
&self.receipt
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrozenSourceReceipt {
pub transaction_id: String,
pub plan_id: String,
pub snapshot_sha256: String,
}
impl FrozenSourceReceipt {
pub fn validate_against(&self, plan: &CompiledDeploymentPlan) -> Result<()> {
plan.validate()?;
require_driver_id(&self.transaction_id, "source transaction")?;
ensure!(self.plan_id == plan.plan_id, "frozen source plan differs");
require_sha256_id(&self.snapshot_sha256, "frozen source snapshot")
}
fn snapshot_component(&self) -> &str {
self.snapshot_sha256
.strip_prefix("sha256-")
.expect("validated frozen source digest")
}
}
pub trait SourcePort {
fn resolve(
&self,
binding: &OperatorBinding,
resolution_id: &str,
selected_at_unix_millis: u64,
) -> Result<ResolvedSource>;
fn freeze(
&self,
transaction_id: &str,
plan: &CompiledDeploymentPlan,
) -> Result<FrozenSourceReceipt>;
fn observe_frozen(
&self,
plan: &CompiledDeploymentPlan,
receipt: &FrozenSourceReceipt,
) -> Result<FrozenSource>;
fn cleanup(&self, transaction_id: &str, receipt: Option<&FrozenSourceReceipt>) -> Result<()>;
}
pub trait RunnerPort {
fn materialize(
&self,
source: &FrozenSource,
plan: &CompiledDeploymentPlan,
staging_root: &Path,
sealed_at_unix_millis: u64,
) -> Result<MaterializedRelease>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MaterializedRelease {
pub release: SealedRelease,
pub root: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct InstalledReleaseObservation {
pub sealed_release_id: String,
pub root: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceCredentialObservation {
pub environment_name: String,
pub delivered_path: PathBuf,
pub device: u64,
pub inode: u64,
pub uid: u32,
pub gid: u32,
pub mode: u32,
pub size: u64,
pub sha256: String,
}
/// Root-owned source metadata for one descriptor that PID1 opens and passes
/// only to the service's initial process. The process must consume and close
/// descriptors 3 and 4 before spawning any child; no filesystem path is
/// projected into the workload environment.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ParentOnlyFileDescriptorObservation {
pub fd_number: u32,
pub fd_name: String,
pub source_path: PathBuf,
pub access: String,
pub device: u64,
pub inode: u64,
pub uid: u32,
pub gid: u32,
pub mode: u32,
pub links: u64,
pub size: u64,
pub sha256: String,
}
/// What a workload driver proved about the native process it started. One
/// variant per kind of host: a systemd unit on the Idunn host, or a process
/// on a managed host reported by that host's actuator. The wire form is
/// untagged so a transaction persisted by the previous Idunn, which knew only
/// the systemd shape, decodes unchanged.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum WorkloadObservation {
Systemd(SystemdWorkloadObservation),
Host(HostWorkloadObservation),
}
impl WorkloadObservation {
pub fn systemd(&self) -> Result<&SystemdWorkloadObservation> {
match self {
Self::Systemd(observation) => Ok(observation),
Self::Host(_) => bail!("workload observation is a host process, not a systemd unit"),
}
}
pub fn host(&self) -> Result<&HostWorkloadObservation> {
match self {
Self::Host(observation) => Ok(observation),
Self::Systemd(_) => bail!("workload observation is a systemd unit, not a host process"),
}
}
pub fn runtime_instance_id(&self) -> &str {
match self {
Self::Systemd(observation) => &observation.runtime_instance_id,
Self::Host(observation) => &observation.runtime_instance_id,
}
}
pub fn executable_sha256(&self) -> &str {
match self {
Self::Systemd(observation) => &observation.executable_sha256,
Self::Host(observation) => &observation.executable_sha256,
}
}
/// One line for a status listing: where the process is and how it is
/// kept, in the vocabulary of its own driver.
pub fn describe(&self) -> String {
match self {
Self::Systemd(observation) => format!(
"unit={} restart={}",
observation.unit, observation.restart_policy
),
Self::Host(observation) => format!(
"host={} pid={} created={}",
observation.host, observation.process_id, observation.process_creation_time
),
}
}
/// Two processes running at once must be distinct at the boundary their
/// host actually enforces. On the Idunn host that is UID, PID namespace
/// and mount namespace. On a managed host it is the process itself:
/// pid and creation time, which the kernel will not hand to two live
/// processes. An incumbent of the other kind cannot be compared and is
/// refused rather than assumed isolated.
pub fn prove_isolation(
candidate: &Self,
incumbent: Option<&Self>,
) -> Result<IsolationEvidence> {
match (candidate, incumbent) {
(Self::Systemd(candidate), None) => {
candidate.require_private_identity("candidate")?;
Ok(IsolationEvidence::Linux(LinuxIsolationEvidence {
candidate_uid: candidate.process_uids[0],
candidate_pid_namespace_id: candidate.pid_namespace_id,
candidate_mount_namespace_id: candidate.mount_namespace_id,
incumbent_uid: None,
incumbent_pid_namespace_id: None,
incumbent_mount_namespace_id: None,
}))
}
(Self::Systemd(candidate), Some(Self::Systemd(incumbent))) => {
candidate.require_private_identity("candidate")?;
incumbent.require_private_identity("incumbent")?;
ensure!(
candidate.process_uids[0] != incumbent.process_uids[0]
&& candidate.pid_namespace_id != incumbent.pid_namespace_id
&& candidate.mount_namespace_id != incumbent.mount_namespace_id,
"candidate and incumbent are not distinct by UID, PID namespace, and mount namespace"
);
Ok(IsolationEvidence::Linux(LinuxIsolationEvidence {
candidate_uid: candidate.process_uids[0],
candidate_pid_namespace_id: candidate.pid_namespace_id,
candidate_mount_namespace_id: candidate.mount_namespace_id,
incumbent_uid: Some(incumbent.process_uids[0]),
incumbent_pid_namespace_id: Some(incumbent.pid_namespace_id),
incumbent_mount_namespace_id: Some(incumbent.mount_namespace_id),
}))
}
(Self::Host(candidate), None) => {
candidate.require_live("candidate")?;
Ok(IsolationEvidence::Host(HostIsolationEvidence {
candidate_process_id: candidate.process_id,
candidate_process_creation_time: candidate.process_creation_time,
incumbent_process_id: None,
incumbent_process_creation_time: None,
}))
}
(Self::Host(candidate), Some(Self::Host(incumbent))) => {
candidate.require_live("candidate")?;
incumbent.require_live("incumbent")?;
ensure!(
candidate.host == incumbent.host,
"candidate and incumbent are on different hosts"
);
ensure!(
(candidate.process_id, candidate.process_creation_time)
!= (incumbent.process_id, incumbent.process_creation_time),
"candidate and incumbent are the same host process"
);
Ok(IsolationEvidence::Host(HostIsolationEvidence {
candidate_process_id: candidate.process_id,
candidate_process_creation_time: candidate.process_creation_time,
incumbent_process_id: Some(incumbent.process_id),
incumbent_process_creation_time: Some(incumbent.process_creation_time),
}))
}
_ => {
bail!("candidate and incumbent were observed by different kinds of workload driver")
}
}
}
}
/// Isolation proof, one shape per kind of host. Untagged for the same reason
/// as the observation: persisted Linux evidence decodes unchanged.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum IsolationEvidence {
Linux(LinuxIsolationEvidence),
Host(HostIsolationEvidence),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LinuxIsolationEvidence {
pub candidate_uid: u32,
pub candidate_pid_namespace_id: u64,
pub candidate_mount_namespace_id: u64,
pub incumbent_uid: Option<u32>,
pub incumbent_pid_namespace_id: Option<u64>,
pub incumbent_mount_namespace_id: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HostIsolationEvidence {
pub candidate_process_id: u32,
pub candidate_process_creation_time: u64,
pub incumbent_process_id: Option<u32>,
pub incumbent_process_creation_time: Option<u64>,
}
/// A process on a managed host as its actuator proved it. `process_creation_time`
/// is the host's own clock for process creation (Windows: FILETIME ticks); the
/// pair with `process_id` names exactly one process for the host's lifetime.
/// `exit_code` is `Some` once the process is gone; nothing restarts it, so an
/// exited observation is permanently stopped.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HostWorkloadObservation {
pub host: String,
pub actuator_identity_id: String,
pub process_id: u32,
pub process_creation_time: u64,
pub session_id: u32,
pub user_sid: String,
pub executable: String,
pub executable_sha256: String,
pub command_line_sha256: String,
pub environment_names: Vec<String>,
pub environment_contract_sha256: String,
pub runtime_bundle: String,
pub runtime_instance_id: String,
pub activation_signer_identity_id: String,
pub activation_signer_public_key: Vec<u8>,
pub exit_code: Option<u32>,
}
impl HostWorkloadObservation {
fn require_live(&self, role: &str) -> Result<()> {
ensure!(self.process_id > 0, "{role} has no host process id");
ensure!(
self.process_creation_time > 0,
"{role} has no host process creation time"
);
ensure!(self.exit_code.is_none(), "{role} host process has exited");
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemdWorkloadObservation {
pub unit: String,
pub unit_description: String,
pub invocation_id: String,
pub exec_main_start_timestamp_monotonic: u64,
pub service_type: String,
pub restart_policy: String,
pub kill_mode: String,
pub dynamic_user: bool,
pub systemd_user: String,
pub systemd_group: String,
pub supplementary_groups: String,
pub capability_bounding_set: String,
pub ambient_capabilities: String,
pub private_mounts: bool,
pub private_pids: bool,
pub protect_proc: String,
pub proc_subset: String,
pub no_new_privileges: bool,
pub umask: String,
pub inaccessible_paths: String,
pub load_credential: String,
pub main_pid: u32,
pub process_start_time: u64,
pub process_uids: [u32; 4],
pub process_gids: [u32; 4],
pub process_groups: Vec<u32>,
pub process_cap_inheritable: u64,
pub process_cap_permitted: u64,
pub process_cap_effective: u64,
pub process_cap_bounding: u64,
pub process_cap_ambient: u64,
pub process_no_new_privileges: bool,
pub process_namespace_pids: Vec<u32>,
pub mount_namespace_id: u64,
pub pid_namespace_id: u64,
pub executable: PathBuf,
pub executable_device: u64,
pub executable_inode: u64,
pub executable_sha256: String,
pub runtime_instance_id: String,
pub working_directory: PathBuf,
pub runtime_bundle: PathBuf,
/// Initial ExecStart evidence. Node runtimes may rewrite argv through
/// `process.title`, so this is validated at launch but is not incarnation
/// identity after the process starts.
pub command_line_sha256: String,
pub environment_names: Vec<String>,
pub environment_contract_sha256: String,
pub control_group: String,
pub credentials_directory: Option<PathBuf>,
pub parent_only_file_descriptors: Vec<ParentOnlyFileDescriptorObservation>,
pub activation_signer_identity_id: String,
pub activation_signer_public_key: Vec<u8>,
pub service_credentials: Vec<ServiceCredentialObservation>,
}
impl SystemdWorkloadObservation {
fn require_private_identity(&self, role: &str) -> Result<()> {
ensure!(
self.dynamic_user
&& self.private_pids
&& self.private_mounts
&& self.process_uids[0] > 0
&& self.pid_namespace_id > 0
&& self.mount_namespace_id > 0,
"{role} lacks dynamic identity or private namespaces"
);
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct LinuxProcessSecurityObservation {
uids: [u32; 4],
gids: [u32; 4],
groups: Vec<u32>,
cap_inheritable: u64,
cap_permitted: u64,
cap_effective: u64,
cap_bounding: u64,
cap_ambient: u64,
no_new_privileges: bool,
namespace_pids: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SystemdUnitObservation {
properties: BTreeMap<String, String>,
open_files: Vec<String>,
}
pub trait WorkloadPort {
fn install(
&self,
plan: &CompiledDeploymentPlan,
release: &MaterializedRelease,
) -> Result<InstalledReleaseObservation>;
fn prepare_activation(
&self,
plan: &CompiledDeploymentPlan,
expected: &IdunnExpectedIncarnationRecord,
launch: IdunnRuntimeActivationLaunch,
) -> Result<IdunnRuntimeActivationRecord>;
fn start_prepared(
&self,
plan: &CompiledDeploymentPlan,
release: &SealedRelease,
installed: &InstalledReleaseObservation,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> Result<WorkloadObservation>;
fn discard_prepared(
&self,
plan: &CompiledDeploymentPlan,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> Result<()>;
fn observe(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
prior: &WorkloadObservation,
) -> Result<WorkloadObservation>;
fn stop(&self, observation: &WorkloadObservation) -> Result<()>;
/// True when this candidate can never run again without a new transaction.
///
/// Idunn launches candidates as transient units with `Restart=no`, so a
/// unit that has failed, or that systemd no longer knows, will not come
/// back on its own. Past the fence every error is otherwise treated as
/// resumable and retried forever, which is right while a candidate can
/// still recover and a trap once it cannot.
fn is_permanently_stopped(&self, observation: &WorkloadObservation) -> Result<bool>;
/// True only when the host lost the workload's unit to a reboot after the
/// generation was admitted at `admitted_at_unix_millis`. The driver owns
/// the evidence; continuity supervision owns the budget it buys (one
/// uncharged re-materialization per generation). Every implementation
/// answers explicitly.
fn lost_to_host_reboot(
&self,
observation: &WorkloadObservation,
admitted_at_unix_millis: u64,
) -> Result<bool>;
}
pub trait TopologyPort {
fn publish_expected(
&self,
expected: &IdunnExpectedIncarnationRecord,
provider_anchor: &ServiceIdentityTrustAnchor,
) -> Result<String>;
/// Remove every record of one incarnation. Other incarnations of the same
/// target, and the target's anchor while any remain, are untouched.
fn withdraw_incarnation(
&self,
expected: &IdunnExpectedIncarnationRecord,
provider_anchor: &ServiceIdentityTrustAnchor,
activation: Option<&IdunnRuntimeActivationRecord>,
lease: Option<&IdunnProcessWriteLeaseRecord>,
) -> Result<()>;
fn publish_observed_activation(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
observation: &WorkloadObservation,
) -> Result<String>;
fn publish_process_write_lease(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
lease: &IdunnProcessWriteLeaseRecord,
) -> Result<String>;
fn withdraw_process_write_lease(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
lease: Option<&IdunnProcessWriteLeaseRecord>,
) -> Result<()>;
/// Odin's correlation for one exact incarnation. A correlation about another
/// incarnation of the same target is not this one's evidence and is never
/// returned for it.
fn receive(
&self,
target: &str,
expected_sha256: &str,
) -> Result<Option<ReceivedOdinTopologyCorrelation>>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReceivedOdinTopologyCorrelation {
pub target: String,
pub expected_sha256: String,
pub canonical_bytes: Vec<u8>,
}
pub trait WriteLeasePort {
fn revoke_exact(&self, incumbent: Option<&IdunnProcessWriteLeaseRecord>) -> Result<()>;
fn observe_empty(&self) -> Result<bool>;
fn observe_exact(&self, lease: &IdunnProcessWriteLeaseRecord) -> Result<bool>;
fn grant(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
warming: &SequenceAdmittedWarming,
lease: &IdunnProcessWriteLeaseRecord,
) -> Result<String>;
fn observe(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
warming: &SequenceAdmittedWarming,
lease: &IdunnProcessWriteLeaseRecord,
) -> Result<bool>;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteObservation {
pub route_id: String,
pub runtime_instance_id: String,
pub membership_sha256: String,
pub signed_presence_sha256: String,
pub observed_at_unix_millis: u64,
}
impl RouteObservation {
pub fn validate(&self) -> Result<()> {
require_driver_id(&self.route_id, "route observation")?;
require_sha256_id(&self.runtime_instance_id, "route runtime instance")?;
require_sha256_id(&self.membership_sha256, "route membership")?;
require_sha256_id(&self.signed_presence_sha256, "route signed presence")?;
ensure!(
self.observed_at_unix_millis > 0,
"route observation has no trusted receipt time"
);
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoutePreflightReceipt {
pub route_id: String,
pub candidate_runtime_instance_id: String,
pub candidate_membership_sha256: String,
pub incumbent_runtime_instance_id: Option<String>,
pub incumbent_membership_sha256: Option<String>,
pub incumbent_configuration: Option<Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RouteSnapshotResponse {
pub message_id: String,
pub canonical_presence: Vec<u8>,
}
const ROUTE_SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(3);
const ROUTE_CONNECT_RETRY_WINDOW: Duration = Duration::from_secs(2);
const ROUTE_CONNECT_RETRY_INTERVAL: Duration = Duration::from_millis(25);
const ROUTE_SNAPSHOT_MAX_BYTES: usize = 1024 * 1024;
const ROUTE_HTTP_MAX_HEADER_BYTES: usize = 32 * 1024;
const ROUTE_HTTP_CHUNK_LINE_BYTES: usize = 128;
const ROUTE_HTTP_SNAPSHOT_PATH: &str = "/cultnet/snapshot";
impl RoutePreflightReceipt {
/// Whether the candidate's membership differs from the baseline the
/// preflight found. A continuity over its admitted route does not: its
/// transaction has no route change to make or to undo.
pub fn changes_route(&self) -> bool {
self.incumbent_membership_sha256.as_deref()
!= Some(self.candidate_membership_sha256.as_str())
}
pub fn validate(&self) -> Result<()> {
require_driver_id(&self.route_id, "route preflight")?;
require_sha256_id(
&self.candidate_runtime_instance_id,
"candidate route runtime instance",
)?;
require_sha256_id(
&self.candidate_membership_sha256,
"candidate route membership",
)?;
match (
&self.incumbent_runtime_instance_id,
&self.incumbent_membership_sha256,
&self.incumbent_configuration,
) {
(None, None, None) => {}
(Some(runtime), Some(membership), Some(configuration)) => {
require_sha256_id(runtime, "incumbent route runtime instance")?;
require_sha256_id(membership, "incumbent route membership")?;
ensure!(
!configuration.is_empty(),
"incumbent route configuration is empty"
);
ensure!(
sha256_id(configuration) == *membership,
"incumbent route configuration differs from its membership digest"
);
}
_ => bail!("incumbent route preflight evidence is partial"),
}
Ok(())
}
}
/// The narrow shape `freeze_exact` needs to fetch and archive a repository at
/// an exact revision. It carries no operator authority (no minimum revision,
/// selection policy, or runners): callers that hold an `OperatorBinding`
/// derive it with `exact_source_from`, and a future verify binding lowers to
/// it directly. Cut 1 shares this type between deploy's freeze and (later)
/// verify; it must not grow a second shape for the same fetch-and-archive
/// core.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ExactSource {
pub origin: String,
pub checkout: PathBuf,
pub gitlinks: BTreeMap<PathBuf, GitlinkBinding>,
pub recipe_path: PathBuf,
}
fn exact_source_from(binding: &OperatorBinding) -> ExactSource {
ExactSource {
origin: binding.repository.origin.clone(),
checkout: binding.repository.checkout.clone(),
gitlinks: binding.repository.gitlinks.clone(),
recipe_path: binding.repository.recipe_path.clone(),
}
}
/// Fixed-argv Git source driver. It never interprets recipe text as a command
/// and never derives source policy from the target repository. The configured
/// identity performs every Git/network read; root Idunn writes only exact Git
/// blob content into a separate transaction-owned immutable store — no
/// `git archive`, so no attribute-driven transform runs on the way out.
pub struct GitSourceDriver {
pub source_cache_root: PathBuf,
pub frozen_source_root: PathBuf,
pub identity: Option<ProcessIdentity>,
pub git_program: PathBuf,
}
impl GitSourceDriver {
pub fn new(
source_cache_root: impl Into<PathBuf>,
frozen_source_root: impl Into<PathBuf>,
identity: Option<ProcessIdentity>,
) -> Self {
Self {
source_cache_root: source_cache_root.into(),
frozen_source_root: frozen_source_root.into(),
identity,
git_program: PathBuf::from("/usr/bin/git"),
}
}
fn git_command<I, S>(&self, args: I) -> Result<Command>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
ensure!(
self.git_program.is_absolute(),
"Git source driver program is not absolute"
);
let mut command = Command::new(&self.git_program);
command
.args(args)
.stdin(Stdio::null())
.env_clear()
.env("HOME", self.source_cache_root.join(".home"))
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_TERMINAL_PROMPT", "0")
.env("LANG", "C.UTF-8");
apply_identity(&mut command, self.identity)?;
Ok(command)
}
fn git<I, S>(&self, args: I) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = self.git_command(args)?.output().with_context(|| {
format!(
"starting fixed-argv Git driver {}",
self.git_program.display()
)
})?;
if !output.status.success() {
bail!(
"Git source driver exited with {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(output)
}
fn git_text<I, S>(&self, args: I) -> Result<String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = self.git(args)?;
let value = String::from_utf8(output.stdout).context("Git emitted non-UTF-8 text")?;
Ok(value.trim().to_owned())
}
fn ensure_checkout(&self, source: &ExactSource) -> Result<()> {
let checkout = &source.checkout;
if !checkout.exists() {
self.git([
OsString::from("-c"),
OsString::from("transfer.fsckObjects=true"),
OsString::from("clone"),
OsString::from("--filter=blob:none"),
OsString::from("--no-checkout"),
OsString::from("--origin"),
OsString::from("origin"),
source.origin.clone().into(),
checkout.as_os_str().to_owned(),
])?;
}
let checkout_metadata = fs::symlink_metadata(checkout)
.with_context(|| format!("inspecting source checkout {}", checkout.display()))?;
ensure!(
checkout_metadata.is_dir() && !checkout_metadata.file_type().is_symlink(),
"source checkout is not a native directory"
);
let canonical_checkout = checkout.canonicalize()?;
ensure!(
canonical_checkout == *checkout,
"source checkout traverses a symlink or noncanonical path"
);
let git_metadata = fs::symlink_metadata(checkout.join(".git"))?;
ensure!(
git_metadata.is_dir() && !git_metadata.file_type().is_symlink(),
"source checkout has no native Git object directory"
);
let actual_origin = self.git_text([
OsString::from("-C"),
checkout.as_os_str().to_owned(),
OsString::from("remote"),
OsString::from("get-url"),
OsString::from("origin"),
])?;
ensure!(
actual_origin == source.origin,
"source checkout origin differs from the operator binding"
);
Ok(())
}
fn admitted_ref_name(binding: &OperatorBinding, resolution_id: &str) -> Result<String> {
require_driver_id(resolution_id, "source resolution")?;
Ok(format!(
"refs/idunn/resolutions/{}/{}",
binding.target, resolution_id
))
}
fn resolve_selected_revision(
&self,
binding: &OperatorBinding,
admitted_ref_revision: &str,
) -> Result<(String, SourceSelection)> {
let checkout = &binding.repository.checkout;
let (revision, selection) = match binding.repository.selection {
SourceSelectionPolicy::PinnedObject => (
binding
.repository
.pinned_revision
.clone()
.context("pinned source binding lost its exact revision")?,
SourceSelection::PinnedObject,
),
SourceSelectionPolicy::RefHead => {
(admitted_ref_revision.to_owned(), SourceSelection::RefHead)
}
SourceSelectionPolicy::SignedRelease => {
bail!("signed-release selection requires a release-authority source port")
}
};
require_git_sha(&revision, "selected source revision")?;
self.git([
OsString::from("-C"),
checkout.as_os_str().to_owned(),
OsString::from("merge-base"),
OsString::from("--is-ancestor"),
binding.repository.minimum_revision.clone().into(),
revision.clone().into(),
])
.context("selected source is below the operator minimum revision")?;
self.git([
OsString::from("-C"),
checkout.as_os_str().to_owned(),
OsString::from("merge-base"),
OsString::from("--is-ancestor"),
revision.clone().into(),
admitted_ref_revision.into(),
])
.context("selected source is outside the fetched admitted ref")?;
Ok((revision, selection))
}
fn git_tree_entries(&self, repository: &Path, revision: &str) -> Result<Vec<GitTreeEntry>> {
let output = self
.git([
OsString::from("-C"),
repository.as_os_str().to_owned(),
OsString::from("ls-tree"),
OsString::from("-r"),
OsString::from("-z"),
revision.into(),
])?
.stdout;
let mut entries = Vec::new();
for record in output
.split(|byte| *byte == 0)
.filter(|record| !record.is_empty())
{
let tab = record
.iter()
.position(|byte| *byte == b'\t')
.context("Git tree record has no path delimiter")?;
let header = std::str::from_utf8(&record[..tab])
.context("Git tree record header is not UTF-8")?;
let path =
std::str::from_utf8(&record[tab + 1..]).context("Git tree path is not UTF-8")?;
let mut fields = header.split(' ');
let mode = fields.next().context("Git tree record has no mode")?;
let kind = fields.next().context("Git tree record has no kind")?;
let object = fields.next().context("Git tree record has no object")?;
ensure!(fields.next().is_none(), "Git tree record has extra fields");
require_git_sha(object, "Git tree object")?;
let path = PathBuf::from(path);
let normalized = normalized_relative(&path)?;
ensure!(
normalized == path.to_string_lossy(),
"Git tree path is not normalized"
);
ensure!(
path.components().all(|component| !matches!(
component,
std::path::Component::Normal(value) if value == ".git"
)),
"Git tree contains forbidden .git metadata"
);
entries.push(GitTreeEntry {
mode: mode.to_owned(),
kind: kind.to_owned(),
object: object.to_owned(),
path,
});
}
entries.sort_by(|left, right| left.path.cmp(&right.path));
ensure!(
entries.windows(2).all(|pair| pair[0].path != pair[1].path),
"Git tree emits a path twice"
);
// S7 (Self's ruling, third Cut 1 fix batch): the case-insensitive
// collision/alias refusal that used to run here is deleted. The host
// is case-sensitive ext4, so a case-fold collision was never a real
// write hazard, and the check was inconsistent (leaf paths only, a
// plain `to_lowercase` rather than Unicode case folding or
// normalization). `.git` look-alikes are now refused by `git fsck`
// itself (S1, `resolve()` and `freeze_exact`), independent of case.
// An exact-name alias (a leaf `a` sharing a root with a tree that
// recurses to `a/pwn`) is still refused, at write time, by
// `ensure_frozen_directory` below: it never writes through an
// existing entry, symlink or otherwise.
Ok(entries)
}
fn exact_recipe_and_gitlinks(
&self,
source: &ExactSource,
revision: &str,
) -> Result<(Vec<u8>, BTreeMap<PathBuf, GitlinkTreeFact>)> {
let entries = self.git_tree_entries(&source.checkout, revision)?;
let recipe = entries
.iter()
.find(|entry| entry.path == source.recipe_path)
.context("selected tree has no deployment recipe")?;
ensure!(
matches!(recipe.mode.as_str(), "100644" | "100755") && recipe.kind == "blob",
"deployment recipe is not a regular Git blob"
);
let recipe_bytes = self
.git([
OsString::from("-C"),
source.checkout.as_os_str().to_owned(),
OsString::from("cat-file"),
OsString::from("blob"),
recipe.object.clone().into(),
])?
.stdout;
ensure!(
!recipe_bytes.is_empty(),
"selected deployment recipe is empty"
);
let observed_gitlinks = entries
.iter()
.filter(|entry| entry.mode == "160000")
.map(|entry| {
ensure!(entry.kind == "commit", "Gitlink has the wrong object kind");
Ok((entry.path.clone(), entry.object.clone()))
})
.collect::<Result<BTreeMap<_, _>>>()?;
let expected_paths = source
.gitlinks
.keys()
.cloned()
.collect::<std::collections::BTreeSet<_>>();
let observed_paths = observed_gitlinks
.keys()
.cloned()
.collect::<std::collections::BTreeSet<_>>();
ensure!(
observed_paths == expected_paths,
"selected tree Gitlinks do not exactly match operator bindings"
);
let gitlinks = observed_gitlinks
.into_iter()
.map(|(path, revision)| {
let origin = source.gitlinks[&path].origin.clone();
(path, GitlinkTreeFact { origin, revision })
})
.collect();
Ok((recipe_bytes, gitlinks))
}
fn prepare_source_root(&self, source: &ExactSource) -> Result<()> {
#[cfg(unix)]
ensure!(
unsafe { libc::geteuid() } != 0 || self.identity.is_some(),
"root Idunn must configure an unprivileged source identity"
);
ensure!(
source.checkout.starts_with(&self.source_cache_root)
&& source.checkout != self.source_cache_root,
"repository checkout is outside Idunn's source authority root"
);
ensure_source_directory(&self.source_cache_root, self.identity)?;
ensure!(
self.source_cache_root.canonicalize()? == self.source_cache_root,
"source cache root traverses a symlink or noncanonical path"
);
ensure_source_directory(&self.source_cache_root.join(".home"), self.identity)?;
ensure_source_directory_tree(
&self.source_cache_root,
source
.checkout
.parent()
.context("repository checkout has no parent directory")?,
self.identity,
)?;
self.ensure_checkout(source)
}
fn verify_exact_source(&self, source: &ExactSource, resolved: &ResolvedSource) -> Result<()> {
let actual_tree = self.git_text([
OsString::from("-C"),
source.checkout.as_os_str().to_owned(),
OsString::from("rev-parse"),
OsString::from(format!("{}^{{tree}}", resolved.facts.revision)),
])?;
ensure!(
actual_tree == resolved.facts.source_tree,
"selected revision no longer resolves to the frozen source tree"
);
let (recipe_bytes, gitlinks) =
self.exact_recipe_and_gitlinks(source, &resolved.facts.revision)?;
ensure!(
recipe_bytes == resolved.recipe_bytes,
"selected recipe object differs from the durable source resolution"
);
ensure!(
gitlinks == resolved.facts.gitlinks,
"Gitlinks differ from the durable source resolution"
);
Ok(())
}
/// F1: fetches every object in `objects` in one bulk round trip, instead
/// of letting a partial (`blob:none`) clone lazily fetch each missing
/// blob one at a time when `stream_blobs` later reads it. `--stdin`
/// (Git 2.36+) takes the object list off argv, so this holds for
/// thousands of objects without an argv-length limit. Objects the
/// checkout already has are asked for again; Git answers from the local
/// pack without a network round trip for those, which is cheap next to
/// the round trips this replaces. `transfer.fsckObjects=true` is defense
/// in depth: Git itself refuses a fetched tree with duplicate names
/// before any of it reaches disk, independent of the explicit `fsck`
/// call S1 added around the revision this bulk fetch serves.
/// The noop negotiation, `--recurse-submodules=no` and explicit
/// `--filter=blob:none` are the arguments Git's own lazy fetch uses for a
/// promisor remote; without them a hosted origin omits the wanted blob and
/// Git's connectivity check fails on the bare blob id.
fn bulk_fetch_objects(&self, repository: &Path, objects: &[String]) -> Result<()> {
if objects.is_empty() {
return Ok(());
}
// A repository with no `origin` remote is necessarily self-contained
// (every real checkout this driver creates has one); skip the fetch
// rather than fail on a repository that already holds everything.
let has_origin = self
.git([
OsString::from("-C"),
repository.as_os_str().to_owned(),
OsString::from("remote"),
OsString::from("get-url"),
OsString::from("origin"),
])
.is_ok();
if !has_origin {
return Ok(());
}
let mut command = self.git_command([
OsString::from("-c"),
OsString::from("transfer.fsckObjects=true"),
OsString::from("-c"),
OsString::from("fetch.negotiationAlgorithm=noop"),
OsString::from("-C"),
repository.as_os_str().to_owned(),
OsString::from("fetch"),
OsString::from("--no-tags"),
OsString::from("--no-write-fetch-head"),
OsString::from("--recurse-submodules=no"),
OsString::from("--filter=blob:none"),
OsString::from("--stdin"),
OsString::from("origin"),
])?;
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn().context("starting bulk Git fetch")?;
let mut stdin = child.stdin.take().context("bulk Git fetch has no stdin")?;
let request: Vec<u8> = objects
.iter()
.flat_map(|object| {
let mut line = object.as_bytes().to_vec();
line.push(b'\n');
line
})
.collect();
let writer = std::thread::spawn(move || -> std::io::Result<()> { stdin.write_all(&request) });
let output = child
.wait_with_output()
.context("waiting for bulk Git fetch to exit")?;
let write_result = writer
.join()
.map_err(|_| anyhow!("bulk Git fetch stdin writer panicked"))?;
write_result.context("writing bulk Git fetch requests")?;
ensure!(
output.status.success(),
"bulk Git fetch exited with {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
);
Ok(())
}
/// Streams every named blob object's raw bytes from `repository` through
/// one `git cat-file --batch` process, with no attribute-driven
/// transform of any kind, calling `on_object(object, reader, size)` once
/// per object **in request order** with a reader positioned at exactly
/// `size` bytes of that object's content; `on_object` must consume all
/// of it. F9: stderr is drained on a dedicated thread and the child is
/// waited on unconditionally, on every return path including an error
/// from `on_object`, so a failure here never leaves a zombie process or
/// a stalled pipe.
fn stream_blobs(
&self,
repository: &Path,
objects: &[String],
mut on_object: impl FnMut(&str, &mut dyn Read, usize) -> Result<()>,
) -> Result<()> {
if objects.is_empty() {
return Ok(());
}
let mut command = self.git_command([
OsString::from("-C"),
repository.as_os_str().to_owned(),
OsString::from("cat-file"),
OsString::from("--batch"),
])?;
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn().context("starting Git cat-file --batch")?;
let mut stdin = child
.stdin
.take()
.context("Git cat-file --batch has no stdin")?;
let stdout = child
.stdout
.take()
.context("Git cat-file --batch has no stdout")?;
let mut stderr = child
.stderr
.take()
.context("Git cat-file --batch has no stderr")?;
let request: Vec<u8> = objects
.iter()
.flat_map(|object| {
let mut line = object.as_bytes().to_vec();
line.push(b'\n');
line
})
.collect();
let writer = std::thread::spawn(move || -> std::io::Result<()> {
stdin.write_all(&request)
// `stdin` drops here, closing the pipe so Git sees end of input.
});
let stderr_reader = std::thread::spawn(move || -> Vec<u8> {
let mut buffer = Vec::new();
let _ = stderr.read_to_end(&mut buffer);
buffer
});
let body: Result<()> = (|| {
let mut reader = std::io::BufReader::new(stdout);
for object in objects {
let mut header = Vec::new();
reader
.read_until(b'\n', &mut header)
.context("reading a Git cat-file --batch header")?;
ensure!(
header.last() == Some(&b'\n'),
"Git cat-file --batch closed before answering every object"
);
header.pop();
let header = String::from_utf8(header)
.context("Git cat-file --batch header is not UTF-8")?;
let mut fields = header.split(' ');
let returned_object = fields
.next()
.context("Git cat-file --batch header names no object")?;
ensure!(
returned_object == object,
"Git cat-file --batch answered objects out of the requested order"
);
let kind_or_missing = fields
.next()
.context("Git cat-file --batch header is malformed")?;
ensure!(
kind_or_missing != "missing",
"Git object {object} is missing from the repository"
);
let size: usize = fields
.next()
.context("Git cat-file --batch header has no size")?
.parse()
.context("Git cat-file --batch size is not a number")?;
ensure!(
fields.next().is_none(),
"Git cat-file --batch header has extra fields"
);
on_object(object, &mut reader, size)?;
let mut trailer = [0u8; 1];
reader
.read_exact(&mut trailer)
.context("reading the newline after a Git cat-file --batch object")?;
ensure!(
trailer[0] == b'\n',
"Git cat-file --batch object content ran past its declared size"
);
}
Ok(())
})();
let stderr_bytes = stderr_reader.join().unwrap_or_default();
let write_result = writer.join().map_err(|_| anyhow!("Git cat-file --batch stdin writer panicked"));
let status = child
.wait()
.context("waiting for Git cat-file --batch to exit")?;
body?;
write_result?.context("writing Git cat-file --batch requests")?;
ensure!(
status.success(),
"Git cat-file --batch exited with {status}: {}",
String::from_utf8_lossy(&stderr_bytes).trim()
);
Ok(())
}
/// Writes every entry of `repository`'s tree at `revision` into
/// `destination`, byte for byte from the object store: no `git archive`,
/// no attribute-driven transform, no smudge or clean filter. Modes and
/// symlinks are preserved exactly as the tree records them. Returns the
/// paths, relative to `destination`, whose content is a Git LFS pointer
/// (LFS content itself is never fetched).
///
/// F1: blobs are bulk-fetched once (`bulk_fetch_objects`) and then
/// streamed straight to their destination file (`stream_blobs`), never
/// held whole in memory. F2/F3 layer (b): every directory this writes
/// into is created by `ensure_frozen_directory`, which never treats an
/// existing symlink or foreign directory as traversable, and every
/// symlink entry is written only in the final pass, after every
/// directory and regular file this tree needs already exists — so
/// nothing written earlier can ever be reached back out through a
/// symlink this call creates.
fn materialize_tree_raw(
&self,
repository: &Path,
revision: &str,
destination: &Path,
frozen_root: &Path,
created_dirs: &mut std::collections::HashSet<PathBuf>,
) -> Result<Vec<PathBuf>> {
ensure_frozen_directory(frozen_root, created_dirs, destination)?;
let entries = self.git_tree_entries(repository, revision)?;
// Pass 1: every directory this tree needs, before any file or
// symlink content is written. No symlink exists yet at this point,
// so nothing here can traverse one.
for entry in &entries {
let target = destination.join(&entry.path);
match entry.mode.as_str() {
"100644" | "100755" | "120000" => {
let parent = target
.parent()
.context("frozen source tree entry has no parent")?;
ensure_frozen_directory(frozen_root, created_dirs, parent)?;
}
"160000" => {
// Gitlinks are materialized by the caller, which knows
// each one's admitted origin; reserve the directory now
// so the caller's own writer has somewhere safe to land.
ensure_frozen_directory(frozen_root, created_dirs, &target)?;
}
other => bail!(
"frozen source tree entry {} has an unsupported mode {other}",
entry.path.display()
),
}
}
// Pass 2: bulk-fetch every blob this tree needs in one round trip
// (F1), then stream each one to disk. Regular files are written
// directly; symlink targets are tiny path strings, buffered here and
// written in Pass 3, last.
let mut object_order: Vec<String> = Vec::new();
let mut entries_by_object: std::collections::HashMap<&str, Vec<&GitTreeEntry>> =
std::collections::HashMap::new();
for entry in entries.iter().filter(|entry| entry.kind == "blob") {
entries_by_object
.entry(entry.object.as_str())
.or_insert_with(|| {
object_order.push(entry.object.clone());
Vec::new()
})
.push(entry);
}
self.bulk_fetch_objects(repository, &object_order)?;
let mut lfs_pointer_paths = Vec::new();
let mut symlink_targets: BTreeMap<PathBuf, Vec<u8>> = BTreeMap::new();
self.stream_blobs(repository, &object_order, |object, reader, size| {
let group = entries_by_object
.get(object)
.context("Git cat-file --batch answered an object nobody requested")?;
let (first, rest) = group
.split_first()
.expect("every requested object groups at least one entry");
let write_file = |entry: &GitTreeEntry, content: &[u8]| -> Result<()> {
let target = destination.join(&entry.path);
fs::write(&target, content).with_context(|| format!("writing {}", target.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = if entry.mode == "100755" { 0o755 } else { 0o644 };
fs::set_permissions(&target, fs::Permissions::from_mode(mode))?;
}
Ok(())
};
match first.mode.as_str() {
"100644" | "100755" => {
let target = destination.join(&first.path);
let mut file = fs::File::create(&target)
.with_context(|| format!("writing {}", target.display()))?;
let mut remaining = size;
let mut buffer = [0u8; 65536];
let mut peek: Vec<u8> = Vec::new();
while remaining > 0 {
let want = buffer.len().min(remaining);
reader
.read_exact(&mut buffer[..want])
.context("reading Git cat-file --batch object content")?;
if peek.len() < 128 {
let take = (128 - peek.len()).min(want);
peek.extend_from_slice(&buffer[..take]);
}
file.write_all(&buffer[..want])
.with_context(|| format!("writing {}", target.display()))?;
remaining -= want;
}
drop(file);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = if first.mode == "100755" { 0o755 } else { 0o644 };
fs::set_permissions(&target, fs::Permissions::from_mode(mode))?;
}
let is_lfs = is_lfs_pointer(&peek);
if is_lfs {
lfs_pointer_paths.push(first.path.clone());
}
for extra in rest {
match extra.mode.as_str() {
"100644" | "100755" => {
let extra_target = destination.join(&extra.path);
fs::copy(&target, &extra_target).with_context(|| {
format!("writing {}", extra_target.display())
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = if extra.mode == "100755" { 0o755 } else { 0o644 };
fs::set_permissions(&extra_target, fs::Permissions::from_mode(mode))?;
}
if is_lfs {
lfs_pointer_paths.push(extra.path.clone());
}
}
"120000" => {
let target_len = fs::symlink_metadata(&target)
.with_context(|| format!("inspecting {}", target.display()))?
.len() as usize;
ensure!(
target_len <= FROZEN_SOURCE_SYMLINK_TARGET_LIMIT,
"frozen source symlink target for {} exceeds the platform's path length limit ({target_len} > {FROZEN_SOURCE_SYMLINK_TARGET_LIMIT} bytes)",
extra.path.display()
);
symlink_targets.insert(extra.path.clone(), fs::read(&target)?);
}
other => bail!(
"frozen source tree entry {} has an unsupported mode {other}",
extra.path.display()
),
}
}
}
"120000" => {
ensure!(
size <= FROZEN_SOURCE_SYMLINK_TARGET_LIMIT,
"frozen source symlink target for {} exceeds the platform's path length limit ({size} > {FROZEN_SOURCE_SYMLINK_TARGET_LIMIT} bytes)",
first.path.display()
);
let mut content = vec![0u8; size];
reader
.read_exact(&mut content)
.context("reading Git cat-file --batch object content")?;
symlink_targets.insert(first.path.clone(), content.clone());
for extra in rest {
match extra.mode.as_str() {
"120000" => {
symlink_targets.insert(extra.path.clone(), content.clone());
}
"100644" | "100755" => write_file(extra, &content)?,
other => bail!(
"frozen source tree entry {} has an unsupported mode {other}",
extra.path.display()
),
}
}
}
other => bail!(
"frozen source tree entry {} has an unsupported mode {other}",
first.path.display()
),
}
Ok(())
})?;
// Pass 3: symlinks, last. Every directory this tree needs already
// exists from Pass 1, and no symlink this call creates existed
// before this point.
for (relative_path, content) in symlink_targets {
let target = destination.join(&relative_path);
let link_target = std::str::from_utf8(&content)
.context("frozen source symlink target is not UTF-8")?;
ensure!(
fs::symlink_metadata(&target).is_err(),
"frozen source writer refuses to overwrite an existing entry at {}",
target.display()
);
#[cfg(unix)]
std::os::unix::fs::symlink(link_target, &target)
.with_context(|| format!("creating symlink {}", target.display()))?;
}
Ok(lfs_pointer_paths)
}
/// Clones a Gitlink's admitted origin at its exact recorded revision and
/// writes its tree raw under `destination_root.join(path)`, the same
/// byte-exact primitive as the superproject, sharing the superproject's
/// `created_dirs` bookkeeping so a Gitlink can never alias a path the
/// superproject (or an earlier Gitlink) already wrote. Returns the
/// materialized LFS-pointer paths, relative to `destination_root`.
fn materialize_gitlink_raw(
&self,
source: &ExactSource,
path: &Path,
fact: &GitlinkTreeFact,
destination_root: &Path,
frozen_root: &Path,
created_dirs: &mut std::collections::HashSet<PathBuf>,
) -> Result<Vec<PathBuf>> {
let checkout_text = source
.checkout
.to_str()
.context("repository checkout path is not UTF-8")?;
let checkout_key = sha256_id(checkout_text.as_bytes());
let gitlink_root = self.source_cache_root.join(".gitlinks").join(
checkout_key
.strip_prefix("sha256-")
.unwrap_or(&checkout_key),
);
ensure_source_directory_tree(&self.source_cache_root, &gitlink_root, self.identity)?;
let checkout = gitlink_root.join(format!("{}-{}", fact.revision, Uuid::new_v4()));
let result = (|| {
self.git([
OsString::from("-c"),
OsString::from("transfer.fsckObjects=true"),
OsString::from("clone"),
OsString::from("--filter=blob:none"),
OsString::from("--no-checkout"),
OsString::from("--origin"),
OsString::from("origin"),
fact.origin.clone().into(),
checkout.as_os_str().to_owned(),
])?;
self.git([
OsString::from("-c"),
OsString::from("transfer.fsckObjects=true"),
OsString::from("-C"),
checkout.as_os_str().to_owned(),
OsString::from("fetch"),
OsString::from("--no-tags"),
OsString::from("origin"),
fact.revision.clone().into(),
])?;
let actual = self.git_text([
OsString::from("-C"),
checkout.as_os_str().to_owned(),
OsString::from("rev-parse"),
OsString::from(format!("{}^{{commit}}", fact.revision)),
])?;
ensure!(actual == fact.revision, "Gitlink exact revision is absent");
ensure!(
!self
.git_tree_entries(&checkout, &fact.revision)?
.iter()
.any(|entry| entry.mode == "160000"),
"nested Gitlinks are not admitted in Idunn v1"
);
self.materialize_tree_raw(
&checkout,
&fact.revision,
&destination_root.join(path),
frozen_root,
created_dirs,
)
})();
let cleanup = if checkout.exists() {
remove_tree_inside(&gitlink_root, &checkout)
} else {
Ok(())
};
match (result, cleanup) {
(Ok(lfs_pointer_paths), Ok(())) => Ok(lfs_pointer_paths
.into_iter()
.map(|relative| path.join(relative))
.collect()),
(Err(error), _) => Err(error),
(Ok(_), Err(error)) => Err(error.context("cleaning exact Gitlink checkout")),
}
}
/// Fetches `source` at the exact `revision` and writes it and its
/// Gitlinks, byte for byte from the Git object store, into a fresh
/// root-owned immutable tree under `root/<transaction_id>`. Returns that
/// tree's root, its content digest, the recipe blob bytes the tree's own
/// Git objects name, and whether any materialized file is a Git LFS
/// pointer (LFS content itself is never fetched). It derives the recipe
/// and Gitlink facts itself from the tree rather than trusting a caller's
/// prior resolution, so it needs no `OperatorBinding` or compiled plan and
/// is safe to call again later for a verify transaction over a different
/// revision of a different repository. It shares its fetch core with
/// `freeze`, which additionally checks the frozen recipe against a
/// durable resolution made at admission time.
pub(crate) fn freeze_exact(
&self,
source: &ExactSource,
revision: &str,
transaction_id: &str,
root: &Path,
) -> Result<(PathBuf, String, Vec<u8>, bool)> {
require_driver_id(transaction_id, "source transaction")?;
require_git_sha(revision, "frozen source revision")?;
#[cfg(unix)]
ensure!(
unsafe { libc::geteuid() } == 0 && self.identity.is_some(),
"freezing source requires root Idunn with an unprivileged Git identity"
);
self.prepare_source_root(source)?;
self.git([
OsString::from("-c"),
OsString::from("transfer.fsckObjects=true"),
OsString::from("-C"),
source.checkout.as_os_str().to_owned(),
OsString::from("fetch"),
OsString::from("--no-tags"),
OsString::from("origin"),
OsString::from(revision),
])
.context("fetching the durable exact source revision")?;
// S1: `transfer.fsckObjects=true` above only inspects objects this
// fetch actually transfers. An object the checkout already holds --
// brought in by an earlier `resolve()` on this same checkout, or by
// any other path into the local store -- is never re-transferred and
// so is never re-checked by that flag. Fsck the selected revision
// explicitly and independently here, so objects that were already
// local get checked too, not only freshly fetched ones.
self.git([
OsString::from("-C"),
source.checkout.as_os_str().to_owned(),
OsString::from("fsck"),
OsString::from("--strict"),
OsString::from("--no-dangling"),
OsString::from(revision),
])
.with_context(|| format!("fsck refused the selected revision {revision}"))?;
let (recipe_bytes, gitlinks) = self.exact_recipe_and_gitlinks(source, revision)?;
let transaction_root = prepare_frozen_transaction_root(root, transaction_id)?;
let partial = transaction_root.join(".partial");
prepare_frozen_source_destination(&partial)?;
let materialization = (|| {
let mut created_dirs = std::collections::HashSet::new();
created_dirs.insert(partial.clone());
let mut lfs_pointer_paths =
self.materialize_tree_raw(&source.checkout, revision, &partial, &partial, &mut created_dirs)?;
for (path, fact) in &gitlinks {
lfs_pointer_paths.extend(self.materialize_gitlink_raw(
source,
path,
fact,
&partial,
&partial,
&mut created_dirs,
)?);
}
let recipe_file = partial.join(&source.recipe_path);
let recipe_metadata = fs::symlink_metadata(&recipe_file)?;
ensure!(
recipe_metadata.is_file() && !recipe_metadata.file_type().is_symlink(),
"frozen deployment recipe is not a regular file"
);
let materialized_recipe =
fs::read(&recipe_file).context("reading frozen deployment recipe")?;
ensure!(
materialized_recipe == recipe_bytes,
"frozen recipe differs from the selected tree's recipe blob"
);
// F4: one hardening pass, last, sets and validates the frozen
// tree's ownership, modes and symlinks in the same walk. A
// separate, later `validate_frozen_source` re-walk was a
// redundant duplicate of exactly this check; it stays only as
// the read-only re-check `observe_frozen` uses on a tree it did
// not just write.
harden_frozen_source(&partial)?;
let snapshot_sha256 = frozen_source_sha256(&partial)?;
Ok::<_, anyhow::Error>((snapshot_sha256, !lfs_pointer_paths.is_empty()))
})();
let (snapshot_sha256, contains_lfs_pointers) = match materialization {
Ok(outcome) => outcome,
Err(error) => {
let _ = remove_frozen_transaction_root(root, transaction_id);
return Err(error);
}
};
let snapshot_component = snapshot_sha256
.strip_prefix("sha256-")
.expect("generated frozen source digest");
let final_root = transaction_root.join(snapshot_component);
fs::rename(&partial, &final_root).context("publishing immutable frozen source")?;
// S6: symlinks are judged against the tree's real, final published
// path, not `.partial`. This must run after the rename and before
// anything trusts the tree.
if let Err(error) = validate_frozen_source_symlinks(&final_root) {
let _ = remove_frozen_transaction_root(root, transaction_id);
return Err(error);
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&transaction_root, fs::Permissions::from_mode(0o500))?;
}
Ok((final_root, snapshot_sha256, recipe_bytes, contains_lfs_pointers))
}
}
/// A Git LFS pointer file's exact, well-known first line (the LFS pointer
/// spec fixes this text). Detecting it needs no LFS tooling: the pointer is
/// itself the blob content when LFS smudging never runs, which
/// `stream_blobs` guarantees by construction.
fn is_lfs_pointer(content: &[u8]) -> bool {
content.starts_with(b"version https://git-lfs.github.com/spec/v1\n")
}
impl SourcePort for GitSourceDriver {
fn resolve(
&self,
binding: &OperatorBinding,
resolution_id: &str,
selected_at_unix_millis: u64,
) -> Result<ResolvedSource> {
binding.validate()?;
ensure!(
selected_at_unix_millis > 0,
"source selection has no timestamp"
);
let source = exact_source_from(binding);
self.prepare_source_root(&source)?;
let fetched_ref = Self::admitted_ref_name(binding, resolution_id)?;
// S1: this fetch brings the superproject's commit and tree objects
// into the local store first, before any other guard ever sees them.
// `freeze_exact`'s own fetch is fsck-guarded, but a fetch by exact
// revision never re-transfers (and so never re-checks) an object
// that is already local -- and this call is what makes it local.
// Without `transfer.fsckObjects=true` here, a hostile `.git`
// look-alike or a tree with duplicate entries lands unchecked.
self.git([
OsString::from("-c"),
OsString::from("transfer.fsckObjects=true"),
OsString::from("-C"),
binding.repository.checkout.as_os_str().to_owned(),
OsString::from("fetch"),
OsString::from("--force"),
OsString::from("--no-tags"),
OsString::from("origin"),
OsString::from(format!(
"+{}:{fetched_ref}",
binding.repository.admitted_ref
)),
])
.context("fetching the admitted ref")?;
let admitted_ref_revision = self.git_text([
OsString::from("-C"),
binding.repository.checkout.as_os_str().to_owned(),
OsString::from("rev-parse"),
OsString::from(format!("{fetched_ref}^{{commit}}")),
])?;
require_git_sha(&admitted_ref_revision, "fetched admitted-ref revision")?;
let (revision, selection) =
self.resolve_selected_revision(binding, &admitted_ref_revision)?;
let source_tree = self.git_text([
OsString::from("-C"),
binding.repository.checkout.as_os_str().to_owned(),
OsString::from("rev-parse"),
OsString::from(format!("{revision}^{{tree}}")),
])?;
require_git_sha(&source_tree, "selected source tree")?;
let (recipe_bytes, gitlinks) = self.exact_recipe_and_gitlinks(&source, &revision)?;
let facts = SourceSelectionFacts {
schema: SOURCE_SELECTION_FACTS_SCHEMA.into(),
origin: binding.repository.origin.clone(),
admitted_ref: binding.repository.admitted_ref.clone(),
admitted_ref_revision,
revision,
source_tree,
recipe_path: binding.repository.recipe_path.clone(),
recipe_blob_sha256: sha256_id(&recipe_bytes),
gitlinks,
selection,
selected_at_unix_millis,
};
facts.validate_against(binding)?;
let resolved = ResolvedSource {
facts,
recipe_bytes,
};
resolved.validate_against(binding)?;
Ok(resolved)
}
fn freeze(
&self,
transaction_id: &str,
plan: &CompiledDeploymentPlan,
) -> Result<FrozenSourceReceipt> {
plan.validate()?;
require_driver_id(transaction_id, "source transaction")?;
let (_, binding) = plan.parsed_inputs()?;
let resolved = ResolvedSource {
facts: plan.source.clone(),
recipe_bytes: plan.recipe_blob.clone(),
};
resolved.validate_against(&binding)?;
#[cfg(unix)]
ensure!(
unsafe { libc::geteuid() } == 0 && self.identity.is_some(),
"freezing source requires root Idunn with an unprivileged Git identity"
);
let source = exact_source_from(&binding);
self.prepare_source_root(&source)?;
// `freeze_exact` fetches the exact revision itself; fetching here too
// would fetch twice for one freeze. `verify_exact_source` below needs
// no fetch of its own: it only reads objects already local, and
// `freeze_exact` has just made the selected revision local.
// `contains_lfs_pointers` is the frozen result's own record that LFS
// content was not fetched (F2); `FrozenSourceReceipt` has no field for
// it yet, so it is not yet carried past this call. Promoting it to a
// persisted, Verse-visible fact is follow-up scope, not this fix.
let (_tree_root, snapshot_sha256, recipe_bytes, _contains_lfs_pointers) = self
.freeze_exact(
&source,
&resolved.facts.revision,
transaction_id,
&self.frozen_source_root,
)?;
self.verify_exact_source(&source, &resolved)?;
ensure!(
recipe_bytes == resolved.recipe_bytes,
"frozen recipe differs from the durable source resolution"
);
let receipt = FrozenSourceReceipt {
transaction_id: transaction_id.to_owned(),
plan_id: plan.plan_id.clone(),
snapshot_sha256,
};
receipt.validate_against(plan)?;
Ok(receipt)
}
fn observe_frozen(
&self,
plan: &CompiledDeploymentPlan,
receipt: &FrozenSourceReceipt,
) -> Result<FrozenSource> {
receipt.validate_against(plan)?;
validate_frozen_source_store(&self.frozen_source_root)?;
let root = self
.frozen_source_root
.join(&receipt.transaction_id)
.join(receipt.snapshot_component());
let canonical_store = self.frozen_source_root.canonicalize()?;
let canonical_root = root.canonicalize()?;
ensure!(
canonical_store == self.frozen_source_root
&& canonical_root == root
&& canonical_root.starts_with(&canonical_store),
"frozen source receipt resolves outside its authority root"
);
validate_frozen_source(&root)?;
ensure!(
frozen_source_sha256(&root)? == receipt.snapshot_sha256,
"frozen source snapshot differs from its receipt"
);
let recipe_path = root.join(&plan.source.recipe_path);
let recipe_metadata = fs::symlink_metadata(&recipe_path)?;
ensure!(
recipe_metadata.is_file() && !recipe_metadata.file_type().is_symlink(),
"observed deployment recipe is not a regular file"
);
ensure!(
fs::read(&recipe_path)? == plan.recipe_blob,
"observed deployment recipe differs from the persisted plan"
);
Ok(FrozenSource {
receipt: receipt.clone(),
facts: plan.source.clone(),
recipe_bytes: plan.recipe_blob.clone(),
root,
})
}
fn cleanup(&self, transaction_id: &str, receipt: Option<&FrozenSourceReceipt>) -> Result<()> {
require_driver_id(transaction_id, "source transaction")?;
if let Some(receipt) = receipt {
ensure!(
receipt.transaction_id == transaction_id,
"frozen source cleanup receipt belongs to another transaction"
);
require_sha256_id(&receipt.snapshot_sha256, "frozen source snapshot")?;
}
let transaction_root = self.frozen_source_root.join(transaction_id);
if !transaction_root.exists() {
return Ok(());
}
remove_frozen_transaction_root(&self.frozen_source_root, transaction_id)
}
}
/// The network a container step is admitted to. `None` and `Bridge` are today's
/// only lowered profiles; `Named` carries an operator-supplied Docker network
/// name forward unchanged, matching the binding's free-form
/// `network_profile` before this cut.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ContainerNetwork {
None,
Bridge,
Named(String),
}
impl ContainerNetwork {
fn docker_value(&self) -> &str {
match self {
ContainerNetwork::None => "none",
ContainerNetwork::Bridge => "bridge",
ContainerNetwork::Named(name) => name,
}
}
}
/// Cut 2 adds `Unconfined`. This cut declares only the variant every runner
/// gets today, so a seccomp override is not yet reached.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ContainerSeccomp {
Default,
}
/// A secret bound into the container's environment from a root-owned,
/// exact-group-bound file, resolved and validated by `ContainerSpec::for_step`
/// before the container ever runs.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SecretMount {
pub env_name: String,
pub host_path: PathBuf,
pub container_path: String,
}
/// A Docker container's full admission shape: the binding's affordances, plus
/// the step's resolved environment. It is deliberately a plain struct with no
/// binding or recipe types in it, so `docker_run_args` can lower it with no
/// filesystem effects and no knowledge of `DockerRunnerBinding`, and so a
/// future verify runner binding can build one directly.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ContainerSpec {
pub image: String,
pub user: String,
pub network: ContainerNetwork,
pub seccomp: ContainerSeccomp,
pub memory_mebibytes: u32,
pub cpu_quota_percent: u32,
pub pids_limit: u32,
pub tmpfs_mebibytes: u32,
pub cache_root: Option<PathBuf>,
pub secret_mounts: Vec<SecretMount>,
pub environment: Vec<(String, String)>,
}
impl ContainerSpec {
/// The one constructor: a runner binding, the step's required environment
/// names, and the source stamp pair. Cache-root and secret validation
/// happen here, against the filesystem, before `docker_run_args` ever
/// runs; that function stays pure.
pub(crate) fn for_step(
runner: &DockerRunnerBinding,
required_environment: &std::collections::BTreeSet<String>,
source_stamp: (&str, &str),
) -> Result<Self> {
let identity = container_identity(&runner.user)?;
if let Some(cache_root) = &runner.cache_root {
ensure_runner_cache_root(cache_root, identity)?;
}
let mut environment = vec![(source_stamp.0.to_owned(), source_stamp.1.to_owned())];
let mut secret_mounts = Vec::new();
for name in required_environment {
ensure!(
name != source_stamp.0,
"runner cannot bind step environment {name}, which collides with the Idunn source stamp"
);
if let Some(value) = runner.environment.get(name) {
environment.push((name.clone(), value.clone()));
} else if let Some(path) = runner.secret_files.get(name) {
validate_runner_secret(path, identity)?;
secret_mounts.push(SecretMount {
env_name: name.clone(),
host_path: path.clone(),
container_path: format!("/run/idunn/secrets/{name}"),
});
} else {
bail!("runner lacks declared step environment {name}")
}
}
let network = match runner.network_profile.as_deref() {
None | Some("none") => ContainerNetwork::None,
Some("bridge") => ContainerNetwork::Bridge,
Some(other) => ContainerNetwork::Named(other.to_owned()),
};
Ok(ContainerSpec {
image: runner.image.clone(),
user: runner.user.clone(),
network,
seccomp: ContainerSeccomp::Default,
memory_mebibytes: runner.memory_mebibytes,
cpu_quota_percent: runner.cpu_quota_percent,
pids_limit: runner.pids_limit,
tmpfs_mebibytes: runner.tmpfs_mebibytes,
cache_root: runner.cache_root.clone(),
secret_mounts,
environment,
})
}
}
/// Lowers `spec` into the exact `docker run` argv for `argv` running in
/// `workspace` at `working_directory`. Pure: it performs no filesystem
/// effects and validates nothing that requires the filesystem (the caller
/// validates the cache root and every secret through `ContainerSpec::for_step`
/// before calling this). It still needs the per-workspace machine-id file's
/// path, which `build_machine_id` computes without touching the filesystem;
/// the caller is responsible for having materialized that file with
/// `build_machine_id_file` first.
pub(crate) fn docker_run_args(
spec: &ContainerSpec,
workspace: &Path,
working_directory: &Path,
argv: &[String],
) -> Result<Vec<OsString>> {
ensure!(!argv.is_empty(), "runner command is empty");
let mut args = vec![
OsString::from("run"),
OsString::from("--rm"),
OsString::from("--network"),
OsString::from(spec.network.docker_value()),
OsString::from("--memory"),
OsString::from(format!("{}m", spec.memory_mebibytes)),
OsString::from("--cpus"),
OsString::from(format!(
"{:.2}",
f64::from(spec.cpu_quota_percent) / 100.0
)),
OsString::from("--mount"),
bind_mount(workspace, "/workspace", false)?,
OsString::from("--user"),
OsString::from(&spec.user),
OsString::from("--cap-drop"),
OsString::from("ALL"),
OsString::from("--security-opt"),
OsString::from("no-new-privileges"),
];
match spec.seccomp {
ContainerSeccomp::Default => {}
}
args.extend([
OsString::from("--read-only"),
OsString::from("--pids-limit"),
OsString::from(spec.pids_limit.to_string()),
OsString::from("--tmpfs"),
OsString::from(format!(
"/tmp:rw,nosuid,nodev,noexec,size={}m",
spec.tmpfs_mebibytes
)),
OsString::from("--mount"),
bind_mount(
&PathBuf::from("/run/idunn/build-machine-ids").join(build_machine_id(workspace)?),
"/etc/machine-id",
true,
)?,
]);
if let Some(cache_root) = &spec.cache_root {
args.push(OsString::from("--mount"));
args.push(bind_mount(cache_root, "/cache", false)?);
}
args.push(OsString::from("--workdir"));
args.push(OsString::from(format!(
"/workspace/{}",
normalized_relative(working_directory)?
)));
for (name, value) in &spec.environment {
args.push(OsString::from("--env"));
args.push(OsString::from(format!("{name}={value}")));
}
for mount in &spec.secret_mounts {
args.push(OsString::from("--env"));
args.push(OsString::from(format!(
"{}={}",
mount.env_name, mount.container_path
)));
args.push(OsString::from("--mount"));
args.push(bind_mount(&mount.host_path, &mount.container_path, true)?);
}
args.push(spec.image.clone().into());
args.extend(argv.iter().map(OsString::from));
Ok(args)
}
/// Docker is only a runner substrate. Recipe argv stays an argv vector, the
/// operator binding selects the exact image/network/mount affordances, and the
/// driver returns complete materialization receipts rather than build truth by
/// convention.
#[derive(Clone)]
pub struct DockerRunnerDriver {
pub docker_program: PathBuf,
}
impl Default for DockerRunnerDriver {
fn default() -> Self {
Self {
docker_program: PathBuf::from("/usr/bin/docker"),
}
}
}
impl DockerRunnerDriver {
fn docker<I, S>(&self, args: I) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
ensure!(
self.docker_program.is_absolute(),
"Docker runner program is not absolute"
);
let output = Command::new(&self.docker_program)
.args(args)
.stdin(Stdio::null())
.output()
.with_context(|| format!("starting Docker runner {}", self.docker_program.display()))?;
if !output.status.success() {
bail!(
"Docker runner exited with {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(output)
}
fn run_in_workspace(
&self,
runner: &DockerRunnerBinding,
workspace: &Path,
working_directory: &Path,
argv: &[String],
required_environment: &std::collections::BTreeSet<String>,
source_stamp: (&str, &str),
) -> Result<()> {
ensure!(!argv.is_empty(), "runner command is empty");
ensure!(
runner.allowed_programs.contains(&argv[0]),
"runner program {} is not operator-bound",
argv[0]
);
build_machine_id_file(workspace)?;
let spec = ContainerSpec::for_step(runner, required_environment, source_stamp)?;
let args = docker_run_args(&spec, workspace, working_directory, argv)?;
self.docker(args)?;
Ok(())
}
fn materialize_external_input(
&self,
declaration: &TargetDeclaration,
binding: &OperatorBinding,
workspaces: &BTreeMap<String, PathBuf>,
input_id: &str,
) -> Result<ExternalInputMaterializationReceipt> {
let input = declaration
.external_inputs
.iter()
.find(|candidate| candidate.id == input_id)
.context("external input declaration disappeared")?;
let runner = binding.runners[&input.runner].docker()?;
let workspace = &workspaces[&input.runner];
let destination = workspace.join(&input.destination);
ensure!(
destination.starts_with(workspace),
"external input escaped its runner workspace"
);
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)?;
}
build_machine_id_file(workspace)?;
let spec = ContainerSpec::for_step(
runner,
&std::collections::BTreeSet::new(),
("IDUNN_EXTERNAL_INPUT_ID", input.id.as_str()),
)?;
let curl_argv = vec![
"curl".to_owned(),
"--fail".to_owned(),
"--location".to_owned(),
"--proto".to_owned(),
"=https".to_owned(),
"--output".to_owned(),
format!("/workspace/{}", normalized_relative(&input.destination)?),
input.url.clone(),
];
let args = docker_run_args(&spec, workspace, Path::new("."), &curl_argv)?;
self.docker(args)?;
let bytes = fs::read(&destination).with_context(|| {
format!(
"reading materialized external input {}",
destination.display()
)
})?;
ensure!(!bytes.is_empty(), "materialized external input is empty");
let digest = raw_sha256(&bytes);
ensure!(
digest == input.sha256,
"materialized external input digest differs from its recipe pin"
);
Ok(ExternalInputMaterializationReceipt {
input_id: input.id.clone(),
url: input.url.clone(),
sha256: format!("sha256-{digest}"),
runner: input.runner.clone(),
destination: input.destination.clone(),
size_bytes: bytes.len().try_into()?,
})
}
fn collect_artifact(
&self,
declaration: &TargetDeclaration,
source: &FrozenSource,
workspaces: &BTreeMap<String, PathBuf>,
staging_root: &Path,
artifact_id: &str,
) -> Result<ArtifactReceipt> {
let artifact = declaration
.artifacts
.iter()
.find(|candidate| candidate.id == artifact_id)
.context("artifact declaration disappeared")?;
let (containment_root, source_path) = match artifact.source_kind {
ArtifactSource::RunnerOutput => {
let workspace = workspaces
.get(
artifact
.runner
.as_deref()
.context("runner artifact lost its runner")?,
)
.context("runner workspace is absent")?;
(workspace.clone(), workspace.join(&artifact.source))
}
ArtifactSource::WorktreeTree => (source.root.clone(), source.root.join(&artifact.source)),
};
ensure!(source_path.exists(), "declared artifact output is absent");
let destination = staging_root.join(&artifact.destination);
ensure!(
destination.starts_with(staging_root),
"artifact destination escaped its release staging root"
);
copy_artifact(&containment_root, &source_path, &destination)?;
check_artifact_modes(artifact, &destination)?;
let (sha256, size_bytes) = digest_artifact(&destination)?;
if let Some(expected) = &artifact.expected_sha256 {
ensure!(
&sha256 == expected,
"artifact output differs from its recipe-pinned digest"
);
}
Ok(ArtifactReceipt {
artifact_id: artifact.id.clone(),
destination: artifact.destination.clone(),
sha256: format!("sha256-{sha256}"),
size_bytes,
executable: artifact.executable,
executable_members: artifact.executable_members.clone(),
})
}
}
impl RunnerPort for DockerRunnerDriver {
fn materialize(
&self,
source: &FrozenSource,
plan: &CompiledDeploymentPlan,
staging_parent: &Path,
sealed_at_unix_millis: u64,
) -> Result<MaterializedRelease> {
plan.validate()?;
source.receipt.validate_against(plan)?;
ensure!(
source.facts == plan.source && source.recipe_bytes == plan.recipe_blob,
"runner source differs from the compiled plan"
);
let (declaration, binding) = plan.parsed_inputs()?;
fs::create_dir_all(staging_parent)?;
let staging_root = staging_parent.join(&source.receipt.transaction_id);
if staging_root.exists() {
remove_tree_inside(staging_parent, &staging_root)?;
}
fs::create_dir_all(&staging_root)?;
let mut workspaces = BTreeMap::new();
for runner_id in binding.runners.keys() {
let workspace = staging_root.join(format!(".runner-{runner_id}"));
copy_tree(&source.root, &workspace)?;
workspaces.insert(runner_id.clone(), workspace);
}
for input in &declaration.external_inputs {
let workspace = &workspaces[&input.runner];
let destination = workspace.join(&input.destination);
ensure!(
destination.starts_with(workspace),
"external input escaped its runner workspace"
);
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)?;
}
}
for (runner_id, workspace) in &workspaces {
assign_runner_tree(
workspace,
container_identity(&binding.runners[runner_id].docker()?.user)?,
)?;
}
let mut external_inputs = Vec::new();
for input in &declaration.external_inputs {
external_inputs.push(self.materialize_external_input(
&declaration,
&binding,
&workspaces,
&input.id,
)?);
}
for step in &declaration.steps {
let runner = binding.runners[&step.runner].docker()?;
let workspace = &workspaces[&step.runner];
for required in &step.required_environment {
ensure!(
runner.environment.contains_key(required)
|| runner.secret_files.contains_key(required),
"step {} lacks operator-bound environment {required}",
step.id
);
}
self.run_in_workspace(
runner,
workspace,
&step.working_directory,
&step.argv,
&step.required_environment,
(
&declaration.source_stamp_environment,
&source.facts.revision,
),
)?;
}
let mut artifacts = Vec::new();
for artifact in &declaration.artifacts {
artifacts.push(self.collect_artifact(
&declaration,
source,
&workspaces,
&staging_root,
&artifact.id,
)?);
}
for workspace in workspaces.values() {
remove_tree_inside(&staging_root, workspace)?;
}
let release = SealedRelease::new(
plan,
artifacts.clone(),
external_inputs.clone(),
sealed_at_unix_millis,
)?;
Ok(MaterializedRelease {
release,
root: staging_root,
})
}
}
/// systemd owns process execution. This driver lowers one validated launch
/// contract into a transient unit and then proves the native process and
/// executable that systemd actually started. It does not decide admission,
/// readiness, write authority, or route membership.
#[derive(Clone)]
pub struct SystemdTransientWorkloadDriver {
pub systemd_run_program: PathBuf,
pub systemctl_program: PathBuf,
pub proc_root: PathBuf,
pub credential_root: PathBuf,
}
fn changed_systemd_workload_identity_fields(
observed: &SystemdWorkloadObservation,
prior: &SystemdWorkloadObservation,
) -> Vec<&'static str> {
let mut changed_fields = Vec::new();
macro_rules! record_changed_fields {
($($field:ident),+ $(,)?) => {
$(
if observed.$field != prior.$field {
changed_fields.push(stringify!($field));
}
)+
};
}
record_changed_fields!(
unit,
unit_description,
invocation_id,
exec_main_start_timestamp_monotonic,
service_type,
restart_policy,
kill_mode,
dynamic_user,
systemd_user,
systemd_group,
supplementary_groups,
capability_bounding_set,
ambient_capabilities,
private_mounts,
private_pids,
protect_proc,
proc_subset,
no_new_privileges,
umask,
inaccessible_paths,
load_credential,
main_pid,
process_start_time,
process_uids,
process_gids,
process_groups,
process_cap_inheritable,
process_cap_permitted,
process_cap_effective,
process_cap_bounding,
process_cap_ambient,
process_no_new_privileges,
process_namespace_pids,
mount_namespace_id,
pid_namespace_id,
executable,
executable_device,
executable_inode,
executable_sha256,
runtime_instance_id,
working_directory,
runtime_bundle,
environment_names,
environment_contract_sha256,
control_group,
credentials_directory,
parent_only_file_descriptors,
activation_signer_identity_id,
activation_signer_public_key,
service_credentials,
);
changed_fields
}
fn systemd_group_matches_launch_contract(
expected_group: Option<(&str, u32)>,
observed_group: &str,
process_uid: u32,
process_gid: u32,
) -> bool {
match expected_group {
Some((group, group_id)) => observed_group == group && process_gid == group_id,
None => process_uid == process_gid,
}
}
impl Default for SystemdTransientWorkloadDriver {
fn default() -> Self {
Self {
systemd_run_program: PathBuf::from("/usr/bin/systemd-run"),
systemctl_program: PathBuf::from("/usr/bin/systemctl"),
proc_root: PathBuf::from("/proc"),
credential_root: PathBuf::from("/run/idunn/activation-credentials"),
}
}
}
impl SystemdTransientWorkloadDriver {
fn command<I, S>(&self, program: &Path, args: I) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
ensure!(
program.is_absolute(),
"workload actuator program is not absolute"
);
let output = Command::new(program)
.args(args)
.stdin(Stdio::null())
.env_clear()
.env("LANG", "C.UTF-8")
.output()
.with_context(|| format!("starting workload actuator {}", program.display()))?;
if !output.status.success() {
bail!(
"workload actuator {} exited with {}: {}",
program.display(),
output.status,
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(output)
}
fn unit_name(&self, prefix: &str, runtime_instance_id: &str) -> Result<String> {
let suffix = runtime_instance_id
.strip_prefix("sha256-")
.context("runtime instance id has no sha256 prefix")?;
ensure!(
suffix.len() == 64 && suffix.bytes().all(|byte| byte.is_ascii_hexdigit()),
"runtime instance id is not a SHA-256 digest"
);
Ok(format!("{prefix}-{suffix}.service"))
}
fn activation_credential_source(
&self,
activation: &IdunnRuntimeActivationRecord,
) -> Result<PathBuf> {
activation.validate()?;
ensure!(
self.credential_root.is_absolute()
&& !self
.credential_root
.as_os_str()
.to_string_lossy()
.chars()
.any(char::is_whitespace),
"activation credential root must be an absolute path without whitespace"
);
Ok(self
.credential_root
.join(format!("{}.credential", activation.runtime_instance_id)))
}
fn write_activation_credential(
&self,
launch: IdunnRuntimeActivationLaunch,
) -> Result<(IdunnRuntimeActivationRecord, PathBuf)> {
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
ensure_activation_credential_root(&self.credential_root)?;
let expected_activation = launch.activation().clone();
let source = self.activation_credential_source(&expected_activation)?;
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
options.mode(0o400);
let mut file = options.open(&source).with_context(|| {
format!(
"creating one-shot activation credential {}",
source.display()
)
})?;
let written = (|| -> Result<IdunnRuntimeActivationRecord> {
#[cfg(unix)]
file.set_permissions(fs::Permissions::from_mode(0o400))?;
let activation = launch
.write_credential(&mut file)
.context("writing one-shot activation credential")?;
file.flush()?;
file.sync_all()?;
Ok(activation)
})();
drop(file);
let activation = match written {
Ok(activation) => activation,
Err(error) => {
return match remove_activation_credential_source(&source) {
Ok(()) => Err(error),
Err(cleanup) => Err(error.context(format!(
"deleting incomplete activation credential also failed: {cleanup:#}"
))),
};
}
};
let validation = (|| -> Result<()> {
ensure!(
activation == expected_activation,
"activation launch changed while writing its credential"
);
validate_activation_credential_source(&source)?;
sync_parent_directory(&source)
})();
if let Err(error) = validation {
return match remove_activation_credential_source(&source) {
Ok(()) => Err(error),
Err(cleanup) => Err(error.context(format!(
"deleting invalid activation credential also failed: {cleanup:#}"
))),
};
}
Ok((activation, source))
}
fn validate_prepared_activation_credential(
&self,
activation: &IdunnRuntimeActivationRecord,
source: &Path,
) -> Result<()> {
validate_activation_credential_source(source)?;
let signer = IdunnRuntimeActivationSigner::from_credential_reader(
open_native_read_only(source).with_context(|| {
format!(
"opening prepared activation credential {}",
source.display()
)
})?,
)?;
ensure!(
signer.identity_id() == activation.activation_signer_identity_id
&& signer.public_key() == activation.activation_signer_public_key,
"prepared activation credential differs from the persisted activation"
);
Ok(())
}
fn parent_only_file_descriptors(
&self,
binding: &OperatorBinding,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> Result<Vec<ParentOnlyFileDescriptorObservation>> {
let activation_source = self.activation_credential_source(activation)?;
self.validate_prepared_activation_credential(activation, &activation_source)?;
let presence_source = binding
.workload
.systemd()?
.secret_files
.get(RUNTIME_PRESENCE_IDENTITY_BINDING)
.context("workload has no parent-only runtime presence identity source")?;
let presence_signer =
open_service_identity_credential_reader::<GameCultProviderHealthIdentity>(
open_native_read_only(presence_source).with_context(|| {
format!(
"opening runtime presence identity source {}",
presence_source.display()
)
})?,
)?;
ensure!(
presence_signer.entry().identity_id == expected.expected_signer_identity_id,
"runtime presence identity source differs from Expected"
);
let descriptors = vec![
observe_parent_only_file_descriptor(
3,
IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME,
&activation_source,
)?,
observe_parent_only_file_descriptor(
4,
RUNTIME_PRESENCE_IDENTITY_FD_NAME,
presence_source,
)?,
];
parent_only_open_file_properties(&descriptors)?;
Ok(descriptors)
}
fn stop_submitted_unit(&self, unit: &str, expected_description: &str) -> Result<()> {
let Some(observation) = self.show_unit(unit)? else {
return Ok(());
};
let values = &observation.properties;
ensure!(
values.get("Description").map(String::as_str) == Some(expected_description),
"refusing to stop a submitted unit whose description changed"
);
self.command(
&self.systemctl_program,
[OsString::from("stop"), OsString::from(unit)],
)?;
if let Some(observation) = self.show_unit(unit)? {
let values = &observation.properties;
ensure!(
values
.get("ActiveState")
.is_some_and(|state| { matches!(state.as_str(), "inactive" | "failed") }),
"submitted systemd unit remained active after stop"
);
}
Ok(())
}
fn install_release(
&self,
plan: &CompiledDeploymentPlan,
release: &MaterializedRelease,
) -> Result<InstalledReleaseObservation> {
release.release.validate_against(plan)?;
let (_, binding) = plan.parsed_inputs()?;
let release_root = &binding.workload.systemd()?.release_root;
fs::create_dir_all(release_root)
.with_context(|| format!("creating release root {}", release_root.display()))?;
let installed = release_root.join(&release.release.sealed_release_id);
if !installed.exists() {
let temporary = release_root.join(format!(
".install-{}-{}",
release.release.sealed_release_id,
Uuid::new_v4()
));
copy_tree(&release.root, &temporary)?;
if let Err(error) = fs::rename(&temporary, &installed) {
let cleanup = remove_tree_inside(release_root, &temporary);
if installed.exists() && error.kind() == ErrorKind::AlreadyExists {
cleanup?;
} else {
cleanup?;
return Err(error).with_context(|| {
format!("installing sealed release {}", installed.display())
});
}
}
}
for artifact in &release.release.artifacts {
let path = installed.join(&artifact.destination);
let (sha256, size_bytes) = digest_artifact(&path)?;
ensure!(
format!("sha256-{sha256}") == artifact.sha256 && size_bytes == artifact.size_bytes,
"installed artifact {} differs from its sealed receipt",
artifact.artifact_id
);
}
harden_installed_release(&installed, &release.release.artifacts)?;
Ok(InstalledReleaseObservation {
sealed_release_id: release.release.sealed_release_id.clone(),
root: installed,
})
}
fn validate_installed_release(
&self,
plan: &CompiledDeploymentPlan,
release: &SealedRelease,
installed: &InstalledReleaseObservation,
) -> Result<()> {
release.validate_against(plan)?;
let (_, binding) = plan.parsed_inputs()?;
ensure!(
installed.sealed_release_id == release.sealed_release_id
&& installed.root
== binding
.workload
.systemd()?
.release_root
.join(&release.sealed_release_id),
"installed release observation belongs to another sealed release"
);
for artifact in &release.artifacts {
let path = installed.root.join(&artifact.destination);
let (sha256, size_bytes) = digest_artifact(&path)?;
ensure!(
format!("sha256-{sha256}") == artifact.sha256 && size_bytes == artifact.size_bytes,
"installed artifact {} differs from its sealed receipt",
artifact.artifact_id
);
}
harden_installed_release(&installed.root, &release.artifacts)
}
fn prepare_runtime_bundle(
&self,
binding: &OperatorBinding,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> Result<PathBuf> {
let bundle = binding
.workload
.systemd()?
.runtime_root
.join(&activation.runtime_instance_id);
write_runtime_bundle_records(&bundle, expected, activation)?;
harden_runtime_bundle(&bundle)?;
let state_group_id = binding
.workload
.systemd()?
.state_group
.as_deref()
.map(resolve_group_id)
.transpose()?;
ensure_bundle_is_reachable_by_workload(&bundle, state_group_id)?;
Ok(bundle)
}
fn show_unit(&self, unit: &str) -> Result<Option<SystemdUnitObservation>> {
ensure!(
self.systemctl_program.is_absolute(),
"systemctl program is not absolute"
);
let output = Command::new(&self.systemctl_program)
.args([
OsString::from("show"),
OsString::from(unit),
OsString::from("--no-pager"),
OsString::from("--property=LoadState"),
OsString::from("--property=ActiveState"),
OsString::from("--property=SubState"),
OsString::from("--property=Description"),
OsString::from("--property=InvocationID"),
OsString::from("--property=MainPID"),
OsString::from("--property=ExecMainStartTimestampMonotonic"),
OsString::from("--property=Type"),
OsString::from("--property=Restart"),
OsString::from("--property=KillMode"),
OsString::from("--property=DynamicUser"),
OsString::from("--property=User"),
OsString::from("--property=Group"),
OsString::from("--property=SupplementaryGroups"),
OsString::from("--property=CapabilityBoundingSet"),
OsString::from("--property=AmbientCapabilities"),
OsString::from("--property=PrivateMounts"),
OsString::from("--property=PrivatePIDs"),
OsString::from("--property=ProtectProc"),
OsString::from("--property=ProcSubset"),
OsString::from("--property=NoNewPrivileges"),
OsString::from("--property=UMask"),
OsString::from("--property=InaccessiblePaths"),
OsString::from("--property=LoadCredential"),
OsString::from("--property=OpenFile"),
OsString::from("--property=WorkingDirectory"),
OsString::from("--property=ControlGroup"),
])
.stdin(Stdio::null())
.env_clear()
.env("LANG", "C.UTF-8")
.output()
.with_context(|| format!("observing systemd unit {unit}"))?;
let text =
std::str::from_utf8(&output.stdout).context("systemd show output is not UTF-8")?;
let mut values = BTreeMap::new();
let mut open_files = Vec::new();
for line in text.lines().filter(|line| !line.is_empty()) {
let (name, value) = line
.split_once('=')
.context("systemd show output is malformed")?;
if name == "OpenFile" {
open_files.push(value.to_owned());
continue;
}
ensure!(
values.insert(name.to_owned(), value.to_owned()).is_none(),
"systemd show property is duplicated"
);
}
if values
.get("LoadState")
.is_some_and(|value| value == "not-found")
{
return Ok(None);
}
ensure!(
output.status.success(),
"systemd show failed for {unit}: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
Ok(Some(SystemdUnitObservation {
properties: values,
open_files,
}))
}
fn observe_unit(
&self,
unit: &str,
expected_executable: &Path,
runtime_instance_id: &str,
activation_signer_identity_id: &str,
activation_signer_public_key: &[u8],
environment_names: &[String],
service_credential_names: &[String],
parent_only_file_descriptors: &[ParentOnlyFileDescriptorObservation],
) -> Result<SystemdWorkloadObservation> {
ensure!(
service_credential_names
.windows(2)
.all(|pair| pair[0] < pair[1]),
"service credential names are not unique and sorted"
);
let unit_observation = self
.show_unit(unit)?
.with_context(|| format!("systemd unit {unit} is absent"))?;
let values = &unit_observation.properties;
ensure!(
unit_observation.open_files
== parent_only_open_file_properties(parent_only_file_descriptors)?,
"systemd parent-only descriptor contract differs from the Idunn launch"
);
ensure!(
values
.get("ActiveState")
.is_some_and(|value| value == "active")
&& values
.get("SubState")
.is_some_and(|value| value == "running"),
"systemd unit {unit} is not running"
);
let invocation_id = values
.get("InvocationID")
.filter(|value| value.len() == 32 && value.bytes().all(|byte| byte.is_ascii_hexdigit()))
.context("systemd unit has no canonical invocation id")?
.clone();
let exec_main_start_timestamp_monotonic =
required_systemd_property(&values, "ExecMainStartTimestampMonotonic")?
.parse::<u64>()
.context("systemd unit has no numeric main-process start timestamp")?;
ensure!(
exec_main_start_timestamp_monotonic > 0,
"systemd unit has no main-process start timestamp"
);
let unit_description = required_systemd_property(&values, "Description")?.to_owned();
let service_type = required_systemd_property(&values, "Type")?.to_owned();
let restart_policy = required_systemd_property(&values, "Restart")?.to_owned();
let kill_mode = required_systemd_property(&values, "KillMode")?.to_owned();
let dynamic_user = parse_systemd_boolean(&values, "DynamicUser")?;
// Recorded as evidence of which identity actually ran, not asserted on.
let systemd_user = systemd_property(&values, "User")?.to_owned();
let systemd_group = systemd_property(&values, "Group")?.to_owned();
let supplementary_groups = systemd_property(&values, "SupplementaryGroups")?.to_owned();
let capability_bounding_set =
systemd_property(&values, "CapabilityBoundingSet")?.to_owned();
let ambient_capabilities = systemd_property(&values, "AmbientCapabilities")?.to_owned();
let private_mounts = parse_systemd_boolean(&values, "PrivateMounts")?;
let private_pids = parse_systemd_boolean(&values, "PrivatePIDs")?;
let protect_proc = required_systemd_property(&values, "ProtectProc")?.to_owned();
let proc_subset = required_systemd_property(&values, "ProcSubset")?.to_owned();
let no_new_privileges = parse_systemd_boolean(&values, "NoNewPrivileges")?;
let umask = required_systemd_property(&values, "UMask")?.to_owned();
let inaccessible_paths =
required_systemd_property(&values, "InaccessiblePaths")?.to_owned();
let load_credential = required_systemd_property(&values, "LoadCredential")?.to_owned();
ensure!(
service_type == "exec"
&& restart_policy == "no"
&& kill_mode == "mixed"
// `User` is deliberately not asserted here. Idunn never emits a
// User property, and systemd reports the identity it *allocated*
// for a DynamicUser unit -- the unit name, or a generated `_du…`
// when that is too long -- so the property is never empty for a
// running workload and cannot tell an allocated identity from a
// pinned one. The invariant it was reaching for, that the workload
// does not run as a persistent named account, is carried by
// `dynamic_user` below and by Idunn never passing User at all.
&& dynamic_user
&& supplementary_groups.is_empty()
&& capability_bounding_set.is_empty()
&& ambient_capabilities.is_empty()
&& private_mounts
&& private_pids
&& protect_proc == "invisible"
&& proc_subset == "all"
&& no_new_privileges
&& umask == "0007"
&& inaccessible_paths == self.credential_root.display().to_string()
&& load_credential
== if service_credential_names.is_empty() {
""
} else {
"[unprintable]"
},
"systemd workload isolation properties differ from the Idunn contract"
);
let working_directory =
PathBuf::from(required_systemd_property(&values, "WorkingDirectory")?);
ensure!(
working_directory.is_absolute(),
"systemd unit working directory is not absolute"
);
let control_group = required_systemd_property(&values, "ControlGroup")?.to_owned();
ensure!(
control_group.starts_with('/') && !control_group.contains(['\n', '\r', '\0']),
"systemd unit control group is invalid"
);
let main_pid: u32 = values
.get("MainPID")
.context("systemd unit has no MainPID")?
.parse()
.context("systemd MainPID is not a u32")?;
ensure!(main_pid > 0, "systemd unit has no live main process");
let process_root = self.proc_root.join(main_pid.to_string());
let process_executable = process_root.join("exe");
let executable = fs::read_link(&process_executable)
.with_context(|| format!("observing executable for process {main_pid}"))?;
let mut executable_file = open_proc_magic_link(&process_executable)?;
let executable_metadata = executable_file.metadata()?;
let expected_executable_metadata = fs::metadata(expected_executable)?;
#[cfg(unix)]
let (executable_device, executable_inode) = {
use std::os::unix::fs::MetadataExt;
ensure!(
executable_metadata.is_file()
&& executable_metadata.dev() == expected_executable_metadata.dev()
&& executable_metadata.ino() == expected_executable_metadata.ino(),
"systemd started an executable inode outside the sealed release"
);
(executable_metadata.dev(), executable_metadata.ino())
};
#[cfg(not(unix))]
let (executable_device, executable_inode) = (0, 0);
ensure!(
fs::canonicalize(&executable)? == fs::canonicalize(expected_executable)?,
"systemd started an executable outside the sealed release"
);
let executable_sha256 = sha256_reader(&mut executable_file)?;
let process_start_time = linux_process_start_time(&process_root.join("stat"))?;
let process_security = linux_process_security(&process_root.join("status"), main_pid)?;
ensure!(
process_security
.uids
.iter()
.all(|uid| *uid == process_security.uids[0])
&& (61_184..=65_519).contains(&process_security.uids[0]),
"systemd workload does not have one unprivileged process uid"
);
ensure!(
process_security
.gids
.iter()
.all(|gid| *gid == process_security.gids[0])
&& process_security.gids[0] > 0
&& process_security
.groups
.iter()
.all(|gid| *gid == process_security.gids[0]),
"systemd workload has foreign supplementary groups"
);
ensure!(
[
process_security.cap_inheritable,
process_security.cap_permitted,
process_security.cap_effective,
process_security.cap_bounding,
process_security.cap_ambient,
]
.iter()
.all(|capabilities| *capabilities == 0),
"systemd workload retained Linux capabilities"
);
ensure!(
process_security.no_new_privileges,
"systemd workload lacks kernel no-new-privileges enforcement"
);
ensure!(
process_security.namespace_pids.len() >= 2
&& process_security.namespace_pids[0] == main_pid
&& process_security.namespace_pids.last() == Some(&1)
&& process_security.namespace_pids.iter().all(|pid| *pid > 0),
"systemd workload is not observed in a private pid namespace"
);
let mount_namespace_id = linux_namespace_id(&process_root.join("ns/mnt"), "mnt")?;
let pid_namespace_id = linux_namespace_id(&process_root.join("ns/pid"), "pid")?;
let observer_mount_namespace_id =
linux_namespace_id(&self.proc_root.join("self/ns/mnt"), "mnt")?;
let observer_pid_namespace_id =
linux_namespace_id(&self.proc_root.join("self/ns/pid"), "pid")?;
ensure!(
mount_namespace_id != observer_mount_namespace_id
&& pid_namespace_id != observer_pid_namespace_id,
"systemd workload did not receive private mount and pid namespaces"
);
let process_control_groups = fs::read_to_string(process_root.join("cgroup"))?;
let process_control_groups = process_control_groups.lines().collect::<Vec<_>>();
ensure!(
process_control_groups.len() == 1
&& process_control_groups[0] == format!("0::{control_group}"),
"systemd MainPID is outside the unit control group"
);
let command_line = fs::read(process_root.join("cmdline"))?;
ensure!(
!command_line.is_empty(),
"systemd MainPID has no command line"
);
let process_environment = read_process_environment(&process_root.join("environ"))?;
let selected_environment =
select_process_environment(&process_environment, environment_names)?;
let runtime_bundle = PathBuf::from(
selected_environment
.get(IDUNN_RUNTIME_BUNDLE_ENVIRONMENT)
.context("systemd MainPID lacks the Idunn runtime bundle environment")?,
);
ensure!(
runtime_bundle.is_absolute(),
"runtime bundle path is not absolute"
);
let credentials_directory = if service_credential_names.is_empty() {
ensure!(
!process_environment
.iter()
.any(|entry| entry.starts_with(b"CREDENTIALS_DIRECTORY=")),
"systemd MainPID exposed an unowned credential directory"
);
None
} else {
let path = PathBuf::from(required_process_environment_value(
&process_environment,
"CREDENTIALS_DIRECTORY",
)?);
ensure!(
path == Path::new("/run/credentials").join(unit),
"systemd MainPID exposed an unexpected credential directory"
);
Some(path)
};
let activation_descriptor = &parent_only_file_descriptors[0];
ensure!(
activation_descriptor.fd_name == IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME
&& activation_descriptor.size == 32
&& activation_descriptor.uid == 0
&& activation_descriptor.gid == 0
&& activation_descriptor.mode == 0o400
&& activation_descriptor.links == 1,
"activation signing descriptor is not exact root-only source material"
);
let mut service_credentials = Vec::new();
for name in service_credential_names {
let delivered_path = credentials_directory
.as_ref()
.context("service credential has no systemd credential directory")?
.join(name);
let delivered_value = delivered_path.display().to_string();
ensure!(
selected_environment.get(name) == Some(&delivered_value),
"workload secret environment does not name its delivered systemd credential"
);
let observer_path =
path_inside_process_root(&process_root.join("root"), &delivered_path)?;
let mut file = open_native_read_only(&observer_path).with_context(|| {
format!(
"opening delivered service credential {}",
delivered_path.display()
)
})?;
let metadata = file.metadata()?;
#[cfg(unix)]
let (device, inode, uid, gid, mode, links) = {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
(
metadata.dev(),
metadata.ino(),
metadata.uid(),
metadata.gid(),
metadata.permissions().mode() & 0o777,
metadata.nlink(),
)
};
#[cfg(not(unix))]
let (device, inode, uid, gid, mode, links) = (0, 0, 0, 0, 0, 0);
// systemd owns credential delivery and does not hand the file to the
// workload by ownership: it writes it root:root with no group or
// world access and grants the workload's (dynamic) uid read through
// a POSIX ACL. Observed on systemd 257:
//
// -r--r-----+ 1 0 0 user::r-- user:<workload uid>:r--
// group::--- mask::r-- other::---
//
// The previous assertion required uid/gid to equal the workload's
// and mode to be exactly 0400, a shape systemd never produces, so
// it failed for every delivered credential. What is verified here is
// what the file mode can carry: root-owned, unaliased, and closed to
// group and world. The workload-only grant itself is the ACL's, and
// is systemd's to enforce rather than ours to re-derive.
ensure!(
metadata.is_file()
&& metadata.len() > 0
&& links == 1
&& uid == 0
&& mode & 0o007 == 0,
"delivered service credential is not root-owned and closed to group and world"
);
service_credentials.push(ServiceCredentialObservation {
environment_name: name.clone(),
delivered_path,
device,
inode,
uid,
gid,
mode,
size: metadata.len(),
sha256: sha256_reader(&mut file)?,
});
}
let final_values = self
.show_unit(unit)?
.with_context(|| format!("systemd unit {unit} vanished during observation"))?;
ensure!(
final_values == unit_observation
&& linux_process_start_time(&process_root.join("stat"))? == process_start_time,
"systemd workload identity changed during native observation"
);
Ok(SystemdWorkloadObservation {
unit: unit.to_owned(),
unit_description,
invocation_id,
exec_main_start_timestamp_monotonic,
service_type,
restart_policy,
kill_mode,
dynamic_user,
systemd_user,
systemd_group,
supplementary_groups,
capability_bounding_set,
ambient_capabilities,
private_mounts,
private_pids,
protect_proc,
proc_subset,
no_new_privileges,
umask,
inaccessible_paths,
load_credential,
main_pid,
process_start_time,
process_uids: process_security.uids,
process_gids: process_security.gids,
process_groups: process_security.groups,
process_cap_inheritable: process_security.cap_inheritable,
process_cap_permitted: process_security.cap_permitted,
process_cap_effective: process_security.cap_effective,
process_cap_bounding: process_security.cap_bounding,
process_cap_ambient: process_security.cap_ambient,
process_no_new_privileges: process_security.no_new_privileges,
process_namespace_pids: process_security.namespace_pids,
mount_namespace_id,
pid_namespace_id,
executable,
executable_device,
executable_inode,
executable_sha256,
runtime_instance_id: runtime_instance_id.to_owned(),
working_directory,
runtime_bundle,
command_line_sha256: sha256_id(&command_line),
environment_names: environment_names.to_vec(),
environment_contract_sha256: sha256_id(&rmp_serde::to_vec(&selected_environment)?),
control_group,
credentials_directory,
parent_only_file_descriptors: parent_only_file_descriptors.to_vec(),
activation_signer_identity_id: activation_signer_identity_id.to_owned(),
activation_signer_public_key: activation_signer_public_key.to_vec(),
service_credentials,
})
}
fn launch_command(
&self,
declaration: &TargetDeclaration,
binding: &OperatorBinding,
installed: &Path,
) -> Result<Vec<OsString>> {
let executable_artifact =
release_artifact(declaration, &declaration.service.executable_artifact)?;
let executable = installed.join(&executable_artifact.destination);
ensure!(executable.is_file(), "sealed service executable is absent");
let mut command = vec![executable.into_os_string()];
for argument in &declaration.service.arguments {
command.push(match argument {
LaunchArgument::Literal { value } => value.into(),
LaunchArgument::Binding { name } => binding.workload.systemd()?.argument_bindings
[name]
.clone()
.into(),
});
}
Ok(command)
}
fn launch_environment(
&self,
binding: &OperatorBinding,
bundle: &Path,
expected: &IdunnExpectedIncarnationRecord,
unit: &str,
) -> Result<BTreeMap<String, String>> {
let mut environment = binding.workload.systemd()?.environment.clone();
let credentials_directory = Path::new("/run/credentials").join(unit);
for name in binding
.workload
.systemd()?
.secret_files
.keys()
.filter(|name| name.as_str() != RUNTIME_PRESENCE_IDENTITY_BINDING)
{
ensure!(
environment
.insert(
name.clone(),
credentials_directory.join(name).display().to_string(),
)
.is_none(),
"workload environment and secret bindings collide"
);
}
ensure!(
environment
.insert(
IDUNN_RUNTIME_BUNDLE_ENVIRONMENT.into(),
bundle.display().to_string(),
)
.is_none(),
"operator binding attempts to replace the Idunn runtime bundle"
);
match &expected.route {
Some(route) => {
let (host, port) = endpoint_host_port(
&route.candidate_endpoint,
&format!("{}://", route.transport),
)?;
ensure!(
environment
.insert(
IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT.into(),
format!("{host}:{port}"),
)
.is_none(),
"operator binding attempts to replace the Idunn candidate bind"
);
let member_port = binding
.route
.as_ref()
.map(|binding_route| binding_route.member_candidate_port(port))
.transpose()?
.flatten();
match member_port {
Some(member_port) => ensure!(
environment
.insert(
IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT.into(),
format!("{host}:{member_port}"),
)
.is_none(),
"operator binding attempts to replace the Idunn member bind"
),
None => ensure!(
!environment.contains_key(IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT),
"workload without a member listener carries an Idunn member bind"
),
}
}
None => ensure!(
!environment.contains_key(IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT)
&& !environment.contains_key(IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT),
"unrouted workload carries an Idunn route bind"
),
}
match &binding.process_write_lease {
Some(write_lease) => ensure!(
environment
.insert(
IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT.into(),
write_lease.record_path.display().to_string(),
)
.is_none(),
"operator binding attempts to replace the Idunn process write lease"
),
None => ensure!(
!environment.contains_key(IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT),
"stateless workload carries an Idunn process write lease"
),
}
Ok(environment)
}
fn validate_launch_observation(
&self,
observation: &SystemdWorkloadObservation,
declaration: &TargetDeclaration,
binding: &OperatorBinding,
installed: &Path,
bundle: &Path,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> Result<()> {
let command = self.launch_command(declaration, binding, installed)?;
let environment = self.launch_environment(binding, bundle, expected, &observation.unit)?;
let environment_names = environment.keys().cloned().collect::<Vec<_>>();
let expected_description = format!(
"Idunn {} {}",
binding.target,
bundle
.file_name()
.context("runtime bundle has no instance id")?
.to_string_lossy()
);
ensure!(
observation.unit.ends_with(".service"),
"workload unit has no service suffix"
);
let expected_group = binding.workload.systemd()?.state_group.as_deref();
let expected_group_id = expected_group.map(resolve_group_id).transpose()?;
let regular_credential_names = binding
.workload
.systemd()?
.secret_files
.keys()
.filter(|name| name.as_str() != RUNTIME_PRESENCE_IDENTITY_BINDING)
.cloned()
.collect::<Vec<_>>();
let expected_credentials_directory = (!regular_credential_names.is_empty())
.then(|| Path::new("/run/credentials").join(&observation.unit));
let descriptor_properties =
parent_only_open_file_properties(&observation.parent_only_file_descriptors)?;
let activation_source = self.activation_credential_source(activation)?;
let presence_source = binding
.workload
.systemd()?
.secret_files
.get(RUNTIME_PRESENCE_IDENTITY_BINDING)
.context("workload has no parent-only runtime presence identity source")?;
let observed_credential_names = observation
.service_credentials
.iter()
.map(|credential| credential.environment_name.clone())
.collect::<Vec<_>>();
// Checked one clause at a time so a mismatch names itself. As a single
// conjunction this reported only "the launch contract differs", leaving
// an operator to bisect thirty conditions by hand against a workload
// that has already exited.
//
// `systemd_user` is deliberately absent: systemd reports the identity it
// allocated for a DynamicUser unit, so it is never empty for a running
// workload. The same applies to `Group` when no fixed state group is
// bound. In that case the numeric primary GID must match the allocated
// UID; a fixed state group still requires both its exact name and GID.
let expected_load_credential = if regular_credential_names.is_empty() {
""
} else {
"[unprintable]"
};
let launch_contract: [(bool, &str); 27] = [
(
observation.unit_description == expected_description,
"unit description",
),
(observation.service_type == "exec", "service type"),
(observation.restart_policy == "no", "restart policy"),
(observation.kill_mode == "mixed", "kill mode"),
(observation.dynamic_user, "dynamic user"),
(
systemd_group_matches_launch_contract(
expected_group.zip(expected_group_id),
&observation.systemd_group,
observation.process_uids[0],
observation.process_gids[0],
),
"systemd group",
),
(
observation.supplementary_groups.is_empty(),
"supplementary groups",
),
(
observation.capability_bounding_set.is_empty(),
"capability bounding set",
),
(
observation.ambient_capabilities.is_empty(),
"ambient capabilities",
),
(observation.private_mounts, "private mounts"),
(observation.private_pids, "private PIDs"),
(observation.protect_proc == "invisible", "protect proc"),
(observation.proc_subset == "all", "proc subset"),
(observation.no_new_privileges, "no new privileges"),
(observation.umask == "0007", "umask"),
(
observation.inaccessible_paths == self.credential_root.display().to_string(),
"inaccessible paths",
),
(
observation.load_credential == expected_load_credential,
"load credential",
),
(
observation.working_directory == installed,
"working directory",
),
(observation.runtime_bundle == bundle, "runtime bundle"),
(
observation.credentials_directory == expected_credentials_directory,
"credentials directory",
),
(
observation.command_line_sha256 == sha256_id(&proc_command_line(&command)),
"command line",
),
(
observation.environment_names == environment_names,
"environment names",
),
(
observation.environment_contract_sha256
== sha256_id(&rmp_serde::to_vec(&environment)?),
"environment contract",
),
(descriptor_properties.len() == 2, "descriptor count"),
(
observation.parent_only_file_descriptors[0].source_path == activation_source
&& observation.parent_only_file_descriptors[0].size == 32
&& observation.parent_only_file_descriptors[1].source_path == *presence_source,
"parent-only descriptors",
),
(
observation.activation_signer_identity_id
== activation.activation_signer_identity_id
&& observation.activation_signer_public_key
== activation.activation_signer_public_key,
"activation signer",
),
(
observed_credential_names == regular_credential_names,
"credential names",
),
];
for (holds, clause) in launch_contract {
ensure!(
holds,
"running workload launch contract differs from the admitted launch: {clause}"
);
}
ensure!(
Path::new(&observation.control_group).file_name()
== Some(OsStr::new(&observation.unit)),
"running workload control group belongs to another unit"
);
Ok(())
}
fn validate_writable_bindings(&self, binding: &OperatorBinding) -> Result<()> {
let Some(state_group) = binding.workload.systemd()?.state_group.as_deref() else {
ensure!(
binding.workload.systemd()?.state_root.is_none()
&& binding.workload.systemd()?.read_write_paths.is_empty(),
"dynamic workload writable paths have no fixed state group"
);
return Ok(());
};
let state_group_id = resolve_group_id(state_group)?;
if let Some(state_root) = &binding.workload.systemd()?.state_root {
validate_workload_writable_path(state_root, state_group_id, true)?;
}
for path in &binding.workload.systemd()?.read_write_paths {
validate_workload_writable_path(path, state_group_id, false)?;
}
Ok(())
}
fn start_transient(
&self,
declaration: &TargetDeclaration,
binding: &OperatorBinding,
installed: &Path,
bundle: &Path,
expected: &IdunnExpectedIncarnationRecord,
parent_only_file_descriptors: &[ParentOnlyFileDescriptorObservation],
unit: &str,
) -> Result<()> {
ensure!(
declaration
.service
.required_environment
.contains(IDUNN_RUNTIME_BUNDLE_ENVIRONMENT),
"target does not declare the standard Idunn runtime bundle"
);
ensure!(
!binding
.workload
.systemd()?
.environment
.contains_key(IDUNN_RUNTIME_BUNDLE_ENVIRONMENT)
&& !binding
.workload
.systemd()?
.secret_files
.contains_key(IDUNN_RUNTIME_BUNDLE_ENVIRONMENT),
"operator binding attempts to replace the Idunn runtime bundle"
);
validate_service_credential_sources(&binding.workload.systemd()?.secret_files)?;
let parent_only_open_files =
parent_only_open_file_properties(parent_only_file_descriptors)?;
let environment = self.launch_environment(binding, bundle, expected, unit)?;
let executable_artifact =
release_artifact(declaration, &declaration.service.executable_artifact)?;
let executable = installed.join(&executable_artifact.destination);
ensure!(executable.is_file(), "sealed service executable is absent");
let unit_description = format!(
"Idunn {} {}",
binding.target,
bundle
.file_name()
.context("runtime bundle has no instance id")?
.to_string_lossy()
);
let mut args = vec![
OsString::from("--no-block"),
OsString::from("--no-ask-password"),
OsString::from("--expand-environment=no"),
OsString::from(format!("--unit={unit}")),
OsString::from("--property=Type=exec"),
OsString::from("--property=Restart=no"),
OsString::from("--property=KillMode=mixed"),
OsString::from("--property=DynamicUser=yes"),
OsString::from("--property=SupplementaryGroups="),
OsString::from("--property=CapabilityBoundingSet="),
OsString::from("--property=AmbientCapabilities="),
OsString::from("--property=NoNewPrivileges=yes"),
OsString::from("--property=PrivateMounts=yes"),
OsString::from("--property=PrivatePIDs=yes"),
OsString::from("--property=ProtectProc=invisible"),
OsString::from("--property=ProcSubset=all"),
OsString::from("--property=PrivateTmp=yes"),
OsString::from("--property=ProtectSystem=strict"),
OsString::from("--property=ProtectHome=yes"),
OsString::from("--property=ProtectControlGroups=yes"),
OsString::from("--property=ProtectKernelModules=yes"),
OsString::from("--property=ProtectKernelTunables=yes"),
OsString::from("--property=RestrictSUIDSGID=yes"),
OsString::from("--property=LockPersonality=yes"),
OsString::from("--property=UMask=0007"),
OsString::from(format!("--property=Description={unit_description}")),
OsString::from(format!(
"--property=InaccessiblePaths={}",
self.credential_root.display()
)),
OsString::from(format!(
"--property=MemoryMax={}M",
binding.workload.systemd()?.memory_mebibytes
)),
OsString::from(format!(
"--property=CPUQuota={}%",
binding.workload.systemd()?.cpu_quota_percent
)),
OsString::from(format!("--working-directory={}", installed.display())),
OsString::from(format!("--property=ReadOnlyPaths={}", installed.display())),
OsString::from(format!("--property=ReadOnlyPaths={}", bundle.display())),
];
for open_file in parent_only_open_files {
args.push(OsString::from(format!("--property=OpenFile={open_file}")));
}
if let Some(state_group) = &binding.workload.systemd()?.state_group {
args.push(OsString::from(format!("--property=Group={state_group}")));
}
if binding.workload.systemd()?.network == WorkloadNetwork::None {
args.push(OsString::from("--property=PrivateNetwork=yes"));
}
if let Some(state_root) = &binding.workload.systemd()?.state_root {
args.push(OsString::from(format!(
"--property=ReadWritePaths={}",
state_root.display()
)));
}
if let Some(write_lease) = &binding.process_write_lease {
for path in [write_lease.record_path.clone(), write_lease.lock_path()] {
args.push(OsString::from(format!(
"--property=ReadOnlyPaths=-{}",
path.display()
)));
}
}
for path in &binding.workload.systemd()?.read_only_paths {
args.push(OsString::from(format!(
"--property=ReadOnlyPaths={}",
path.display()
)));
}
for path in &binding.workload.systemd()?.read_write_paths {
args.push(OsString::from(format!(
"--property=ReadWritePaths={}",
path.display()
)));
}
if binding.workload.systemd()?.devices.is_empty() {
args.push(OsString::from("--property=PrivateDevices=yes"));
} else {
args.push(OsString::from("--property=DevicePolicy=closed"));
for device in &binding.workload.systemd()?.devices {
args.push(OsString::from(format!(
"--property=DeviceAllow={} rw",
device.display()
)));
}
}
for (name, value) in environment {
args.push(OsString::from(format!("--setenv={name}={value}")));
}
for (name, path) in binding
.workload
.systemd()?
.secret_files
.iter()
.filter(|(name, _)| name.as_str() != RUNTIME_PRESENCE_IDENTITY_BINDING)
{
args.push(OsString::from(format!(
"--property=LoadCredential={name}:{}",
path.display()
)));
}
args.extend(self.launch_command(declaration, binding, installed)?);
self.command(&self.systemd_run_program, args)?;
Ok(())
}
}
impl WorkloadPort for SystemdTransientWorkloadDriver {
fn install(
&self,
plan: &CompiledDeploymentPlan,
release: &MaterializedRelease,
) -> Result<InstalledReleaseObservation> {
self.install_release(plan, release)
}
fn prepare_activation(
&self,
plan: &CompiledDeploymentPlan,
expected: &IdunnExpectedIncarnationRecord,
launch: IdunnRuntimeActivationLaunch,
) -> Result<IdunnRuntimeActivationRecord> {
plan.validate()?;
expected.validate()?;
let proposed_activation = launch.activation();
proposed_activation.validate()?;
ensure!(
expected.plan_id == plan.plan_id
&& proposed_activation.expected_projection_sha256 == expected.canonical_sha256()?,
"prepared activation does not belong to the deployment plan"
);
let (_, binding) = plan.parsed_inputs()?;
ensure!(
expected.target == binding.target,
"prepared activation target differs from the operator binding"
);
let unit = self.unit_name(
&binding.workload.systemd()?.unit_prefix,
&proposed_activation.runtime_instance_id,
)?;
ensure!(
self.show_unit(&unit)?.is_none(),
"refusing to replace prepared activation material while its deterministic unit exists"
);
ensure_activation_credential_root(&self.credential_root)?;
let source = self.activation_credential_source(proposed_activation)?;
match fs::symlink_metadata(&source) {
Ok(_) => remove_activation_credential_source(&source)
.context("retiring an orphaned pre-persistence activation credential")?,
Err(error) if error.kind() == ErrorKind::NotFound => {}
Err(error) => {
return Err(error).with_context(|| {
format!(
"inspecting pre-persistence activation credential {}",
source.display()
)
});
}
}
let (activation, written_source) = self.write_activation_credential(launch)?;
if written_source != source {
let error =
anyhow::anyhow!("prepared activation used a nondeterministic credential path");
return match remove_activation_credential_source(&written_source) {
Ok(()) => Err(error),
Err(cleanup) => Err(error.context(format!(
"deleting nondeterministic activation credential also failed: {cleanup:#}"
))),
};
}
Ok(activation)
}
fn start_prepared(
&self,
plan: &CompiledDeploymentPlan,
release: &SealedRelease,
installed: &InstalledReleaseObservation,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> Result<WorkloadObservation> {
plan.validate()?;
release.validate_against(plan)?;
self.validate_installed_release(plan, release, installed)?;
expected.validate()?;
activation.validate()?;
ensure!(
expected.plan_id == plan.plan_id
&& expected.sealed_release_id == release.sealed_release_id
&& activation.expected_projection_sha256 == expected.canonical_sha256()?,
"workload inputs do not describe one sealed incarnation"
);
let (declaration, binding) = plan.parsed_inputs()?;
self.validate_writable_bindings(&binding)?;
let executable_artifact =
release_artifact(&declaration, &declaration.service.executable_artifact)?;
let executable = installed.root.join(&executable_artifact.destination);
let unit = self.unit_name(
&binding.workload.systemd()?.unit_prefix,
&activation.runtime_instance_id,
)?;
let bundle = self.prepare_runtime_bundle(&binding, expected, activation)?;
let service_credential_names = binding
.workload
.systemd()?
.secret_files
.keys()
.filter(|name| name.as_str() != RUNTIME_PRESENCE_IDENTITY_BINDING)
.cloned()
.collect::<Vec<_>>();
let environment_names = self
.launch_environment(&binding, &bundle, expected, &unit)?
.into_keys()
.collect::<Vec<_>>();
let parent_only_file_descriptors =
self.parent_only_file_descriptors(&binding, expected, activation)?;
let unit_exists = self.show_unit(&unit)?.is_some();
if !unit_exists {
self.start_transient(
&declaration,
&binding,
&installed.root,
&bundle,
expected,
&parent_only_file_descriptors,
&unit,
)?;
}
let mut last_error = None;
for _ in 0..100 {
match self.observe_unit(
&unit,
&executable,
&activation.runtime_instance_id,
&activation.activation_signer_identity_id,
&activation.activation_signer_public_key,
&environment_names,
&service_credential_names,
&parent_only_file_descriptors,
) {
Ok(observation) => {
let validation = self.validate_launch_observation(
&observation,
&declaration,
&binding,
&installed.root,
&bundle,
expected,
&activation,
);
match validation.and_then(|()| {
ensure!(
observation.executable_sha256 == expected.artifact_sha256,
"started workload executable differs from Expected"
);
Ok(())
}) {
Ok(()) => return Ok(WorkloadObservation::Systemd(observation)),
Err(error) => last_error = Some(error),
}
}
Err(error) => last_error = Some(error),
}
thread::sleep(Duration::from_millis(50));
}
Err(last_error.unwrap_or_else(|| anyhow::anyhow!("systemd did not expose the candidate")))
}
fn discard_prepared(
&self,
plan: &CompiledDeploymentPlan,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> Result<()> {
plan.validate()?;
expected.validate()?;
activation.validate()?;
ensure!(
expected.plan_id == plan.plan_id
&& activation.expected_projection_sha256 == expected.canonical_sha256()?,
"discarded activation does not belong to the prepared deployment"
);
let (_, binding) = plan.parsed_inputs()?;
ensure!(
binding.target == expected.target,
"discarded activation target differs from its binding"
);
let unit = self.unit_name(
&binding.workload.systemd()?.unit_prefix,
&activation.runtime_instance_id,
)?;
let unit_description = format!(
"Idunn {} {}",
binding.target, activation.runtime_instance_id
);
self.stop_submitted_unit(&unit, &unit_description)
.context("stopping prepared candidate without a durable workload observation")?;
remove_activation_credential_source(&self.activation_credential_source(activation)?)
.context("deleting discarded activation credential source")
}
fn observe(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
prior: &WorkloadObservation,
) -> Result<WorkloadObservation> {
expected.validate()?;
activation.validate()?;
let prior = prior.systemd()?;
ensure!(
activation.expected_projection_sha256 == expected.canonical_sha256()?
&& activation.runtime_instance_id == prior.runtime_instance_id,
"workload observation belongs to another activation"
);
let observed = self.observe_unit(
&prior.unit,
&prior.executable,
&activation.runtime_instance_id,
&activation.activation_signer_identity_id,
&activation.activation_signer_public_key,
&prior.environment_names,
&prior
.service_credentials
.iter()
.map(|credential| credential.environment_name.clone())
.collect::<Vec<_>>(),
&prior.parent_only_file_descriptors,
)?;
let changed_fields = changed_systemd_workload_identity_fields(&observed, prior);
if !changed_fields.is_empty() {
bail!(
"native workload identity changed after observation: {}",
changed_fields.join(", ")
);
}
ensure!(
observed.executable_sha256 == expected.artifact_sha256,
"native workload executable differs from the sealed artifact"
);
let credential_source = self.activation_credential_source(activation)?;
remove_activation_credential_source(&credential_source)
.context("retiring a recovered activation credential source")?;
let after_unlink = self.observe_unit(
&prior.unit,
&prior.executable,
&activation.runtime_instance_id,
&activation.activation_signer_identity_id,
&activation.activation_signer_public_key,
&prior.environment_names,
&prior
.service_credentials
.iter()
.map(|credential| credential.environment_name.clone())
.collect::<Vec<_>>(),
&prior.parent_only_file_descriptors,
)?;
ensure!(
changed_systemd_workload_identity_fields(&after_unlink, &observed).is_empty(),
"native workload identity changed after recovered credential cleanup"
);
Ok(WorkloadObservation::Systemd(after_unlink))
}
fn is_permanently_stopped(&self, observation: &WorkloadObservation) -> Result<bool> {
let observation = observation.systemd()?;
ensure!(
observation.restart_policy == "no",
"workload unit does not carry the admitted Restart=no policy"
);
let Some(unit) = self.show_unit(&observation.unit)? else {
// systemd has forgotten the unit entirely, so nothing will start it.
return Ok(true);
};
Ok(unit.properties.get("ActiveState").map(String::as_str) == Some("failed"))
}
fn lost_to_host_reboot(
&self,
observation: &WorkloadObservation,
admitted_at_unix_millis: u64,
) -> Result<bool> {
let observation = observation.systemd()?;
if self.show_unit(&observation.unit)?.is_some() {
return Ok(false);
}
// A transient unit does not survive a reboot, so an absent unit is a
// reboot's loss only when the kernel booted after admission.
let stat = std::fs::read_to_string(self.proc_root.join("stat"))
.context("could not read the kernel boot time")?;
let btime = stat
.lines()
.find_map(|line| line.strip_prefix("btime "))
.context("the kernel boot time is missing from stat")?
.trim()
.parse::<u64>()
.context("the kernel boot time is not a number")?;
Ok(btime.saturating_mul(1000) > admitted_at_unix_millis)
}
fn stop(&self, observation: &WorkloadObservation) -> Result<()> {
let observation = observation.systemd()?;
let Some(unit_observation) = self.show_unit(&observation.unit)? else {
return Ok(());
};
let values = &unit_observation.properties;
let active = required_systemd_property(&values, "ActiveState")?;
let sub = required_systemd_property(&values, "SubState")?;
if active == "active" && sub == "running" {
let current = self.observe_unit(
&observation.unit,
&observation.executable,
&observation.runtime_instance_id,
&observation.activation_signer_identity_id,
&observation.activation_signer_public_key,
&observation.environment_names,
&observation
.service_credentials
.iter()
.map(|credential| credential.environment_name.clone())
.collect::<Vec<_>>(),
&observation.parent_only_file_descriptors,
)?;
let changed_fields =
changed_systemd_workload_identity_fields(&current, observation);
if !changed_fields.is_empty() {
bail!(
"refusing to stop a workload whose native identity changed: {}",
changed_fields.join(", ")
);
}
} else {
ensure!(
values.get("InvocationID") == Some(&observation.invocation_id)
&& values.get("Description") == Some(&observation.unit_description)
&& matches!(active, "inactive" | "failed"),
"refusing to stop a foreign or transitional systemd unit"
);
}
self.command(
&self.systemctl_program,
[OsString::from("stop"), OsString::from(&observation.unit)],
)?;
if let Some(unit_observation) = self.show_unit(&observation.unit)? {
let values = &unit_observation.properties;
ensure!(
values
.get("ActiveState")
.is_some_and(|state| state == "inactive" || state == "failed"),
"systemd unit remained active after stop"
);
}
Ok(())
}
}
/// One dedicated CultCache file is the write-lease authority for one target.
/// The admitted target holds its sibling lock shared for that process lifetime.
/// Grant and revocation therefore make one nonblocking CAS attempt; revocation
/// follows an exact incumbent stop. A foreign or stuck holder cannot freeze
/// Idunn's control loop. Route membership never enters this store.
pub struct CultCacheWriteLeaseDriver {
pub target: String,
pub record_path: PathBuf,
}
impl CultCacheWriteLeaseDriver {
pub fn new(target: impl Into<String>, record_path: impl Into<PathBuf>) -> Self {
Self {
target: target.into(),
record_path: record_path.into(),
}
}
fn current(&self) -> Result<Option<(CultCacheEnvelope, IdunnProcessWriteLeaseRecord)>> {
validate_root_authority_path(&self.record_path)?;
self.read_record()
}
/// The lease as its holder reads it: the same custody, envelope, key and
/// target validation Idunn applies, without requiring the reader to be
/// root. The holder is a service uid by construction; root is who writes.
pub(crate) fn current_for_holder(
&self,
) -> Result<Option<(CultCacheEnvelope, IdunnProcessWriteLeaseRecord)>> {
validate_authority_path_custody(&self.record_path)?;
self.read_record()
}
fn read_record(&self) -> Result<Option<(CultCacheEnvelope, IdunnProcessWriteLeaseRecord)>> {
if !self.record_path.exists() {
return Ok(None);
}
let entries = SingleFileMessagePackBackingStore::new(&self.record_path)
.pull_all_read_only_snapshot()?;
match entries.as_slice() {
[] => Ok(None),
[envelope]
if envelope.r#type == IdunnProcessWriteLeaseRecord::TYPE
&& envelope.schema_id.as_deref() == Some(IDUNN_PROCESS_WRITE_LEASE_SCHEMA) =>
{
let lease = IdunnProcessWriteLeaseRecord::decode_canonical(&envelope.payload)?;
ensure!(
envelope.key == lease.target,
"write-lease key is not its target"
);
ensure!(
lease.target == self.target,
"write-lease store belongs to another target"
);
Ok(Some((envelope.clone(), lease)))
}
_ => bail!("process write-lease store is foreign or ambiguous"),
}
}
fn envelope(&self, lease: &IdunnProcessWriteLeaseRecord) -> Result<CultCacheEnvelope> {
Ok(CultCacheEnvelope {
key: lease.target.clone(),
r#type: IdunnProcessWriteLeaseRecord::TYPE.into(),
payload: lease.canonical_bytes()?,
stored_at: rfc3339_millis(lease.issued_at_unix_millis)?,
schema_id: Some(IDUNN_PROCESS_WRITE_LEASE_SCHEMA.into()),
})
}
fn validate_grant(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
warming: &SequenceAdmittedWarming,
lease: &IdunnProcessWriteLeaseRecord,
) -> Result<()> {
expected.validate()?;
activation.validate()?;
lease.validate()?;
ensure!(
self.target == expected.target,
"process write lease does not bind the exact warming candidate"
);
ensure!(
warming.runtime_instance_id() == activation.runtime_instance_id.as_str(),
"process write lease does not bind the exact warming candidate"
);
lease_binds_candidate(
expected,
activation,
lease,
std::slice::from_ref(&warming.signed_presence_sha256().to_owned()),
)
}
}
/// The one rule for what makes a process write lease this incarnation's: it
/// binds this Expected, this activation and one of the given provider-signed
/// Warming proofs. Idunn grants with it and the lease holder accepts with it.
pub(crate) fn lease_binds_candidate(
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
lease: &IdunnProcessWriteLeaseRecord,
warming_presence_sha256es: &[String],
) -> Result<()> {
let expected_sha256 = expected.canonical_sha256()?;
let activation_sha256 = activation.canonical_sha256()?;
ensure!(
lease.target == expected.target
&& lease.expected_projection_sha256 == expected_sha256
&& lease.plan_id == expected.plan_id
&& lease.incarnation_id == expected.incarnation_id
&& lease.sealed_release_id == expected.sealed_release_id
&& lease.activation_witness_sha256 == activation_sha256
&& lease.runtime_id == expected.runtime_id
&& lease.runtime_instance_id == activation.runtime_instance_id
&& lease.state_schema_generation
== expected
.state_schema_generation
.as_deref()
.context("write-lease Expected has no state generation")?
&& lease.state_contract_sha256
== expected
.state_contract_sha256
.as_deref()
.context("write-lease Expected has no state contract")?
&& warming_presence_sha256es
.iter()
.any(|sha256| sha256 == &lease.warming_presence_sha256),
"process write lease does not bind the exact warming candidate"
);
Ok(())
}
impl WriteLeasePort for CultCacheWriteLeaseDriver {
fn revoke_exact(&self, incumbent: Option<&IdunnProcessWriteLeaseRecord>) -> Result<()> {
if let Some(incumbent) = incumbent {
incumbent.validate()?;
ensure!(
incumbent.target == self.target,
"incumbent write lease belongs to another target"
);
}
let current = self.current()?;
let Some((envelope, current_lease)) = current else {
return Ok(());
};
ensure!(
incumbent == Some(&current_lease),
"refusing to revoke an unexpected process write lease"
);
let store = SingleFileMessagePackBackingStore::new(&self.record_path);
match store.try_compare_exchange_snapshot(std::slice::from_ref(&envelope), &[])? {
TryCompareExchangeSnapshotOutcome::Exchanged => Ok(()),
TryCompareExchangeSnapshotOutcome::Mismatch => {
bail!("process write lease changed while fencing the incumbent")
}
TryCompareExchangeSnapshotOutcome::LockContended => {
bail!("process write lease is held by a shared-lock consumer")
}
}
}
fn observe_empty(&self) -> Result<bool> {
Ok(self.current()?.is_none())
}
fn observe_exact(&self, lease: &IdunnProcessWriteLeaseRecord) -> Result<bool> {
lease.validate()?;
ensure!(
lease.target == self.target,
"observed process write lease belongs to another target"
);
Ok(self
.current()?
.is_some_and(|(_, current)| current == *lease))
}
fn grant(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
warming: &SequenceAdmittedWarming,
lease: &IdunnProcessWriteLeaseRecord,
) -> Result<String> {
self.validate_grant(expected, activation, warming, lease)?;
if let Some((_, current)) = self.current()? {
ensure!(
current == *lease,
"another process already owns the write lease"
);
harden_root_authority_file(&self.record_path)?;
harden_root_authority_file(&authority_lock_path(&self.record_path))?;
return lease.canonical_sha256();
}
let envelope = self.envelope(lease)?;
let store = SingleFileMessagePackBackingStore::new(&self.record_path);
match store.try_compare_exchange_snapshot(&[], &[envelope])? {
TryCompareExchangeSnapshotOutcome::Exchanged => {}
TryCompareExchangeSnapshotOutcome::Mismatch => {
bail!("process write-lease grant lost its empty-store compare-exchange")
}
TryCompareExchangeSnapshotOutcome::LockContended => {
bail!("process write lease is held by a shared-lock consumer")
}
}
harden_root_authority_file(&self.record_path)?;
harden_root_authority_file(&authority_lock_path(&self.record_path))?;
lease.canonical_sha256()
}
fn observe(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
warming: &SequenceAdmittedWarming,
lease: &IdunnProcessWriteLeaseRecord,
) -> Result<bool> {
self.validate_grant(expected, activation, warming, lease)?;
Ok(self
.current()?
.is_some_and(|(_, current)| current == *lease))
}
}
/// Idunn projects desired identity plus its own activation and current-lease
/// facts here. Service presence is absent by construction; only Odin may
/// correlate these records with signed runtime observation into Present/Ready.
///
/// The store is published 0644 and read lock-free by Odin and Idunn; its lock
/// is Idunn's alone (`exchange_behind_private_lock`), so no other uid can hold
/// it and stall a publish.
///
/// Every record is keyed by the incarnation it describes, never by the target
/// alone. A target being replaced has two incarnations at once -- the admitted
/// incumbent and the sealed candidate -- and both are projected side by side.
/// Publishing the candidate therefore cannot touch what the incumbent is; the
/// one target-keyed record is the runtime presence trust anchor, which is
/// provider lookup material and the same for every incarnation.
pub struct CultCacheTopologyDriver {
pub projection_store: PathBuf,
pub correlation_store: PathBuf,
}
/// The key one incarnation's projection records live under.
///
/// This is a contract with Odin's reader (`IncarnationRef::key` in
/// `odin-daemon`): `{target}@{expected projection sha256}`.
pub fn incarnation_key(expected: &IdunnExpectedIncarnationRecord) -> Result<String> {
Ok(incarnation_key_of(
&expected.target,
&expected.canonical_sha256()?,
))
}
pub fn incarnation_key_of(target: &str, expected_sha256: &str) -> String {
format!("{target}@{expected_sha256}")
}
/// The three record types that belong to one incarnation.
fn is_incarnation_record_type(record_type: &str) -> bool {
matches!(
record_type,
IdunnExpectedIncarnationRecord::TYPE
| IdunnRuntimeActivationRecord::TYPE
| IdunnProcessWriteLeaseRecord::TYPE
)
}
/// One incarnation's records as currently projected, each checked to be the
/// exact document the caller names. A record present under the key but
/// differing from the caller's is a substitution and is refused.
struct ProjectedIncarnation {
expected: Option<CultCacheEnvelope>,
activation: Option<CultCacheEnvelope>,
lease: Option<CultCacheEnvelope>,
}
impl ProjectedIncarnation {
fn read(
entries: &[CultCacheEnvelope],
expected: &IdunnExpectedIncarnationRecord,
activation: Option<&IdunnRuntimeActivationRecord>,
lease: Option<&IdunnProcessWriteLeaseRecord>,
) -> Result<Self> {
let key = incarnation_key(expected)?;
let projected_expected =
projection_entry(entries, IdunnExpectedIncarnationRecord::TYPE, &key)?;
if let Some(envelope) = projected_expected {
ensure!(
envelope.schema_id.as_deref() == Some(IDUNN_EXPECTED_INCARNATION_SCHEMA)
&& IdunnExpectedIncarnationRecord::decode_canonical(&envelope.payload)?
== *expected,
"projected Expected under this incarnation key is substituted"
);
}
let projected_activation =
projection_entry(entries, IdunnRuntimeActivationRecord::TYPE, &key)?;
if let Some(envelope) = projected_activation {
let current = IdunnRuntimeActivationRecord::decode_canonical(&envelope.payload)?;
ensure!(
envelope.schema_id.as_deref() == Some(IDUNN_RUNTIME_ACTIVATION_SCHEMA)
&& activation == Some(&current),
"projected activation under this incarnation key is substituted"
);
}
let projected_lease = projection_entry(entries, IdunnProcessWriteLeaseRecord::TYPE, &key)?;
if let Some(envelope) = projected_lease {
let current = IdunnProcessWriteLeaseRecord::decode_canonical(&envelope.payload)?;
ensure!(
envelope.schema_id.as_deref() == Some(IDUNN_PROCESS_WRITE_LEASE_SCHEMA)
&& lease == Some(&current),
"projected write lease under this incarnation key is substituted"
);
}
Ok(Self {
expected: projected_expected.cloned(),
activation: projected_activation.cloned(),
lease: projected_lease.cloned(),
})
}
}
fn expected_envelope(expected: &IdunnExpectedIncarnationRecord) -> Result<CultCacheEnvelope> {
Ok(CultCacheEnvelope {
key: incarnation_key(expected)?,
r#type: IdunnExpectedIncarnationRecord::TYPE.into(),
payload: expected.canonical_bytes()?,
stored_at: chrono::Utc::now().to_rfc3339(),
schema_id: Some(IDUNN_EXPECTED_INCARNATION_SCHEMA.into()),
})
}
fn anchor_envelope(anchor: &GameCultServiceTrustAnchorRecord) -> Result<CultCacheEnvelope> {
Ok(CultCacheEnvelope {
key: anchor.trust_anchor_id.clone(),
r#type: GameCultServiceTrustAnchorRecord::TYPE.into(),
payload: rmp_serde::to_vec(anchor)?,
stored_at: rfc3339_millis(anchor.bound_at_unix_millis)?,
schema_id: Some(GAMECULT_SERVICE_TRUST_ANCHOR_SCHEMA.into()),
})
}
fn activation_envelope(
key: &str,
activation: &IdunnRuntimeActivationRecord,
) -> Result<CultCacheEnvelope> {
Ok(CultCacheEnvelope {
key: key.to_owned(),
r#type: IdunnRuntimeActivationRecord::TYPE.into(),
payload: activation.canonical_bytes()?,
stored_at: rfc3339_millis(activation.issued_at_unix_millis)?,
schema_id: Some(IDUNN_RUNTIME_ACTIVATION_SCHEMA.into()),
})
}
/// Same-content envelopes compare equal regardless of `stored_at`, which is
/// a publication timestamp and not part of the record's identity.
fn same_record(current: &CultCacheEnvelope, replacement: &CultCacheEnvelope) -> bool {
current.key == replacement.key
&& current.r#type == replacement.r#type
&& current.schema_id == replacement.schema_id
&& current.payload == replacement.payload
}
impl CultCacheTopologyDriver {
fn snapshot(&self) -> Result<Vec<CultCacheEnvelope>> {
if !self.projection_store.exists() {
return Ok(Vec::new());
}
SingleFileMessagePackBackingStore::new(&self.projection_store).pull_all_read_only_snapshot()
}
/// Apply one whole-snapshot mutation with compare-and-swap. `mutate`
/// returns `None` when the snapshot already has the shape it wants, and
/// the replacement set otherwise; every writer below is one of these.
///
/// Never waits: a publish is done exactly when this returns `Ok`. Only a
/// lost race (the store moved on) is retried at once; a held lock is an
/// error and a no-op, retried by the caller's own retry.
fn mutate<F>(&self, mutate: F) -> Result<()>
where
F: Fn(&[CultCacheEnvelope]) -> Result<Option<Vec<CultCacheEnvelope>>>,
{
for _ in 0..8 {
let entries = self.snapshot()?;
let Some(replacement) = mutate(&entries)? else {
return Ok(());
};
match exchange_behind_private_lock(&self.projection_store, &entries, &replacement)? {
TryCompareExchangeSnapshotOutcome::Exchanged => {
publish_file_mode(&self.projection_store)?;
return Ok(());
}
TryCompareExchangeSnapshotOutcome::Mismatch => {}
TryCompareExchangeSnapshotOutcome::LockContended => {
return Err(LockContended { store: "topology projection" }.into());
}
}
}
bail!("CultCache projection changed repeatedly during publication")
}
fn other_incarnations_of(entries: &[CultCacheEnvelope], target: &str, key: &str) -> bool {
entries.iter().any(|envelope| {
envelope.r#type == IdunnExpectedIncarnationRecord::TYPE
&& envelope.key != key
&& envelope
.key
.split_once('@')
.is_some_and(|(owner, _)| owner == target)
})
}
/// Records this contract does not own: the three incarnation types keyed by
/// the bare target, as the previous single-slot projection wrote them. A
/// publish for that target retires them; nothing reads them.
fn is_legacy_slot_record(envelope: &CultCacheEnvelope, target: &str) -> bool {
is_incarnation_record_type(&envelope.r#type) && envelope.key == target
}
/// Demote one admitted incarnation to Expected-only. Its activation and
/// write lease are withdrawn; its Expected and anchor are ensured present.
/// This is for an incarnation whose process is gone -- continuity about
/// to restart it, or an abort that already fenced and stopped it -- and
/// says nothing about any other incarnation of the target.
pub fn demote_to_expected_only(
&self,
expected: &IdunnExpectedIncarnationRecord,
provider_anchor: &ServiceIdentityTrustAnchor,
activation: &IdunnRuntimeActivationRecord,
lease: Option<&IdunnProcessWriteLeaseRecord>,
) -> Result<String> {
expected.validate()?;
activation.validate()?;
ensure!(
activation.expected_projection_sha256 == expected.canonical_sha256()?,
"admitted activation does not bind the admitted Expected projection"
);
if let Some(lease) = lease {
validate_topology_lease(expected, activation, lease)?;
}
let anchor = runtime_presence_trust_anchor(expected, provider_anchor)?;
let key = incarnation_key(expected)?;
self.mutate(|entries| {
let projected = ProjectedIncarnation::read(entries, expected, Some(activation), lease)?;
let current_anchor = projection_entry(
entries,
GameCultServiceTrustAnchorRecord::TYPE,
&anchor.trust_anchor_id,
)?;
if let Some(envelope) = current_anchor {
ensure!(
service_trust_anchor_from_envelope(envelope)? == anchor,
"refusing to replace an unknown runtime presence trust anchor"
);
}
if projected.expected.is_some()
&& current_anchor.is_some()
&& projected.activation.is_none()
&& projected.lease.is_none()
{
return Ok(None);
}
let mut replacement = entries
.iter()
.filter(|envelope| {
!(envelope.key == key
|| (envelope.r#type == GameCultServiceTrustAnchorRecord::TYPE
&& envelope.key == anchor.trust_anchor_id))
})
.cloned()
.collect::<Vec<_>>();
replacement.push(match projected.expected {
Some(envelope) => envelope,
None => expected_envelope(expected)?,
});
replacement.push(match current_anchor {
Some(envelope) => envelope.clone(),
None => anchor_envelope(&anchor)?,
});
Ok(Some(replacement))
})?;
expected.canonical_sha256()
}
/// Whether the projection currently names an activation for this
/// incarnation. Continuity asks so it can tell a projection that still
/// describes a dead incarnation from one already demoted to Expected-only.
pub fn projected_activation_is_present(
&self,
expected: &IdunnExpectedIncarnationRecord,
) -> Result<bool> {
let key = incarnation_key(expected)?;
Ok(
projection_entry(&self.snapshot()?, IdunnRuntimeActivationRecord::TYPE, &key)?
.is_some(),
)
}
/// The activation the projection currently names under this incarnation's
/// key, if any. Read-only: it never judges whether the activation is the
/// right one, so a caller can tell an exact issuer from a substitute
/// before it demotes anything.
pub fn projected_activation(
&self,
expected: &IdunnExpectedIncarnationRecord,
) -> Result<Option<IdunnRuntimeActivationRecord>> {
let key = incarnation_key(expected)?;
let entries = self.snapshot()?;
projection_entry(&entries, IdunnRuntimeActivationRecord::TYPE, &key)?
.map(|envelope| IdunnRuntimeActivationRecord::decode_canonical(&envelope.payload))
.transpose()
.map_err(Into::into)
}
/// Every incarnation currently projected for one target, by its Expected.
pub fn projected_incarnations(
&self,
target: &str,
) -> Result<Vec<IdunnExpectedIncarnationRecord>> {
let mut incarnations = Vec::new();
for envelope in self.snapshot()? {
if envelope.r#type != IdunnExpectedIncarnationRecord::TYPE {
continue;
}
let Some((owner, _)) = envelope.key.split_once('@') else {
continue;
};
if owner != target {
continue;
}
if envelope.schema_id.as_deref() != Some(IDUNN_EXPECTED_INCARNATION_SCHEMA) {
continue;
}
let expected = IdunnExpectedIncarnationRecord::decode_canonical(&envelope.payload)?;
if incarnation_key(&expected)? == envelope.key {
incarnations.push(expected);
}
}
Ok(incarnations)
}
/// Remove everything projected under one incarnation that nothing owns any
/// more: not the admitted generation, not a live transaction. The caller
/// has established that; no record under the key is compared, because a
/// stale incarnation is stale whatever it carries. The target's anchor
/// stays while any other incarnation of the target remains.
pub fn withdraw_stale_incarnation(
&self,
expected: &IdunnExpectedIncarnationRecord,
provider_anchor: &ServiceIdentityTrustAnchor,
) -> Result<()> {
expected.validate()?;
let anchor = runtime_presence_trust_anchor(expected, provider_anchor)?;
let key = incarnation_key(expected)?;
self.mutate(|entries| {
if !entries.iter().any(|envelope| envelope.key == key) {
return Ok(None);
}
let anchor_stays = Self::other_incarnations_of(entries, &expected.target, &key);
Ok(Some(
entries
.iter()
.filter(|envelope| {
!(envelope.key == key
|| (!anchor_stays
&& envelope.r#type == GameCultServiceTrustAnchorRecord::TYPE
&& envelope.key == anchor.trust_anchor_id))
})
.cloned()
.collect(),
))
})
}
/// Whether the projection already carries this Expected and its anchor.
/// Deliberately says nothing about the activation.
pub fn admitted_expected_projection_is_exact(
&self,
expected: &IdunnExpectedIncarnationRecord,
provider_anchor: &ServiceIdentityTrustAnchor,
) -> Result<bool> {
expected.validate()?;
let entries = self.snapshot()?;
let anchor = runtime_presence_trust_anchor(expected, provider_anchor)?;
let key = incarnation_key(expected)?;
let expected_is_exact =
match projection_entry(&entries, IdunnExpectedIncarnationRecord::TYPE, &key)? {
Some(envelope) => {
envelope.schema_id.as_deref() == Some(IDUNN_EXPECTED_INCARNATION_SCHEMA)
&& IdunnExpectedIncarnationRecord::decode_canonical(&envelope.payload)?
== *expected
}
None => false,
};
let anchor_is_exact = match projection_entry(
&entries,
GameCultServiceTrustAnchorRecord::TYPE,
&anchor.trust_anchor_id,
)? {
Some(envelope) => service_trust_anchor_from_envelope(envelope)? == anchor,
None => false,
};
Ok(expected_is_exact && anchor_is_exact)
}
pub fn admitted_runtime_projection_is_exact(
&self,
expected: &IdunnExpectedIncarnationRecord,
provider_anchor: &ServiceIdentityTrustAnchor,
activation: &IdunnRuntimeActivationRecord,
lease: Option<&IdunnProcessWriteLeaseRecord>,
) -> Result<bool> {
expected.validate()?;
activation.validate()?;
ensure!(
activation.expected_projection_sha256 == expected.canonical_sha256()?,
"admitted activation does not bind its Expected projection"
);
if let Some(lease) = lease {
validate_topology_lease(expected, activation, lease)?;
}
let entries = self.snapshot()?;
let anchor = runtime_presence_trust_anchor(expected, provider_anchor)?;
let projected = ProjectedIncarnation::read(&entries, expected, Some(activation), lease)
.context("admitted runtime projection was replaced")?;
let anchor_is_exact = match projection_entry(
&entries,
GameCultServiceTrustAnchorRecord::TYPE,
&anchor.trust_anchor_id,
)? {
Some(envelope) => {
ensure!(
service_trust_anchor_from_envelope(envelope)? == anchor,
"admitted runtime presence trust anchor was replaced"
);
true
}
None => false,
};
Ok(projected.expected.is_some()
&& anchor_is_exact
&& projected.activation.is_some()
&& projected.lease.is_some() == lease.is_some())
}
}
fn runtime_presence_trust_anchor(
expected: &IdunnExpectedIncarnationRecord,
provider_anchor: &ServiceIdentityTrustAnchor,
) -> Result<GameCultServiceTrustAnchorRecord> {
expected.validate()?;
ensure!(
provider_anchor.schema_version
== <GameCultProviderHealthIdentity as ServiceIdentityProfile>::TRUST_ANCHOR_SCHEMA,
"runtime presence trust anchor schema is unsupported"
);
ensure!(
derive_service_identity_id::<GameCultProviderHealthIdentity>(&provider_anchor.public_key)?
== provider_anchor.identity_id
&& provider_anchor.identity_id == expected.expected_signer_identity_id,
"runtime presence trust anchor does not bind the Expected signer"
);
let bound_at_unix_millis: u64 =
chrono::DateTime::parse_from_rfc3339(&provider_anchor.identity_created_at)?
.timestamp_millis()
.try_into()
.context("runtime presence trust anchor creation time predates Unix epoch")?;
let anchor = GameCultServiceTrustAnchorRecord {
schema_version: GAMECULT_SERVICE_TRUST_ANCHOR_SCHEMA.into(),
trust_anchor_id: runtime_presence_trust_anchor_id(&expected.target),
service_id: expected.target.clone(),
runtime_id: expected.runtime_id.clone(),
signer_identity_id: provider_anchor.identity_id.clone(),
signer_public_key: provider_anchor.public_key.clone(),
signature_algorithm: "ed25519".into(),
signing_purpose: GAMECULT_RUNTIME_PRESENCE_HEALTH_SIGNING_PURPOSE.into(),
signed_schema: GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA.into(),
binding_authority: "root".into(),
bound_at_unix_millis,
expires_at_unix_millis: None,
private_state_exposed: false,
};
anchor.validate()?;
Ok(anchor)
}
fn runtime_presence_trust_anchor_id(target: &str) -> String {
format!("root/{target}/runtime-presence")
}
fn validate_topology_lease(
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
lease: &IdunnProcessWriteLeaseRecord,
) -> Result<()> {
expected.validate()?;
activation.validate()?;
lease.validate()?;
ensure!(
expected.write_lease_required
&& lease.target == expected.target
&& lease.expected_projection_sha256 == expected.canonical_sha256()?
&& lease.plan_id == expected.plan_id
&& lease.incarnation_id == expected.incarnation_id
&& lease.sealed_release_id == expected.sealed_release_id
&& lease.activation_witness_sha256 == activation.canonical_sha256()?
&& lease.state_schema_generation
== expected
.state_schema_generation
.as_deref()
.context("write-lease Expected has no state generation")?
&& lease.state_contract_sha256
== expected
.state_contract_sha256
.as_deref()
.context("write-lease Expected has no state contract")?
&& lease.runtime_id == expected.runtime_id
&& lease.runtime_instance_id == activation.runtime_instance_id,
"topology write lease does not bind the exact Expected activation"
);
Ok(())
}
fn service_trust_anchor_from_envelope(
envelope: &CultCacheEnvelope,
) -> Result<GameCultServiceTrustAnchorRecord> {
ensure!(
envelope.schema_id.as_deref() == Some(GAMECULT_SERVICE_TRUST_ANCHOR_SCHEMA),
"service trust-anchor projection schema is foreign"
);
let anchor: GameCultServiceTrustAnchorRecord = rmp_serde::from_slice(&envelope.payload)?;
ensure!(
rmp_serde::to_vec(&anchor)? == envelope.payload,
"service trust-anchor projection is noncanonical"
);
anchor.validate()?;
Ok(anchor)
}
fn projection_entry<'a>(
entries: &'a [CultCacheEnvelope],
r#type: &str,
key: &str,
) -> Result<Option<&'a CultCacheEnvelope>> {
let mut matches = entries
.iter()
.filter(|entry| entry.r#type == r#type && entry.key == key);
let current = matches.next();
ensure!(
matches.next().is_none(),
"CultCache projection identity is ambiguous"
);
Ok(current)
}
impl TopologyPort for CultCacheTopologyDriver {
fn publish_expected(
&self,
expected: &IdunnExpectedIncarnationRecord,
provider_anchor: &ServiceIdentityTrustAnchor,
) -> Result<String> {
expected.validate()?;
let anchor = runtime_presence_trust_anchor(expected, provider_anchor)?;
let key = incarnation_key(expected)?;
let target = expected.target.clone();
self.mutate(|entries| {
let expected_record = expected_envelope(expected)?;
let anchor_record = anchor_envelope(&anchor)?;
let current_expected =
projection_entry(entries, IdunnExpectedIncarnationRecord::TYPE, &key)?;
let current_anchor = projection_entry(
entries,
GameCultServiceTrustAnchorRecord::TYPE,
&anchor.trust_anchor_id,
)?;
let legacy = entries
.iter()
.any(|envelope| Self::is_legacy_slot_record(envelope, &target));
if !legacy
&& current_expected.is_some_and(|current| same_record(current, &expected_record))
&& current_anchor.is_some_and(|current| same_record(current, &anchor_record))
{
return Ok(None);
}
// Only the Expected and anchor records are replaced. An activation
// or lease already projected under this key is this incarnation's
// own runtime fact and is left exactly as published.
let mut replacement = entries
.iter()
.filter(|envelope| {
!((envelope.key == key
&& envelope.r#type == IdunnExpectedIncarnationRecord::TYPE)
|| (envelope.r#type == GameCultServiceTrustAnchorRecord::TYPE
&& envelope.key == anchor.trust_anchor_id)
|| Self::is_legacy_slot_record(envelope, &target))
})
.cloned()
.collect::<Vec<_>>();
replacement.push(match current_expected {
Some(current) if same_record(current, &expected_record) => current.clone(),
_ => expected_record,
});
replacement.push(anchor_record);
Ok(Some(replacement))
})?;
expected.canonical_sha256()
}
fn withdraw_incarnation(
&self,
expected: &IdunnExpectedIncarnationRecord,
provider_anchor: &ServiceIdentityTrustAnchor,
activation: Option<&IdunnRuntimeActivationRecord>,
lease: Option<&IdunnProcessWriteLeaseRecord>,
) -> Result<()> {
expected.validate()?;
let exact_anchor = runtime_presence_trust_anchor(expected, provider_anchor)?;
if let Some(activation) = activation {
activation.validate()?;
ensure!(
activation.expected_projection_sha256 == expected.canonical_sha256()?
&& activation.runtime_id == expected.runtime_id,
"withdrawn activation does not bind the exact Expected projection"
);
}
if let Some(lease) = lease {
validate_topology_lease(
expected,
activation.context("withdrawn lease has no exact activation")?,
lease,
)?;
}
let key = incarnation_key(expected)?;
self.mutate(|entries| {
let projected = ProjectedIncarnation::read(entries, expected, activation, lease)?;
let anchor_stays = Self::other_incarnations_of(entries, &expected.target, &key);
let current_anchor = projection_entry(
entries,
GameCultServiceTrustAnchorRecord::TYPE,
&exact_anchor.trust_anchor_id,
)?;
if let Some(envelope) = current_anchor {
ensure!(
service_trust_anchor_from_envelope(envelope)? == exact_anchor,
"refusing to withdraw a substituted runtime presence trust anchor"
);
}
let nothing_to_remove = projected.expected.is_none()
&& projected.activation.is_none()
&& projected.lease.is_none()
&& (anchor_stays || current_anchor.is_none());
if nothing_to_remove {
return Ok(None);
}
Ok(Some(
entries
.iter()
.filter(|envelope| {
!(envelope.key == key
|| (!anchor_stays
&& envelope.r#type == GameCultServiceTrustAnchorRecord::TYPE
&& envelope.key == exact_anchor.trust_anchor_id))
})
.cloned()
.collect(),
))
})
}
fn publish_observed_activation(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
observation: &WorkloadObservation,
) -> Result<String> {
expected.validate()?;
activation.validate()?;
ensure!(
activation.expected_projection_sha256 == expected.canonical_sha256()?
&& activation.runtime_instance_id == observation.runtime_instance_id()
&& observation.executable_sha256() == expected.artifact_sha256,
"observed activation does not name the Expected native process"
);
let key = incarnation_key(expected)?;
self.mutate(|entries| {
// The Expected must be this one. A prior activation under the key
// is replaced without comparison: every launch is issued a fresh
// one, and the observation just made is the authority on which is
// current.
let projected_expected =
projection_entry(entries, IdunnExpectedIncarnationRecord::TYPE, &key)?
.context("observed activation has no current Expected projection")?;
ensure!(
projected_expected.schema_id.as_deref() == Some(IDUNN_EXPECTED_INCARNATION_SCHEMA)
&& IdunnExpectedIncarnationRecord::decode_canonical(
&projected_expected.payload
)? == *expected,
"observed activation's Expected projection is substituted"
);
let record = activation_envelope(&key, activation)?;
if projection_entry(entries, IdunnRuntimeActivationRecord::TYPE, &key)?
.is_some_and(|current| same_record(current, &record))
{
return Ok(None);
}
let mut replacement = entries
.iter()
.filter(|envelope| {
!(envelope.key == key && envelope.r#type == IdunnRuntimeActivationRecord::TYPE)
})
.cloned()
.collect::<Vec<_>>();
replacement.push(record);
Ok(Some(replacement))
})?;
activation.canonical_sha256()
}
fn publish_process_write_lease(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
lease: &IdunnProcessWriteLeaseRecord,
) -> Result<String> {
validate_topology_lease(expected, activation, lease)?;
let key = incarnation_key(expected)?;
let anchor_id = runtime_presence_trust_anchor_id(&expected.target);
self.mutate(|entries| {
let projected =
ProjectedIncarnation::read(entries, expected, Some(activation), Some(lease))?;
ensure!(
projected.expected.is_some(),
"process write lease has no current Expected projection"
);
ensure!(
projected.activation.is_some(),
"process write lease has no observed activation projection"
);
let projected_anchor = service_trust_anchor_from_envelope(
projection_entry(entries, GameCultServiceTrustAnchorRecord::TYPE, &anchor_id)?
.context("process write lease has no runtime presence trust anchor")?,
)?;
ensure!(
projected_anchor.service_id == expected.target
&& projected_anchor.runtime_id == expected.runtime_id
&& projected_anchor.signer_identity_id == expected.expected_signer_identity_id
&& projected_anchor.signing_purpose
== GAMECULT_RUNTIME_PRESENCE_HEALTH_SIGNING_PURPOSE
&& projected_anchor.signed_schema == GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA,
"process write lease runtime presence trust anchor is substituted"
);
if projected.lease.is_some() {
return Ok(None);
}
let mut replacement = entries.to_vec();
replacement.push(CultCacheEnvelope {
key: key.clone(),
r#type: IdunnProcessWriteLeaseRecord::TYPE.into(),
payload: lease.canonical_bytes()?,
stored_at: rfc3339_millis(lease.issued_at_unix_millis)?,
schema_id: Some(IDUNN_PROCESS_WRITE_LEASE_SCHEMA.into()),
});
Ok(Some(replacement))
})?;
lease.canonical_sha256()
}
fn withdraw_process_write_lease(
&self,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
lease: Option<&IdunnProcessWriteLeaseRecord>,
) -> Result<()> {
expected.validate()?;
activation.validate()?;
if let Some(lease) = lease {
validate_topology_lease(expected, activation, lease)?;
}
let key = incarnation_key(expected)?;
self.mutate(|entries| {
let current = projection_entry(entries, IdunnProcessWriteLeaseRecord::TYPE, &key)?;
let Some(envelope) = current else {
return Ok(None);
};
let projected = IdunnProcessWriteLeaseRecord::decode_canonical(&envelope.payload)?;
ensure!(
envelope.schema_id.as_deref() == Some(IDUNN_PROCESS_WRITE_LEASE_SCHEMA)
&& lease == Some(&projected),
"refusing to withdraw an unexpected process write-lease projection"
);
Ok(Some(
entries
.iter()
.filter(|candidate| *candidate != envelope)
.cloned()
.collect(),
))
})
}
fn receive(
&self,
target: &str,
expected_sha256: &str,
) -> Result<Option<ReceivedOdinTopologyCorrelation>> {
require_driver_id(target, "topology target")?;
if !self.correlation_store.exists() {
return Ok(None);
}
let key = incarnation_key_of(target, expected_sha256);
let entries = SingleFileMessagePackBackingStore::new(&self.correlation_store)
.pull_all_read_only_snapshot()?;
let mut matches = entries.iter().filter(|envelope| {
envelope.r#type == OdinRuntimeTopologyCorrelationRecord::TYPE && envelope.key == key
});
let Some(envelope) = matches.next() else {
return Ok(None);
};
ensure!(
matches.next().is_none(),
"Odin topology correlation is ambiguous"
);
ensure!(
envelope.schema_id.as_deref() == Some(ODIN_RUNTIME_TOPOLOGY_CORRELATION_SCHEMA),
"Odin topology correlation schema is foreign"
);
Ok(Some(ReceivedOdinTopologyCorrelation {
target: target.to_owned(),
expected_sha256: expected_sha256.to_owned(),
canonical_bytes: envelope.payload.clone(),
}))
}
}
/// nginx owns proxy mechanics. Idunn supplies one exact backend membership,
/// validates the complete nginx configuration, and reloads it. Configuration
/// bytes are actuator state, not proof that the selected runtime answered on
/// the stable route; the control plane admits that proof separately.
///
/// The host firewall allow for the stable endpoint is the same authority as
/// the fragment: a route that is admitted is reachable, a route that is
/// withdrawn is not, and neither is something an operator opens by hand. The
/// driver owns exactly the one rule it writes, tagged with its route id, and
/// touches no other.
pub struct NginxRouteDriver {
pub binding: RouteBinding,
pub nginx_program: PathBuf,
pub systemd_run_program: PathBuf,
pub systemctl_program: PathBuf,
pub ufw_program: PathBuf,
pub preflight_root: PathBuf,
}
/// The host programs and scratch directory the route driver actuates through.
/// The daemon owns one value and every driver is built from it, so the paths
/// are chosen in one place and a test can point them at stubs.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RouteActuators {
pub nginx: PathBuf,
pub systemd_run: PathBuf,
pub systemctl: PathBuf,
pub ufw: PathBuf,
pub preflight_root: PathBuf,
}
impl Default for RouteActuators {
fn default() -> Self {
Self {
nginx: PathBuf::from("/usr/sbin/nginx"),
systemd_run: PathBuf::from("/usr/bin/systemd-run"),
systemctl: PathBuf::from("/usr/bin/systemctl"),
ufw: PathBuf::from("/usr/sbin/ufw"),
preflight_root: PathBuf::from("/run/idunn/route-preflight"),
}
}
}
/// Why a route actuation is being asked for. Only a `Forward` change is
/// counted against the ceiling and can be refused: restoring the admitted or
/// incumbent route, and every actuation of a continuity, is survival, not
/// deployment, so a `Survival` is always admitted and only recorded.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RouteActuation {
Forward,
Survival,
}
/// A publish that lost to a held projection-directory lock. Transient by
/// kind: the holder lets go, and the caller's own retry decides what a later
/// attempt does. It names the store kind, never its path.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LockContended {
pub(crate) store: &'static str,
}
impl std::fmt::Display for LockContended {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "the {} is locked by another holder", self.store)
}
}
impl std::error::Error for LockContended {}
/// A forward route actuation the ceiling refused. `reopens_at_unix_millis` is
/// when the oldest counted actuation leaves the window.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteActuationRefused {
pub target: String,
pub used: usize,
pub reopens_at_unix_millis: u64,
}
impl std::fmt::Display for RouteActuationRefused {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"route actuation ceiling reached for {}: {} in the window, reopens at unix ms {}",
self.target, self.used, self.reopens_at_unix_millis
)
}
}
impl std::error::Error for RouteActuationRefused {}
/// The one rate authority over route actuation. The driver asks it before
/// every group of host mutations (fragment write, firewall, `nginx -t`,
/// reload, private-mount validation) and does none of them when it refuses.
/// Its owner keeps the count durably, so the ceiling survives a restart.
pub trait RouteActuationGate {
fn admit(&self, kind: RouteActuation) -> Result<()>;
}
impl NginxRouteDriver {
pub fn new(binding: RouteBinding) -> Self {
Self::with_actuators(binding, &RouteActuators::default())
}
pub fn with_actuators(binding: RouteBinding, actuators: &RouteActuators) -> Self {
Self {
binding,
nginx_program: actuators.nginx.clone(),
systemd_run_program: actuators.systemd_run.clone(),
systemctl_program: actuators.systemctl.clone(),
ufw_program: actuators.ufw.clone(),
preflight_root: actuators.preflight_root.clone(),
}
}
/// The firewall rules this route owns, one per stable socket (the
/// presence endpoint, then the member endpoint when the binding has one):
/// inbound to the socket's address and port, its transport's protocol.
fn endpoint_rules(&self) -> Result<Vec<Vec<OsString>>> {
let protocol = match self.binding.driver {
RouteDriver::NginxStreamTcp => "tcp",
RouteDriver::NginxStreamUdp => "udp",
};
let mut sockets = vec![self.binding.stable_socket()?];
sockets.extend(self.binding.member_stable_socket()?);
Ok(sockets
.into_iter()
.map(|(host, port)| {
vec![
OsString::from("allow"),
OsString::from("in"),
OsString::from("to"),
OsString::from(host.to_string()),
OsString::from("port"),
OsString::from(port.to_string()),
OsString::from("proto"),
OsString::from(protocol),
]
})
.collect())
}
fn admit_endpoint(&self) -> Result<()> {
for mut args in self.endpoint_rules()? {
args.push(OsString::from("comment"));
args.push(OsString::from(format!(
"Idunn route {}",
self.binding.route_id
)));
self.command(&self.ufw_program, args)
.context("admitting a route endpoint on the host firewall")?;
}
Ok(())
}
fn withdraw_endpoint(&self) -> Result<()> {
let mut first_error = None;
for rule in self.endpoint_rules()? {
let mut args = vec![OsString::from("delete")];
args.extend(rule);
match self.command(&self.ufw_program, args) {
Ok(_) => {}
// ufw reports a rule that is already gone as a failure; for a
// withdrawal that is the state being asked for.
Err(error) if format!("{error:#}").contains("non-existent") => {}
Err(error) => {
first_error.get_or_insert(
error.context("withdrawing a route endpoint from the host firewall"),
);
}
}
}
match first_error {
Some(error) => Err(error),
None => Ok(()),
}
}
fn command<I, S>(&self, program: &Path, args: I) -> Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
ensure!(
program.is_absolute(),
"route actuator program is not absolute"
);
let output = Command::new(program)
.args(args)
.stdin(Stdio::null())
.env_clear()
.env("LANG", "C.UTF-8")
.output()
.with_context(|| format!("starting route actuator {}", program.display()))?;
if !output.status.success() {
bail!(
"route actuator {} exited with {}: {}",
program.display(),
output.status,
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(output)
}
pub(crate) fn render(&self, expected: &IdunnExpectedIncarnationRecord) -> Result<Vec<u8>> {
expected.validate()?;
let expected_projection_sha256 = expected.canonical_sha256()?;
let route = expected
.route
.as_ref()
.context("expected incarnation has no route")?;
ensure!(
route.route_id == self.binding.route_id
&& route.stable_endpoint == self.binding.stable_endpoint,
"route driver binding differs from Expected"
);
let endpoint_prefix = match (self.binding.driver, route.transport.as_str()) {
(RouteDriver::NginxStreamTcp, "http") => "http://",
(RouteDriver::NginxStreamTcp, "tcp") => "tcp://",
(RouteDriver::NginxStreamUdp, "rudp") => "rudp://",
_ => bail!("route driver cannot carry the Expected transport"),
};
let (candidate_host, candidate_port) =
endpoint_host_port(&route.candidate_endpoint, endpoint_prefix)?;
ensure!(
candidate_host == self.binding.private_host
&& (self.binding.private_port_start..=self.binding.private_port_end)
.contains(&candidate_port),
"Expected candidate endpoint is outside the route binding"
);
let upstream = nginx_identifier(&self.binding.route_id)?;
let rendered = match self.binding.driver {
RouteDriver::NginxStreamTcp => {
let (stable_host, stable_port) =
endpoint_host_port(&route.stable_endpoint, endpoint_prefix)?;
let member_blocks = match (
self.binding.member_stable_socket()?,
self.binding.member_candidate_port(candidate_port)?,
) {
(Some((member_host, member_stable_port)), Some(member_port)) => {
let member_upstream = format!("{upstream}_member");
let max_connections = self
.binding
.member
.as_ref()
.map_or(0, |member| member.max_connections);
// One constant key: the limit is on the route's members
// together, so a flood of them can never take the
// connections the presence block needs.
format!(
"limit_conn_zone \"{member_upstream}\" zone={member_upstream}:64k;\nupstream {member_upstream} {{\n server {candidate_host}:{member_port};\n}}\nserver {{\n listen {member_host}:{member_stable_port};\n limit_conn {member_upstream} {max_connections};\n proxy_pass {member_upstream};\n}}\n"
)
}
_ => String::new(),
};
format!(
"# Idunn Expected {expected_projection_sha256}\nupstream {upstream} {{\n server {candidate_host}:{candidate_port};\n}}\nserver {{\n listen {stable_host}:{stable_port};\n proxy_pass {upstream};\n}}\n{member_blocks}"
)
}
RouteDriver::NginxStreamUdp => {
let (stable_host, stable_port) =
endpoint_host_port(&route.stable_endpoint, "rudp://")?;
format!(
"# Idunn Expected {expected_projection_sha256}\nupstream {upstream} {{\n server {candidate_host}:{candidate_port};\n}}\nserver {{\n listen {stable_host}:{stable_port} udp reuseport;\n proxy_pass {upstream};\n}}\n"
)
}
};
Ok(rendered.into_bytes())
}
fn current_configuration(&self) -> Result<Option<Vec<u8>>> {
Ok(match fs::read(&self.binding.config_path) {
// A fragment with no bytes grants no route, so it is absence, not a
// membership of zero servers. The distinction matters on the first
// deployment of a routed target: the preflight bind-mounts its
// candidate over config_path, and systemd materializes that mount
// point, leaving an empty file behind on the host. Read as content,
// it made the baseline "change" during validation, and then made
// every retry report an unadmitted incumbent -- the target could
// never be deployed a first time, and the wedge was permanent.
Ok(bytes) if bytes.is_empty() => None,
Ok(bytes) => Some(bytes),
Err(error) if error.kind() == ErrorKind::NotFound => None,
Err(error) => return Err(error).context("reading route fragment"),
})
}
fn write_fragment(&self, content: Option<&[u8]>) -> Result<()> {
match content {
Some(bytes) => atomic_replace(&self.binding.config_path, bytes),
None => match fs::remove_file(&self.binding.config_path) {
Ok(()) => sync_parent_directory(&self.binding.config_path),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
Err(error) => Err(error).context("removing route fragment"),
},
}
}
fn reload(&self) -> Result<()> {
self.command(&self.nginx_program, [OsString::from("-t")])?;
self.command(
&self.systemctl_program,
[
OsString::from("reload"),
OsString::from(&self.binding.reload_unit),
],
)?;
Ok(())
}
fn validate_candidate_in_private_mount(&self, rendered: &[u8]) -> Result<()> {
ensure_route_preflight_root(&self.preflight_root)?;
let route = nginx_identifier(&self.binding.route_id)?;
let nonce = Uuid::new_v4();
let candidate = self
.preflight_root
.join(format!("{route}-{nonce}.candidate"));
write_root_owned_file(&candidate, rendered, 0o400)?;
let unit = format!("idunn-nginx-preflight-{nonce}");
let validation = self.command(
&self.systemd_run_program,
[
OsString::from("--wait"),
OsString::from("--collect"),
OsString::from("--quiet"),
OsString::from(format!("--unit={unit}")),
OsString::from("--property=Type=exec"),
OsString::from("--property=PrivateMounts=yes"),
systemd_read_only_bind_property(&candidate, &self.binding.config_path)?,
OsString::from("--"),
self.nginx_program.clone().into_os_string(),
OsString::from("-t"),
],
);
let cleanup = remove_exact_root_owned_file(&candidate, 0o400);
match (validation, cleanup) {
(Ok(_), Ok(())) => Ok(()),
(Err(validation), Ok(())) => {
Err(validation).context("candidate route is invalid in a private mount namespace")
}
(Ok(_), Err(cleanup)) => {
Err(cleanup).context("deleting route preflight material")
}
(Err(validation), Err(cleanup)) => Err(validation).context(format!(
"candidate route is invalid; deleting its private preflight material also failed: {cleanup:#}"
)),
}
}
/// Put `prior` back as the route's fragment, and always write and reload:
/// on a transaction path, bytes on disk that already equal `prior` may be
/// a write whose reload a crash cut off. A reload that fails puts the
/// previous fragment back, so the disk shows what nginx still serves.
///
/// With no prior membership the stable endpoint routes to nothing, and
/// its firewall allow is withdrawn first: a failing `ufw delete` then
/// retries without touching nginx, and a crash after it is safe, because
/// a rule already gone reads as withdrawn.
fn restore(&self, prior: Option<&[u8]>, gate: &dyn RouteActuationGate) -> Result<()> {
gate.admit(RouteActuation::Survival)?;
if prior.is_none() {
self.withdraw_endpoint()?;
}
let current = self.current_configuration()?;
self.write_fragment(prior)?;
if let Err(reload) = self.reload() {
return match self.write_fragment(current.as_deref()) {
Ok(()) => Err(reload).context("reloading the restored route"),
Err(undo) => Err(reload).context(format!(
"reloading the restored route; putting the previous fragment back also failed: {undo:#}"
)),
};
}
Ok(())
}
pub fn preflight(
&self,
expected: &IdunnExpectedIncarnationRecord,
runtime_instance_id: &str,
incumbent: Option<&RouteObservation>,
gate: &dyn RouteActuationGate,
) -> Result<RoutePreflightReceipt> {
let rendered = self.render(expected)?;
ensure!(!rendered.is_empty(), "candidate route rendered empty");
let current = self.current_configuration()?;
let incumbent_membership_sha256 = current.as_ref().map(|bytes| sha256_id(bytes));
match incumbent {
Some(incumbent) => ensure!(
incumbent.route_id == self.binding.route_id
&& Some(incumbent.membership_sha256.as_str())
== incumbent_membership_sha256.as_deref(),
"route preflight baseline differs from the admitted incumbent"
),
None => ensure!(
incumbent_membership_sha256.is_none(),
"route preflight found an unadmitted incumbent"
),
}
gate.admit(RouteActuation::Forward)?;
self.validate_candidate_in_private_mount(&rendered)?;
let after = self.current_configuration()?;
ensure!(
after == current,
"nginx route baseline changed during candidate validation"
);
let receipt = RoutePreflightReceipt {
route_id: self.binding.route_id.clone(),
candidate_runtime_instance_id: runtime_instance_id.to_owned(),
candidate_membership_sha256: sha256_id(&rendered),
incumbent_runtime_instance_id: incumbent
.map(|observation| observation.runtime_instance_id.clone()),
incumbent_membership_sha256,
incumbent_configuration: current,
};
receipt.validate()?;
Ok(receipt)
}
/// Install the candidate route membership and require nginx to accept the
/// complete configuration. Success is not route admission: callers must
/// still obtain a fresh signed runtime observation through the stable
/// listener.
pub fn install(
&self,
expected: &IdunnExpectedIncarnationRecord,
runtime_instance_id: &str,
preflight: &RoutePreflightReceipt,
rollback_allowed: bool,
gate: &dyn RouteActuationGate,
) -> Result<String> {
preflight.validate()?;
let rendered = self.render(expected)?;
ensure!(
preflight.route_id == self.binding.route_id
&& preflight.candidate_runtime_instance_id == runtime_instance_id
&& preflight.candidate_membership_sha256 == sha256_id(&rendered),
"route preflight does not authorize this candidate membership"
);
let prior = self.current_configuration()?;
let prior_sha256 = prior.as_ref().map(|bytes| sha256_id(bytes));
let candidate_already_written = prior.as_deref() == Some(rendered.as_slice());
ensure!(
candidate_already_written
|| (prior == preflight.incumbent_configuration
&& prior_sha256 == preflight.incumbent_membership_sha256),
"route baseline changed after preflight"
);
// A candidate whose membership is the baseline's (a continuity over
// its admitted route) changes nothing, so nothing is written, and no
// crash can have cut a write of this transaction short.
if !preflight.changes_route() {
return Ok(sha256_id(&rendered));
}
// Otherwise bytes already on disk prove nothing: a crash may have cut
// off the reload after an earlier attempt's write. So it always
// writes and reloads.
gate.admit(RouteActuation::Forward)?;
atomic_replace(&self.binding.config_path, &rendered)?;
if let Err(error) = self.admit_endpoint().and_then(|()| self.reload()) {
// nginx did not load the candidate, so it still serves the
// baseline. The disk is put back to match it, which is the whole
// rollback, and a retry installs and reloads again rather than
// mistaking unloaded bytes for a live route. Past the fence the
// firewall allow stays for that retry; before it, an allow the
// baseline never had goes too.
let context = if rollback_allowed {
"candidate route validation or reload failed"
} else {
"candidate route reload failed after incumbent route authority was fenced; the fragment was put back for retry"
};
let undo = self.write_fragment(prior.as_deref()).and_then(|()| {
if rollback_allowed && prior.is_none() {
self.withdraw_endpoint()
} else {
Ok(())
}
});
return match undo {
Ok(()) => Err(error).context(context),
Err(undo) => Err(error).context(format!(
"{context}; putting the route back also failed: {undo:#}"
)),
};
}
ensure!(
self.current_configuration()?.as_deref() == Some(rendered.as_slice()),
"candidate route fragment changed during reload"
);
Ok(sha256_id(&rendered))
}
pub fn observe_membership(
&self,
expected: &IdunnExpectedIncarnationRecord,
membership_sha256: &str,
) -> Result<bool> {
let rendered = self.render(expected)?;
ensure!(
membership_sha256 == sha256_id(&rendered),
"route observation does not describe the expected membership"
);
Ok(self.current_configuration()?.as_deref() == Some(rendered.as_slice()))
}
/// Put the route back exactly as the candidate found it.
///
/// The preflight receipt captured the incumbent's configuration before this
/// candidate's membership was installed, so restoring it is precise whether
/// there was an incumbent (its bytes) or none (removal). Used when a
/// transaction is abandoned after the fence.
pub fn withdraw_candidate_membership(
&self,
preflight: &RoutePreflightReceipt,
gate: &dyn RouteActuationGate,
) -> Result<()> {
preflight.validate()?;
ensure!(
preflight.route_id == self.binding.route_id,
"route preflight receipt describes another route"
);
if !preflight.changes_route() {
return Ok(());
}
self.restore(preflight.incumbent_configuration.as_deref(), gate)
}
/// Restore the exact membership owned by an admitted generation. A failed
/// reload puts back the bytes it replaced, so the disk still shows what
/// nginx serves and the next pass sees the drift again rather than taking
/// unloaded bytes for an adopted route. The caller must still challenge
/// the stable listener to prove that nginx workers adopted this membership.
///
/// A fragment already equal to the admitted membership is left alone: no
/// write, no `nginx -t`, no firewall call, no reload. Supervision is the
/// only path that skips on equal bytes.
pub fn restore_admitted_membership(
&self,
expected: &IdunnExpectedIncarnationRecord,
membership_sha256: &str,
gate: &dyn RouteActuationGate,
) -> Result<()> {
let rendered = self.render(expected)?;
ensure!(
membership_sha256 == sha256_id(&rendered),
"admitted route receipt does not describe the expected membership"
);
if self.current_configuration()?.as_deref() == Some(rendered.as_slice()) {
return Ok(());
}
gate.admit(RouteActuation::Survival)?;
self.validate_candidate_in_private_mount(&rendered)?;
let replaced = self.current_configuration()?;
atomic_replace(&self.binding.config_path, &rendered)?;
if let Err(reload) = self.admit_endpoint().and_then(|()| self.reload()) {
return match self.write_fragment(replaced.as_deref()) {
Ok(()) => Err(reload).context("reloading the exact admitted route membership"),
Err(undo) => Err(reload).context(format!(
"reloading the exact admitted route membership; putting the replaced fragment back also failed: {undo:#}"
)),
};
}
ensure!(
self.current_configuration()?.as_deref() == Some(rendered.as_slice()),
"admitted route fragment changed during restoration"
);
Ok(())
}
/// Ask the stable listener for one freshly minted provider-owned presence
/// statement. This method validates the CultNet wire exchange and exact
/// document selection only; Idunn's control plane authenticates and
/// correlates the signed payload against current authority.
pub fn request_runtime_presence(
&self,
expected: &IdunnExpectedIncarnationRecord,
message_id: &str,
) -> Result<RouteSnapshotResponse> {
let route = expected
.route
.as_ref()
.context("route challenge has no Expected route")?;
ensure!(
route.route_id == self.binding.route_id
&& route.stable_endpoint == self.binding.stable_endpoint,
"route challenge binding differs from Expected"
);
Ok(self.request_runtime_presence_at(expected, message_id, &route.stable_endpoint)?)
}
/// The bootstrap exception for the first managed Odin observes Warming on
/// the exact candidate endpoint. It does not install or admit a route.
pub fn request_candidate_runtime_presence(
&self,
expected: &IdunnExpectedIncarnationRecord,
message_id: &str,
) -> Result<RouteSnapshotResponse, ChallengeFailure> {
let route = expected
.route
.as_ref()
.context("candidate challenge has no Expected route")
.map_err(refused)?;
// Rendering holds the binding to the Expected route, id and endpoints.
self.render(expected).map_err(refused)?;
self.request_runtime_presence_at(expected, message_id, &route.candidate_endpoint)
}
fn request_runtime_presence_at(
&self,
expected: &IdunnExpectedIncarnationRecord,
message_id: &str,
endpoint: &str,
) -> Result<RouteSnapshotResponse, ChallengeFailure> {
expected.validate().map_err(refused)?;
require_driver_id(message_id, "route challenge message").map_err(refused)?;
let route = expected
.route
.as_ref()
.context("route challenge has no Expected route")
.map_err(refused)?;
let request = CultNetMessage::SnapshotRequest {
message_id: message_id.to_owned(),
schema_ids: Some(vec![GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA.into()]),
record_keys: Some(vec![expected.target.clone()]),
};
let endpoint_prefix = match route.transport.as_str() {
"http" => "http://",
"tcp" => "tcp://",
"rudp" => "rudp://",
_ => return Err(refused(anyhow!("route challenge transport is unsupported"))),
};
let (host, port) = endpoint_host_port(endpoint, endpoint_prefix).map_err(refused)?;
let target = SocketAddr::new(
host.parse()
.context("route challenge endpoint host is not an IP address")
.map_err(refused)?,
port,
);
let response = match route.transport.as_str() {
"http" => {
let payload =
encode_cultnet_message_to_vec(&request, CultNetWireContract::CultNetSchemaV0)
.map_err(refused)?;
let response = request_http_snapshot(target, &payload)?;
decode_cultnet_message_from_slice(&response, CultNetWireContract::CultNetSchemaV0)
.map_err(refused)?
}
"tcp" => {
let payload =
encode_cultnet_message_to_vec(&request, CultNetWireContract::CultNetSchemaV0)
.map_err(refused)?;
let response = request_tcp_snapshot(target, &payload)?;
decode_cultnet_message_from_slice(&response, CultNetWireContract::CultNetSchemaV0)
.map_err(refused)?
}
// The RUDP client reports every failure as prose, so a bad answer
// cannot be told from no answer here: all of it reads as silence.
"rudp" => request_raw_snapshot_from_rudp_catalog(CultMeshRudpSnapshotOptions {
target,
runtime_id: "idunn-route-observer".into(),
message_id: message_id.to_owned(),
schema_ids: Some(vec![GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA.into()]),
record_keys: Some(vec![expected.target.clone()]),
..CultMeshRudpSnapshotOptions::default()
})
.map_err(silent)?,
_ => return Err(refused(anyhow!("route challenge transport is unsupported"))),
};
exact_route_snapshot_response(response, message_id, &expected.target).map_err(refused)
}
/// Restore the exact preflight baseline. This may only replace the exact
/// candidate authorized by the receipt; an unrelated writer is never
/// overwritten as a side effect of rollback.
pub fn rollback(
&self,
expected: &IdunnExpectedIncarnationRecord,
runtime_instance_id: &str,
preflight: &RoutePreflightReceipt,
gate: &dyn RouteActuationGate,
) -> Result<()> {
preflight.validate()?;
let rendered = self.render(expected)?;
ensure!(
preflight.route_id == self.binding.route_id
&& preflight.candidate_runtime_instance_id == runtime_instance_id
&& preflight.candidate_membership_sha256 == sha256_id(&rendered),
"route preflight does not authorize rollback of this candidate membership"
);
ensure!(
self.current_configuration()?.as_deref() == Some(rendered.as_slice()),
"candidate route changed before rollback"
);
if !preflight.changes_route() {
return Ok(());
}
self.restore(preflight.incumbent_configuration.as_deref(), gate)
}
}
/// Why a direct presence challenge produced no document to authenticate.
#[derive(Debug)]
pub enum ChallengeFailure {
/// Nothing answered: the connection was refused, or no byte arrived before
/// the timeout, or the peer closed with zero bytes. A process still
/// starting does this, and it is waiting, not a fault. Once any byte has
/// arrived, a stall or a stop is `Refused`, not this.
Silent(anyhow::Error),
/// Something answered wrongly, or the challenge could not be made: a
/// malformed or foreign answer, an oversized one, or a local configuration
/// error. Waiting does not fix it.
Refused(anyhow::Error),
}
impl std::fmt::Display for ChallengeFailure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Silent(error) | Self::Refused(error) => write!(formatter, "{error:#}"),
}
}
}
impl std::error::Error for ChallengeFailure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Silent(error) | Self::Refused(error) => Some(error.as_ref()),
}
}
}
fn silent(error: impl Into<anyhow::Error>) -> ChallengeFailure {
ChallengeFailure::Silent(error.into())
}
fn refused(error: impl Into<anyhow::Error>) -> ChallengeFailure {
ChallengeFailure::Refused(error.into())
}
fn ensure_or_refuse(condition: bool, message: &'static str) -> Result<(), ChallengeFailure> {
if condition {
Ok(())
} else {
Err(refused(anyhow!(message)))
}
}
/// One answer as it arrives, over either transport. A peer that stops is judged
/// the same way on both: no byte at all is silence (a process still starting
/// does that), and anything short of a whole answer after some bytes is a bad
/// answer. Waiting does not finish an answer that has begun and stopped.
struct Answer<R> {
source: R,
seen: usize,
}
impl<R: Read> Answer<R> {
fn new(source: R) -> Self {
Self { source, seen: 0 }
}
fn stopped(&self, what: &str) -> ChallengeFailure {
let error = anyhow!("stable CultNet route stopped {what}");
if self.seen == 0 {
silent(error)
} else {
refused(error)
}
}
fn read_some(&mut self, buffer: &mut [u8], what: &str) -> Result<usize, ChallengeFailure> {
loop {
match self.source.read(buffer) {
Ok(0) => return Err(self.stopped(what)),
Ok(read) => {
self.seen += read;
return Ok(read);
}
Err(error) if error.kind() == ErrorKind::Interrupted => {}
Err(_) => return Err(self.stopped(what)),
}
}
}
fn read_all(&mut self, buffer: &mut [u8], what: &str) -> Result<(), ChallengeFailure> {
let mut filled = 0;
while filled < buffer.len() {
filled += self.read_some(&mut buffer[filled..], what)?;
}
Ok(())
}
/// One CRLF-terminated line, without its terminator.
fn read_line(&mut self, bound: usize, what: &str) -> Result<Vec<u8>, ChallengeFailure> {
let mut line = Vec::new();
loop {
let mut byte = [0_u8];
self.read_all(&mut byte, what)?;
line.push(byte[0]);
if line.ends_with(b"\r\n") {
line.truncate(line.len() - 2);
return Ok(line);
}
ensure_or_refuse(
line.len() <= bound,
"stable CultNet HTTP response line exceeds its bound",
)?;
}
}
}
pub(crate) fn request_tcp_snapshot(target: SocketAddr, payload: &[u8]) -> Result<Vec<u8>, ChallengeFailure> {
let mut stream = connect_route_socket(target)
.with_context(|| format!("connecting stable CultNet TCP route {target}"))
.map_err(silent)?;
stream
.set_read_timeout(Some(ROUTE_SNAPSHOT_TIMEOUT))
.map_err(refused)?;
stream
.set_write_timeout(Some(ROUTE_SNAPSHOT_TIMEOUT))
.map_err(refused)?;
stream
.write_all(&encode_frame(payload).map_err(refused)?)
.and_then(|()| stream.flush())
.map_err(silent)?;
let mut answer = Answer::new(stream);
let mut header = [0_u8; 4];
answer.read_all(&mut header, "before its response frame header ended")?;
let length = u32::from_be_bytes(header) as usize;
ensure_or_refuse(
(1..=ROUTE_SNAPSHOT_MAX_BYTES).contains(&length),
"stable CultNet TCP response exceeds the route observation bound",
)?;
let mut response = vec![0_u8; length];
answer.read_all(&mut response, "before its response frame ended")?;
Ok(response)
}
fn request_http_snapshot(target: SocketAddr, payload: &[u8]) -> Result<Vec<u8>, ChallengeFailure> {
let mut stream = connect_route_socket(target)
.with_context(|| format!("connecting stable CultNet HTTP route {target}"))
.map_err(silent)?;
stream
.set_read_timeout(Some(ROUTE_SNAPSHOT_TIMEOUT))
.map_err(refused)?;
stream
.set_write_timeout(Some(ROUTE_SNAPSHOT_TIMEOUT))
.map_err(refused)?;
let head = format!(
"POST {ROUTE_HTTP_SNAPSHOT_PATH} HTTP/1.1\r\nHost: {target}\r\nContent-Type: application/msgpack\r\nAccept: application/msgpack\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
payload.len()
);
stream
.write_all(head.as_bytes())
.and_then(|()| stream.write_all(payload))
.and_then(|()| stream.flush())
.map_err(silent)?;
// The response is read by its own framing, never to end of stream: a peer
// that ignores `Connection: close` and keeps the socket open has still
// answered in full.
let mut answer = Answer::new(BufReader::new(stream));
let mut head_bytes = 0_usize;
// Headers and chunked trailers draw on the one budget.
let mut head_line = |answer: &mut Answer<BufReader<TcpStream>>, what: &str| {
let line = answer.read_line(ROUTE_HTTP_MAX_HEADER_BYTES, what)?;
head_bytes += line.len() + 2;
ensure_or_refuse(
head_bytes <= ROUTE_HTTP_MAX_HEADER_BYTES,
"stable CultNet HTTP response headers exceed their bound",
)?;
Ok::<_, ChallengeFailure>(line)
};
let headers = "before its headers ended";
let status = head_line(&mut answer, headers)?;
let status = std::str::from_utf8(&status)
.context("stable CultNet HTTP response status is not UTF-8")
.map_err(refused)?;
if !is_http_success(status) {
return Err(refused(anyhow!(
"stable CultNet HTTP route rejected the snapshot challenge: {status}"
)));
}
let mut content_length = None;
let mut chunked = false;
loop {
let line = head_line(&mut answer, headers)?;
if line.is_empty() {
break;
}
let line = std::str::from_utf8(&line)
.context("stable CultNet HTTP response headers are not UTF-8")
.map_err(refused)?;
let (name, value) = line
.split_once(':')
.context("stable CultNet HTTP response contains a malformed header")
.map_err(refused)?;
// RFC 9112 5.1: a name with whitespace around it (or none) is not a
// field name, and a recipient must not guess which one was meant.
ensure_or_refuse(
!name.is_empty() && !name.bytes().any(|byte| byte.is_ascii_whitespace()),
"stable CultNet HTTP response has a malformed header name",
)?;
if name.eq_ignore_ascii_case("transfer-encoding") {
ensure_or_refuse(
value.trim().eq_ignore_ascii_case("chunked"),
"stable CultNet HTTP response uses unsupported transfer encoding",
)?;
chunked = true;
}
if name.eq_ignore_ascii_case("content-length") {
ensure_or_refuse(
content_length.is_none(),
"stable CultNet HTTP response repeats Content-Length",
)?;
content_length = Some(
parse_digits(value.trim_matches([' ', '\t']), 10)
.context("stable CultNet HTTP Content-Length is invalid")
.map_err(refused)?,
);
}
}
let body = "before its body ended";
match (content_length, chunked) {
(Some(length), false) => {
ensure_or_refuse(
length <= ROUTE_SNAPSHOT_MAX_BYTES,
"stable CultNet HTTP response body exceeds the route observation bound",
)?;
let mut response = vec![0_u8; length];
answer.read_all(&mut response, body)?;
Ok(response)
}
(None, true) => {
let mut response = Vec::new();
loop {
let size = answer.read_line(ROUTE_HTTP_CHUNK_LINE_BYTES, body)?;
// RFC 9112 7.1: `chunk-size [ chunk-ext ] CRLF`, where an
// extension may follow whitespace. Nothing else is trimmed.
let size = std::str::from_utf8(&size)
.ok()
.and_then(|line| match line.split_once(';') {
Some((digits, _extension)) => {
parse_digits(digits.trim_end_matches([' ', '\t']), 16)
}
None => parse_digits(line, 16),
})
.context("stable CultNet HTTP chunk size is invalid")
.map_err(refused)?;
if size == 0 {
// Trailers, if any, end at the empty line.
while !head_line(&mut answer, body)?.is_empty() {}
return Ok(response);
}
ensure_or_refuse(
response.len().saturating_add(size) <= ROUTE_SNAPSHOT_MAX_BYTES,
"stable CultNet HTTP response body exceeds the route observation bound",
)?;
let start = response.len();
response.resize(start + size, 0);
answer.read_all(&mut response[start..], body)?;
let mut end = [0_u8; 2];
answer.read_all(&mut end, body)?;
ensure_or_refuse(&end == b"\r\n", "stable CultNet HTTP chunk is not terminated")?;
}
}
_ => Err(refused(anyhow!(
"stable CultNet HTTP response has no single body framing"
))),
}
}
/// A number written only in digits of `radix`: no sign, no whitespace, not
/// empty. `from_str_radix` alone accepts a leading `+`, which RFC 9112 does
/// not.
fn parse_digits(text: &str, radix: u32) -> Option<usize> {
if text.is_empty() || !text.chars().all(|digit| digit.is_digit(radix)) {
return None;
}
usize::from_str_radix(text, radix).ok()
}
/// A 2xx status line of HTTP/1.0 or HTTP/1.1.
fn is_http_success(status: &str) -> bool {
let Some(rest) = status
.strip_prefix("HTTP/1.1 ")
.or_else(|| status.strip_prefix("HTTP/1.0 "))
else {
return false;
};
let code = rest.as_bytes();
code.len() >= 3
&& code[0] == b'2'
&& code[1].is_ascii_digit()
&& code[2].is_ascii_digit()
&& (code.len() == 3 || code[3] == b' ')
}
/// nginx reload is signal-driven: `systemctl reload` can return before its
/// replacement workers bind a newly admitted stable endpoint. Retry only the
/// refused TCP connect during that bounded handoff window. Once connected, the
/// caller still requires the normal exact signed route challenge.
fn connect_route_socket(target: SocketAddr) -> std::io::Result<TcpStream> {
let deadline = Instant::now() + ROUTE_CONNECT_RETRY_WINDOW;
loop {
match TcpStream::connect_timeout(&target, ROUTE_SNAPSHOT_TIMEOUT) {
Ok(stream) => return Ok(stream),
Err(error)
if error.kind() == ErrorKind::ConnectionRefused && Instant::now() < deadline =>
{
thread::sleep(ROUTE_CONNECT_RETRY_INTERVAL);
}
Err(error) => return Err(error),
}
}
}
pub(crate) fn exact_route_snapshot_response(
response: CultNetMessage,
message_id: &str,
target: &str,
) -> Result<RouteSnapshotResponse> {
let CultNetMessage::SnapshotResponseRaw {
message_id: response_id,
documents,
} = response
else {
bail!("stable route returned a non-raw snapshot response")
};
ensure!(
response_id == message_id,
"stable route snapshot response belongs to another challenge"
);
ensure!(
documents.len() == 1,
"stable route snapshot response is not the exact singleton presence document"
);
let document = documents
.into_iter()
.next()
.expect("singleton response length was checked");
ensure!(
document.schema_id == GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA
&& document.record_key == target
&& document.payload_encoding == CultNetRawPayloadEncoding::Messagepack,
"stable route substituted the runtime presence document"
);
ensure!(
!document.payload.is_empty() && document.payload.len() <= ROUTE_SNAPSHOT_MAX_BYTES,
"stable route presence payload is empty or exceeds its bound"
);
Ok(RouteSnapshotResponse {
message_id: response_id,
canonical_presence: document.payload,
})
}
pub(crate) fn release_artifact<'a>(
declaration: &'a TargetDeclaration,
artifact_id: &str,
) -> Result<&'a ArtifactOutput> {
declaration
.artifacts
.iter()
.find(|artifact| artifact.id == artifact_id)
.with_context(|| format!("target declares no artifact {artifact_id}"))
}
/// The two records every Idunn-launched workload reads at start: its Expected
/// and its activation, each immutable once written. Shared by every workload
/// driver; the systemd driver hardens the directory afterwards, the host
/// actuator writes it inside the host user's own profile.
pub(crate) fn write_runtime_bundle_records(
bundle: &Path,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> Result<()> {
fs::create_dir_all(bundle)
.with_context(|| format!("creating runtime bundle {}", bundle.display()))?;
write_immutable_record(
&bundle.join("expected.cc"),
CultCacheEnvelope {
key: expected.target.clone(),
r#type: IdunnExpectedIncarnationRecord::TYPE.into(),
payload: expected.canonical_bytes()?,
stored_at: rfc3339_millis(activation.issued_at_unix_millis)?,
schema_id: Some(IDUNN_EXPECTED_INCARNATION_SCHEMA.into()),
},
)?;
write_immutable_record(
&bundle.join("activation.cc"),
CultCacheEnvelope {
key: expected.target.clone(),
r#type: IdunnRuntimeActivationRecord::TYPE.into(),
payload: activation.canonical_bytes()?,
stored_at: rfc3339_millis(activation.issued_at_unix_millis)?,
schema_id: Some(IDUNN_RUNTIME_ACTIVATION_SCHEMA.into()),
},
)
}
pub(crate) fn write_immutable_record(path: &Path, envelope: CultCacheEnvelope) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut store = SingleFileMessagePackBackingStore::new(path);
match store.pull_all_read_only_snapshot()?.as_slice() {
[] => store.push(&envelope),
[current] if current == &envelope => Ok(()),
_ => bail!(
"immutable runtime document {} already differs",
path.display()
),
}
}
fn upsert_record(path: &Path, replacement: CultCacheEnvelope) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let store = SingleFileMessagePackBackingStore::new(path);
for _ in 0..8 {
let entries = store.pull_all_read_only_snapshot()?;
let mut matches = entries
.iter()
.filter(|entry| entry.r#type == replacement.r#type && entry.key == replacement.key);
let current = matches.next().cloned();
ensure!(
matches.next().is_none(),
"CultCache projection identity is ambiguous"
);
if current.as_ref() == Some(&replacement) {
return Ok(());
}
if store.compare_exchange(
&[CultCacheExpectedEnvelope {
r#type: replacement.r#type.clone(),
key: replacement.key.clone(),
current,
}],
std::slice::from_ref(&replacement),
)? {
return Ok(());
}
}
bail!("CultCache projection changed too often to publish")
}
pub(crate) fn rfc3339_millis(millis: u64) -> Result<String> {
Ok(
chrono::DateTime::from_timestamp_millis(i64::try_from(millis)?)
.context("runtime timestamp is out of range")?
.to_rfc3339(),
)
}
#[cfg(unix)]
fn validate_frozen_source_store(store: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
ensure!(store.is_absolute(), "frozen source store is not absolute");
let canonical_store = store
.canonicalize()
.with_context(|| format!("resolving frozen source store {}", store.display()))?;
ensure!(
canonical_store == store,
"frozen source store traverses a symlink or noncanonical path"
);
let metadata = fs::symlink_metadata(store)?;
ensure!(
metadata.is_dir()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.permissions().mode() & 0o022 == 0,
"frozen source store is not root-owned and nonwritable"
);
Ok(())
}
#[cfg(not(unix))]
fn validate_frozen_source_store(_store: &Path) -> Result<()> {
bail!("frozen source materialization requires a Unix actuator")
}
#[cfg(unix)]
fn prepare_frozen_transaction_root(store: &Path, transaction_id: &str) -> Result<PathBuf> {
use std::os::unix::fs::PermissionsExt;
require_driver_id(transaction_id, "source transaction")?;
validate_frozen_source_store(store)?;
let transaction_root = store.join(transaction_id);
if transaction_root.exists() {
remove_frozen_transaction_root(store, transaction_id)?;
}
fs::create_dir(&transaction_root)?;
fs::set_permissions(&transaction_root, fs::Permissions::from_mode(0o700))?;
Ok(transaction_root)
}
#[cfg(not(unix))]
fn prepare_frozen_transaction_root(_store: &Path, _transaction_id: &str) -> Result<PathBuf> {
bail!("frozen source materialization requires a Unix actuator")
}
#[cfg(unix)]
fn remove_frozen_transaction_root(store: &Path, transaction_id: &str) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
require_driver_id(transaction_id, "source transaction")?;
validate_frozen_source_store(store)?;
let transaction_root = store.join(transaction_id);
let metadata = fs::symlink_metadata(&transaction_root)?;
ensure!(
metadata.is_dir()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.permissions().mode() & 0o022 == 0,
"frozen source transaction root is not Idunn-owned"
);
fs::set_permissions(&transaction_root, fs::Permissions::from_mode(0o700))?;
fs::remove_dir_all(&transaction_root).with_context(|| {
format!(
"removing exact frozen source transaction {}",
transaction_root.display()
)
})
}
#[cfg(not(unix))]
fn remove_frozen_transaction_root(_store: &Path, _transaction_id: &str) -> Result<()> {
bail!("frozen source cleanup requires a Unix actuator")
}
#[cfg(unix)]
fn prepare_frozen_source_destination(destination: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
ensure!(
unsafe { libc::geteuid() } == 0,
"frozen source materialization requires root Idunn"
);
ensure!(
destination.is_absolute(),
"frozen source destination is not absolute"
);
let parent = destination
.parent()
.context("frozen source destination has no parent")?;
ensure!(destination != parent, "frozen source destination is broad");
let canonical_parent = parent
.canonicalize()
.with_context(|| format!("resolving frozen source parent {}", parent.display()))?;
ensure!(
canonical_parent == parent,
"frozen source parent traverses a symlink or noncanonical path"
);
let parent_metadata = fs::symlink_metadata(parent)?;
ensure!(
parent_metadata.is_dir()
&& !parent_metadata.file_type().is_symlink()
&& parent_metadata.uid() == 0
&& parent_metadata.permissions().mode() & 0o022 == 0,
"frozen source parent is not root-owned and nonwritable"
);
if destination.exists() {
remove_frozen_source_destination(destination)?;
}
fs::create_dir(destination)
.with_context(|| format!("creating frozen source {}", destination.display()))?;
fs::set_permissions(destination, fs::Permissions::from_mode(0o700))?;
Ok(())
}
#[cfg(not(unix))]
fn prepare_frozen_source_destination(_destination: &Path) -> Result<()> {
bail!("frozen source materialization requires a Unix actuator")
}
#[cfg(unix)]
fn remove_frozen_source_destination(destination: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
ensure!(
destination.is_absolute(),
"frozen source destination is not absolute"
);
let parent = destination
.parent()
.context("frozen source destination has no parent")?;
ensure!(destination != parent, "frozen source destination is broad");
let parent_metadata = fs::symlink_metadata(parent)?;
let metadata = fs::symlink_metadata(destination)?;
ensure!(
parent_metadata.is_dir()
&& !parent_metadata.file_type().is_symlink()
&& parent_metadata.uid() == 0
&& parent_metadata.permissions().mode() & 0o022 == 0,
"frozen source parent is not root-owned and nonwritable"
);
ensure!(
metadata.is_dir()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.permissions().mode() & 0o022 == 0,
"existing frozen source is not an Idunn-owned directory"
);
fs::remove_dir_all(destination)
.with_context(|| format!("removing exact frozen source {}", destination.display()))
}
#[cfg(not(unix))]
fn remove_frozen_source_destination(_destination: &Path) -> Result<()> {
bail!("frozen source materialization requires a Unix actuator")
}
#[cfg(unix)]
fn harden_frozen_source(root: &Path) -> Result<()> {
harden_frozen_source_tree(root, root)
}
#[cfg(unix)]
fn harden_frozen_source_tree(root: &Path, current: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = fs::symlink_metadata(current)?;
ensure!(metadata.uid() == 0, "frozen source entry is not root-owned");
ensure!(
current == root || current.file_name() != Some(OsStr::new(".git")),
"frozen source contains forbidden .git metadata"
);
if metadata.is_dir() {
let mut entries = fs::read_dir(current)?.collect::<std::io::Result<Vec<_>>>()?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
harden_frozen_source_tree(root, &entry.path())?;
}
fs::set_permissions(current, fs::Permissions::from_mode(0o555))?;
} else if metadata.is_file() {
let mode = if metadata.permissions().mode() & 0o111 == 0 {
0o444
} else {
0o555
};
fs::set_permissions(current, fs::Permissions::from_mode(mode))?;
} else if metadata.file_type().is_symlink() {
// S6: only the target's shape is checked here, against `.partial`,
// the temporary name the tree is built under. Whether the chain
// actually resolves inside the root -- and whether a dangling
// target still counts as inside it -- is decided later, by
// `validate_frozen_source_symlinks`, against the tree's real, final
// published path. A link built to look correct under `.partial` and
// dangling or escaping once renamed (or the reverse) must be judged
// by what it resolves to at the name every later reader actually
// opens, not by this transient one.
let target = fs::read_link(current)?;
ensure!(target.is_relative(), "frozen source symlink is absolute");
} else {
bail!("frozen source contains a special filesystem entry")
}
Ok(())
}
/// S6 (Self's ruling, third Cut 1 fix batch): resolves a frozen source
/// symlink's full chain against the tree's real, final published root. Every
/// component the chain crosses that exists on disk is resolved for real
/// (`fs::canonicalize`, recursing through any symlink it meets), so a
/// self-referential directory cannot lexically undercount how far the chain
/// travels -- the same defense F5 relied on. Only the walk's trailing,
/// still-nonexistent components may be missing: once resolution reaches a
/// component that does not exist, nothing on disk remains to be tricked by,
/// so the rest of the target is appended lexically and containment keeps
/// being checked at every step. A dangling link is accepted exactly when
/// every component that exists stays inside the root; one whose real or
/// lexical remainder ever leaves the root is refused, dangling or not.
///
/// S4-1 (Self's ruling, fourth Cut 1 fix batch): the previous shape recursed
/// once per path component and re-resolved every shared prefix from
/// scratch, so a tree of `n` tiny self-doubling symlinks
/// (`a0 -> .`, `ak -> a{k-1}/a{k-1}`) cost `2^n` real filesystem
/// resolutions -- `MAX_HOPS` bounded chain DEPTH, not the WORK spent getting
/// there. This is now an explicit iterative walk: one work stack of frames
/// stands in for the old call stack (so nested symlink expansion never
/// recurses in Rust), each already-resolved symlink is memoised by its own
/// path the first time it is followed, and `MAX_LINK_TRAVERSALS` charges
/// every *distinct* symlink actually read against one shared budget for the
/// whole resolution -- a memoised hit costs nothing. A chain of `n` distinct
/// symlinks, doubling or not, now costs O(n) real reads no matter how many
/// times its target text repeats a name.
#[cfg(unix)]
enum FrozenSymlinkStep {
CurDir,
ParentDir,
Normal(std::ffi::OsString),
}
#[cfg(unix)]
fn frozen_symlink_steps(target: &Path) -> Result<std::collections::VecDeque<FrozenSymlinkStep>> {
let mut steps = std::collections::VecDeque::new();
for component in target.components() {
steps.push_back(match component {
std::path::Component::CurDir => FrozenSymlinkStep::CurDir,
std::path::Component::ParentDir => FrozenSymlinkStep::ParentDir,
std::path::Component::Normal(part) => FrozenSymlinkStep::Normal(part.to_owned()),
_ => bail!("frozen source symlink target has a non-normal path component"),
});
}
Ok(steps)
}
/// One in-progress symlink expansion on the explicit work stack.
/// `symlink_path` is `None` only for the outermost frame, the one resolving
/// the caller's own starting path rather than a nested symlink this walk
/// met along the way; every other frame's `symlink_path` is memoised under
/// its own key once the frame finishes.
#[cfg(unix)]
struct FrozenSymlinkFrame {
symlink_path: Option<PathBuf>,
resolved: PathBuf,
remaining: std::collections::VecDeque<FrozenSymlinkStep>,
}
/// Reads and charges one symlink's target against the shared traversal
/// budget, returning the frame that will walk its (already-decoded) steps.
/// Shared by the outermost call and by every nested symlink the nesting walk
/// discovers -- the two call sites the old recursion covered with the same
/// function.
#[cfg(unix)]
fn open_frozen_symlink_frame(
canonical_root: &Path,
symlink_path: &Path,
budget: &mut u32,
) -> Result<FrozenSymlinkFrame> {
ensure!(
*budget > 0,
"frozen source symlink chain exceeds the traversal budget"
);
*budget -= 1;
let target = fs::read_link(symlink_path)?;
ensure!(target.is_relative(), "frozen source symlink is absolute");
let parent = symlink_path
.parent()
.context("frozen source symlink has no parent")?;
let resolved = parent.canonicalize().with_context(|| {
format!(
"resolving frozen source symlink chain at {}",
symlink_path.display()
)
})?;
ensure!(
resolved.starts_with(canonical_root),
"frozen source symlink escapes its root"
);
Ok(FrozenSymlinkFrame {
symlink_path: Some(symlink_path.to_path_buf()),
resolved,
remaining: frozen_symlink_steps(&target)?,
})
}
#[cfg(unix)]
fn resolve_frozen_source_symlink(canonical_root: &Path, path: &Path) -> Result<PathBuf> {
// Total distinct symlinks this one resolution may read, matching the old
// `MAX_HOPS` chain-depth bound in spirit: a memoised re-visit is free, so
// this is now a bound on real work rather than on nesting depth.
const MAX_LINK_TRAVERSALS: u32 = 40;
let mut budget = MAX_LINK_TRAVERSALS;
let mut memo: std::collections::HashMap<PathBuf, PathBuf> = std::collections::HashMap::new();
let mut stack: Vec<FrozenSymlinkFrame> =
vec![open_frozen_symlink_frame(canonical_root, path, &mut budget)?];
loop {
let top = stack.len() - 1;
match stack[top].remaining.pop_front() {
None => {
let frame = stack.pop().expect("resolver stack is never empty");
if let Some(symlink_path) = frame.symlink_path {
memo.insert(symlink_path, frame.resolved.clone());
}
match stack.last_mut() {
None => return Ok(frame.resolved),
Some(parent) => parent.resolved = frame.resolved,
}
}
Some(FrozenSymlinkStep::CurDir) => {}
Some(FrozenSymlinkStep::ParentDir) => {
let frame = &mut stack[top];
ensure!(
frame.resolved.pop() && frame.resolved.starts_with(canonical_root),
"frozen source symlink escapes its root"
);
}
Some(FrozenSymlinkStep::Normal(part)) => {
let frame = &mut stack[top];
frame.resolved.push(&part);
ensure!(
frame.resolved.starts_with(canonical_root),
"frozen source symlink escapes its root"
);
let candidate = frame.resolved.clone();
match fs::symlink_metadata(&candidate) {
Ok(metadata) if metadata.file_type().is_symlink() => {
if let Some(cached) = memo.get(&candidate) {
stack[top].resolved = cached.clone();
} else {
stack.push(open_frozen_symlink_frame(
canonical_root,
&candidate,
&mut budget,
)?);
}
}
Ok(_) => {
let real = candidate.canonicalize().with_context(|| {
format!("resolving frozen source symlink chain at {}", candidate.display())
})?;
stack[top].resolved = real;
}
Err(ref error) if error.kind() == std::io::ErrorKind::NotFound => {
// Nothing exists here yet: the remaining components,
// including this one, are appended lexically (no real
// symlink can hide along a path nothing on disk has
// reached), and containment is still enforced on
// every step that follows.
}
Err(error) => {
return Err(error)
.with_context(|| format!("inspecting {}", candidate.display()));
}
}
}
}
if let Some(frame) = stack.last() {
ensure!(
frame.resolved.starts_with(canonical_root),
"frozen source symlink escapes its root"
);
}
}
}
#[cfg(unix)]
fn validate_frozen_source_symlink(root: &Path, path: &Path) -> Result<()> {
let canonical_root = root.canonicalize().context("resolving frozen source root")?;
resolve_frozen_source_symlink(&canonical_root, path).map(|_| ())
}
/// S6: run once, against the tree's real, final published path -- after the
/// atomic rename off `.partial`, in `freeze_exact`, and again (read-only)
/// whenever `observe_frozen` re-checks a tree it did not just write. Separate
/// from `validate_frozen_source_tree`'s ownership and mode checks (already
/// covered by `harden_frozen_source` at write time) so this stays one cheap
/// extra walk, not a second full re-hardening pass.
#[cfg(unix)]
fn validate_frozen_source_symlinks(root: &Path) -> Result<()> {
validate_frozen_source_symlinks_tree(root, root)
}
#[cfg(unix)]
fn validate_frozen_source_symlinks_tree(root: &Path, current: &Path) -> Result<()> {
let metadata = fs::symlink_metadata(current)?;
if metadata.is_dir() {
let mut entries = fs::read_dir(current)?.collect::<std::io::Result<Vec<_>>>()?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
validate_frozen_source_symlinks_tree(root, &entry.path())?;
}
} else if metadata.file_type().is_symlink() {
validate_frozen_source_symlink(root, current)?;
}
Ok(())
}
#[cfg(not(unix))]
fn harden_frozen_source(_root: &Path) -> Result<()> {
bail!("frozen source materialization requires Unix permissions")
}
#[cfg(unix)]
fn validate_frozen_source(root: &Path) -> Result<()> {
validate_frozen_source_tree(root, root)
}
#[cfg(unix)]
fn validate_frozen_source_tree(root: &Path, current: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = fs::symlink_metadata(current)?;
ensure!(metadata.uid() == 0, "frozen source entry is not root-owned");
ensure!(
current == root || current.file_name() != Some(OsStr::new(".git")),
"frozen source contains forbidden .git metadata"
);
if metadata.is_dir() {
ensure!(
metadata.permissions().mode() & 0o777 == 0o555,
"frozen source directory is not 0555"
);
let mut entries = fs::read_dir(current)?.collect::<std::io::Result<Vec<_>>>()?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
validate_frozen_source_tree(root, &entry.path())?;
}
} else if metadata.is_file() {
ensure!(
matches!(metadata.permissions().mode() & 0o777, 0o444 | 0o555),
"frozen source file has a noncanonical mode"
);
} else if metadata.file_type().is_symlink() {
validate_frozen_source_symlink(root, current)?;
} else {
bail!("frozen source contains a special filesystem entry")
}
Ok(())
}
#[cfg(not(unix))]
fn validate_frozen_source(_root: &Path) -> Result<()> {
bail!("frozen source observation requires Unix permissions")
}
#[cfg(not(unix))]
fn validate_frozen_source_symlinks(_root: &Path) -> Result<()> {
bail!("frozen source observation requires Unix permissions")
}
#[cfg(unix)]
fn frozen_source_sha256(root: &Path) -> Result<String> {
let mut hasher = Sha256::new();
hash_frozen_source_tree(root, root, &mut hasher)?;
Ok(format!("sha256-{:x}", hasher.finalize()))
}
#[cfg(unix)]
fn hash_frozen_source_tree(root: &Path, current: &Path, hasher: &mut Sha256) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut entries = fs::read_dir(current)?.collect::<std::io::Result<Vec<_>>>()?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
let path = entry.path();
let relative = normalized_relative(path.strip_prefix(root)?)?;
let metadata = fs::symlink_metadata(&path)?;
if metadata.is_dir() {
hasher.update(b"dir\0");
hasher.update(relative.as_bytes());
hasher.update(b"\0");
hash_frozen_source_tree(root, &path, hasher)?;
} else if metadata.is_file() {
hasher.update(b"file\0");
hasher.update(relative.as_bytes());
hasher.update(b"\0");
hasher.update(
(metadata.permissions().mode() & 0o111 != 0)
.to_string()
.as_bytes(),
);
hasher.update(b"\0");
hasher.update(metadata.len().to_le_bytes());
ensure!(
hash_file_streamed(&path, hasher)? == metadata.len(),
"frozen source file changed while hashing"
);
} else if metadata.file_type().is_symlink() {
let target = fs::read_link(&path)?;
hasher.update(b"link\0");
hasher.update(relative.as_bytes());
hasher.update(b"\0");
hasher.update(target.as_os_str().as_encoded_bytes());
hasher.update(b"\0");
} else {
bail!("frozen source contains a special filesystem entry")
}
}
Ok(())
}
#[cfg(not(unix))]
fn frozen_source_sha256(_root: &Path) -> Result<String> {
bail!("frozen source observation requires Unix permissions")
}
#[cfg(unix)]
fn harden_installed_release(root: &Path, artifacts: &[ArtifactReceipt]) -> Result<()> {
let mut executable_paths = BTreeSet::new();
for artifact in artifacts {
let destination = root.join(&artifact.destination);
for member in &artifact.executable_members {
executable_paths.insert(destination.join(member));
}
if artifact.executable {
executable_paths.insert(destination);
}
}
harden_root_tree(root, root, &mut executable_paths)?;
ensure!(
executable_paths.is_empty(),
"declared executable is missing or not a regular file"
);
Ok(())
}
/// Each regular file the walk seals 0555 is removed from `executable_paths`;
/// whatever remains was declared executable and never sealed.
#[cfg(unix)]
fn harden_root_tree(
root: &Path,
current: &Path,
executable_paths: &mut BTreeSet<PathBuf>,
) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = fs::symlink_metadata(current)?;
ensure!(
metadata.uid() == 0,
"sealed release entry is not root-owned"
);
if metadata.is_dir() {
let mut entries = fs::read_dir(current)?.collect::<std::io::Result<Vec<_>>>()?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
harden_root_tree(root, &entry.path(), &mut *executable_paths)?;
}
fs::set_permissions(current, fs::Permissions::from_mode(0o555))?;
} else if metadata.is_file() {
fs::set_permissions(
current,
fs::Permissions::from_mode(if executable_paths.remove(current) {
0o555
} else {
0o444
}),
)?;
} else if metadata.file_type().is_symlink() {
validate_release_symlink(root, current)?;
} else {
bail!("sealed release contains a special filesystem entry")
}
Ok(())
}
#[cfg(unix)]
fn validate_release_symlink(root: &Path, path: &Path) -> Result<()> {
let target = fs::read_link(path)?;
ensure!(target.is_relative(), "sealed release symlink is absolute");
ensure!(
target.components().all(|component| matches!(
component,
std::path::Component::CurDir | std::path::Component::Normal(_)
)),
"sealed release symlink has a parent traversal"
);
let destination = path
.parent()
.context("sealed release symlink has no parent")?
.join(target);
ensure!(
destination.starts_with(root),
"sealed release symlink escaped its release"
);
Ok(())
}
#[cfg(not(unix))]
fn harden_installed_release(_root: &Path, _artifacts: &[ArtifactReceipt]) -> Result<()> {
bail!("systemd workload installation requires Unix permissions")
}
#[cfg(unix)]
fn harden_runtime_bundle(bundle: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = fs::symlink_metadata(bundle)?;
let parent = bundle.parent().context("runtime bundle has no parent")?;
let parent_metadata = fs::symlink_metadata(parent)?;
ensure!(
metadata.is_dir() && !metadata.file_type().is_symlink() && metadata.uid() == 0,
"runtime bundle is not a root-owned directory"
);
ensure!(
parent_metadata.is_dir()
&& !parent_metadata.file_type().is_symlink()
&& parent_metadata.uid() == 0
&& parent_metadata.permissions().mode() & 0o022 == 0,
"runtime bundle root is not root-owned and service-nonwritable"
);
for name in [
"expected.cc",
"expected.cc.lock",
"activation.cc",
"activation.cc.lock",
] {
let path = bundle.join(name);
let metadata = fs::symlink_metadata(&path)?;
ensure!(
metadata.is_file() && !metadata.file_type().is_symlink() && metadata.uid() == 0,
"runtime bundle document is not root-owned"
);
fs::set_permissions(path, fs::Permissions::from_mode(0o444))?;
}
fs::set_permissions(bundle, fs::Permissions::from_mode(0o555))?;
Ok(())
}
#[cfg(not(unix))]
fn harden_runtime_bundle(_bundle: &Path) -> Result<()> {
bail!("systemd runtime bundles require Unix permissions")
}
fn observe_parent_only_file_descriptor(
fd_number: u32,
fd_name: &str,
source_path: &Path,
) -> Result<ParentOnlyFileDescriptorObservation> {
validate_open_file_component(fd_name, "parent-only descriptor name")?;
ensure!(
source_path.is_absolute(),
"parent-only descriptor source is not absolute"
);
validate_open_file_component(
&source_path.as_os_str().to_string_lossy(),
"parent-only descriptor source",
)?;
let mut file = open_native_read_only(source_path).with_context(|| {
format!(
"opening parent-only descriptor source {}",
source_path.display()
)
})?;
let metadata = file.metadata()?;
ensure!(
metadata.is_file() && metadata.len() > 0,
"parent-only descriptor source is not one nonempty native file"
);
#[cfg(unix)]
let (device, inode, uid, gid, mode, links) = {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
(
metadata.dev(),
metadata.ino(),
metadata.uid(),
metadata.gid(),
metadata.permissions().mode() & 0o777,
metadata.nlink(),
)
};
#[cfg(not(unix))]
let (device, inode, uid, gid, mode, links) = (0, 0, 0, 0, 0, 0);
ensure!(
uid == 0 && gid == 0 && mode == 0o400 && links == 1,
"parent-only descriptor source is not root-owned, 0400, and singly linked"
);
Ok(ParentOnlyFileDescriptorObservation {
fd_number,
fd_name: fd_name.to_owned(),
source_path: source_path.to_owned(),
access: "read-only".into(),
device,
inode,
uid,
gid,
mode,
links,
size: metadata.len(),
sha256: sha256_reader(&mut file)?,
})
}
fn parent_only_open_file_properties(
descriptors: &[ParentOnlyFileDescriptorObservation],
) -> Result<Vec<String>> {
ensure!(
descriptors.len() == 2
&& descriptors[0].fd_number == 3
&& descriptors[0].fd_name == IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME
&& descriptors[1].fd_number == 4
&& descriptors[1].fd_name == RUNTIME_PRESENCE_IDENTITY_FD_NAME,
"parent-only descriptor set is not the exact ordered activation/presence pair"
);
descriptors
.iter()
.map(|descriptor| {
validate_open_file_component(&descriptor.fd_name, "parent-only descriptor name")?;
validate_open_file_component(
&descriptor.source_path.as_os_str().to_string_lossy(),
"parent-only descriptor source",
)?;
ensure!(
descriptor.source_path.is_absolute()
&& descriptor.access == "read-only"
&& descriptor.uid == 0
&& descriptor.gid == 0
&& descriptor.mode == 0o400
&& descriptor.links == 1
&& descriptor.size > 0,
"parent-only descriptor metadata is outside the Idunn contract"
);
Ok(format!(
"{}:{}:{}",
descriptor.source_path.display(),
descriptor.fd_name,
descriptor.access
))
})
.collect()
}
fn validate_open_file_component(value: &str, label: &str) -> Result<()> {
ensure!(
!value.is_empty() && !value.contains(':') && !value.chars().any(char::is_control),
"{label} cannot be represented by systemd OpenFile"
);
Ok(())
}
#[cfg(unix)]
fn ensure_activation_credential_root(path: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
ensure!(
unsafe { libc::geteuid() } == 0,
"activation credential actuation requires root Idunn"
);
ensure!(
path.is_absolute(),
"activation credential root is not absolute"
);
let parent = path
.parent()
.context("activation credential root has no parent")?;
let parent_metadata = fs::symlink_metadata(parent).with_context(|| {
format!(
"inspecting activation credential parent {}",
parent.display()
)
})?;
ensure!(
parent_metadata.is_dir()
&& !parent_metadata.file_type().is_symlink()
&& parent_metadata.uid() == 0
&& parent_metadata.permissions().mode() & 0o022 == 0
&& parent.canonicalize()? == parent,
"activation credential parent is not a canonical root-owned directory"
);
match fs::symlink_metadata(path) {
Ok(metadata) => ensure!(
metadata.is_dir()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == 0
&& metadata.permissions().mode() & 0o777 == 0o700
&& path.canonicalize()? == path,
"activation credential root is not an exact root-only native directory"
),
Err(error) if error.kind() == ErrorKind::NotFound => {
fs::create_dir(path).with_context(|| {
format!("creating activation credential root {}", path.display())
})?;
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
let metadata = fs::symlink_metadata(path)?;
ensure!(
metadata.is_dir()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == 0
&& metadata.permissions().mode() & 0o777 == 0o700,
"new activation credential root has the wrong authority"
);
}
Err(error) => {
return Err(error).with_context(|| {
format!("inspecting activation credential root {}", path.display())
});
}
}
Ok(())
}
#[cfg(not(unix))]
fn ensure_activation_credential_root(_path: &Path) -> Result<()> {
bail!("systemd activation credentials require a Unix authority path")
}
#[cfg(unix)]
fn validate_activation_credential_source(path: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = fs::symlink_metadata(path)
.with_context(|| format!("inspecting activation credential source {}", path.display()))?;
ensure!(
metadata.is_file()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == 0
&& metadata.permissions().mode() & 0o777 == 0o400
&& metadata.nlink() == 1
&& metadata.len() == 32,
"activation credential source is not one exact root-owned 0400 32-byte file"
);
Ok(())
}
#[cfg(not(unix))]
fn validate_activation_credential_source(_path: &Path) -> Result<()> {
bail!("systemd activation credentials require Unix file authority")
}
#[cfg(unix)]
fn validate_service_credential_sources(sources: &BTreeMap<String, PathBuf>) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
for (name, path) in sources {
ensure!(
path.is_absolute(),
"service credential {name} is not absolute"
);
let parent = path
.parent()
.context("service credential source has no parent")?;
let parent_metadata = fs::symlink_metadata(parent).with_context(|| {
format!("inspecting service credential parent {}", parent.display())
})?;
let metadata = fs::symlink_metadata(path)
.with_context(|| format!("inspecting service credential source {}", path.display()))?;
ensure!(
parent.canonicalize()? == parent
&& parent_metadata.is_dir()
&& !parent_metadata.file_type().is_symlink()
&& parent_metadata.uid() == 0
&& parent_metadata.permissions().mode() & 0o022 == 0
&& path.canonicalize()? == path.as_path()
&& metadata.is_file()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == 0
&& metadata.permissions().mode() & 0o777 == 0o400
&& metadata.nlink() == 1
&& metadata.len() > 0,
"service credential {name} is not one canonical root-owned 0400 file"
);
}
Ok(())
}
#[cfg(not(unix))]
fn validate_service_credential_sources(_sources: &BTreeMap<String, PathBuf>) -> Result<()> {
bail!("systemd service credentials require Unix file authority")
}
fn remove_activation_credential_source(path: &Path) -> Result<()> {
match fs::symlink_metadata(path) {
Ok(metadata) => {
validate_activation_credential_source_for_removal(path, &metadata)?;
fs::remove_file(path).with_context(|| {
format!("deleting activation credential source {}", path.display())
})?;
sync_parent_directory(path)?;
ensure!(
matches!(fs::symlink_metadata(path), Err(error) if error.kind() == ErrorKind::NotFound),
"activation credential source remained after deletion"
);
Ok(())
}
Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
Err(error) => Err(error)
.with_context(|| format!("inspecting activation credential source {}", path.display())),
}
}
#[cfg(unix)]
fn validate_activation_credential_source_for_removal(
path: &Path,
metadata: &fs::Metadata,
) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let parent = path
.parent()
.context("activation credential source has no parent")?;
let parent_metadata = fs::symlink_metadata(parent)?;
ensure!(
parent_metadata.is_dir()
&& !parent_metadata.file_type().is_symlink()
&& parent_metadata.uid() == 0
&& parent_metadata.gid() == 0
&& parent_metadata.permissions().mode() & 0o777 == 0o700
&& metadata.is_file()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == 0
&& metadata.nlink() == 1
&& metadata.len() <= 32,
"refusing to delete a surprising activation credential source"
);
Ok(())
}
#[cfg(not(unix))]
fn validate_activation_credential_source_for_removal(
_path: &Path,
_metadata: &fs::Metadata,
) -> Result<()> {
bail!("systemd activation credentials require Unix file authority")
}
fn sync_parent_directory(path: &Path) -> Result<()> {
let parent = path.parent().context("file has no parent to synchronize")?;
let directory = open_native_read_only(parent)?;
directory
.sync_all()
.with_context(|| format!("synchronizing directory {}", parent.display()))
}
fn open_native_read_only(path: &Path) -> Result<fs::File> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW);
}
Ok(options.open(path)?)
}
fn open_proc_magic_link(path: &Path) -> Result<fs::File> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_CLOEXEC);
}
options
.open(path)
.with_context(|| format!("opening procfs executable {}", path.display()))
}
fn sha256_reader(reader: &mut impl Read) -> Result<String> {
let mut digest = Sha256::new();
let mut buffer = [0_u8; 64 * 1024];
loop {
let read = reader.read(&mut buffer)?;
if read == 0 {
break;
}
digest.update(&buffer[..read]);
}
Ok(format!("sha256-{:x}", digest.finalize()))
}
fn path_inside_process_root(process_root: &Path, absolute_path: &Path) -> Result<PathBuf> {
ensure!(
process_root.is_absolute() && absolute_path.is_absolute(),
"process-root projection requires absolute paths"
);
ensure!(
absolute_path.components().all(|component| matches!(
component,
std::path::Component::RootDir | std::path::Component::Normal(_)
)),
"process-root projection path is not normalized"
);
Ok(process_root.join(absolute_path.strip_prefix(Path::new("/"))?))
}
fn linux_process_security(
status_path: &Path,
expected_host_pid: u32,
) -> Result<LinuxProcessSecurityObservation> {
let status = fs::read_to_string(status_path)
.with_context(|| format!("reading process status {}", status_path.display()))?;
let uids = linux_status_u32_array(&status, "Uid")?;
let gids = linux_status_u32_array(&status, "Gid")?;
let groups = linux_status_u32_values(&status, "Groups")?;
let cap_inheritable = linux_status_hex_u64(&status, "CapInh")?;
let cap_permitted = linux_status_hex_u64(&status, "CapPrm")?;
let cap_effective = linux_status_hex_u64(&status, "CapEff")?;
let cap_bounding = linux_status_hex_u64(&status, "CapBnd")?;
let cap_ambient = linux_status_hex_u64(&status, "CapAmb")?;
let no_new_privileges = match linux_status_value(&status, "NoNewPrivs")? {
"1" => true,
"0" => false,
_ => bail!("process NoNewPrivs value is invalid"),
};
let namespace_pids = linux_status_u32_values(&status, "NSpid")?;
ensure!(
namespace_pids.first() == Some(&expected_host_pid),
"process status belongs to another host pid"
);
Ok(LinuxProcessSecurityObservation {
uids,
gids,
groups,
cap_inheritable,
cap_permitted,
cap_effective,
cap_bounding,
cap_ambient,
no_new_privileges,
namespace_pids,
})
}
fn linux_status_value<'a>(status: &'a str, name: &str) -> Result<&'a str> {
let prefix = format!("{name}:");
let matches = status
.lines()
.filter_map(|line| line.strip_prefix(&prefix))
.collect::<Vec<_>>();
ensure!(
matches.len() == 1,
"process status has zero or duplicate {name} fields"
);
Ok(matches[0].trim())
}
fn linux_status_u32_values(status: &str, name: &str) -> Result<Vec<u32>> {
linux_status_value(status, name)?
.split_whitespace()
.map(|value| {
value
.parse::<u32>()
.with_context(|| format!("process status {name} value is not a u32"))
})
.collect()
}
fn linux_status_u32_array(status: &str, name: &str) -> Result<[u32; 4]> {
linux_status_u32_values(status, name)?
.try_into()
.map_err(|_| anyhow::anyhow!("process status {name} does not have four values"))
}
fn linux_status_hex_u64(status: &str, name: &str) -> Result<u64> {
u64::from_str_radix(linux_status_value(status, name)?, 16)
.with_context(|| format!("process status {name} value is not hexadecimal"))
}
fn linux_namespace_id(path: &Path, kind: &str) -> Result<u64> {
let target = fs::read_link(path)
.with_context(|| format!("reading Linux namespace link {}", path.display()))?;
let target = target
.to_str()
.context("Linux namespace link is not UTF-8")?;
let prefix = format!("{kind}:[");
let id = target
.strip_prefix(&prefix)
.and_then(|value| value.strip_suffix(']'))
.context("Linux namespace link has an unexpected shape")?;
let id = id
.parse::<u64>()
.context("Linux namespace id is not a u64")?;
ensure!(id > 0, "Linux namespace id is zero");
Ok(id)
}
#[cfg(unix)]
fn resolve_group_id(group: &str) -> Result<u32> {
use std::ffi::CString;
if let Ok(group_id) = group.parse::<u32>() {
ensure!(group_id > 0, "state group must be unprivileged");
return Ok(group_id);
}
let group = CString::new(group).context("state group contains a NUL byte")?;
let mut buffer_size = 16_384_usize;
loop {
let mut entry = std::mem::MaybeUninit::<libc::group>::uninit();
let mut result = std::ptr::null_mut();
let mut buffer = vec![0_u8; buffer_size];
let status = unsafe {
libc::getgrnam_r(
group.as_ptr(),
entry.as_mut_ptr(),
buffer.as_mut_ptr().cast(),
buffer.len(),
&mut result,
)
};
if status == libc::ERANGE && buffer_size < 1_048_576 {
buffer_size *= 2;
continue;
}
ensure!(
status == 0,
"resolving state group failed with errno {status}"
);
ensure!(!result.is_null(), "configured state group does not exist");
let entry = unsafe { entry.assume_init() };
ensure!(entry.gr_gid > 0, "state group must be unprivileged");
return Ok(entry.gr_gid);
}
}
#[cfg(not(unix))]
fn resolve_group_id(_group: &str) -> Result<u32> {
bail!("systemd workload groups require a Unix actuator")
}
#[cfg(unix)]
fn validate_workload_writable_path(
path: &Path,
state_group_id: u32,
require_setgid: bool,
) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = fs::symlink_metadata(path)
.with_context(|| format!("inspecting workload writable path {}", path.display()))?;
let mode = metadata.permissions().mode();
ensure!(
!metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == state_group_id
&& mode & 0o002 == 0,
"workload writable path {} is not preprovisioned root:state-group without world write",
path.display()
);
if metadata.is_dir() {
ensure!(
mode & 0o070 == 0o070 && (!require_setgid || mode & 0o2000 != 0),
"workload writable directory {} lacks group access or setgid inheritance",
path.display()
);
} else {
ensure!(
metadata.is_file() && mode & 0o060 == 0o060,
"workload writable file is special or lacks group access"
);
}
Ok(())
}
#[cfg(not(unix))]
fn validate_workload_writable_path(
_path: &Path,
_state_group_id: u32,
_require_setgid: bool,
) -> Result<()> {
bail!("systemd workload writable paths require Unix permissions")
}
fn linux_process_start_time(stat_path: &Path) -> Result<u64> {
let stat = fs::read_to_string(stat_path)
.with_context(|| format!("reading process stat {}", stat_path.display()))?;
let close = stat
.rfind(')')
.context("process stat has no command terminator")?;
let fields = stat[close + 1..].split_whitespace().collect::<Vec<_>>();
fields
.get(19)
.context("process stat has no start-time field")?
.parse()
.context("process start-time field is not a u64")
}
fn required_systemd_property<'a>(
values: &'a BTreeMap<String, String>,
name: &str,
) -> Result<&'a str> {
let value = systemd_property(values, name)?;
ensure!(!value.is_empty(), "systemd unit has no {name}");
Ok(value)
}
fn systemd_property<'a>(values: &'a BTreeMap<String, String>, name: &str) -> Result<&'a str> {
values
.get(name)
.map(String::as_str)
.with_context(|| format!("systemd unit did not expose {name}"))
}
fn parse_systemd_boolean(values: &BTreeMap<String, String>, name: &str) -> Result<bool> {
match systemd_property(values, name)? {
"yes" => Ok(true),
"no" => Ok(false),
_ => bail!("systemd unit exposed a non-boolean {name}"),
}
}
fn read_process_environment(path: &Path) -> Result<Vec<Vec<u8>>> {
let bytes = fs::read(path)
.with_context(|| format!("reading process environment {}", path.display()))?;
Ok(bytes
.split(|byte| *byte == 0)
.filter(|entry| !entry.is_empty())
.map(<[u8]>::to_vec)
.collect())
}
fn required_process_environment_value<'a>(entries: &'a [Vec<u8>], name: &str) -> Result<&'a str> {
let prefix = format!("{name}=").into_bytes();
let matches = entries
.iter()
.filter_map(|entry| entry.strip_prefix(prefix.as_slice()))
.collect::<Vec<_>>();
ensure!(
matches.len() == 1,
"process environment has zero or duplicate {name} values"
);
let value = std::str::from_utf8(matches[0])
.with_context(|| format!("process environment {name} is not UTF-8"))?;
ensure!(!value.is_empty(), "process environment {name} is empty");
Ok(value)
}
fn select_process_environment(
entries: &[Vec<u8>],
names: &[String],
) -> Result<BTreeMap<String, String>> {
ensure!(
names.windows(2).all(|pair| pair[0] < pair[1]),
"workload environment witness names are not unique and sorted"
);
let mut selected = BTreeMap::new();
for name in names {
let prefix = format!("{name}=").into_bytes();
let matches = entries
.iter()
.filter(|entry| entry.starts_with(&prefix))
.collect::<Vec<_>>();
ensure!(
matches.len() == 1,
"workload process environment has missing or duplicate {name}"
);
let value = std::str::from_utf8(&matches[0][prefix.len()..])
.with_context(|| format!("workload environment {name} is not UTF-8"))?;
selected.insert(name.clone(), value.to_owned());
}
Ok(selected)
}
fn proc_command_line(command: &[OsString]) -> Vec<u8> {
let mut encoded = Vec::new();
for argument in command {
encoded.extend_from_slice(argument.as_encoded_bytes());
encoded.push(0);
}
encoded
}
fn endpoint_host_port(endpoint: &str, scheme: &str) -> Result<(String, u16)> {
let authority = endpoint
.strip_prefix(scheme)
.with_context(|| format!("endpoint does not use {scheme}"))?;
ensure!(
!authority
.chars()
.any(|character| matches!(character, '/' | '?' | '#' | '@'))
&& !authority.starts_with('['),
"route endpoint is not a plain host and port"
);
let (host, port) = authority
.rsplit_once(':')
.context("route endpoint has no port")?;
ensure!(
!host.is_empty()
&& host
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-')),
"route endpoint host is invalid"
);
let port: u16 = port.parse().context("route endpoint port is not a u16")?;
ensure!(port > 0, "route endpoint port is zero");
Ok((host.to_owned(), port))
}
fn nginx_identifier(route_id: &str) -> Result<String> {
ensure!(!route_id.is_empty(), "nginx route id is empty");
let mut identifier = String::from("idunn_");
for byte in route_id.bytes() {
match byte {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' => {
identifier.push(char::from(byte));
}
b'-' | b'.' => identifier.push('_'),
_ => bail!("route id cannot lower to an nginx identifier"),
}
}
Ok(identifier)
}
fn atomic_replace(path: &Path, bytes: &[u8]) -> Result<()> {
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
ensure!(path.is_absolute(), "route path is not absolute");
let parent = path.parent().context("route path has no parent")?;
ensure_route_authority_parent(parent)?;
match fs::symlink_metadata(path) {
Ok(_) => validate_root_owned_file(path, 0o644)?,
Err(error) if error.kind() == ErrorKind::NotFound => {}
Err(error) => return Err(error).context("inspecting route fragment"),
}
let file_name = path
.file_name()
.context("route path has no file name")?
.to_string_lossy();
let temporary = parent.join(format!(".{file_name}.idunn-{}", Uuid::new_v4()));
let mut options = fs::OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
options.mode(0o644);
let mut file = options
.open(&temporary)
.with_context(|| format!("creating route stage {}", temporary.display()))?;
#[cfg(unix)]
file.set_permissions(fs::Permissions::from_mode(0o644))?;
file.write_all(bytes)?;
file.sync_all()?;
drop(file);
validate_root_owned_file(&temporary, 0o644)?;
if let Err(error) = fs::rename(&temporary, path) {
let _ = fs::remove_file(&temporary);
return Err(error).with_context(|| format!("publishing route {}", path.display()));
}
sync_parent_directory(path)?;
validate_root_owned_file(path, 0o644)
}
#[cfg(unix)]
fn ensure_route_authority_parent(path: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
ensure!(
unsafe { libc::geteuid() } == 0,
"route actuation requires root Idunn"
);
ensure!(path.is_absolute(), "route authority parent is not absolute");
if !path.exists() {
let ancestor = path
.parent()
.context("route authority parent has no ancestor")?;
let ancestor_metadata = fs::symlink_metadata(ancestor)?;
ensure!(
ancestor.canonicalize()? == ancestor
&& ancestor_metadata.is_dir()
&& !ancestor_metadata.file_type().is_symlink()
&& ancestor_metadata.uid() == 0
&& ancestor_metadata.permissions().mode() & 0o022 == 0,
"route authority ancestor is not canonical root-owned and nonwritable"
);
fs::create_dir(path)?;
fs::set_permissions(path, fs::Permissions::from_mode(0o755))?;
}
let metadata = fs::symlink_metadata(path)?;
ensure!(
path.canonicalize()? == path
&& metadata.is_dir()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == 0
&& metadata.permissions().mode() & 0o022 == 0,
"route authority parent is not canonical root-owned and nonwritable"
);
Ok(())
}
#[cfg(not(unix))]
fn ensure_route_authority_parent(_path: &Path) -> Result<()> {
bail!("nginx route actuation requires Unix file authority")
}
#[cfg(unix)]
fn ensure_route_preflight_root(path: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
ensure!(path.is_absolute(), "route preflight root is not absolute");
if !path.exists() {
let parent = path
.parent()
.context("route preflight root has no parent")?;
ensure_route_authority_parent(parent)?;
fs::create_dir(path)?;
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
}
let metadata = fs::symlink_metadata(path)?;
ensure!(
unsafe { libc::geteuid() } == 0
&& path.canonicalize()? == path
&& metadata.is_dir()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == 0
&& metadata.permissions().mode() & 0o777 == 0o700,
"route preflight root is not one canonical root-only directory"
);
Ok(())
}
#[cfg(not(unix))]
fn ensure_route_preflight_root(_path: &Path) -> Result<()> {
bail!("nginx route preflight requires Unix file authority")
}
#[cfg(unix)]
fn write_root_owned_file(path: &Path, bytes: &[u8], mode: u32) -> Result<()> {
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
let mut options = OpenOptions::new();
options.create_new(true).write(true).mode(mode);
let mut file = options.open(path)?;
file.set_permissions(fs::Permissions::from_mode(mode))?;
file.write_all(bytes)?;
file.sync_all()?;
drop(file);
sync_parent_directory(path)?;
validate_root_owned_file(path, mode)
}
#[cfg(not(unix))]
fn write_root_owned_file(_path: &Path, _bytes: &[u8], _mode: u32) -> Result<()> {
bail!("root-owned route material requires Unix file authority")
}
#[cfg(unix)]
fn validate_root_owned_file(path: &Path, mode: u32) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = fs::symlink_metadata(path)?;
ensure!(
path.canonicalize()? == path
&& metadata.is_file()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == 0
&& metadata.permissions().mode() & 0o777 == mode
&& metadata.nlink() == 1,
"route authority material is not one canonical root-owned file"
);
Ok(())
}
#[cfg(not(unix))]
fn validate_root_owned_file(_path: &Path, _mode: u32) -> Result<()> {
bail!("root-owned route material requires Unix file authority")
}
fn remove_exact_root_owned_file(path: &Path, mode: u32) -> Result<()> {
validate_root_owned_file(path, mode)?;
fs::remove_file(path)?;
sync_parent_directory(path)
}
/// A build container's own machine identity, mounted read-only at
/// `/etc/machine-id`.
///
/// The image carries no machine-id, and code that binds a service identity to
/// the machine -- CultLib's Linux protector does exactly this -- cannot run
/// without one, so a target whose tests enrol an identity cannot be built at
/// all. The host's machine-id is not the answer: it would let a build container
/// protect a seed that unwraps on the host, which is precisely the property the
/// binding exists to deny.
///
/// So each frozen workspace gets its own identity, derived from the workspace
/// path. It is stable for the life of a build, and an identity enrolled inside
/// one is unusable anywhere else, including on this host.
/// Every directory above the runtime bundle must be traversable by the
/// workload, which runs under `DynamicUser` and is in no group but the state
/// group.
///
/// This is checked here rather than left to the operator because the failure is
/// silent and expensive: CultLib's backing store reports an unreadable file as
/// an *empty* store, so a workload that cannot traverse to its bundle sees zero
/// records rather than a permission error. Odin reported "runtime authority
/// store must contain exactly one record" and Heimdall simply decided it held
/// no write lease and warmed forever. Neither names the actual fault.
///
/// `ReadOnlyPaths=` on the bundle does not help: it binds the leaf into the
/// unit's namespace but grants no traversal on the path above it.
#[cfg(unix)]
fn ensure_bundle_is_reachable_by_workload(
bundle: &Path,
state_group_id: Option<u32>,
) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
for ancestor in bundle.ancestors().skip(1) {
if ancestor == Path::new("/") || ancestor.as_os_str().is_empty() {
break;
}
let metadata =
fs::metadata(ancestor).with_context(|| format!("reading {}", ancestor.display()))?;
let mode = metadata.permissions().mode();
// The workload's uid is allocated by DynamicUser and owns nothing here,
// so traversal can only come from the state group or from other.
let by_group = state_group_id == Some(metadata.gid()) && mode & 0o010 != 0;
ensure!(
mode & 0o001 != 0 || by_group,
"{} is not traversable by the workload, so the runtime bundle cannot be read; give it o+x (0711 leaks no names) or group-own it by the state group with g+x",
ancestor.display()
);
}
Ok(())
}
#[cfg(not(unix))]
fn ensure_bundle_is_reachable_by_workload(
_bundle: &Path,
_state_group_id: Option<u32>,
) -> Result<()> {
Ok(())
}
/// One published file: `0644`, because Idunn's `UMask=027` would land it
/// `0640 root:root` and deny its readers. A chmod by hand does not hold, since
/// each publish writes a new file.
pub(crate) fn publish_file_mode(path: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if path.exists() {
fs::set_permissions(path, fs::Permissions::from_mode(0o644))
.with_context(|| format!("publishing {}", path.display()))?;
}
}
Ok(())
}
/// The one write primitive for a store in the projection directory: one
/// nonblocking compare-exchange of the whole store against the snapshot it was
/// decided on, behind a lock only Idunn can hold.
///
/// Readers of a published store take no lock, so nobody but Idunn needs to
/// open its lock, and a lock another uid can open is a lock another uid can
/// hold to stall every publish. CultCache creates a lock with mode 0666, so
/// the directory's default ACL would decide who may (Yggdrasil's lands it
/// 0644). This makes the lock private first, by this rule: an absent lock is
/// created `O_EXCL` 0600; a lock that is not (a regular file, ours, 0600) is
/// replaced by a fresh 0600 file renamed over it, because a chmod does not
/// revoke a descriptor another uid already holds, while the rename leaves that
/// holder only an orphaned inode. A private lock is opened as it is. No
/// existing lock is ever chmodded. Nothing here names a path in an error.
pub(crate) fn exchange_behind_private_lock(
store: &Path,
expected: &[CultCacheEnvelope],
replacements: &[CultCacheEnvelope],
) -> Result<TryCompareExchangeSnapshotOutcome> {
make_lock_private(store)?;
SingleFileMessagePackBackingStore::new(store)
.try_compare_exchange_snapshot(expected, replacements)
}
#[cfg(unix)]
fn make_lock_private(store: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let lock = authority_lock_path(store);
if let Some(parent) = lock.parent() {
fs::create_dir_all(parent).context("creating the store directory")?;
}
// SAFETY: geteuid has no preconditions.
let euid = unsafe { libc::geteuid() };
// The daemon is the only writer, so a lost create race is defensive only.
for _ in 0..3 {
match fs::symlink_metadata(&lock) {
Ok(metadata)
if metadata.is_file()
&& metadata.uid() == euid
&& metadata.permissions().mode() & 0o777 == 0o600 =>
{
return Ok(());
}
Ok(_) => return replace_with_private_lock(&lock),
Err(error) if error.kind() == ErrorKind::NotFound => {
match create_private_file(&lock) {
Ok(()) => return Ok(()),
Err(error) if error.kind() == ErrorKind::AlreadyExists => {}
Err(error) => return Err(error).context("creating a private store lock"),
}
}
Err(error) => return Err(error).context("inspecting a store lock"),
}
}
bail!("a store lock kept changing while it was made private")
}
#[cfg(not(unix))]
fn make_lock_private(store: &Path) -> Result<()> {
let _ = store;
Ok(())
}
/// A new file nothing else has open, mode 0600 (the create mode is intersected
/// with the umask and the directory's default ACL, which only narrow it).
#[cfg(unix)]
fn create_private_file(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::OpenOptionsExt;
OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
.map(drop)
}
/// Stage a fresh private file beside `lock` and rename it over the lock.
#[cfg(unix)]
fn replace_with_private_lock(lock: &Path) -> Result<()> {
let mut staged = lock.as_os_str().to_os_string();
staged.push(format!(".{}", Uuid::new_v4().simple()));
let staged = PathBuf::from(staged);
create_private_file(&staged).context("staging a private store lock")?;
fs::rename(&staged, lock).map_err(|error| {
let _ = fs::remove_file(&staged);
anyhow::Error::new(error).context("replacing a store lock with a private one")
})
}
fn build_machine_id(workspace: &Path) -> Result<String> {
let text = workspace
.to_str()
.context("frozen workspace path is not UTF-8")?;
// systemd machine-id format: exactly 32 lowercase hex digits.
let digest = sha256_id(text.as_bytes());
Ok(digest
.strip_prefix("sha256-")
.unwrap_or(&digest)
.chars()
.take(32)
.collect())
}
fn build_machine_id_file(workspace: &Path) -> Result<PathBuf> {
let root = PathBuf::from("/run/idunn/build-machine-ids");
fs::create_dir_all(&root).context("creating the build machine-id root")?;
let id = build_machine_id(workspace)?;
let path = root.join(&id);
if !path.exists() {
fs::write(
&path,
format!(
"{id}
"
),
)
.context("writing the build machine-id")?;
#[cfg(unix)]
fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(0o444))
.context("sealing the build machine-id")?;
}
Ok(path)
}
fn bind_mount(source: &Path, destination: &str, read_only: bool) -> Result<OsString> {
ensure!(source.is_absolute(), "Docker bind source is not absolute");
let text = source.to_str().context("Docker bind source is not UTF-8")?;
ensure!(
!text
.chars()
.any(|character| matches!(character, ',' | '\n' | '\r')),
"Docker bind source contains a forbidden mount character"
);
let read_only = if read_only { ",readonly" } else { "" };
Ok(OsString::from(format!(
"type=bind,src={text},dst={destination}{read_only}"
)))
}
fn systemd_read_only_bind_property(source: &Path, destination: &Path) -> Result<OsString> {
ensure!(
source.is_absolute() && destination.is_absolute(),
"systemd bind paths are not absolute"
);
let source = source
.to_str()
.context("systemd bind source is not UTF-8")?;
let destination = destination
.to_str()
.context("systemd bind destination is not UTF-8")?;
ensure!(
[source, destination]
.iter()
.all(|path| !path.contains([':', '\n', '\r', '\0'])),
"systemd bind path contains an unescaped property delimiter"
);
Ok(OsString::from(format!(
"--property=BindReadOnlyPaths={source}:{destination}"
)))
}
fn normalized_relative(path: &Path) -> Result<String> {
ensure!(path.is_relative(), "runner path is not relative");
let mut parts = Vec::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {}
std::path::Component::Normal(value) => parts.push(
value
.to_str()
.context("runner path contains non-UTF-8")?
.to_owned(),
),
_ => bail!("runner path escapes its workspace"),
}
}
Ok(parts.join("/"))
}
pub(crate) fn copy_tree(source: &Path, destination: &Path) -> Result<()> {
ensure!(source.is_dir(), "source tree is not a directory");
ensure!(!destination.exists(), "destination tree already exists");
fs::create_dir_all(destination)?;
copy_tree_contents(source, destination)
}
fn copy_tree_contents(source: &Path, destination: &Path) -> Result<()> {
let mut entries = fs::read_dir(source)?.collect::<std::io::Result<Vec<_>>>()?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
let source_path = entry.path();
let destination_path = destination.join(entry.file_name());
let metadata = fs::symlink_metadata(&source_path)?;
if metadata.is_dir() {
fs::create_dir(&destination_path)?;
copy_tree_contents(&source_path, &destination_path)?;
} else if metadata.is_file() {
fs::copy(&source_path, &destination_path)?;
} else if metadata.file_type().is_symlink() {
copy_symlink(&source_path, &destination_path)?;
} else {
bail!("source tree contains a special filesystem entry")
}
}
Ok(())
}
#[cfg(unix)]
fn copy_symlink(source: &Path, destination: &Path) -> Result<()> {
std::os::unix::fs::symlink(fs::read_link(source)?, destination)?;
Ok(())
}
#[cfg(windows)]
fn copy_symlink(source: &Path, destination: &Path) -> Result<()> {
let target = fs::read_link(source)?;
if source.is_dir() {
std::os::windows::fs::symlink_dir(target, destination)?;
} else {
std::os::windows::fs::symlink_file(target, destination)?;
}
Ok(())
}
/// F5: `source` may name a path reached through an intermediate symlink a
/// runner's own build output created (`fs::symlink_metadata` follows every
/// path component but the last, so a symlinked directory earlier in `source`
/// would otherwise be followed transparently). `root` is resolved and
/// checked to contain the fully resolved `source` with the same
/// whole-chain-aware resolver `validate_frozen_source_symlink` uses, before
/// anything is read from it.
pub(crate) fn copy_artifact(root: &Path, source: &Path, destination: &Path) -> Result<()> {
let canonical_root = root.canonicalize().context("resolving artifact root")?;
let canonical_source = source
.canonicalize()
.with_context(|| format!("resolving artifact source {}", source.display()))?;
ensure!(
canonical_source.starts_with(&canonical_root),
"artifact source escapes its root"
);
let metadata = fs::symlink_metadata(&canonical_source)?;
if metadata.is_dir() {
copy_tree(&canonical_source, destination)
} else if metadata.is_file() {
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&canonical_source, destination)?;
Ok(())
} else {
bail!("artifact output is not a regular file or directory")
}
}
/// The one seal-time owner of mode declarations for a copied artifact: a
/// directory output is never `executable` (it declares `executable_members`),
/// a file output never declares members, and every member is a regular file
/// reached through real directories, never through a symlink. Errors name the
/// rule, never the member path.
pub(crate) fn check_artifact_modes(artifact: &ArtifactOutput, destination: &Path) -> Result<()> {
let is_dir = fs::symlink_metadata(destination)
.context("inspecting a copied artifact")?
.is_dir();
ensure!(
!(is_dir && artifact.executable),
"directory artifact {} cannot be executable; declare executable_members",
artifact.id
);
if !is_dir {
ensure!(
artifact.executable_members.is_empty(),
"file artifact {} cannot declare executable_members",
artifact.id
);
return Ok(());
}
for member in &artifact.executable_members {
ensure!(!member.as_os_str().is_empty(), "executable member is empty");
let mut current = destination.to_path_buf();
let mut components = member.components().peekable();
while let Some(component) = components.next() {
let std::path::Component::Normal(name) = component else {
bail!("executable member has a forbidden path component");
};
current.push(name);
let metadata = match fs::symlink_metadata(&current) {
Ok(metadata) => metadata,
Err(error) if error.kind() == ErrorKind::NotFound => {
bail!("executable member is missing")
}
Err(error) => return Err(error).context("inspecting an executable member"),
};
if components.peek().is_some() {
ensure!(
metadata.is_dir(),
"executable member passes through a symlink or non-directory"
);
} else {
ensure!(metadata.is_file(), "executable member is not a regular file");
}
}
}
Ok(())
}
/// Streams `path` into `hasher` in 64 KiB chunks, so peak memory is the buffer
/// and not the largest file in the tree. Returns the bytes read; a caller that
/// hashed a length first must check the two agree.
pub(crate) fn hash_file_streamed(path: &Path, hasher: &mut Sha256) -> Result<u64> {
let mut file = fs::File::open(path).context("opening a file to hash it")?;
let mut buffer = [0u8; 1 << 16];
let mut total = 0_u64;
loop {
let read = file.read(&mut buffer).context("reading a file to hash it")?;
if read == 0 {
return Ok(total);
}
hasher.update(&buffer[..read]);
total += read as u64;
}
}
pub(crate) fn digest_artifact(path: &Path) -> Result<(String, u64)> {
let metadata = fs::symlink_metadata(path)?;
if metadata.is_file() {
let mut hasher = Sha256::new();
let streamed = hash_file_streamed(path, &mut hasher)?;
ensure!(streamed == metadata.len(), "artifact file changed while hashing");
return Ok((format!("{:x}", hasher.finalize()), streamed));
}
ensure!(metadata.is_dir(), "artifact is not a file or directory");
let mut hasher = Sha256::new();
let mut size = 0_u64;
digest_tree(path, path, &mut hasher, &mut size)?;
ensure!(size > 0, "artifact directory has no file content");
Ok((format!("{:x}", hasher.finalize()), size))
}
fn digest_tree(root: &Path, current: &Path, hasher: &mut Sha256, size: &mut u64) -> Result<()> {
let mut entries = fs::read_dir(current)?.collect::<std::io::Result<Vec<_>>>()?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
let path = entry.path();
let relative = path.strip_prefix(root)?;
let relative = normalized_relative(relative)?;
let metadata = fs::symlink_metadata(&path)?;
if metadata.is_dir() {
hasher.update(b"dir\0");
hasher.update(relative.as_bytes());
hasher.update(b"\0");
digest_tree(root, &path, hasher, size)?;
} else if metadata.is_file() {
let length = metadata.len();
hasher.update(b"file\0");
hasher.update(relative.as_bytes());
hasher.update(b"\0");
hasher.update(length.to_le_bytes());
ensure!(
hash_file_streamed(&path, hasher)? == length,
"artifact file changed while hashing"
);
*size = size.saturating_add(length);
} else if metadata.file_type().is_symlink() {
let target = fs::read_link(&path)?;
hasher.update(b"link\0");
hasher.update(relative.as_bytes());
hasher.update(b"\0");
hasher.update(target.as_os_str().as_encoded_bytes());
*size = size.saturating_add(target.as_os_str().len().try_into()?);
} else {
bail!("artifact tree contains a special filesystem entry")
}
}
Ok(())
}
pub(crate) fn raw_sha256(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}
pub(crate) fn remove_tree_inside(root: &Path, target: &Path) -> Result<()> {
let root = root.canonicalize()?;
let target = target.canonicalize()?;
ensure!(
target.starts_with(&root) && target != root,
"refusing broad tree removal"
);
fs::remove_dir_all(&target)
.with_context(|| format!("removing disposable runner workspace {}", target.display()))
}
pub(crate) fn sha256_id(bytes: &[u8]) -> String {
format!("sha256-{:x}", Sha256::digest(bytes))
}
fn require_git_sha(value: &str, label: &str) -> Result<()> {
ensure!(
value.len() == 40
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)),
"{label} is not a lowercase SHA-1 object id"
);
Ok(())
}
fn require_sha256_id(value: &str, label: &str) -> Result<()> {
let digest = value
.strip_prefix("sha256-")
.with_context(|| format!("{label} has no sha256 prefix"))?;
ensure!(
digest.len() == 64
&& digest
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)),
"{label} is not a lowercase SHA-256 id"
);
Ok(())
}
fn require_driver_id(value: &str, label: &str) -> Result<()> {
ensure!(
!value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|byte| { byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-') }),
"{label} id is invalid"
);
Ok(())
}
fn ensure_source_directory_tree(
root: &Path,
directory: &Path,
identity: Option<ProcessIdentity>,
) -> Result<()> {
ensure!(
directory.starts_with(root),
"source directory escaped Idunn's source authority root"
);
ensure_source_directory(root, identity)?;
let relative = directory.strip_prefix(root)?;
let mut current = root.to_owned();
for component in relative.components() {
let std::path::Component::Normal(component) = component else {
bail!("source directory is not normalized")
};
current.push(component);
ensure_source_directory(&current, identity)?;
}
Ok(())
}
fn container_identity(value: &str) -> Result<ProcessIdentity> {
let (uid, gid) = value
.split_once(':')
.context("runner user is not numeric UID:GID")?;
let identity = ProcessIdentity {
uid: uid.parse().context("runner UID is not a u32")?,
gid: gid.parse().context("runner GID is not a u32")?,
};
ensure!(
identity.uid > 0 && identity.gid > 0,
"runner identity must be unprivileged"
);
Ok(identity)
}
#[cfg(unix)]
fn ensure_runner_cache_root(path: &Path, identity: ProcessIdentity) -> Result<()> {
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
ensure!(
unsafe { libc::geteuid() } == 0,
"runner cache admission requires root Idunn"
);
let parent = path.parent().context("runner cache root has no parent")?;
let parent_metadata = fs::symlink_metadata(parent)
.with_context(|| format!("inspecting runner cache parent {}", parent.display()))?;
ensure!(
parent_metadata.is_dir()
&& !parent_metadata.file_type().is_symlink()
&& parent_metadata.uid() == 0
&& parent_metadata.permissions().mode() & 0o022 == 0,
"runner cache parent is not root-owned and nonwritable"
);
match fs::symlink_metadata(path) {
Ok(metadata) => ensure!(
metadata.is_dir()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == identity.uid
&& metadata.gid() == identity.gid
&& metadata.permissions().mode() & 0o777 == 0o700,
"runner cache is not a dedicated exact-identity 0700 directory"
),
Err(error) if error.kind() == ErrorKind::NotFound => {
fs::create_dir(path)
.with_context(|| format!("creating runner cache {}", path.display()))?;
let path_c = std::ffi::CString::new(path.as_os_str().as_bytes())
.context("runner cache path contains a NUL byte")?;
if unsafe { libc::lchown(path_c.as_ptr(), identity.uid, identity.gid) } != 0 {
return Err(std::io::Error::last_os_error())
.context("assigning runner cache owner");
}
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
}
Err(error) => {
return Err(error)
.with_context(|| format!("inspecting runner cache {}", path.display()));
}
}
Ok(())
}
#[cfg(not(unix))]
fn ensure_runner_cache_root(_path: &Path, _identity: ProcessIdentity) -> Result<()> {
bail!("runner cache admission requires a Unix actuator")
}
#[cfg(unix)]
fn validate_runner_secret(path: &Path, identity: ProcessIdentity) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
ensure!(
unsafe { libc::geteuid() } == 0,
"runner secret admission requires root Idunn"
);
let parent = path.parent().context("runner secret has no parent")?;
let parent_metadata = fs::symlink_metadata(parent)
.with_context(|| format!("inspecting runner secret parent {}", parent.display()))?;
let metadata = fs::symlink_metadata(path)
.with_context(|| format!("inspecting runner secret {}", path.display()))?;
ensure!(
parent_metadata.is_dir()
&& !parent_metadata.file_type().is_symlink()
&& parent_metadata.uid() == 0
&& parent_metadata.permissions().mode() & 0o022 == 0,
"runner secret parent is not root-owned and nonwritable"
);
ensure!(
metadata.is_file()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == identity.gid
&& metadata.permissions().mode() & 0o777 == 0o440,
"runner secret is not root-owned, exact-group-bound, and 0440"
);
Ok(())
}
#[cfg(not(unix))]
fn validate_runner_secret(_path: &Path, _identity: ProcessIdentity) -> Result<()> {
bail!("runner secret admission requires a Unix actuator")
}
#[cfg(unix)]
fn assign_runner_tree(path: &Path, identity: ProcessIdentity) -> Result<()> {
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::PermissionsExt;
ensure!(
unsafe { libc::geteuid() } == 0,
"runner workspace ownership requires root Idunn"
);
let metadata = fs::symlink_metadata(path)?;
if metadata.is_dir() {
let mut entries = fs::read_dir(path)?.collect::<std::io::Result<Vec<_>>>()?;
entries.sort_by_key(|entry| entry.file_name());
for entry in entries {
assign_runner_tree(&entry.path(), identity)?;
}
fs::set_permissions(path, fs::Permissions::from_mode(0o750))?;
} else if metadata.is_file() {
let executable = metadata.permissions().mode() & 0o111 != 0;
fs::set_permissions(
path,
fs::Permissions::from_mode(if executable { 0o750 } else { 0o640 }),
)?;
} else if !metadata.file_type().is_symlink() {
bail!("runner workspace contains a special filesystem entry")
}
let path_c = std::ffi::CString::new(path.as_os_str().as_bytes())
.context("runner workspace path contains a NUL byte")?;
let result = unsafe { libc::lchown(path_c.as_ptr(), identity.uid, identity.gid) };
if result != 0 {
return Err(std::io::Error::last_os_error()).context("assigning runner workspace owner");
}
Ok(())
}
#[cfg(not(unix))]
fn assign_runner_tree(_path: &Path, _identity: ProcessIdentity) -> Result<()> {
bail!("non-root runner identities require a Unix actuator")
}
fn ensure_source_directory(path: &Path, identity: Option<ProcessIdentity>) -> Result<()> {
match fs::symlink_metadata(path) {
Ok(metadata) => {
ensure!(
metadata.is_dir() && !metadata.file_type().is_symlink(),
"source authority path {} is not a native directory",
path.display()
);
validate_source_directory_owner(path, &metadata, identity)
}
Err(error) if error.kind() == ErrorKind::NotFound => {
let parent = path
.parent()
.context("source authority directory has no parent")?;
ensure!(
parent.is_dir(),
"source authority parent {} is absent",
parent.display()
);
fs::create_dir(path).with_context(|| {
format!("creating source authority directory {}", path.display())
})?;
assign_source_directory_owner(path, identity)?;
let metadata = fs::symlink_metadata(path)?;
validate_source_directory_owner(path, &metadata, identity)
}
Err(error) => Err(error)
.with_context(|| format!("inspecting source authority path {}", path.display())),
}
}
pub(crate) fn authority_lock_path(path: &Path) -> PathBuf {
let mut lock = path.as_os_str().to_os_string();
lock.push(".lock");
PathBuf::from(lock)
}
#[cfg(unix)]
fn validate_root_authority_path(path: &Path) -> Result<()> {
ensure!(
unsafe { libc::geteuid() } == 0,
"process write-lease actuation requires root Idunn"
);
validate_authority_path_custody(path)
}
#[cfg(unix)]
fn validate_authority_path_custody(path: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let parent = path
.parent()
.context("process write-lease path has no parent")?;
let parent_metadata = fs::symlink_metadata(parent)
.with_context(|| format!("inspecting write-lease parent {}", parent.display()))?;
ensure!(
parent_metadata.is_dir()
&& !parent_metadata.file_type().is_symlink()
&& parent_metadata.uid() == 0
&& parent_metadata.permissions().mode() & 0o022 == 0,
"write-lease parent is not root-owned and service-nonwritable"
);
for authority_file in [path.to_owned(), authority_lock_path(path)] {
match fs::symlink_metadata(&authority_file) {
Ok(metadata) => ensure!(
metadata.is_file()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == parent_metadata.gid()
&& metadata.permissions().mode() & 0o022 == 0,
"write-lease authority file {} is not root-owned and service-nonwritable",
authority_file.display()
),
Err(error) if error.kind() == ErrorKind::NotFound => {}
Err(error) => {
return Err(error).with_context(|| {
format!(
"inspecting write-lease authority file {}",
authority_file.display()
)
});
}
}
}
Ok(())
}
#[cfg(not(unix))]
fn validate_root_authority_path(_path: &Path) -> Result<()> {
bail!("process write-lease actuation requires a Unix authority path")
}
#[cfg(not(unix))]
fn validate_authority_path_custody(_path: &Path) -> Result<()> {
bail!("process write-lease actuation requires a Unix authority path")
}
#[cfg(unix)]
fn harden_root_authority_file(path: &Path) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let parent = path
.parent()
.context("process write-lease authority file has no parent")?;
let parent_metadata = fs::symlink_metadata(parent)?;
let metadata = fs::symlink_metadata(path)?;
ensure!(
metadata.is_file()
&& !metadata.file_type().is_symlink()
&& metadata.uid() == 0
&& metadata.gid() == parent_metadata.gid(),
"new write-lease authority file has the wrong owner"
);
fs::set_permissions(path, fs::Permissions::from_mode(0o640))?;
Ok(())
}
#[cfg(not(unix))]
fn harden_root_authority_file(_path: &Path) -> Result<()> {
bail!("process write-lease actuation requires Unix permissions")
}
#[cfg(unix)]
fn assign_source_directory_owner(path: &Path, identity: Option<ProcessIdentity>) -> Result<()> {
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::PermissionsExt;
if let Some(identity) = identity {
let path = std::ffi::CString::new(path.as_os_str().as_bytes())
.context("source authority path contains a NUL byte")?;
let result = unsafe { libc::chown(path.as_ptr(), identity.uid, identity.gid) };
if result != 0 {
return Err(std::io::Error::last_os_error())
.context("assigning source authority directory owner");
}
}
fs::set_permissions(path, fs::Permissions::from_mode(0o750))?;
Ok(())
}
#[cfg(not(unix))]
fn assign_source_directory_owner(_path: &Path, identity: Option<ProcessIdentity>) -> Result<()> {
ensure!(
identity.is_none(),
"configured source identities require a Unix actuator"
);
Ok(())
}
#[cfg(unix)]
fn validate_source_directory_owner(
path: &Path,
metadata: &fs::Metadata,
identity: Option<ProcessIdentity>,
) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let expected = identity.unwrap_or(ProcessIdentity {
uid: unsafe { libc::geteuid() },
gid: unsafe { libc::getegid() },
});
ensure!(
metadata.uid() == expected.uid && metadata.gid() == expected.gid,
"source authority directory {} has the wrong owner",
path.display()
);
ensure!(
metadata.permissions().mode() & 0o200 != 0,
"source authority directory {} is not owner-writable",
path.display()
);
Ok(())
}
#[cfg(not(unix))]
fn validate_source_directory_owner(
_path: &Path,
_metadata: &fs::Metadata,
identity: Option<ProcessIdentity>,
) -> Result<()> {
ensure!(
identity.is_none(),
"configured source identities require a Unix actuator"
);
Ok(())
}
fn apply_identity(command: &mut Command, identity: Option<ProcessIdentity>) -> Result<()> {
let Some(identity) = identity else {
return Ok(());
};
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
ensure!(
identity.uid > 0 && identity.gid > 0,
"configured source identity must be unprivileged"
);
unsafe {
command.pre_exec(move || {
if libc::setgroups(0, std::ptr::null()) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::setgid(identity.gid) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::setuid(identity.uid) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
Ok(())
}
#[cfg(not(unix))]
{
let _ = command;
let _ = identity;
bail!("configured process identities require a Unix actuator")
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A gate that never refuses, for driver tests that are not about the ceiling.
struct Unmetered;
impl RouteActuationGate for Unmetered {
fn admit(&self, _kind: RouteActuation) -> Result<()> {
Ok(())
}
}
// ---------------------------------------------------------------------
// A direct challenge fails one of two ways. Silence waits; a bad answer,
// or a challenge that cannot be made, is an error.
// ---------------------------------------------------------------------
/// A peer that reads one snapshot request and writes back whatever `reply`
/// builds from the challenge id, then closes. Returns the port it listens on.
fn scripted_peer(reply: impl FnOnce(&str) -> Vec<u8> + Send + 'static) -> u16 {
lingering_peer(reply, Duration::ZERO)
}
/// `scripted_peer`, except the socket stays open for `linger` after the
/// answer: keep-alive, ignoring `Connection: close`.
fn lingering_peer(
reply: impl FnOnce(&str) -> Vec<u8> + Send + 'static,
linger: Duration,
) -> u16 {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("a free port");
let port = listener.local_addr().expect("a bound port").port();
thread::spawn(move || {
let Ok((mut stream, _)) = listener.accept() else {
return;
};
let mut request = Vec::new();
let mut chunk = [0_u8; 4096];
let message_id = loop {
let Ok(read) = stream.read(&mut chunk) else {
return;
};
if read == 0 {
return;
}
request.extend_from_slice(&chunk[..read]);
let Some(end) = request.windows(4).position(|window| window == b"\r\n\r\n") else {
continue;
};
if let Ok(CultNetMessage::SnapshotRequest { message_id, .. }) =
decode_cultnet_message_from_slice(
&request[end + 4..],
CultNetWireContract::CultNetSchemaV0,
)
{
break message_id;
}
};
let _ = stream.write_all(&reply(&message_id));
let _ = stream.flush();
thread::sleep(linger);
});
port
}
fn http_ok(body: &[u8]) -> Vec<u8> {
let mut response = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
)
.into_bytes();
response.extend_from_slice(body);
response
}
fn raw_snapshot(message_id: &str, schema: &str, key: &str, payload: Vec<u8>) -> Vec<u8> {
http_ok(
&encode_cultnet_message_to_vec(
&CultNetMessage::SnapshotResponseRaw {
message_id: message_id.into(),
documents: vec![cultnet_rs::CultNetRawDocumentRecord {
schema_id: schema.into(),
record_key: key.into(),
stored_at: "1970-01-01T00:00:00.100Z".into(),
payload_encoding: cultnet_rs::CultNetRawPayloadEncoding::Messagepack,
payload,
source_runtime_id: None,
source_agent_id: None,
source_role: None,
tags: None,
}],
},
CultNetWireContract::CultNetSchemaV0,
)
.expect("an encodable response"),
)
}
fn honest(message_id: &str) -> Vec<u8> {
raw_snapshot(
message_id,
cultnet_rs::GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA,
"service",
vec![1, 2, 3],
)
}
/// One candidate challenge to `port`, with the Expected adjusted by `adjust`.
fn challenge(
port: u16,
adjust: impl FnOnce(&mut IdunnExpectedIncarnationRecord),
) -> Result<RouteSnapshotResponse, ChallengeFailure> {
challenge_over("http", RouteDriver::NginxStreamTcp, port, adjust)
}
/// The same challenge over another transport: `transport` names both the
/// Expected's transport and the endpoint scheme.
fn challenge_over(
transport: &str,
driver: RouteDriver,
port: u16,
adjust: impl FnOnce(&mut IdunnExpectedIncarnationRecord),
) -> Result<RouteSnapshotResponse, ChallengeFailure> {
let temp = tempfile::tempdir().expect("a temporary directory");
let mut candidate = expected();
candidate.route = Some(cultnet_rs::IdunnExpectedRoute {
route_id: "service".into(),
transport: transport.into(),
stable_endpoint: format!("{transport}://127.0.0.1:17999"),
candidate_endpoint: format!("{transport}://127.0.0.1:{port}"),
});
adjust(&mut candidate);
NginxRouteDriver::new(RouteBinding {
driver,
route_id: "service".into(),
stable_endpoint: format!("{transport}://127.0.0.1:17999"),
private_host: "127.0.0.1".into(),
private_port_start: port,
private_port_end: port,
config_path: temp.path().join("service.conf"),
reload_unit: "nginx.service".into(),
member: None,
})
.request_candidate_runtime_presence(&candidate, "candidate-probe")
}
/// A TCP peer that reads one framed snapshot request and writes back the
/// bytes `reply` builds from the challenge id, verbatim.
fn framed_peer(reply: impl FnOnce(&str) -> Vec<u8> + Send + 'static) -> u16 {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("a free port");
let port = listener.local_addr().expect("a bound port").port();
thread::spawn(move || {
let Ok((mut stream, _)) = listener.accept() else {
return;
};
let mut header = [0_u8; 4];
if stream.read_exact(&mut header).is_err() {
return;
}
let mut request = vec![0_u8; u32::from_be_bytes(header) as usize];
if stream.read_exact(&mut request).is_err() {
return;
}
let Ok(CultNetMessage::SnapshotRequest { message_id, .. }) =
decode_cultnet_message_from_slice(&request, CultNetWireContract::CultNetSchemaV0)
else {
return;
};
let _ = stream.write_all(&reply(&message_id));
});
port
}
/// The body of an `honest` HTTP answer, framed for TCP.
fn framed_honest(message_id: &str) -> Vec<u8> {
let response = honest(message_id);
let body = response
.windows(4)
.position(|window| window == b"\r\n\r\n")
.map(|end| &response[end + 4..])
.expect("an HTTP answer");
encode_frame(body).expect("a frame")
}
fn tcp_challenge(port: u16) -> Result<RouteSnapshotResponse, ChallengeFailure> {
challenge_over("tcp", RouteDriver::NginxStreamTcp, port, |_| {})
}
#[test]
fn a_tcp_challenge_takes_the_exact_framed_answer_and_waits_only_on_silence() {
let answer = tcp_challenge(framed_peer(framed_honest)).expect("the exact answer");
assert_eq!(answer.message_id, "candidate-probe");
assert_eq!(answer.canonical_presence, [1, 2, 3]);
assert_silent(tcp_challenge(framed_peer(|_| Vec::new())), "a peer that hangs up");
// Some bytes and then nothing is a bad answer, not a late one.
assert_refused(
tcp_challenge(framed_peer(|_| vec![0, 0, 0, 9, 1])),
"a frame that never finishes",
);
// A frame that claims more than the observation bound, or nothing, is
// an answer that is wrong, not one that is late.
assert_refused(
tcp_challenge(framed_peer(|_| {
((ROUTE_SNAPSHOT_MAX_BYTES + 1) as u32).to_be_bytes().to_vec()
})),
"a frame over the bound",
);
assert_refused(
tcp_challenge(framed_peer(|_| 0_u32.to_be_bytes().to_vec())),
"an empty frame",
);
assert_refused(
tcp_challenge(framed_peer(|_| encode_frame(b"not cultnet").expect("a frame"))),
"a frame that is not CultNet",
);
}
#[test]
fn a_rudp_challenge_that_nothing_answers_is_silence() {
let socket = std::net::UdpSocket::bind("127.0.0.1:0").expect("a free port");
let port = socket.local_addr().expect("a bound port").port();
// Bound but never read: nothing answers, and nothing refuses either.
assert_silent(
challenge_over("rudp", RouteDriver::NginxStreamUdp, port, |_| {}),
"a rudp peer that never answers",
);
drop(socket);
}
/// A challenge that cannot be made says why, whatever the message.
#[test]
fn a_challenge_failure_shows_and_chains_its_cause() {
for failure in [
silent(anyhow!("cause of silence")),
refused(anyhow!("cause of refusal")),
] {
let shown = failure.to_string();
assert!(shown.starts_with("cause of "), "{shown}");
let source = std::error::Error::source(&failure).expect("the underlying error");
assert_eq!(source.to_string(), shown);
}
}
// ---------------------------------------------------------------------
// One rule for a peer that stops, on either transport: no byte at all is
// silence; some bytes and then nothing is a bad answer. An answer is read
// by its own framing, so a peer that keeps the socket open has still
// answered.
// ---------------------------------------------------------------------
fn http_chunked(body: &[u8], chunk: usize, trailer: bool) -> Vec<u8> {
let mut response = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n".to_vec();
for piece in body.chunks(chunk) {
response.extend_from_slice(format!("{:x};ext=1\r\n", piece.len()).as_bytes());
response.extend_from_slice(piece);
response.extend_from_slice(b"\r\n");
}
response.extend_from_slice(if trailer {
b"0\r\nTrailer: x\r\n\r\n"
} else {
b"0\r\n\r\n"
});
response
}
fn honest_body(message_id: &str) -> Vec<u8> {
let response = honest(message_id);
let end = response
.windows(4)
.position(|window| window == b"\r\n\r\n")
.expect("an HTTP answer");
response[end + 4..].to_vec()
}
const LINGER: Duration = Duration::from_secs(6);
#[test]
fn an_http_answer_is_read_by_its_framing_not_to_the_end_of_the_stream() {
let started = Instant::now();
let answer = challenge(lingering_peer(honest, LINGER), |_| {}).expect("a whole answer");
assert_eq!(answer.canonical_presence, [1, 2, 3]);
assert!(started.elapsed() < LINGER / 2, "the challenge waited on the socket");
for trailer in [true, false] {
let started = Instant::now();
let answer = challenge(
lingering_peer(move |id| http_chunked(&honest_body(id), 7, trailer), LINGER),
|_| {},
)
.expect("a chunked answer");
assert_eq!(answer.canonical_presence, [1, 2, 3]);
assert!(started.elapsed() < LINGER / 2, "the challenge waited on the socket");
}
}
#[test]
fn an_http_status_that_is_not_success_is_refused_even_when_the_peer_lingers() {
for status in [
"404 Not Found",
"301 Moved Permanently",
"503 Service Unavailable",
"199 Odd",
] {
// The body is the honest answer: only the status refuses it.
let reply = move |id: &str| {
let body = honest_body(id);
let mut reply =
format!("HTTP/1.1 {status}\r\nContent-Length: {}\r\n\r\n", body.len())
.into_bytes();
reply.extend_from_slice(&body);
reply
};
let started = Instant::now();
assert_refused(challenge(lingering_peer(reply, LINGER), |_| {}), status);
assert!(started.elapsed() < LINGER / 2, "{status} waited on the socket");
}
}
#[test]
fn only_a_two_hundred_series_http_1_status_line_is_success() {
for status in ["HTTP/1.1 200 OK", "HTTP/1.0 200 OK", "HTTP/1.1 204", "HTTP/1.1 299 x"] {
assert!(is_http_success(status), "{status}");
}
for status in [
"",
"HTTP/2 200 OK",
"HTTP/1.1 ",
"HTTP/1.1 20",
"HTTP/1.1 2x0 OK",
"HTTP/1.1 20x OK",
"HTTP/1.1 2000 OK",
"HTTP/1.1 300 OK",
"HTTP/1.1 404 Not Found",
"200 OK",
] {
assert!(!is_http_success(status), "{status:?}");
}
}
#[test]
fn a_peer_that_sent_some_bytes_and_stopped_is_refused_on_either_transport() {
// No byte at all is silence, on both.
assert_silent(challenge(scripted_peer(|_| Vec::new()), |_| {}), "an HTTP peer with nothing to say");
assert_silent(tcp_challenge(framed_peer(|_| Vec::new())), "a TCP peer with nothing to say");
// Some bytes and then nothing is a bad answer, wherever it stops.
let head = b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n";
for (why, bytes) in [
("a status line that stops", b"HTTP/1.1 2".to_vec()),
("headers that stop", b"HTTP/1.1 200 OK\r\nContent-Le".to_vec()),
("a body that stops", [head.as_slice(), b"abc"].concat()),
(
"a chunk that stops",
b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nab".to_vec(),
),
(
"a chunked body that never ends",
b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n1\r\na\r\n".to_vec(),
),
] {
assert_refused(challenge(scripted_peer(move |_| bytes), |_| {}), why);
}
for (why, bytes) in [
("a 3-byte banner", b"OK\n".to_vec()),
("a frame header that stops", vec![0, 0]),
("a frame that stops", vec![0, 0, 0, 9, 1]),
] {
assert_refused(tcp_challenge(framed_peer(move |_| bytes)), why);
}
}
#[test]
fn an_http_answer_that_frames_its_body_wrongly_is_refused() {
for (why, reply) in [
(
"an unsupported transfer encoding",
b"HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip\r\n\r\n".to_vec(),
),
(
"both framings",
b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 1\r\n\r\n0\r\n\r\n"
.to_vec(),
),
("no framing", b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\nabc".to_vec()),
(
"a repeated Content-Length",
b"HTTP/1.1 200 OK\r\nContent-Length: 1\r\nContent-Length: 1\r\n\r\na".to_vec(),
),
(
"an invalid Content-Length",
b"HTTP/1.1 200 OK\r\nContent-Length: many\r\n\r\n".to_vec(),
),
(
"a Content-Length over the bound",
format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n",
ROUTE_SNAPSHOT_MAX_BYTES + 1
)
.into_bytes(),
),
("a malformed header", b"HTTP/1.1 200 OK\r\nno colon here\r\n\r\n".to_vec()),
(
"an invalid chunk size",
b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n".to_vec(),
),
(
"an unterminated chunk",
b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n1\r\naXX0\r\n\r\n".to_vec(),
),
(
"chunks over the bound",
format!(
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n{:x}\r\n",
ROUTE_SNAPSHOT_MAX_BYTES + 1
)
.into_bytes(),
),
(
"one header line over the bound",
[
b"HTTP/1.1 200 OK\r\nX: ".to_vec(),
vec![b'a'; ROUTE_HTTP_MAX_HEADER_BYTES + 1],
]
.concat(),
),
("headers that are not UTF-8", b"HTTP/1.1 200 OK\r\nX: \xff\r\n\r\n".to_vec()),
("a status that is not UTF-8", b"HTTP/1.1 \xff\r\n\r\n".to_vec()),
] {
assert_refused(challenge(scripted_peer(move |_| reply), |_| {}), why);
}
}
/// Every header line is small and the answer is honest and whole; only the
/// headers' total size refuses it.
#[test]
fn headers_that_are_over_their_bound_together_refuse_an_otherwise_honest_answer() {
let filler = b"X: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n";
let answer_with = |lines: usize| {
move |id: &str| {
let body = honest_body(id);
let mut reply =
format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n", body.len()).into_bytes();
reply.extend_from_slice(&filler.repeat(lines));
reply.extend_from_slice(b"\r\n");
reply.extend_from_slice(&body);
reply
}
};
let lines = ROUTE_HTTP_MAX_HEADER_BYTES / filler.len();
// Just inside the bound, then just outside it (a whole line over).
challenge(scripted_peer(answer_with(lines - 2)), |_| {}).expect("headers inside the bound");
assert_refused(
challenge(scripted_peer(answer_with(lines + 1)), |_| {}),
"headers over the bound together",
);
}
// ---------------------------------------------------------------------
// The reader's limits are pinned at their edges, on answers that are
// honest in every other way, so only the limit under test can refuse them
// and a peer that simply stops is not what refuses.
// ---------------------------------------------------------------------
const CHUNKED_HEAD: &[u8] = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n";
fn snapshot_request() -> Vec<u8> {
encode_cultnet_message_to_vec(
&CultNetMessage::SnapshotRequest {
message_id: "probe".into(),
schema_ids: None,
record_keys: None,
},
CultNetWireContract::CultNetSchemaV0,
)
.expect("an encodable request")
}
/// The raw body the HTTP reader hands back, with no document judgment.
fn http_body(
reply: impl FnOnce(&str) -> Vec<u8> + Send + 'static,
) -> Result<Vec<u8>, ChallengeFailure> {
let port = scripted_peer(reply);
request_http_snapshot(SocketAddr::from(([127, 0, 0, 1], port)), &snapshot_request())
}
fn tcp_body(
reply: impl FnOnce(&str) -> Vec<u8> + Send + 'static,
) -> Result<Vec<u8>, ChallengeFailure> {
let port = framed_peer(reply);
request_tcp_snapshot(SocketAddr::from(([127, 0, 0, 1], port)), &snapshot_request())
}
fn assert_body_refused(outcome: Result<Vec<u8>, ChallengeFailure>, why: &str) {
assert!(
matches!(outcome, Err(ChallengeFailure::Refused(_))),
"{why} must be refused: {:?}",
outcome.map(|body| body.len())
);
}
fn chunked(pieces: &[usize]) -> Vec<u8> {
let mut reply = CHUNKED_HEAD.to_vec();
for size in pieces {
reply.extend_from_slice(format!("{size:x}\r\n").as_bytes());
reply.extend(std::iter::repeat_n(b'a', *size));
reply.extend_from_slice(b"\r\n");
}
reply.extend_from_slice(b"0\r\n\r\n");
reply
}
#[test]
fn every_framing_takes_exactly_the_bound_and_refuses_one_byte_more() {
let most = ROUTE_SNAPSHOT_MAX_BYTES;
// Whole answers are sent in every case, so only the bound refuses.
let http_ok_of = |length: usize| move |_: &str| http_ok(&vec![b'a'; length]);
assert_eq!(http_body(http_ok_of(most)).expect("Content-Length at the bound").len(), most);
assert_body_refused(http_body(http_ok_of(most + 1)), "Content-Length over the bound");
for (pieces, taken) in [
(vec![most], true),
(vec![most + 1], false),
(vec![most - 1, 1], true),
(vec![most - 1, 2], false),
] {
let outcome = http_body(move |_| chunked(&pieces));
if taken {
assert_eq!(outcome.expect("chunks at the bound").len(), most);
} else {
assert_body_refused(outcome, "chunks over the bound");
}
}
let framed = |length: usize| move |_: &str| encode_frame(&vec![b'a'; length]).expect("a frame");
assert_eq!(tcp_body(framed(most)).expect("a frame at the bound").len(), most);
assert_eq!(tcp_body(framed(1)).expect("the smallest frame").len(), 1);
assert_body_refused(tcp_body(framed(most + 1)), "a frame over the bound");
assert_body_refused(tcp_body(|_| 0_u32.to_be_bytes().to_vec()), "an empty frame");
}
#[test]
fn a_peer_that_sends_one_byte_and_stops_is_refused_not_silent() {
assert_body_refused(http_body(|_| b"H".to_vec()), "one HTTP byte");
assert_body_refused(tcp_body(|_| vec![0]), "one TCP byte");
assert!(matches!(http_body(|_| Vec::new()), Err(ChallengeFailure::Silent(_))));
assert!(matches!(tcp_body(|_| Vec::new()), Err(ChallengeFailure::Silent(_))));
}
/// An honest chunked answer with `after_body` in place of its end.
fn chunked_honest_then(id: &str, after_body: &[u8]) -> Vec<u8> {
chunked_honest_sized(id, "", "", after_body)
}
/// An honest chunked answer whose chunk-size line is the hex length between
/// `before` and `after`.
fn chunked_honest_sized(id: &str, before: &str, after: &str, after_body: &[u8]) -> Vec<u8> {
let body = honest_body(id);
let mut reply = CHUNKED_HEAD.to_vec();
reply.extend_from_slice(format!("{before}{:x}{after}\r\n", body.len()).as_bytes());
reply.extend_from_slice(&body);
reply.extend_from_slice(b"\r\n");
reply.extend_from_slice(after_body);
reply
}
#[test]
fn a_chunked_answer_must_end_its_trailers_and_terminate_each_chunk() {
// The same answer, ended properly, is taken.
challenge(scripted_peer(|id| chunked_honest_then(id, b"0\r\n\r\n")), |_| {})
.expect("an honest chunked answer");
challenge(scripted_peer(|id| chunked_honest_then(id, b"0\r\nT: 1\r\nU: 2\r\n\r\n")), |_| {})
.expect("an honest chunked answer with trailers");
// RFC 9112 7.1: whitespace may precede a chunk extension and nothing
// else. The answer is otherwise honest, so only the size line differs.
for (before, after) in [("", ";x"), ("", " ;x"), ("", "\t;x"), ("", ";x=1;y")] {
challenge(
scripted_peer(move |id| chunked_honest_sized(id, before, after, b"0\r\n\r\n")),
|_| {},
)
.unwrap_or_else(|error| panic!("size line {before:?}+size+{after:?}: {error:?}"));
}
for (why, before, after) in [
("whitespace before the chunk size", " ", ""),
("a tab before the chunk size", "\t", ""),
("whitespace after the chunk size and no extension", "", " "),
("whitespace before the size, ahead of an extension", " ", ";x"),
("a vertical tab before an extension", "", "\x0b;x"),
("a carriage return before an extension", "", "\r;x"),
("a bare carriage return ends the size line", "", "\r"),
("a doubled carriage return ends the size line", "", "\r\r"),
] {
assert_refused(
challenge(
scripted_peer(move |id| chunked_honest_sized(id, before, after, b"0\r\n\r\n")),
|_| {},
),
why,
);
}
// A trailer that begins and then the peer stops.
assert_refused(
challenge(scripted_peer(|id| chunked_honest_then(id, b"0\r\nT: 1\r\n")), |_| {}),
"a partial trailer section",
);
// Chunk data followed by something other than CRLF, from a peer that
// then carries on as though nothing were wrong.
assert_refused(
challenge(
lingering_peer(
|id| {
let body = honest_body(id);
let mut reply = CHUNKED_HEAD.to_vec();
reply.extend_from_slice(format!("{:x}\r\n", body.len()).as_bytes());
reply.extend_from_slice(&body);
reply.extend_from_slice(b"XX0\r\n\r\n");
reply
},
LINGER,
),
|_| {},
),
"a chunk that is not terminated",
);
}
/// Headers and trailers draw on one budget. The head is fixed, so the
/// trailer line that lands the total exactly on the bound is computed.
#[test]
fn trailers_share_the_header_budget_and_are_bounded_at_its_edge() {
// The trailer line is `T: <padding>`, counted with its CRLF, plus the
// empty line ending the trailers (2): together they fill what the head
// left of the budget.
let padding = ROUTE_HTTP_MAX_HEADER_BYTES - CHUNKED_HEAD.len() - 2 - 2 - "T: ".len();
let reply_with = move |padding: usize| {
move |id: &str| {
let mut trailers = format!("0\r\nT: {}\r\n", "a".repeat(padding)).into_bytes();
trailers.extend_from_slice(b"\r\n");
chunked_honest_then(id, &trailers)
}
};
challenge(scripted_peer(reply_with(padding)), |_| {}).expect("trailers exactly at the bound");
let started = Instant::now();
assert_refused(
challenge(lingering_peer(reply_with(padding + 1), LINGER), |_| {}),
"trailers one byte over the bound",
);
assert!(started.elapsed() < ROUTE_SNAPSHOT_TIMEOUT / 2, "the refusal waited on the socket");
// Trailers that never end are cut off at the bound, not at the timeout.
let started = Instant::now();
assert_refused(
challenge(
lingering_peer(
|id| {
let line = b"T: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n";
let mut endless = b"0\r\n".to_vec();
endless.extend_from_slice(
&line.repeat(2 * ROUTE_HTTP_MAX_HEADER_BYTES / line.len()),
);
chunked_honest_then(id, &endless)
},
LINGER,
),
|_| {},
),
"trailers that never end",
);
assert!(started.elapsed() < ROUTE_SNAPSHOT_TIMEOUT / 2, "the refusal waited on the socket");
}
/// RFC 9112 strictness, on answers that are honest but for the one field.
#[test]
fn numbers_and_header_names_are_read_strictly() {
// `{field}:{value}` then the true length, so a value of " +" is a signed one.
let content_length = |field: &'static str, value: &'static str| {
move |id: &str| {
let body = honest_body(id);
let mut reply =
format!("HTTP/1.1 200 OK\r\n{field}:{value}{}\r\n\r\n", body.len()).into_bytes();
reply.extend_from_slice(&body);
reply
}
};
// Optional whitespace around a value is allowed.
challenge(scripted_peer(content_length("Content-Length", " \t ")), |_| {})
.expect("Content-Length with optional whitespace");
for (why, field, value) in [
("a signed Content-Length", "Content-Length", " +"),
("whitespace before the colon", "Content-Length ", " "),
("a header name with leading whitespace", " Content-Length", " "),
] {
assert_refused(challenge(scripted_peer(content_length(field, value)), |_| {}), why);
}
assert_refused(
challenge(
scripted_peer(|id| {
let body = honest_body(id);
let mut reply = format!(
"HTTP/1.1 200 OK\r\nX-Other : y\r\nContent-Length: {}\r\n\r\n",
body.len()
)
.into_bytes();
reply.extend_from_slice(&body);
reply
}),
|_| {},
),
"whitespace before the colon of another header",
);
assert_refused(
challenge(
scripted_peer(|id| {
let body = honest_body(id);
let mut reply = CHUNKED_HEAD.to_vec();
reply.extend_from_slice(format!("+{:x}\r\n", body.len()).as_bytes());
reply.extend_from_slice(&body);
reply.extend_from_slice(b"\r\n0\r\n\r\n");
reply
}),
|_| {},
),
"a signed chunk size",
);
}
/// A reader that fails `interruptions` times with `kind`, then serves `bytes`.
struct Flaky {
kind: ErrorKind,
interruptions: usize,
bytes: &'static [u8],
}
impl Read for Flaky {
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
if self.interruptions > 0 {
self.interruptions -= 1;
return Err(self.kind.into());
}
let served = self.bytes.len().min(buffer.len());
buffer[..served].copy_from_slice(&self.bytes[..served]);
self.bytes = &self.bytes[served..];
Ok(served)
}
}
#[test]
fn an_interrupted_read_is_retried_and_any_other_failure_is_a_stopped_peer() {
let mut interrupted = Answer::new(Flaky {
kind: ErrorKind::Interrupted,
interruptions: 3,
bytes: b"abcd",
});
let mut buffer = [0_u8; 4];
interrupted.read_all(&mut buffer, "in the test").expect("the read is retried");
assert_eq!(&buffer, b"abcd");
// Before any byte the peer is silent; after some, it is refused.
let mut failing = Answer::new(Flaky {
kind: ErrorKind::TimedOut,
interruptions: 1,
bytes: b"abcd",
});
assert!(matches!(
failing.read_all(&mut buffer, "in the test"),
Err(ChallengeFailure::Silent(_))
));
let mut failing = Answer::new(Flaky {
kind: ErrorKind::TimedOut,
interruptions: 0,
bytes: b"ab",
});
let mut wanted = [0_u8; 3];
failing.read_all(&mut wanted[..2], "in the test").expect("two bytes");
assert!(matches!(
failing.read_all(&mut wanted[2..], "in the test"),
Err(ChallengeFailure::Refused(_))
));
}
fn assert_silent(outcome: Result<RouteSnapshotResponse, ChallengeFailure>, why: &str) {
assert!(
matches!(outcome, Err(ChallengeFailure::Silent(_))),
"{why} must read as silence: {outcome:?}"
);
}
fn assert_refused(outcome: Result<RouteSnapshotResponse, ChallengeFailure>, why: &str) {
assert!(
matches!(outcome, Err(ChallengeFailure::Refused(_))),
"{why} must be an error, not a wait: {outcome:?}"
);
}
#[test]
fn an_exact_answer_is_taken() {
let port = scripted_peer(honest);
let answer = challenge(port, |_| {}).expect("the exact answer");
assert_eq!(answer.message_id, "candidate-probe");
assert_eq!(answer.canonical_presence, [1, 2, 3]);
}
#[test]
fn a_challenge_that_nothing_answers_is_silence() {
assert_silent(challenge(scripted_peer(|_| Vec::new()), |_| {}), "a peer that hangs up");
// Half a response, then the peer is gone.
assert_refused(
challenge(scripted_peer(|_| b"HTTP/1.1 200 OK\r\nContent-Le".to_vec()), |_| {}),
"a response that never finished its headers",
);
// Nothing listens.
let reservation = std::net::TcpListener::bind("127.0.0.1:0").expect("a free port");
let port = reservation.local_addr().expect("a bound port").port();
drop(reservation);
assert_silent(challenge(port, |_| {}), "a closed port");
}
#[test]
fn a_bad_answer_is_refused_not_waited_on() {
assert_refused(
challenge(
scripted_peer(|_| {
raw_snapshot(
"candidate-another",
cultnet_rs::GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA,
"service",
vec![1],
)
}),
|_| {},
),
"an answer to another challenge",
);
assert_refused(
challenge(
scripted_peer(|id| raw_snapshot(id, "some.other.schema", "service", vec![1])),
|_| {},
),
"a substituted document schema",
);
assert_refused(
challenge(
scripted_peer(|id| {
raw_snapshot(
id,
cultnet_rs::GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA,
"another-target",
vec![1],
)
}),
|_| {},
),
"a substituted record key",
);
assert_refused(
challenge(
scripted_peer(|id| {
raw_snapshot(
id,
cultnet_rs::GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA,
"service",
vec![0; ROUTE_SNAPSHOT_MAX_BYTES + 1],
)
}),
|_| {},
),
"an oversized payload",
);
assert_refused(
challenge(
scripted_peer(|id| {
http_ok(
&encode_cultnet_message_to_vec(
&CultNetMessage::SnapshotRequest {
message_id: id.into(),
schema_ids: None,
record_keys: None,
},
CultNetWireContract::CultNetSchemaV0,
)
.expect("an encodable message"),
)
}),
|_| {},
),
"a response that is not a raw snapshot",
);
assert_refused(
challenge(
scripted_peer(|_| {
b"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
.to_vec()
}),
|_| {},
),
"an HTTP error",
);
assert_refused(
challenge(scripted_peer(|_| http_ok(b"not a cultnet message")), |_| {}),
"a body that is not CultNet",
);
assert_refused(
challenge(scripted_peer(|_| b"HTTP/1.1 200 OK
Content-Le".to_vec()), |_| {}),
"a response that never finished its headers",
);
}
#[test]
fn a_challenge_that_cannot_be_made_is_an_error_not_a_wait() {
let port = scripted_peer(honest);
assert_refused(
challenge(port, |candidate| {
candidate.route.as_mut().unwrap().route_id = "another-route".into();
}),
"a binding that differs from Expected",
);
assert_refused(
challenge(port, |candidate| {
candidate.route.as_mut().unwrap().transport = "smtp".into();
}),
"an unsupported transport",
);
assert_refused(
challenge(port, |candidate| {
candidate.route.as_mut().unwrap().candidate_endpoint =
format!("http://candidate.example:{port}");
}),
"a host that is not an IP address",
);
assert_refused(
challenge(port, |candidate| {
candidate.route.as_mut().unwrap().candidate_endpoint =
format!("http://127.0.0.1:{}", port.wrapping_add(1));
}),
"a candidate endpoint outside the route binding",
);
assert_refused(
challenge(port, |candidate| candidate.route = None),
"an Expected with no route",
);
}
/// The candidate path renders the Expected first, and rendering parses the
/// same endpoints, so it never reaches the parse in the challenge. The
/// stable path does not render: a bound stable endpoint that is not a plain
/// host and port is caught by the challenge's own parse, and it is a
/// configuration error, not a listener that has not answered yet.
#[test]
fn a_stable_endpoint_that_does_not_parse_is_an_error_not_a_wait() {
let temp = tempfile::tempdir().expect("a temporary directory");
let driver = NginxRouteDriver::new(RouteBinding {
driver: RouteDriver::NginxStreamTcp,
route_id: "service".into(),
stable_endpoint: "http://127.0.0.1:17999".into(),
private_host: "127.0.0.1".into(),
private_port_start: 17998,
private_port_end: 17998,
config_path: temp.path().join("service.conf"),
reload_unit: "nginx.service".into(),
member: None,
});
let mut candidate = expected();
candidate.route = Some(cultnet_rs::IdunnExpectedRoute {
route_id: "service".into(),
transport: "http".into(),
stable_endpoint: "http://127.0.0.1:17999".into(),
candidate_endpoint: "http://127.0.0.1:17998".into(),
});
for endpoint in [
"http://127.0.0.1",
"http://127.0.0.1:0",
"http://127.0.0.1:17999/path",
"tcp://127.0.0.1:17999",
] {
assert_refused(
driver.request_runtime_presence_at(&candidate, "stable-probe", endpoint),
endpoint,
);
}
}
#[test]
fn route_connect_retries_connection_refused_until_listener_is_ready() -> Result<()> {
let reservation = std::net::TcpListener::bind("127.0.0.1:0")?;
let address = reservation.local_addr()?;
drop(reservation);
// The listener never accepts: the kernel completes a handshake from
// its backlog, so the client connects as soon as it is bound. Nothing
// here can block on a connection that never arrives; the listener only
// has to outlive the connect, and gives up waiting after a bound.
let (connected, connect_done) = std::sync::mpsc::channel::<()>();
let server = thread::spawn(move || -> std::io::Result<()> {
thread::sleep(Duration::from_millis(75));
let listener = std::net::TcpListener::bind(address)?;
let _ = connect_done.recv_timeout(Duration::from_secs(30));
drop(listener);
Ok(())
});
let stream = connect_route_socket(address);
let _ = connected.send(());
server
.join()
.map_err(|_| anyhow!("route readiness test server panicked"))??;
drop(stream?);
Ok(())
}
/// Admission audit, claim 3: `is_permanently_stopped` now lets the
/// Isolation gate skip `prove_isolation` against the incumbent. Enumerate
/// what it answers `true` for, using a fake `systemctl show` that replays
/// whatever unit state the test writes. The states that matter are the
/// transitional ones -- a restarting, activating, or briefly inactive unit
/// must read as NOT permanently stopped -- and a systemctl failure must be
/// an error, never a quiet "stopped".
#[cfg(unix)]
#[test]
fn is_permanently_stopped_only_for_failed_or_forgotten_units() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
let systemctl = temp.path().join("systemctl");
let state = temp.path().join("systemctl.state");
let fail = temp.path().join("systemctl.fail");
std::fs::write(
&systemctl,
"#!/bin/sh\nif [ \"$1\" != \"show\" ]; then exit 64; fi\ncat \"$0.state\"\nif [ -e \"$0.fail\" ]; then exit 1; fi\nexit 0\n",
)?;
std::fs::set_permissions(&systemctl, std::fs::Permissions::from_mode(0o755))?;
let driver = SystemdTransientWorkloadDriver {
systemctl_program: systemctl,
..Default::default()
};
let observation = audit_workload_observation("no");
let show = |load: &str, active: &str, sub: &str| -> String {
format!(
"LoadState={load}\nActiveState={active}\nSubState={sub}\nInvocationID=inv\nDescription=Idunn test\n"
)
};
for (active, sub) in [
("active", "running"),
("activating", "start"),
("activating", "auto-restart"),
("deactivating", "stop-sigterm"),
("deactivating", "final-sigterm"),
("inactive", "dead"),
("reloading", "reload"),
] {
std::fs::write(&state, show("loaded", active, sub))?;
assert!(
!driver.is_permanently_stopped(&observation)?,
"ActiveState={active} SubState={sub} must not read as permanently stopped"
);
}
std::fs::write(&state, show("loaded", "failed", "failed"))?;
assert!(
driver.is_permanently_stopped(&observation)?,
"failed is permanent under Restart=no"
);
// systemd has no such unit: `systemctl show` exits 0 with not-found.
std::fs::write(&state, show("not-found", "inactive", "dead"))?;
assert!(
driver.is_permanently_stopped(&observation)?,
"a forgotten unit cannot restart"
);
// A systemctl failure with no properties is an error, not "stopped".
std::fs::write(&state, "")?;
std::fs::write(&fail, "")?;
assert!(
driver.is_permanently_stopped(&observation).is_err(),
"a systemctl failure must not be read as a stopped incumbent"
);
std::fs::remove_file(&fail)?;
// A failure that still printed properties (systemctl exited non-zero
// after output) is also an error.
std::fs::write(&state, show("loaded", "failed", "failed"))?;
std::fs::write(&fail, "")?;
assert!(driver.is_permanently_stopped(&observation).is_err());
std::fs::remove_file(&fail)?;
// The recorded policy is asked before systemd is: any incumbent whose
// observation carries another Restart policy makes the Isolation gate
// error out (pre-fencing abort), whatever systemd says.
std::fs::write(&state, show("loaded", "failed", "failed"))?;
let restarting = audit_workload_observation("always");
let error = driver
.is_permanently_stopped(&restarting)
.expect_err("Restart=always incumbent must not be judged stopped");
assert!(format!("{error:#}").contains("Restart=no"), "{error:#}");
Ok(())
}
/// Continuity's one free re-materialization rests on this: an absent unit
/// is a reboot's loss only when the kernel booted after admission.
#[cfg(unix)]
#[test]
fn lost_to_host_reboot_needs_an_absent_unit_and_a_boot_after_admission() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
let systemctl = temp.path().join("systemctl");
let state = temp.path().join("systemctl.state");
std::fs::write(&systemctl, "#!/bin/sh
cat \"$0.state\"
exit 0
")?;
std::fs::set_permissions(&systemctl, std::fs::Permissions::from_mode(0o755))?;
let proc_root = temp.path().join("proc");
std::fs::create_dir(&proc_root)?;
let driver = SystemdTransientWorkloadDriver {
systemctl_program: systemctl,
proc_root: proc_root.clone(),
..Default::default()
};
let observation = audit_workload_observation("no");
let absent = "LoadState=not-found
ActiveState=inactive
SubState=dead
";
let failed = "LoadState=loaded
ActiveState=failed
SubState=failed
InvocationID=inv
Description=Idunn test
";
let boot = |btime: &str| -> Result<()> {
Ok(std::fs::write(
proc_root.join("stat"),
format!("cpu 1 2 3
btime {btime}
processes 9
"),
)?)
};
let admitted_at = 1_700_000_000_000_u64;
std::fs::write(&state, absent)?;
boot("1700000001")?;
assert!(driver.lost_to_host_reboot(&observation, admitted_at)?);
boot("1700000000")?;
assert!(!driver.lost_to_host_reboot(&observation, admitted_at)?, "booted at admission");
boot("1699999999")?;
assert!(!driver.lost_to_host_reboot(&observation, admitted_at)?, "booted before admission");
// A unit systemd still knows died on its own, not to a reboot.
std::fs::write(&state, failed)?;
boot("1700000001")?;
assert!(!driver.lost_to_host_reboot(&observation, admitted_at)?);
// No boot time is not evidence either way.
std::fs::write(&state, absent)?;
std::fs::write(proc_root.join("stat"), "cpu 1 2 3
")?;
assert!(driver.lost_to_host_reboot(&observation, admitted_at).is_err());
boot("soon")?;
assert!(driver.lost_to_host_reboot(&observation, admitted_at).is_err());
std::fs::remove_file(proc_root.join("stat"))?;
assert!(driver.lost_to_host_reboot(&observation, admitted_at).is_err());
Ok(())
}
fn host_audit_observation(pid: u32, created: u64) -> HostWorkloadObservation {
HostWorkloadObservation {
host: "raven".into(),
actuator_identity_id: "actuator".into(),
process_id: pid,
process_creation_time: created,
session_id: 1,
user_sid: "S-1-5-21-1-2-3-1001".into(),
executable: "C:/GameCult/idunn/releases/muninn/x/muninn.exe".into(),
executable_sha256: format!("sha256-{}", "1".repeat(64)),
command_line_sha256: format!("sha256-{}", "2".repeat(64)),
environment_names: vec!["GAMECULT_IDUNN_RUNTIME_BUNDLE".into()],
environment_contract_sha256: format!("sha256-{}", "3".repeat(64)),
runtime_bundle: "C:/GameCult/idunn/runtime/muninn/x".into(),
runtime_instance_id: format!("sha256-{}", "a".repeat(64)),
activation_signer_identity_id: "activation".into(),
activation_signer_public_key: vec![1; 32],
exit_code: None,
}
}
/// The previous Idunn persisted the systemd observation alone, under both
/// MessagePack encodings CultCache uses. The enum must read those bytes
/// as the systemd variant, and a host observation must not be mistaken
/// for one.
#[test]
fn persisted_systemd_observations_decode_as_the_systemd_variant() -> Result<()> {
let systemd = systemd_audit_observation("no", 61_000);
for bytes in [
rmp_serde::to_vec(&systemd)?,
rmp_serde::to_vec_named(&systemd)?,
] {
let decoded: WorkloadObservation = rmp_serde::from_slice(&bytes)?;
assert_eq!(decoded, WorkloadObservation::Systemd(systemd.clone()));
}
let host = host_audit_observation(4242, 133_000_000_000_000_000);
for bytes in [rmp_serde::to_vec(&host)?, rmp_serde::to_vec_named(&host)?] {
let decoded: WorkloadObservation = rmp_serde::from_slice(&bytes)?;
assert_eq!(decoded, WorkloadObservation::Host(host.clone()));
}
let linux = IsolationEvidence::Linux(LinuxIsolationEvidence {
candidate_uid: 1,
candidate_pid_namespace_id: 2,
candidate_mount_namespace_id: 3,
incumbent_uid: None,
incumbent_pid_namespace_id: None,
incumbent_mount_namespace_id: None,
});
let host_evidence = IsolationEvidence::Host(HostIsolationEvidence {
candidate_process_id: 1,
candidate_process_creation_time: 2,
incumbent_process_id: Some(3),
incumbent_process_creation_time: Some(4),
});
for evidence in [linux, host_evidence] {
for bytes in [
rmp_serde::to_vec(&evidence)?,
rmp_serde::to_vec_named(&evidence)?,
] {
let decoded: IsolationEvidence = rmp_serde::from_slice(&bytes)?;
assert_eq!(decoded, evidence);
}
}
Ok(())
}
#[test]
fn workload_identity_ignores_mutable_argv_but_names_real_identity_drift() {
let prior = systemd_audit_observation("no", 61_000);
let mut observed = prior.clone();
observed.command_line_sha256 = format!("sha256-{}", "f".repeat(64));
assert!(changed_systemd_workload_identity_fields(&observed, &prior).is_empty());
observed.main_pid += 1;
assert_eq!(
changed_systemd_workload_identity_fields(&observed, &prior),
vec!["main_pid"]
);
}
#[test]
fn host_isolation_is_two_live_distinct_processes_on_one_host() {
let live = |pid: u32, created: u64| {
WorkloadObservation::Host(host_audit_observation(pid, created))
};
let incumbent = live(100, 5);
assert!(WorkloadObservation::prove_isolation(&live(101, 6), Some(&incumbent)).is_ok());
assert!(WorkloadObservation::prove_isolation(&live(100, 5), Some(&incumbent)).is_err());
let mut exited = host_audit_observation(101, 6);
exited.exit_code = Some(1);
let exited = WorkloadObservation::Host(exited);
assert!(WorkloadObservation::prove_isolation(&exited, Some(&incumbent)).is_err());
let systemd = audit_workload_observation("no");
assert!(WorkloadObservation::prove_isolation(&live(101, 6), Some(&systemd)).is_err());
}
fn audit_workload_observation(restart_policy: &str) -> WorkloadObservation {
let uid = 61_000u32;
WorkloadObservation::Systemd(systemd_audit_observation(restart_policy, uid))
}
fn systemd_audit_observation(restart_policy: &str, uid: u32) -> SystemdWorkloadObservation {
SystemdWorkloadObservation {
unit: format!("idunn-{}.service", "a".repeat(64)),
unit_description: "Idunn test".into(),
invocation_id: "inv".into(),
exec_main_start_timestamp_monotonic: 1,
service_type: "exec".into(),
restart_policy: restart_policy.into(),
kill_mode: "mixed".into(),
dynamic_user: true,
systemd_user: format!("u{uid}"),
systemd_group: format!("u{uid}"),
supplementary_groups: String::new(),
capability_bounding_set: String::new(),
ambient_capabilities: String::new(),
private_mounts: true,
private_pids: true,
protect_proc: "invisible".into(),
proc_subset: "all".into(),
no_new_privileges: true,
umask: "0007".into(),
inaccessible_paths: String::new(),
load_credential: String::new(),
main_pid: 4242,
process_start_time: 1,
process_uids: [uid; 4],
process_gids: [uid; 4],
process_groups: vec![uid],
process_cap_inheritable: 0,
process_cap_permitted: 0,
process_cap_effective: 0,
process_cap_bounding: 0,
process_cap_ambient: 0,
process_no_new_privileges: true,
process_namespace_pids: vec![1],
mount_namespace_id: 7,
pid_namespace_id: 8,
executable: PathBuf::from("/opt/test/bin/service"),
executable_device: 1,
executable_inode: 1,
executable_sha256: format!("sha256-{}", "1".repeat(64)),
runtime_instance_id: format!("sha256-{}", "a".repeat(64)),
working_directory: PathBuf::from("/opt/test"),
runtime_bundle: PathBuf::from("/run/test"),
command_line_sha256: format!("sha256-{}", "2".repeat(64)),
environment_names: Vec::new(),
environment_contract_sha256: format!("sha256-{}", "3".repeat(64)),
control_group: "/system.slice/idunn-test.service".into(),
credentials_directory: None,
parent_only_file_descriptors: Vec::new(),
activation_signer_identity_id: "activation".into(),
activation_signer_public_key: vec![1; 32],
service_credentials: Vec::new(),
}
}
#[test]
fn dynamic_primary_group_is_checked_by_numeric_identity() {
assert!(systemd_group_matches_launch_contract(
None,
"_du12345",
61_000,
61_000
));
assert!(!systemd_group_matches_launch_contract(
None,
"_du12345",
61_000,
61_001
));
}
#[test]
fn fixed_state_group_keeps_its_name_and_gid_contract() {
assert!(systemd_group_matches_launch_contract(
Some(("streampixels-service-state", 61_234)),
"streampixels-service-state",
61_000,
61_234
));
assert!(!systemd_group_matches_launch_contract(
Some(("streampixels-service-state", 61_234)),
"_du12345",
61_000,
61_234
));
assert!(!systemd_group_matches_launch_contract(
Some(("streampixels-service-state", 61_234)),
"streampixels-service-state",
61_000,
61_235
));
}
/// A native process that is the Expected's: what `publish_observed_activation`
/// checks the activation against.
#[cfg(target_os = "linux")]
fn observation_of(
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> WorkloadObservation {
let mut host = host_audit_observation(1, 1);
host.runtime_instance_id = activation.runtime_instance_id.clone();
host.executable_sha256 = expected.artifact_sha256.clone();
WorkloadObservation::Host(host)
}
#[cfg(target_os = "linux")]
fn mode_of(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
fs::metadata(path).unwrap().permissions().mode() & 0o777
}
#[cfg(target_os = "linux")]
fn inode_of(path: &Path) -> u64 {
use std::os::unix::fs::MetadataExt;
fs::metadata(path).unwrap().ino()
}
/// Idunn's `UMask=027` alone lands a file 0640: the store still has to be
/// published, and its lock has to stay private.
#[cfg(target_os = "linux")]
#[test]
fn a_published_store_is_readable_by_a_workload_and_its_lock_is_not() -> Result<()> {
use crate::incident::tests::under_service_umask;
let temp = tempfile::tempdir()?;
let (expected, _, _, anchor) = authenticated_warming(temp.path())?;
let driver = topology_driver(temp.path(), "topology");
under_service_umask(|| driver.publish_expected(&expected, &anchor))?;
assert_eq!(mode_of(&driver.projection_store), 0o644);
assert_eq!(mode_of(&authority_lock_path(&driver.projection_store)), 0o600);
Ok(())
}
/// The body's condition: the projection directory carries the default ACL
/// `u::rw g::r o::r`, under which the umask is ignored and a lock CultCache
/// creates lands 0644. Every writer publishes the store 0644 and leaves
/// the lock 0600, and another uid cannot open the lock.
#[cfg(target_os = "linux")]
#[test]
fn under_a_default_acl_the_projection_is_published_and_its_lock_is_private() -> Result<()> {
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use crate::incident::tests::{give_default_acl, readable_by_another_uid, under_service_umask};
let temp = tempfile::tempdir()?;
let (expected, activation, _, anchor) = authenticated_warming(temp.path())?;
let observation = observation_of(&expected, &activation);
let dir = temp.path().join("projection");
fs::create_dir(&dir)?;
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755))?;
give_default_acl(&dir);
let driver = topology_driver(&dir, "topology");
let store = driver.projection_store.clone();
let lock = authority_lock_path(&store);
// Control: the lock CultCache would have made here. Without the ACL in
// force this reads 0640 and the assertions below prove nothing.
let control = dir.join("control.lock");
under_service_umask(|| {
OpenOptions::new().create(true).write(true).mode(0o666).open(&control)
})?;
assert_eq!(mode_of(&control), 0o644, "the default ACL is not in force");
let published = |step: &str| {
assert_eq!(mode_of(&store), 0o644, "the store after {step}");
assert_eq!(mode_of(&lock), 0o600, "the lock after {step}");
};
under_service_umask(|| driver.publish_expected(&expected, &anchor))?;
published("publish_expected");
let private_lock = inode_of(&lock);
under_service_umask(|| {
driver.publish_observed_activation(&expected, &activation, &observation)
})?;
published("publish_observed_activation");
under_service_umask(|| {
driver.demote_to_expected_only(&expected, &anchor, &activation, None)
})?;
published("demote_to_expected_only");
under_service_umask(|| driver.withdraw_incarnation(&expected, &anchor, None, None))?;
published("withdraw_incarnation");
assert_eq!(inode_of(&lock), private_lock, "a private lock was replaced");
// The consequence: another uid cannot open the lock, so it cannot hold
// it. Provable only as root with setpriv, and only if that uid can
// read the control file, which shows the switch itself works.
if readable_by_another_uid(&control) == Some(true) {
assert_eq!(readable_by_another_uid(&lock), Some(false));
} else {
eprintln!("UNPROVEN: no uid switch here; the lock modes above stand alone");
}
Ok(())
}
/// Another process holds the lock: the publish is an error and a no-op, and
/// it lands once the lock is free. Nothing waits (`run_while_held` counts
/// the opens of the lock), and the error names the store kind, not a path.
#[cfg(target_os = "linux")]
#[test]
fn a_topology_publish_never_waits_on_a_held_lock_and_lands_when_freed() -> Result<()> {
use crate::incident::tests::{eventually, hold_lock_of, run_while_held};
let temp = tempfile::tempdir()?;
let (expected, activation, _, anchor) = authenticated_warming(temp.path())?;
let observation = observation_of(&expected, &activation);
let driver = topology_driver(temp.path(), "topology");
let error = run_while_held(hold_lock_of(&driver.projection_store), 1, || {
driver.publish_expected(&expected, &anchor)
})
.unwrap_err();
assert_eq!(error.to_string(), "the topology projection is locked by another holder");
assert!(!driver.projection_store.exists(), "a contended publish wrote the store");
eventually(|| driver.publish_expected(&expected, &anchor));
let landed = fs::read(&driver.projection_store)?;
// An existing store is unchanged by a contended write of any kind.
let error = run_while_held(hold_lock_of(&driver.projection_store), 1, || {
driver.publish_observed_activation(&expected, &activation, &observation)
})
.unwrap_err();
assert_eq!(error.to_string(), "the topology projection is locked by another holder");
assert_eq!(fs::read(&driver.projection_store)?, landed);
eventually(|| driver.publish_observed_activation(&expected, &activation, &observation));
assert_ne!(fs::read(&driver.projection_store)?, landed);
Ok(())
}
/// The load-bearing rule (a chmod does not revoke a holder): a wider lock
/// that another process already holds is replaced by a fresh private one,
/// so the holder keeps only an orphaned inode and cannot block a publish.
#[cfg(target_os = "linux")]
#[test]
fn a_holder_of_a_wider_lock_cannot_block_a_publish() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
use crate::incident::tests::hold_lock_of;
let temp = tempfile::tempdir()?;
let (expected, _, _, anchor) = authenticated_warming(temp.path())?;
let driver = topology_driver(temp.path(), "topology");
let lock = authority_lock_path(&driver.projection_store);
fs::write(&lock, b"")?;
fs::set_permissions(&lock, fs::Permissions::from_mode(0o644))?;
let wider = inode_of(&lock);
let holder = hold_lock_of(&driver.projection_store);
driver.publish_expected(&expected, &anchor)?;
assert_eq!(mode_of(&lock), 0o600);
assert_ne!(inode_of(&lock), wider, "the wider lock was chmodded, not replaced");
assert!(holder.is_held(), "the publish ended the holder instead of outliving it");
assert_eq!(driver.snapshot()?.len(), 2);
Ok(())
}
/// Every lock that is not a regular file, ours, mode 0600 is replaced, and
/// a private one is left alone.
#[cfg(target_os = "linux")]
#[test]
fn every_lock_that_is_not_private_is_replaced_and_a_private_one_is_not() -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let temp = tempfile::tempdir()?;
let (expected, activation, _, anchor) = authenticated_warming(temp.path())?;
let observation = observation_of(&expected, &activation);
let driver = topology_driver(temp.path(), "topology");
let lock = authority_lock_path(&driver.projection_store);
driver.publish_expected(&expected, &anchor)?;
// Each call changes the projection, so each one exchanges.
let toggle = || -> Result<()> {
if driver.projected_activation_is_present(&expected)? {
driver
.demote_to_expected_only(&expected, &anchor, &activation, None)
.map(drop)
} else {
driver
.publish_observed_activation(&expected, &activation, &observation)
.map(drop)
}
};
// SAFETY: geteuid has no preconditions.
let euid = unsafe { libc::geteuid() };
for (shape, mode, owner) in [
("0640", 0o640, None),
("0666", 0o666, None),
("0700", 0o700, None),
("0400", 0o400, None),
("0604", 0o604, None),
("0600 owned by another uid", 0o600, Some(65534)),
] {
if owner.is_some() && euid != 0 {
eprintln!("UNPROVEN: not root, so a lock owned by another uid cannot be made");
continue;
}
fs::set_permissions(&lock, fs::Permissions::from_mode(mode))?;
if let Some(uid) = owner {
std::os::unix::fs::chown(&lock, Some(uid), Some(uid))?;
}
let before = inode_of(&lock);
toggle()?;
assert_ne!(inode_of(&lock), before, "a {shape} lock was kept");
assert_eq!(mode_of(&lock), 0o600, "after a {shape} lock");
assert_eq!(fs::metadata(&lock)?.uid(), euid, "after a {shape} lock");
}
// A lock that is a symlink is replaced, never followed.
let target = temp.path().join("elsewhere");
fs::write(&target, b"")?;
fs::set_permissions(&target, fs::Permissions::from_mode(0o600))?;
fs::remove_file(&lock)?;
std::os::unix::fs::symlink(&target, &lock)?;
toggle()?;
assert!(!fs::symlink_metadata(&lock)?.file_type().is_symlink());
assert_eq!(mode_of(&lock), 0o600);
assert!(target.exists());
// A private lock is opened as it is: two more exchanges, one inode.
let kept = inode_of(&lock);
toggle()?;
toggle()?;
assert_eq!(inode_of(&lock), kept);
Ok(())
}
/// A lock that cannot be inspected is an inspection error, and one that
/// cannot be created is a creation error: neither is mistaken for the
/// other, and neither is retried as if another creator had won.
#[cfg(target_os = "linux")]
#[test]
fn a_lock_that_cannot_be_inspected_or_created_says_which() {
let temp = tempfile::tempdir().unwrap();
// The lock's name is one byte past NAME_MAX: inspecting it fails with
// an error that is not "not found".
let long = temp.path().join("a".repeat(251));
let error = exchange_behind_private_lock(&long, &[], &[]).unwrap_err();
assert!(format!("{error:#}").contains("inspecting a store lock"), "{error:#}");
// Absent, but nothing can be created beside it: procfs refuses.
let error = exchange_behind_private_lock(Path::new("/proc/self/idunn-store.cc"), &[], &[])
.unwrap_err();
assert!(format!("{error:#}").contains("creating a private store lock"), "{error:#}");
}
/// Many first publishers race to create the same lock. Whoever loses finds
/// a private lock and carries on: no call fails for having lost.
#[cfg(target_os = "linux")]
#[test]
fn publishers_racing_to_create_the_lock_all_find_it_private() {
use std::sync::Barrier;
const PUBLISHERS: usize = 8;
for round in 0..100 {
let temp = tempfile::tempdir().unwrap();
let store = temp.path().join("topology.cc");
let start = Barrier::new(PUBLISHERS);
let outcomes = thread::scope(|scope| {
(0..PUBLISHERS)
.map(|_| {
scope.spawn(|| {
start.wait();
exchange_behind_private_lock(&store, &[], &[]).map(drop)
})
})
.collect::<Vec<_>>()
.into_iter()
.map(|publisher| publisher.join().unwrap())
.collect::<Vec<_>>()
});
for outcome in outcomes {
outcome.unwrap_or_else(|error| panic!("round {round}: {error:#}"));
}
assert_eq!(mode_of(&authority_lock_path(&store)), 0o600);
}
}
/// A lost race is retried at once, against the store as it now is; a race
/// lost every time is an error rather than a loop.
#[cfg(target_os = "linux")]
#[test]
fn a_lost_race_is_retried_against_the_new_store_and_never_forever() -> Result<()> {
use std::cell::Cell;
let temp = tempfile::tempdir()?;
let driver = topology_driver(temp.path(), "topology");
let rival = |key: &str| CultCacheEnvelope {
key: key.into(),
r#type: "test.rival".into(),
payload: vec![0x90],
stored_at: "2026-01-01T00:00:00+00:00".into(),
schema_id: Some("test.rival.v1".into()),
};
// Lost once: a rival writes between the snapshot and the exchange.
let calls = Cell::new(0);
driver.mutate(|entries| {
calls.set(calls.get() + 1);
if calls.get() == 1 {
upsert_record(&driver.projection_store, rival("first")).unwrap();
}
let mut next = entries.to_vec();
next.push(rival(&format!("mine-{}", calls.get())));
Ok(Some(next))
})?;
assert_eq!(calls.get(), 2);
let keys = driver.snapshot()?.into_iter().map(|e| e.key).collect::<Vec<_>>();
assert_eq!(keys, ["first", "mine-2"]);
// Lost every time: bounded, and the store is the rival's.
let calls = Cell::new(0);
let error = driver
.mutate(|entries| {
calls.set(calls.get() + 1);
upsert_record(&driver.projection_store, rival(&format!("rival-{}", calls.get()))).unwrap();
let mut next = entries.to_vec();
next.push(rival("never"));
Ok(Some(next))
})
.unwrap_err();
assert!(error.to_string().contains("changed repeatedly"), "{error}");
assert_eq!(calls.get(), 8);
assert!(driver.snapshot()?.iter().all(|entry| entry.key != "never"));
Ok(())
}
#[cfg(unix)]
#[test]
fn an_untraversable_runtime_root_is_refused_rather_than_silently_empty() {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().unwrap();
let runtime_root = temp.path().join("runtime");
let bundle = runtime_root.join("sha256-abc");
std::fs::create_dir_all(&bundle).unwrap();
std::fs::set_permissions(&runtime_root, std::fs::Permissions::from_mode(0o700)).unwrap();
let error = ensure_bundle_is_reachable_by_workload(&bundle, None).unwrap_err();
assert!(
error
.to_string()
.contains("not traversable by the workload")
);
std::fs::set_permissions(&runtime_root, std::fs::Permissions::from_mode(0o711)).unwrap();
ensure_bundle_is_reachable_by_workload(&bundle, None).unwrap();
// A 0750 root group-owned by the state group is the other correct
// shape, and is what the write-lease hardening expects: it derives the
// record's group from this directory.
std::fs::set_permissions(&runtime_root, std::fs::Permissions::from_mode(0o750)).unwrap();
let owning_group =
std::os::unix::fs::MetadataExt::gid(&std::fs::metadata(&runtime_root).unwrap());
ensure_bundle_is_reachable_by_workload(&bundle, Some(owning_group)).unwrap();
assert!(ensure_bundle_is_reachable_by_workload(&bundle, Some(owning_group + 1)).is_err());
}
#[test]
fn build_machine_id_is_well_formed_per_workspace_and_never_the_host() {
let first = build_machine_id(Path::new("/var/lib/gamecult/idunn/staging/tx-a")).unwrap();
let second = build_machine_id(Path::new("/var/lib/gamecult/idunn/staging/tx-b")).unwrap();
assert_eq!(first.len(), 32);
assert!(
first
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
);
assert_ne!(first, second);
assert_eq!(
first,
build_machine_id(Path::new("/var/lib/gamecult/idunn/staging/tx-a")).unwrap(),
"a workspace's build identity must be stable for the life of the build"
);
// The point of the derived identity is that it is not this host's: an
// identity a build enrols must not unwrap anywhere but that build.
if let Ok(host) = std::fs::read_to_string("/etc/machine-id") {
assert_ne!(first, host.trim());
}
}
use cultnet_rs::{
GameCultProviderHealthIdentity, IDUNN_EXPECTED_INCARNATION_SCHEMA,
IDUNN_PROCESS_WRITE_LEASE_SCHEMA, IdunnServiceIdentity,
OdinRuntimeTopologyCorrelationPurpose, OdinTopologyAuthenticationContext,
OdinTopologyIdentity, authenticate_odin_runtime_topology_correlation,
enroll_service_identity_at, verify_runtime_authority,
};
fn digest(byte: char) -> String {
format!("sha256-{}", byte.to_string().repeat(64))
}
fn parent_only_descriptor(
fd_number: u32,
fd_name: &str,
source: &str,
) -> ParentOnlyFileDescriptorObservation {
ParentOnlyFileDescriptorObservation {
fd_number,
fd_name: fd_name.into(),
source_path: PathBuf::from(source),
access: "read-only".into(),
device: 1,
inode: u64::from(fd_number),
uid: 0,
gid: 0,
mode: 0o400,
links: 1,
size: 32,
sha256: digest('a'),
}
}
#[test]
fn signer_sources_lower_to_exactly_two_ordered_parent_only_open_files() -> Result<()> {
let activation = parent_only_descriptor(
3,
IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME,
"/run/idunn/activation-credentials/activation.credential",
);
let presence = parent_only_descriptor(
4,
RUNTIME_PRESENCE_IDENTITY_FD_NAME,
"/etc/gamecult/service/runtime-presence-identity.cc",
);
assert_eq!(
parent_only_open_file_properties(&[activation.clone(), presence.clone()])?,
vec![
"/run/idunn/activation-credentials/activation.credential:gamecult-idunn-runtime-activation-key:read-only",
"/etc/gamecult/service/runtime-presence-identity.cc:gamecult-runtime-presence-identity:read-only",
]
);
assert!(parent_only_open_file_properties(&[presence, activation]).is_err());
Ok(())
}
#[cfg(unix)]
fn git_at(repository: &Path, arguments: &[&str]) -> Result<String> {
let output = Command::new("/usr/bin/git")
.arg("-C")
.arg(repository)
.args(arguments)
.env_clear()
.env("HOME", repository)
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("LANG", "C.UTF-8")
.output()?;
ensure!(
output.status.success(),
"test Git failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}
fn fixture_docker_runner_binding() -> DockerRunnerBinding {
DockerRunnerBinding {
image: "eureka-verify-rust".to_owned(),
user: "65532:65532".to_owned(),
affordances: Default::default(),
cache_root: None,
allowed_programs: std::collections::BTreeSet::from(["cargo".to_owned()]),
environment: BTreeMap::new(),
secret_files: BTreeMap::new(),
network_profile: None,
memory_mebibytes: 8192,
cpu_quota_percent: 250,
pids_limit: 512,
tmpfs_mebibytes: 256,
}
}
/// Cut 1, R-Cut1-1: `docker_run_args` lowers a `ContainerSpec` to an exact
/// argv. Pinned byte for byte so a revert (dropping `--cap-drop ALL`), a
/// loosening (defaulting the network to `bridge` instead of `none`), or a
/// function-of-input change (`--cpus` computed as `quota/50` instead of
/// `quota/100`, which this fixture's `cpu_quota_percent = 250` catches:
/// `2.50` against `5.00`) all fail this test.
#[cfg(unix)]
#[test]
fn container_spec_lowers_to_exact_docker_argv() -> Result<()> {
let workspace = PathBuf::from("/var/lib/gamecult/idunn/staging/txn-1/.runner-rust");
let runner = fixture_docker_runner_binding();
let spec = ContainerSpec::for_step(
&runner,
&std::collections::BTreeSet::new(),
("IDUNN_SOURCE_REVISION", "abc123"),
)?;
let argv = vec!["cargo".to_owned(), "test".to_owned(), "--lib".to_owned()];
let args = docker_run_args(&spec, &workspace, Path::new("build"), &argv)?;
let machine_id = build_machine_id(&workspace)?;
let expected: Vec<OsString> = [
"run".to_owned(),
"--rm".to_owned(),
"--network".to_owned(),
"none".to_owned(),
"--memory".to_owned(),
"8192m".to_owned(),
"--cpus".to_owned(),
"2.50".to_owned(),
"--mount".to_owned(),
format!("type=bind,src={},dst=/workspace", workspace.display()),
"--user".to_owned(),
"65532:65532".to_owned(),
"--cap-drop".to_owned(),
"ALL".to_owned(),
"--security-opt".to_owned(),
"no-new-privileges".to_owned(),
"--read-only".to_owned(),
"--pids-limit".to_owned(),
"512".to_owned(),
"--tmpfs".to_owned(),
"/tmp:rw,nosuid,nodev,noexec,size=256m".to_owned(),
"--mount".to_owned(),
format!(
"type=bind,src=/run/idunn/build-machine-ids/{machine_id},dst=/etc/machine-id,readonly"
),
"--workdir".to_owned(),
"/workspace/build".to_owned(),
"--env".to_owned(),
"IDUNN_SOURCE_REVISION=abc123".to_owned(),
"eureka-verify-rust".to_owned(),
"cargo".to_owned(),
"test".to_owned(),
"--lib".to_owned(),
]
.into_iter()
.map(OsString::from)
.collect();
assert_eq!(args, expected);
Ok(())
}
/// Cut 1, R-Cut1-2 (F6): the only environment a step's container receives
/// is the source stamp plus the names it lists in `required_environment`.
/// The fixture binding carries a variable the step does not name; its
/// mutant is a `ContainerSpec::for_step` that copies every binding
/// `environment` entry instead of only the required ones.
#[test]
fn required_environment_is_the_only_environment() -> Result<()> {
let mut runner = fixture_docker_runner_binding();
runner.environment = BTreeMap::from([
("USED".to_owned(), "1".to_owned()),
("UNUSED".to_owned(), "2".to_owned()),
]);
let required = std::collections::BTreeSet::from(["USED".to_owned()]);
let spec =
ContainerSpec::for_step(&runner, &required, ("IDUNN_SOURCE_REVISION", "abc123"))?;
assert_eq!(
spec.environment,
vec![
("IDUNN_SOURCE_REVISION".to_owned(), "abc123".to_owned()),
("USED".to_owned(), "1".to_owned()),
]
);
assert!(spec.secret_mounts.is_empty());
Ok(())
}
/// Cut 1 fix batch, F1: `required_environment_is_the_only_environment`
/// only pinned the exact-name case. The prefix-passthrough mutant (S3a)
/// copies any binding environment entry whose name merely starts with a
/// required name; the ambient mutant (S3b) copies any `GAMECULT_*` or
/// `IDUNN_*` binding entry regardless of whether it is required. This
/// fixture's binding carries both `USEDLONGER` (a prefix collision with
/// the required `USED`) and `GAMECULT_AMBIENT` / `IDUNN_AMBIENT`, and the
/// expected environment excludes every one of them.
#[test]
fn required_environment_rejects_prefix_and_ambient_passthrough() -> Result<()> {
let mut runner = fixture_docker_runner_binding();
runner.environment = BTreeMap::from([
("USED".to_owned(), "1".to_owned()),
("USEDLONGER".to_owned(), "prefix-collision".to_owned()),
("GAMECULT_AMBIENT".to_owned(), "ambient".to_owned()),
("IDUNN_AMBIENT".to_owned(), "ambient".to_owned()),
]);
let required = std::collections::BTreeSet::from(["USED".to_owned()]);
let spec =
ContainerSpec::for_step(&runner, &required, ("IDUNN_SOURCE_REVISION", "abc123"))?;
assert_eq!(
spec.environment,
vec![
("IDUNN_SOURCE_REVISION".to_owned(), "abc123".to_owned()),
("USED".to_owned(), "1".to_owned()),
]
);
Ok(())
}
/// Cut 1 fix batch, Stamp override: a runner cannot bind a step
/// environment name that collides with the Idunn source stamp, even when
/// the binding names it and the step requires it. This is enforced at
/// `ContainerSpec::for_step`, the one place every runner's environment is
/// assembled, rather than only at deploy's separate `admit` check.
#[test]
fn container_spec_for_step_refuses_stamp_override() {
let mut runner = fixture_docker_runner_binding();
runner.environment = BTreeMap::from([("STAMP".to_owned(), "override".to_owned())]);
let required = std::collections::BTreeSet::from(["STAMP".to_owned()]);
let result = ContainerSpec::for_step(&runner, &required, ("STAMP", "abc123"));
assert!(
result.is_err(),
"expected a runner binding to be refused when it can override the source stamp"
);
}
/// Cut 1 fix batch, F1: `docker_run_args` pins the argv exactly for every
/// network branch, not only the default `None` case. The prior test's
/// mutants that survived here were `--cap-drop ALL` dropped only when the
/// network is `bridge` (S1) and `--read-only` dropped only when the
/// network is `Named` (S5 covers the explicit-`none`-becomes-`bridge`
/// loosening separately below). Every branch must carry every security
/// flag identically; only `--network` differs.
#[cfg(unix)]
#[test]
fn docker_run_args_pins_every_network_branch() -> Result<()> {
let workspace = PathBuf::from("/var/lib/gamecult/idunn/staging/txn-net/.runner-rust");
let runner = fixture_docker_runner_binding();
let argv = vec!["cargo".to_owned(), "test".to_owned()];
let machine_id = build_machine_id(&workspace)?;
let base = |network: &str| -> Vec<OsString> {
[
"run".to_owned(),
"--rm".to_owned(),
"--network".to_owned(),
network.to_owned(),
"--memory".to_owned(),
"8192m".to_owned(),
"--cpus".to_owned(),
"2.50".to_owned(),
"--mount".to_owned(),
format!("type=bind,src={},dst=/workspace", workspace.display()),
"--user".to_owned(),
"65532:65532".to_owned(),
"--cap-drop".to_owned(),
"ALL".to_owned(),
"--security-opt".to_owned(),
"no-new-privileges".to_owned(),
"--read-only".to_owned(),
"--pids-limit".to_owned(),
"512".to_owned(),
"--tmpfs".to_owned(),
"/tmp:rw,nosuid,nodev,noexec,size=256m".to_owned(),
"--mount".to_owned(),
format!(
"type=bind,src=/run/idunn/build-machine-ids/{machine_id},dst=/etc/machine-id,readonly"
),
"--workdir".to_owned(),
"/workspace/build".to_owned(),
"--env".to_owned(),
"IDUNN_SOURCE_REVISION=abc123".to_owned(),
"eureka-verify-rust".to_owned(),
"cargo".to_owned(),
"test".to_owned(),
]
.into_iter()
.map(OsString::from)
.collect()
};
for (profile, expected_network) in
[(None, "none"), (Some("bridge"), "bridge"), (Some("odin-verse"), "odin-verse")]
{
let mut r = runner.clone();
r.network_profile = profile.map(str::to_owned);
let spec = ContainerSpec::for_step(
&r,
&std::collections::BTreeSet::new(),
("IDUNN_SOURCE_REVISION", "abc123"),
)?;
let args = docker_run_args(&spec, &workspace, Path::new("build"), &argv)?;
assert_eq!(args, base(expected_network), "network branch {profile:?}");
}
Ok(())
}
/// Cut 1 fix batch, F1, S5: an explicit `network_profile = "none"` must
/// lower to `ContainerNetwork::None`, not `Bridge`.
#[test]
fn explicit_none_network_profile_is_none_not_bridge() -> Result<()> {
let mut runner = fixture_docker_runner_binding();
runner.network_profile = Some("none".to_owned());
let spec = ContainerSpec::for_step(
&runner,
&std::collections::BTreeSet::new(),
("IDUNN_SOURCE_REVISION", "abc123"),
)?;
assert_eq!(spec.network, ContainerNetwork::None);
Ok(())
}
/// Cut 1 fix batch, F1, S2a/S2b/S2c: `--cpus` is pinned across a range of
/// quotas that a round-to-nearest-0.5 mutant, a floor-to-50 mutant, and a
/// round-to-integer mutant each compute differently from the true
/// `quota / 100`.
#[test]
fn docker_run_args_pins_cpu_quota_across_the_range() -> Result<()> {
let workspace = PathBuf::from("/var/lib/gamecult/idunn/staging/txn-cpu/.runner-rust");
let argv = vec!["cargo".to_owned(), "test".to_owned()];
for (quota, expected) in [
(100u32, "1.00"),
(150, "1.50"),
(250, "2.50"),
(333, "3.33"),
(800, "8.00"),
] {
let mut runner = fixture_docker_runner_binding();
runner.cpu_quota_percent = quota;
let spec = ContainerSpec::for_step(
&runner,
&std::collections::BTreeSet::new(),
("IDUNN_SOURCE_REVISION", "abc123"),
)?;
let args = docker_run_args(&spec, &workspace, Path::new("."), &argv)?;
let cpus_index = args
.iter()
.position(|a| a == OsStr::new("--cpus"))
.expect("--cpus flag present");
assert_eq!(
args[cpus_index + 1],
OsString::from(expected),
"quota {quota}"
);
}
Ok(())
}
/// Cut 1 fix batch, F1, S6: a cache root adds exactly one `--mount` for
/// `/cache`, never a second mount of the cache's parent directory. This
/// fixture asserts the full argv, so any extra mount breaks equality.
#[cfg(unix)]
#[test]
fn docker_run_args_mounts_cache_root_exactly_once() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
// Left uncreated: `ensure_runner_cache_root` creates it itself, owned
// by the runner's exact identity at mode 0700.
let cache = temp.path().join("cache");
let workspace = PathBuf::from("/var/lib/gamecult/idunn/staging/txn-cache/.runner-rust");
let argv = vec!["cargo".to_owned(), "test".to_owned()];
let machine_id = build_machine_id(&workspace)?;
// Absent: no cache mount at all.
let mut runner = fixture_docker_runner_binding();
let spec = ContainerSpec::for_step(
&runner,
&std::collections::BTreeSet::new(),
("IDUNN_SOURCE_REVISION", "abc123"),
)?;
let args = docker_run_args(&spec, &workspace, Path::new("."), &argv)?;
assert!(
!args.iter().any(|a| a == OsStr::new("/cache") || a.to_string_lossy().contains("dst=/cache")),
"no cache mount expected when cache_root is absent"
);
// Present: exactly one mount, of the cache root itself.
runner.cache_root = Some(cache.clone());
let spec = ContainerSpec::for_step(
&runner,
&std::collections::BTreeSet::new(),
("IDUNN_SOURCE_REVISION", "abc123"),
)?;
let args = docker_run_args(&spec, &workspace, Path::new("."), &argv)?;
let expected: Vec<OsString> = [
"run".to_owned(),
"--rm".to_owned(),
"--network".to_owned(),
"none".to_owned(),
"--memory".to_owned(),
"8192m".to_owned(),
"--cpus".to_owned(),
"2.50".to_owned(),
"--mount".to_owned(),
format!("type=bind,src={},dst=/workspace", workspace.display()),
"--user".to_owned(),
"65532:65532".to_owned(),
"--cap-drop".to_owned(),
"ALL".to_owned(),
"--security-opt".to_owned(),
"no-new-privileges".to_owned(),
"--read-only".to_owned(),
"--pids-limit".to_owned(),
"512".to_owned(),
"--tmpfs".to_owned(),
"/tmp:rw,nosuid,nodev,noexec,size=256m".to_owned(),
"--mount".to_owned(),
format!(
"type=bind,src=/run/idunn/build-machine-ids/{machine_id},dst=/etc/machine-id,readonly"
),
"--mount".to_owned(),
format!("type=bind,src={},dst=/cache", cache.display()),
"--workdir".to_owned(),
"/workspace/".to_owned(),
"--env".to_owned(),
"IDUNN_SOURCE_REVISION=abc123".to_owned(),
"eureka-verify-rust".to_owned(),
"cargo".to_owned(),
"test".to_owned(),
]
.into_iter()
.map(OsString::from)
.collect();
assert_eq!(args, expected);
Ok(())
}
/// Cut 1 fix batch, F1, S4: a secret mount's bind is read-only. This
/// fixture also exercises `for_step`'s live secret validation, so it
/// doubles as the positive twin of S9 below.
#[cfg(unix)]
#[test]
fn docker_run_args_mounts_secrets_read_only() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let secret = temp.path().join("secret.cc");
fs::write(&secret, b"s").unwrap();
ensure!(
Command::new("/bin/chown")
.arg("0:65532")
.arg(&secret)
.status()?
.success(),
"chowning fixture secret"
);
fs::set_permissions(&secret, fs::Permissions::from_mode(0o440))?;
let mut runner = fixture_docker_runner_binding();
runner.secret_files = BTreeMap::from([("A_SECRET".to_owned(), secret.clone())]);
let required = std::collections::BTreeSet::from(["A_SECRET".to_owned()]);
let spec = ContainerSpec::for_step(&runner, &required, ("IDUNN_SOURCE_REVISION", "abc123"))?;
let workspace = PathBuf::from("/var/lib/gamecult/idunn/staging/txn-secret/.runner-rust");
let args = docker_run_args(
&spec,
&workspace,
Path::new("."),
&vec!["cargo".to_owned(), "test".to_owned()],
)?;
let secret_text = secret.display().to_string();
let mount = args
.iter()
.map(|a| a.to_string_lossy().into_owned())
.find(|a| a.starts_with("type=bind") && a.contains(&secret_text))
.expect("secret mount present");
assert!(mount.ends_with(",readonly"), "secret mount must be read-only: {mount}");
Ok(())
}
/// F6/N1 (Soul, second Cut 1 fix batch): `--read-only` is present on a
/// Named network runner even when a cache root is also mounted. Neither
/// branch alone exercised this combination before.
#[cfg(unix)]
#[test]
fn docker_run_args_keeps_read_only_with_a_named_network_and_a_cache_root() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let cache = temp.path().join("cache");
let mut runner = fixture_docker_runner_binding();
runner.network_profile = Some("build-net".to_owned());
runner.cache_root = Some(cache);
let spec = ContainerSpec::for_step(
&runner,
&std::collections::BTreeSet::new(),
("IDUNN_SOURCE_REVISION", "abc123"),
)?;
let workspace = PathBuf::from("/var/lib/gamecult/idunn/staging/txn-n1/.runner-rust");
let args = docker_run_args(&spec, &workspace, Path::new("."), &vec!["cargo".to_owned(), "test".to_owned()])?;
assert!(
args.iter().any(|a| a == OsStr::new("--read-only")),
"a Named network plus a cache root must not drop --read-only: {args:?}"
);
Ok(())
}
/// F6/N2 (Soul, second Cut 1 fix batch): a secret mount stays read-only
/// even when the runner also carries a plain environment entry.
#[cfg(unix)]
#[test]
fn docker_run_args_keeps_secret_mounts_read_only_alongside_plain_environment() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let secret = temp.path().join("secret.cc");
fs::write(&secret, b"s").unwrap();
ensure!(
Command::new("/bin/chown").arg("0:65532").arg(&secret).status()?.success(),
"chowning fixture secret"
);
fs::set_permissions(&secret, fs::Permissions::from_mode(0o440))?;
let mut runner = fixture_docker_runner_binding();
runner.secret_files = BTreeMap::from([("A_SECRET".to_owned(), secret.clone())]);
runner.environment = BTreeMap::from([("PLAIN_ONE".to_owned(), "x".to_owned())]);
let required = std::collections::BTreeSet::from(["A_SECRET".to_owned(), "PLAIN_ONE".to_owned()]);
let spec = ContainerSpec::for_step(&runner, &required, ("IDUNN_SOURCE_REVISION", "abc123"))?;
let workspace = PathBuf::from("/var/lib/gamecult/idunn/staging/txn-n2/.runner-rust");
let args = docker_run_args(&spec, &workspace, Path::new("."), &vec!["cargo".to_owned(), "test".to_owned()])?;
let secret_text = secret.display().to_string();
let mount = args
.iter()
.map(|a| a.to_string_lossy().into_owned())
.find(|a| a.starts_with("type=bind") && a.contains(&secret_text))
.expect("secret mount present");
assert!(
mount.ends_with(",readonly"),
"a plain environment entry must not make a secret mount writable: {mount}"
);
Ok(())
}
/// F6/N3 (Soul, second Cut 1 fix batch): the Idunn source stamp survives
/// as the container's environment even when the runner carries both a
/// secret and a plain environment entry.
#[cfg(unix)]
#[test]
fn container_spec_for_step_keeps_the_source_stamp_with_a_secret_and_plain_environment() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let secret = temp.path().join("secret.cc");
fs::write(&secret, b"s").unwrap();
ensure!(
Command::new("/bin/chown").arg("0:65532").arg(&secret).status()?.success(),
"chowning fixture secret"
);
fs::set_permissions(&secret, fs::Permissions::from_mode(0o440))?;
let mut runner = fixture_docker_runner_binding();
runner.secret_files = BTreeMap::from([("A_SECRET".to_owned(), secret)]);
runner.environment = BTreeMap::from([("PLAIN_ONE".to_owned(), "x".to_owned())]);
let required = std::collections::BTreeSet::from(["A_SECRET".to_owned(), "PLAIN_ONE".to_owned()]);
let spec = ContainerSpec::for_step(&runner, &required, ("IDUNN_SOURCE_REVISION", "abc123"))?;
assert_eq!(
spec.environment.first(),
Some(&("IDUNN_SOURCE_REVISION".to_owned(), "abc123".to_owned())),
"the source stamp must survive alongside a secret and a plain environment entry"
);
Ok(())
}
/// F6/N5 (Soul, second Cut 1 fix batch): `freeze_exact` records that the
/// frozen tree contains a Git LFS pointer, and the pointer bytes
/// themselves -- never the real LFS content, which is never fetched --
/// are what land on disk.
#[cfg(unix)]
#[test]
fn freeze_exact_sets_the_lfs_pointer_flag_for_a_pointer_file() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(&origin_repo, &["config", "user.email", "idunn-test@example.invalid"])?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
let pointer = b"version https://git-lfs.github.com/spec/v1\noid sha256:0000000000000000000000000000000000000000000000000000000000000000\nsize 3\n";
fs::write(origin_repo.join("lfs.bin"), pointer)?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown").args(["-R", "1000:1000"]).arg(&origin_repo).status()?.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity { uid: 1000, gid: 1000 };
let driver = GitSourceDriver::new(source_cache_root.clone(), frozen_source_root.clone(), Some(identity));
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let (tree_root, _snapshot_sha256, _recipe_bytes, contains_lfs_pointers) =
driver.freeze_exact(&source, &revision, "txn-lfs", &frozen_source_root)?;
assert!(contains_lfs_pointers, "an LFS pointer file must set the flag");
assert_eq!(fs::read(tree_root.join("lfs.bin"))?, pointer);
Ok(())
}
/// Cut 1 fix batch, F1, S9: `ContainerSpec::for_step` must call
/// `validate_runner_secret`, not merely mount whatever path the binding
/// names. This fixture's secret file has the wrong mode (`0644` instead
/// of the required `0440`), so the real validator refuses it; a mutant
/// that skips the call would let this through.
#[cfg(unix)]
#[test]
fn container_spec_for_step_validates_secret_files() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let secret = temp.path().join("secret.cc");
fs::write(&secret, b"s").unwrap();
ensure!(
Command::new("/bin/chown")
.arg("0:65532")
.arg(&secret)
.status()?
.success(),
"chowning fixture secret"
);
// Wrong: group-writable/world-readable, not the required 0440.
fs::set_permissions(&secret, fs::Permissions::from_mode(0o644))?;
let mut runner = fixture_docker_runner_binding();
runner.secret_files = BTreeMap::from([("A_SECRET".to_owned(), secret)]);
let required = std::collections::BTreeSet::from(["A_SECRET".to_owned()]);
let result = ContainerSpec::for_step(&runner, &required, ("IDUNN_SOURCE_REVISION", "abc123"));
assert!(result.is_err(), "expected an invalid secret file to be refused");
Ok(())
}
/// Cut 1 fix batch, F1, S10: `ContainerSpec::for_step` must call
/// `ensure_runner_cache_root`. This fixture's cache root sits under a
/// world-writable parent, which the real validator refuses; a mutant
/// that skips the call would let this through.
#[cfg(unix)]
#[test]
fn container_spec_for_step_validates_cache_root() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let bad_parent = temp.path().join("bad-parent");
fs::create_dir(&bad_parent)?;
// Wrong: world-writable, not root-owned-and-nonwritable.
fs::set_permissions(&bad_parent, fs::Permissions::from_mode(0o777))?;
let mut runner = fixture_docker_runner_binding();
runner.cache_root = Some(bad_parent.join("cache"));
let result = ContainerSpec::for_step(
&runner,
&std::collections::BTreeSet::new(),
("IDUNN_SOURCE_REVISION", "abc123"),
);
assert!(result.is_err(), "expected an unsafe cache root parent to be refused");
Ok(())
}
/// Cut 1, R-Cut1-3: `freeze_exact` archives an exact revision through the
/// new entry point (no `OperatorBinding` or compiled plan), and the
/// archived recipe file must equal the recipe blob it read from the same
/// tree. Reuses the bare-repository fixture pattern from
/// `exact_git_archive_becomes_root_owned_immutable_source_without_git_metadata`.
/// Its mutant skips the "materialized recipe equals the tree blob" check.
#[cfg(unix)]
#[test]
fn freeze_exact_is_byte_exact_and_recipe_checked() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
// The unprivileged identity below clones this fixture as a local
// path, which puts Git's ownership check in play: a real origin is a
// network remote with no local uid to compare against, but this
// fixture is a directory this (root) test process just created. Hand
// it to the unprivileged identity so the clone sees itself as owner,
// the way a real remote never has to.
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let (tree_root, snapshot_sha256, recipe_bytes, contains_lfs_pointers) =
driver.freeze_exact(&source, &revision, "txn-1", &frozen_source_root)?;
assert_eq!(recipe_bytes, b"target = 'test'\n".to_vec());
assert!(!contains_lfs_pointers);
assert!(tree_root.join("deployment.toml").is_file());
assert!(!tree_root.join(".git").exists());
assert!(snapshot_sha256.starts_with("sha256-"));
Ok(())
}
/// Cut 1, R-Cut1-3 negative twin: an `export-subst` recipe means `git
/// archive` writes a substituted `$Format:%H$` token into the archived
/// file. Before the F2 fix batch, `git archive` (what `freeze_exact` used
/// to materialize with) expanded that token while `git cat-file blob`
/// (what `exact_recipe_and_gitlinks` reads) returned it unexpanded, so the
/// two byte strings the recipe-bytes-equality check compares genuinely
/// differed and freeze_exact refused. After F2, `freeze_exact` no longer
/// archives anything — the recipe is written raw, the same as everything
/// else — so `export-subst` is no longer a transform at all and the
/// frozen recipe keeps its literal, unexpanded token. This is now a
/// byte-exactness positive case rather than a rejection case; the
/// recipe-bytes-equality check itself has no remaining fixture that can
/// make it fail (not yet reached — see the F2 fix-batch report).
#[cfg(unix)]
#[test]
fn freeze_exact_keeps_an_export_subst_recipe_literal() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(
origin_repo.join(".gitattributes"),
b"deployment.toml export-subst\n",
)?;
fs::write(
origin_repo.join("deployment.toml"),
b"target = 'test'\n# $Format:%H$\n",
)?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let (tree_root, _snapshot_sha256, recipe_bytes, _contains_lfs_pointers) =
driver.freeze_exact(&source, &revision, "txn-2", &frozen_source_root)?;
assert_eq!(recipe_bytes, b"target = 'test'\n# $Format:%H$\n".to_vec());
assert_eq!(
fs::read(tree_root.join("deployment.toml"))?,
b"target = 'test'\n# $Format:%H$\n".to_vec()
);
Ok(())
}
/// Cut 1 fix batch, F5, S7: `freeze_exact` must materialize every
/// declared Gitlink, not silently skip the loop. This fixture archives a
/// main repository with one Gitlink and asserts the child's own file
/// lands at the Gitlink's path in the frozen tree; a mutant that skips
/// the Gitlink loop leaves that path absent.
#[cfg(unix)]
#[test]
fn freeze_exact_materializes_gitlinks() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let sub_repo = temp.path().join("sub");
fs::create_dir(&sub_repo)?;
git_at(&sub_repo, &["init", "--initial-branch=main"])?;
git_at(&sub_repo, &["config", "user.name", "Idunn Test"])?;
git_at(&sub_repo, &["config", "user.email", "idunn-test@example.invalid"])?;
fs::write(sub_repo.join("lib.txt"), b"vendored content\n")?;
git_at(&sub_repo, &["add", "--all"])?;
git_at(&sub_repo, &["commit", "-m", "sub fixture"])?;
let sub_revision = git_at(&sub_repo, &["rev-parse", "HEAD"])?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(
&origin_repo,
&[
"update-index",
"--add",
"--cacheinfo",
&format!("160000,{sub_revision},vendor/sub"),
],
)?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
for repo in [&origin_repo, &sub_repo] {
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(repo)
.status()?
.success(),
"chowning the fixture repository"
);
}
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::from([(
PathBuf::from("vendor/sub"),
GitlinkBinding {
origin: sub_repo.to_string_lossy().into_owned(),
},
)]),
recipe_path: PathBuf::from("deployment.toml"),
};
let (tree_root, _snapshot_sha256, _recipe_bytes, contains_lfs_pointers) =
driver.freeze_exact(&source, &revision, "txn-gitlink", &frozen_source_root)?;
assert!(!contains_lfs_pointers);
let materialized = fs::read_to_string(tree_root.join("vendor/sub/lib.txt"))
.context("reading materialized Gitlink content")?;
assert_eq!(materialized, "vendored content\n");
Ok(())
}
/// F1 (Self's ruling, second Cut 1 fix batch): every blob a frozen tree
/// needs is fetched in one bulk round trip, not lazily one object at a
/// time as `cat-file --batch` reads them. A spy standing in for
/// `/usr/bin/git` records every invocation; exactly one of them is the
/// bulk `fetch --stdin` call.
#[cfg(unix)]
#[test]
fn freeze_exact_bulk_fetches_every_blob_in_one_round_trip() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
for i in 0..8 {
fs::write(origin_repo.join(format!("f{i}.txt")), format!("file {i}\n"))?;
}
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let log = temp.path().join("git-invocations.log");
// Pre-created and world-writable: the spy runs under the
// unprivileged Git identity, which cannot create a new file in this
// root-owned 0755 directory, only append to one that already exists.
fs::write(&log, b"")?;
fs::set_permissions(&log, fs::Permissions::from_mode(0o666))?;
let spy = temp.path().join("git-spy.sh");
fs::write(
&spy,
format!(
"#!/bin/sh\necho \"$@\" >> {}\nexec /usr/bin/git \"$@\"\n",
log.display()
),
)?;
fs::set_permissions(&spy, fs::Permissions::from_mode(0o755))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let mut driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
driver.git_program = spy;
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
driver.freeze_exact(&source, &revision, "txn-bulk", &frozen_source_root)?;
let log_text = fs::read_to_string(&log).unwrap_or_default();
let bulk_fetches = log_text
.lines()
.filter(|line| line.contains("fetch") && line.contains("--stdin"))
.count();
assert_eq!(
bulk_fetches, 1,
"expected exactly one bulk --stdin fetch; invocations:\n{log_text}"
);
// Git's own lazy fetch for a promisor remote sends exactly these; a
// hosted origin (GitHub) omits an explicitly wanted blob, and Git's
// connectivity check then fails, without them. A file:// origin
// delivers the blob either way, so only the arguments can pin this.
let bulk_line = log_text
.lines()
.find(|line| line.contains("fetch") && line.contains("--stdin"))
.unwrap_or_default();
for required in [
"fetch.negotiationAlgorithm=noop",
"--recurse-submodules=no",
"--filter=blob:none",
] {
assert!(
bulk_line.contains(required),
"bulk fetch lacks {required}: {bulk_line}"
);
}
Ok(())
}
/// A real partial clone: the origin advertises `uploadpack.allowFilter`,
/// so `--filter=blob:none` is honoured and the checkout holds no blobs.
/// Every other fixture's origin ignores the filter and clones in full,
/// so `bulk_fetch_objects` never had a missing blob to fetch and never
/// met Git's post-fetch connectivity check on a bare blob id. The blob
/// is checked with `GIT_NO_LAZY_FETCH=1`, so only the bulk fetch itself
/// can have delivered it.
#[cfg(unix)]
#[test]
fn bulk_fetch_objects_delivers_a_blob_into_a_blob_none_partial_clone() -> Result<()> {
let temp = tempfile::tempdir()?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
git_at(&origin_repo, &["config", "uploadpack.allowFilter", "true"])?;
git_at(&origin_repo, &["config", "uploadpack.allowAnySHA1InWant", "true"])?;
fs::write(origin_repo.join("payload.txt"), b"partial clone payload\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let blob = git_at(&origin_repo, &["rev-parse", "HEAD:payload.txt"])?;
let source_cache_root = temp.path().join("source-cache");
fs::create_dir(&source_cache_root)?;
let driver = GitSourceDriver::new(
source_cache_root.clone(),
temp.path().join("frozen"),
None,
);
let checkout = source_cache_root.join("checkout");
driver.git([
OsString::from("clone"),
OsString::from("--filter=blob:none"),
OsString::from("--no-checkout"),
OsString::from(format!("file://{}", origin_repo.display())),
checkout.as_os_str().to_owned(),
])?;
let blob_present = || -> Result<bool> {
Ok(Command::new("/usr/bin/git")
.arg("-C")
.arg(&checkout)
.args(["cat-file", "-t", &blob])
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("GIT_NO_LAZY_FETCH", "1")
.output()?
.status
.success())
};
ensure!(
!blob_present()?,
"fixture is not a partial clone: the blob is already local"
);
driver.bulk_fetch_objects(&checkout, std::slice::from_ref(&blob))?;
assert!(
blob_present()?,
"bulk fetch succeeded but did not deliver the blob"
);
Ok(())
}
/// F2/F3 (Self's rulings, second Cut 1 fix batch): a tree with two
/// entries literally named `a` at the same level -- one a symlink
/// pointing outside the frozen root, one a subtree holding `pwn` -- must
/// be refused before anything is written, not partially materialized.
/// `git hash-object --literally` builds the tree directly, bypassing the
/// checks `git mktree` would apply, the same construction Soul's probe
/// used to find the regression this pins.
#[cfg(unix)]
#[test]
fn freeze_exact_refuses_a_duplicate_named_tree_that_would_write_outside_its_root() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let hash_object = |repo: &Path, content: &[u8]| -> Result<String> {
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(repo)
.args(["hash-object", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(content)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object failed");
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let outside = temp.path().join("outside-root-owned");
fs::create_dir(&outside)?;
fs::set_permissions(&outside, fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "good"])?;
let good = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
// `file://`, not a plain path: a plain local path triggers Git's
// optimized same-filesystem clone, which never runs the
// pack/transfer code path `transfer.fsckObjects` gates. `file://`
// forces the real transfer, the same one a real remote gets, so
// this fixture also exercises F2 layer (a)'s fsck defense.
origin: format!("file://{}", origin_repo.display()),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
// Freeze the earlier, good commit first, while `main` still points
// at it: this is what actually creates the checkout, through
// `ensure_checkout`'s own (also fsck-guarded) clone. The malformed
// commit built below moves `main` only after this call, so it is
// fetched into an *already-existing* checkout: only the explicit,
// per-revision `fetch` in `freeze_exact` -- not the initial clone --
// is what has to refuse it. That isolates this fixture to the one
// fetch F2 layer (a)'s fsck flag actually guards.
driver.freeze_exact(&source, &good, "txn-good", &frozen_source_root)?;
let recipe = hash_object(&origin_repo, b"target = 'test'\n")?;
let pwn = hash_object(&origin_repo, b"written through a symlink by root\n")?;
let link = hash_object(&origin_repo, outside.to_string_lossy().as_bytes())?;
let subtree_output = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&origin_repo)
.args(["mktree"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write as _;
child
.stdin
.take()
.unwrap()
.write_all(format!("100644 blob {pwn}\tpwn\n").as_bytes())?;
child.wait_with_output()
})?;
ensure!(subtree_output.status.success(), "git mktree failed");
let subtree = String::from_utf8(subtree_output.stdout)?.trim().to_owned();
let hex = |s: &str| -> Vec<u8> {
(0..20)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
.collect()
};
let mut raw = Vec::new();
for (mode, name, sha) in [
("120000", "a", &link),
("40000", "a", &subtree),
("100644", "deployment.toml", &recipe),
] {
raw.extend_from_slice(format!("{mode} {name}\0").as_bytes());
raw.extend(hex(sha));
}
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&origin_repo)
.args(["hash-object", "-t", "tree", "--literally", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(&raw)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object --literally failed");
let tree = String::from_utf8(output.stdout)?.trim().to_owned();
// Not `git_at`: the repository is already chowned to the
// unprivileged identity above, and this process is root, so plain
// `git` here refuses it as "dubious ownership" without `safe.directory`.
let git_owned = |args: &[&str]| -> Result<String> {
let output = Command::new("git")
.args([
"-c", "safe.directory=*",
"-c", "user.name=Idunn Test",
"-c", "user.email=idunn-test@example.invalid",
"-C",
])
.arg(&origin_repo)
.args(args)
.output()?;
ensure!(output.status.success(), "git {:?} failed: {}", args, String::from_utf8_lossy(&output.stderr));
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let commit = git_owned(&["commit-tree", &tree, "-m", "dup"])?;
git_owned(&["update-ref", "refs/heads/main", &commit])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let result = driver.freeze_exact(&source, &commit, "txn-dup", &frozen_source_root);
assert!(
result.is_err(),
"a tree with a leaf entry aliasing another entry's ancestor must be refused"
);
assert!(
!outside.join("pwn").exists(),
"the writer must never place content outside the frozen root, even on refusal"
);
Ok(())
}
/// S7 (Self's ruling, third Cut 1 fix batch): two regular files whose
/// names differ only by case are two distinct, individually valid Git
/// blobs. `git fsck` accepts them, the host is case-sensitive ext4, and
/// the writer places both side by side without ever colliding. The
/// case-insensitive refusal this used to pin is deleted; this fixture now
/// pins the opposite -- that a mutant reintroducing it is caught.
#[cfg(unix)]
#[test]
fn freeze_exact_accepts_two_paths_that_differ_only_by_case() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
fs::write(origin_repo.join("README.txt"), b"upper\n")?;
fs::write(origin_repo.join("readme.txt"), b"lower\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let (frozen_root, ..) = driver.freeze_exact(&source, &revision, "txn-case", &frozen_source_root)?;
assert!(
frozen_root.join("README.txt").is_file() && frozen_root.join("readme.txt").is_file(),
"two paths that differ only by case must both freeze, side by side, on a case-sensitive host"
);
Ok(())
}
/// F3 (Self's ruling, second Cut 1 fix batch), isolated from every layer
/// above it: `ensure_frozen_directory` is the writer's own primitive, and
/// it must refuse a path that already has *something* at it -- even a
/// plain pre-existing directory this call did not itself create --
/// rather than reuse it. Calling it directly, with a pre-seeded root a
/// real freeze would never produce, is what proves the writer itself
/// carries this invariant on its own, rather than relying on `git fsck`
/// upstream or a write-order accident downstream to prevent the same
/// outcome.
#[cfg(unix)]
#[test]
fn ensure_frozen_directory_refuses_an_existing_entry_it_did_not_create() -> Result<()> {
use std::os::unix::fs::{PermissionsExt, symlink};
let temp = tempfile::tempdir()?;
let root = temp.path().join("root");
fs::create_dir(&root)?;
fs::set_permissions(&root, fs::Permissions::from_mode(0o755))?;
// A directory nobody's `created_dirs` bookkeeping knows about.
let foreign_dir = root.join("foreign");
fs::create_dir(&foreign_dir)?;
let mut created_dirs = std::collections::HashSet::new();
created_dirs.insert(root.clone());
assert!(
ensure_frozen_directory(&root, &mut created_dirs, &foreign_dir).is_err(),
"a pre-existing directory this call did not create must be refused, not reused"
);
// A symlink standing where a directory is wanted.
let link_dir = root.join("link");
symlink("/tmp", &link_dir)?;
let mut created_dirs = std::collections::HashSet::new();
created_dirs.insert(root.clone());
assert!(
ensure_frozen_directory(&root, &mut created_dirs, &link_dir).is_err(),
"a symlink standing where a directory is wanted must be refused"
);
// The positive case: an absent path is created and remembered.
let fresh_dir = root.join("fresh");
let mut created_dirs = std::collections::HashSet::new();
created_dirs.insert(root.clone());
ensure_frozen_directory(&root, &mut created_dirs, &fresh_dir)?;
assert!(fresh_dir.is_dir());
assert!(created_dirs.contains(&fresh_dir));
// Calling it again for the same, now-tracked path is a no-op.
ensure_frozen_directory(&root, &mut created_dirs, &fresh_dir)?;
Ok(())
}
/// F4 (Self's ruling, second Cut 1 fix batch): `freeze_exact` refuses a
/// symlink that escapes the frozen root, whether the target is absolute
/// or a relative `..` walk, before the tree is ever published. This
/// exercises the guard through the public `freeze_exact` entry point
/// (not the `harden_frozen_source` unit alone), so removing the call
/// inside `freeze_exact` -- not just the function -- is what this pins.
#[cfg(unix)]
#[test]
fn freeze_exact_refuses_an_absolute_or_escaping_symlink() -> Result<()> {
use std::os::unix::fs::{PermissionsExt, symlink};
for (label, make_link) in [
(
"absolute",
Box::new(|repo: &Path| symlink("/etc/passwd", repo.join("l"))) as Box<dyn Fn(&Path) -> std::io::Result<()>>,
),
(
"escaping",
Box::new(|repo: &Path| {
fs::create_dir(repo.join("d"))?;
symlink("../../../outside-the-root", repo.join("d/l"))
}),
),
] {
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
make_link(&origin_repo)?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let result = driver.freeze_exact(&source, &revision, "txn-escape", &frozen_source_root);
assert!(result.is_err(), "{label} symlink must be refused");
}
Ok(())
}
/// F5 (Self's ruling, second Cut 1 fix batch): a lexical `..`-count is
/// not enough once the chain passes back through another symlink. `D`
/// points at `.` (itself), and `L` walks through a dozen `D` hops before
/// finally leaving with `..` -- lexically that undercounts how far the
/// chain really goes, because each `D` the OS resolves is a fresh copy
/// of the root, not one ordinary path component. `L` actually resolves
/// to `/etc/passwd`.
#[cfg(unix)]
#[test]
fn freeze_exact_refuses_a_symlink_chain_that_escapes_through_a_self_referential_directory() -> Result<()> {
use std::os::unix::fs::{PermissionsExt, symlink};
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
symlink(".", origin_repo.join("D"))?;
let chain = "D/D/D/D/D/D/D/D/D/D/D/D/../../../../../../../../../../../../etc/passwd";
symlink(chain, origin_repo.join("L"))?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let result = driver.freeze_exact(&source, &revision, "txn-chain", &frozen_source_root);
assert!(
result.is_err(),
"a symlink chain that resolves outside the root through a self-referential directory must be refused"
);
Ok(())
}
/// F2 (Self's ruling, Cut 1 fix batch): `freeze_exact` writes blobs raw
/// from the object store, with no attribute-driven transform, across
/// every transform `.gitattributes` can name: `eol=crlf`, `export-ignore`,
/// `export-subst`, `ident`, a symlink, the executable bit, and a Gitlink.
/// Every frozen file's bytes are asserted equal to `git cat-file blob` at
/// the selected revision — the same check the recipe already gets, now
/// over the whole tree.
#[cfg(unix)]
#[test]
fn freeze_exact_is_byte_exact_across_every_attribute_transform() -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let sub_repo = temp.path().join("sub");
fs::create_dir(&sub_repo)?;
git_at(&sub_repo, &["init", "--initial-branch=main"])?;
git_at(&sub_repo, &["config", "user.name", "Idunn Test"])?;
git_at(&sub_repo, &["config", "user.email", "idunn-test@example.invalid"])?;
fs::write(sub_repo.join("lib.txt"), b"sub content\n")?;
git_at(&sub_repo, &["add", "--all"])?;
git_at(&sub_repo, &["commit", "-m", "sub fixture"])?;
let sub_revision = git_at(&sub_repo, &["rev-parse", "HEAD"])?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(
origin_repo.join(".gitattributes"),
b"crlf.txt eol=crlf\nignored.txt export-ignore\nsubst.txt export-subst\nident.txt ident\n",
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
fs::write(origin_repo.join("crlf.txt"), b"a\nb\n")?;
fs::write(origin_repo.join("ignored.txt"), b"ignored\n")?;
fs::write(origin_repo.join("subst.txt"), b"rev $Format:%H$\n")?;
fs::write(origin_repo.join("ident.txt"), b"$Id$\n")?;
let script = origin_repo.join("run.sh");
fs::write(&script, b"#!/bin/sh\nexit 0\n")?;
fs::set_permissions(&script, fs::Permissions::from_mode(0o755))?;
symlink("deployment.toml", origin_repo.join("link"))?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(
&origin_repo,
&[
"update-index",
"--add",
"--cacheinfo",
&format!("160000,{sub_revision},vendor/sub"),
],
)?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
for repo in [&origin_repo, &sub_repo] {
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(repo)
.status()?
.success(),
"chowning the fixture repository"
);
}
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::from([(
PathBuf::from("vendor/sub"),
GitlinkBinding {
origin: sub_repo.to_string_lossy().into_owned(),
},
)]),
recipe_path: PathBuf::from("deployment.toml"),
};
let (tree_root, _snapshot_sha256, _recipe_bytes, contains_lfs_pointers) =
driver.freeze_exact(&source, &revision, "txn-byte-exact", &frozen_source_root)?;
assert!(!contains_lfs_pointers);
for name in [
"deployment.toml",
"crlf.txt",
"ignored.txt",
"subst.txt",
"ident.txt",
"run.sh",
".gitattributes",
] {
let output = Command::new("/usr/bin/git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&origin_repo)
.args(["cat-file", "blob", &format!("{revision}:{name}")])
.env_clear()
.env("HOME", &origin_repo)
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.output()?;
ensure!(
output.status.success(),
"reading blob {name} for comparison: {}",
String::from_utf8_lossy(&output.stderr)
);
let frozen = fs::read(tree_root.join(name))
.with_context(|| format!("reading frozen {name}"))?;
assert_eq!(frozen, output.stdout, "{name} is not byte-exact");
}
let script_metadata = fs::symlink_metadata(tree_root.join("run.sh"))?;
assert_eq!(script_metadata.uid(), 0);
assert_eq!(script_metadata.permissions().mode() & 0o111, 0o111);
assert_eq!(
fs::read_link(tree_root.join("link"))?,
PathBuf::from("deployment.toml")
);
let gitlink_output = Command::new("/usr/bin/git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&sub_repo)
.args(["cat-file", "blob", &format!("{sub_revision}:lib.txt")])
.env_clear()
.env("HOME", &sub_repo)
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.output()?;
ensure!(
gitlink_output.status.success(),
"reading Gitlink blob for comparison: {}",
String::from_utf8_lossy(&gitlink_output.stderr)
);
assert_eq!(
fs::read(tree_root.join("vendor/sub/lib.txt"))?,
gitlink_output.stdout
);
Ok(())
}
/// The deploy-equivalence twin F2's ruling asked for: for a fixture with
/// only a `text eol=lf` attribute (Eve's real `packages/*/dist/**`
/// pattern) and no attribute that would actually transform bytes, the new
/// raw freeze's tree equals today's `git archive` tree exactly, so this
/// cut does not change deploy for a target like it. The second file in
/// the same fixture carries `eol=crlf` over LF-stored content, an
/// attribute that genuinely does transform on archive; the test asserts
/// the two trees diverge there, so the equivalence check has teeth rather
/// than trivially passing.
#[cfg(unix)]
#[test]
fn freeze_exact_matches_archive_when_untransformed_and_diverges_when_transformed() -> Result<()>
{
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(
origin_repo.join(".gitattributes"),
b"untransformed.txt text eol=lf\ntransformed.txt eol=crlf\n",
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
// Eve's actual shape: stored LF, `eol=lf` is a no-op transform.
fs::write(origin_repo.join("untransformed.txt"), b"a\nb\n")?;
// Stored LF, but `eol=crlf` genuinely rewrites it on checkout/archive.
fs::write(origin_repo.join("transformed.txt"), b"a\nb\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: origin_repo.to_string_lossy().into_owned(),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let (tree_root, _snapshot_sha256, _recipe_bytes, _contains_lfs_pointers) =
driver.freeze_exact(&source, &revision, "txn-equivalence", &frozen_source_root)?;
// Reconstruct today's `git archive` tree independently of production
// code, so this test proves equivalence against the actual old
// mechanism rather than against another copy of the new one.
let archived = temp.path().join("archived");
fs::create_dir(&archived)?;
let mut archive = Command::new("/usr/bin/git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&origin_repo)
.args(["archive", "--format=tar", &revision])
.env_clear()
.env("HOME", &origin_repo)
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let archive_stdout = archive.stdout.take().context("git archive has no stdout")?;
let extractor_status = Command::new("/bin/tar")
.args(["--extract", "--file=-", "--directory"])
.arg(&archived)
.arg("--no-same-owner")
.stdin(Stdio::from(archive_stdout))
.status()?;
let archive_output = archive.wait_with_output()?;
ensure!(
archive_output.status.success(),
"git archive failed reconstructing the fixture tree: {}",
String::from_utf8_lossy(&archive_output.stderr)
);
ensure!(
extractor_status.success(),
"reconstructing the archived fixture tree failed"
);
assert_eq!(
fs::read(tree_root.join("untransformed.txt"))?,
fs::read(archived.join("untransformed.txt"))?,
"an eol=lf file over already-LF content must match today's archive exactly"
);
assert_ne!(
fs::read(tree_root.join("transformed.txt"))?,
fs::read(archived.join("transformed.txt"))?,
"an eol=crlf file genuinely transforms on archive, so raw and archived bytes must diverge"
);
Ok(())
}
fn expected() -> IdunnExpectedIncarnationRecord {
IdunnExpectedIncarnationRecord {
schema_version: IDUNN_EXPECTED_INCARNATION_SCHEMA.into(),
target: "service".into(),
plan_id: digest('1'),
incarnation_id: "incarnation-1".into(),
sealed_release_id: digest('2'),
source_repository: "github.com/GameCult/Service".into(),
source_revision: "3".repeat(40),
recipe_sha256: digest('4'),
runtime_id: "service-runtime".into(),
expected_signer_identity_id: "service-signer".into(),
health_contract: "service.health.v1".into(),
artifact_sha256: digest('5'),
state_schema_generation: Some("state-v1".into()),
state_contract_sha256: Some(digest('6')),
write_lease_required: true,
route: None,
capabilities: Vec::new(),
dependencies: Vec::new(),
}
}
fn authenticated_warming(
root: &Path,
) -> Result<(
IdunnExpectedIncarnationRecord,
IdunnRuntimeActivationRecord,
SequenceAdmittedWarming,
ServiceIdentityTrustAnchor,
)> {
let provider = enroll_service_identity_at::<GameCultProviderHealthIdentity>(
&root.join("provider.cc"),
)?;
let idunn = enroll_service_identity_at::<IdunnServiceIdentity>(&root.join("idunn.cc"))?;
let odin = enroll_service_identity_at::<OdinTopologyIdentity>(&root.join("odin.cc"))?;
let mut expected = expected();
expected.expected_signer_identity_id = provider.entry().identity_id.clone();
let launch = IdunnRuntimeActivationLaunch::issue(&expected, digest('7'), 100, &idunn)?;
let activation = launch.activation().clone();
launch.write_credential(std::io::sink())?;
let authority = verify_runtime_authority(
&expected,
&activation,
&idunn.trust_anchor()?,
&provider.entry().public_key,
)?;
let mut warming = OdinRuntimeTopologyCorrelationRecord {
schema_version: ODIN_RUNTIME_TOPOLOGY_CORRELATION_SCHEMA.into(),
target: expected.target.clone(),
expected_projection_sha256: expected.canonical_sha256()?,
expected: true,
current_activation_sha256: Some(activation.canonical_sha256()?),
signed_presence_sha256: Some(digest('9')),
observed_presence_state: Some("warming".into()),
observed_presence_publisher_sequence: Some(1),
observed_write_lease_sha256: None,
observed_capabilities: Vec::new(),
runtime_id: expected.runtime_id.clone(),
runtime_instance_id: Some(activation.runtime_instance_id.clone()),
present: true,
ready: false,
dependencies: Vec::new(),
disagreements: Vec::new(),
signer_identity_id: odin.entry().identity_id.clone(),
publisher_sequence: 1,
observed_at_unix_millis: 110,
signature_algorithm: "ed25519".into(),
signature: Vec::new(),
};
warming.signature = odin
.sign::<OdinRuntimeTopologyCorrelationPurpose>(&warming.unsigned_signature_payload()?)
.signature;
let warming = authenticate_odin_runtime_topology_correlation(
&warming.canonical_bytes()?,
&authority,
None,
&odin.entry().public_key,
OdinTopologyAuthenticationContext {
trusted_received_at_unix_millis: 120,
maximum_age_millis: 30,
maximum_future_skew_millis: 5,
},
)?;
let warming = SequenceAdmittedWarming::for_test("test-transaction", warming)?;
Ok((expected, activation, warming, provider.trust_anchor()?))
}
fn lease(
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
warming: &SequenceAdmittedWarming,
) -> IdunnProcessWriteLeaseRecord {
IdunnProcessWriteLeaseRecord {
schema_version: IDUNN_PROCESS_WRITE_LEASE_SCHEMA.into(),
target: expected.target.clone(),
expected_projection_sha256: expected.canonical_sha256().unwrap(),
plan_id: expected.plan_id.clone(),
incarnation_id: expected.incarnation_id.clone(),
sealed_release_id: expected.sealed_release_id.clone(),
activation_witness_sha256: activation.canonical_sha256().unwrap(),
state_schema_generation: expected.state_schema_generation.clone().unwrap(),
state_contract_sha256: expected.state_contract_sha256.clone().unwrap(),
runtime_id: expected.runtime_id.clone(),
runtime_instance_id: activation.runtime_instance_id.clone(),
warming_presence_sha256: warming.signed_presence_sha256().to_owned(),
lease_epoch: 1,
issued_at_unix_millis: 200,
}
}
fn topology_driver(root: &Path, name: &str) -> CultCacheTopologyDriver {
CultCacheTopologyDriver {
projection_store: root.join(format!("{name}.cc")),
correlation_store: root.join(format!("{name}-correlation.cc")),
}
}
fn project_activation(
driver: &CultCacheTopologyDriver,
expected: &IdunnExpectedIncarnationRecord,
activation: &IdunnRuntimeActivationRecord,
) -> Result<()> {
ensure!(
activation.expected_projection_sha256 == expected.canonical_sha256()?,
"test activation belongs to another Expected"
);
upsert_record(
&driver.projection_store,
CultCacheEnvelope {
key: incarnation_key(expected)?,
r#type: IdunnRuntimeActivationRecord::TYPE.into(),
payload: activation.canonical_bytes()?,
stored_at: rfc3339_millis(activation.issued_at_unix_millis)?,
schema_id: Some(IDUNN_RUNTIME_ACTIVATION_SCHEMA.into()),
},
)
}
/// Withdrawing the last membership withdraws the endpoint's allow, and a
/// rule that is already gone is the state being asked for, not an error.
#[cfg(unix)]
#[test]
fn withdrawing_the_last_route_membership_withdraws_its_firewall_allow() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
let program = |name: &str, body: &str| -> Result<PathBuf> {
let path = temp.path().join(name);
fs::write(&path, body)?;
fs::set_permissions(&path, fs::Permissions::from_mode(0o755))?;
Ok(path)
};
let ufw = program(
"ufw",
"#!/bin/sh\nif [ \"$1\" = \"delete\" ] && [ -e \"$0.absent\" ]; then echo 'Could not delete non-existent rule' >&2; exit 1; fi\necho \"$*\" >> \"$0.calls\"\nexit 0\n",
)?;
let driver = NginxRouteDriver {
binding: RouteBinding {
driver: RouteDriver::NginxStreamUdp,
route_id: "odin-rendezvous".into(),
stable_endpoint: "rudp://10.77.0.1:17971".into(),
private_host: "127.0.0.1".into(),
private_port_start: 17972,
private_port_end: 17979,
config_path: temp.path().join("odin.conf"),
reload_unit: "nginx.service".into(),
member: None,
},
nginx_program: program("nginx", "#!/bin/sh\nexit 0\n")?,
systemd_run_program: program("systemd-run", "#!/bin/sh\nexit 64\n")?,
systemctl_program: program("systemctl", "#!/bin/sh\nexit 0\n")?,
ufw_program: ufw.clone(),
preflight_root: temp.path().join("preflight"),
};
assert_eq!(
driver
.endpoint_rules()?
.iter()
.map(|rule| rule
.iter()
.map(|value| value.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(" "))
.collect::<Vec<_>>(),
vec!["allow in to 10.77.0.1 port 17971 proto udp"]
);
driver.restore(None, &Unmetered)?;
assert_eq!(
fs::read_to_string(ufw.with_extension("calls"))?,
"delete allow in to 10.77.0.1 port 17971 proto udp\n"
);
fs::write(ufw.with_extension("absent"), b"gone\n")?;
driver.restore(None, &Unmetered)?;
Ok(())
}
/// Refuses every `Forward` actuation, admits every `Survival`, and records
/// what it was asked.
struct ForwardRefusing(std::cell::RefCell<Vec<RouteActuation>>);
impl RouteActuationGate for ForwardRefusing {
fn admit(&self, kind: RouteActuation) -> Result<()> {
self.0.borrow_mut().push(kind);
match kind {
RouteActuation::Forward => Err(RouteActuationRefused {
target: "service".into(),
used: 12,
reopens_at_unix_millis: 99,
}
.into()),
RouteActuation::Survival => Ok(()),
}
}
}
/// Stub host programs that log every call, a driver over them, and a
/// candidate whose membership it can render.
#[cfg(unix)]
fn stubbed_route_driver(
temp: &Path,
) -> Result<(NginxRouteDriver, IdunnExpectedIncarnationRecord, PathBuf)> {
use std::os::unix::fs::PermissionsExt;
let calls = temp.join("calls");
let program = |name: &str| -> Result<PathBuf> {
let path = temp.join(name);
fs::write(
&path,
format!("#!/bin/sh\necho \"{name} $*\" >> '{}'\nexit 0\n", calls.display()),
)?;
fs::set_permissions(&path, fs::Permissions::from_mode(0o755))?;
Ok(path)
};
let driver = NginxRouteDriver::with_actuators(
RouteBinding {
driver: RouteDriver::NginxStreamTcp,
route_id: "service-route".into(),
stable_endpoint: "http://127.0.0.1:4103".into(),
private_host: "127.0.0.1".into(),
private_port_start: 4104,
private_port_end: 4109,
config_path: temp.join("service.conf"),
reload_unit: "nginx.service".into(),
member: None,
},
&RouteActuators {
nginx: program("nginx")?,
systemd_run: program("systemd-run")?,
systemctl: program("systemctl")?,
ufw: program("ufw")?,
preflight_root: temp.join("preflight"),
},
);
let mut candidate = expected();
candidate.route = Some(cultnet_rs::IdunnExpectedRoute {
route_id: "service-route".into(),
transport: "http".into(),
stable_endpoint: "http://127.0.0.1:4103".into(),
candidate_endpoint: "http://127.0.0.1:4104".into(),
});
Ok((driver, candidate, calls))
}
#[cfg(unix)]
fn preflight_receipt(
driver: &NginxRouteDriver,
candidate: &IdunnExpectedIncarnationRecord,
) -> Result<RoutePreflightReceipt> {
Ok(RoutePreflightReceipt {
route_id: "service-route".into(),
candidate_runtime_instance_id: digest('a'),
candidate_membership_sha256: sha256_id(&driver.render(candidate)?),
incumbent_runtime_instance_id: None,
incumbent_membership_sha256: None,
incumbent_configuration: None,
})
}
fn with_member_listener(driver: &mut NginxRouteDriver) {
driver.binding.member = Some(crate::deployment::MemberListenerBinding {
stable_endpoint: "http://127.0.0.1:4120".into(),
private_port_start: 4121,
max_connections: 200,
});
}
/// The fragment as the base commit rendered it, computed from its format
/// string and not from the code under test.
#[cfg(unix)]
fn base_fragment(sha: &str, candidate_port: u16) -> String {
format!(
"# Idunn Expected {sha}\nupstream idunn_service_route {{\n server 127.0.0.1:{candidate_port};\n}}\nserver {{\n listen 127.0.0.1:4103;\n proxy_pass idunn_service_route;\n}}\n"
)
}
/// A route without a member listener renders the bytes it always did; a
/// member listener adds a second upstream and server block to the one
/// fragment, under the one Expected comment.
#[cfg(unix)]
#[test]
fn a_member_listener_renders_a_second_block_only_when_bound() -> Result<()> {
let temp = tempfile::tempdir()?;
let (mut driver, mut candidate, _calls) = stubbed_route_driver(temp.path())?;
let sha = candidate.canonical_sha256()?;
assert_eq!(
String::from_utf8(driver.render(&candidate)?)?,
base_fragment(&sha, 4104)
);
with_member_listener(&mut driver);
let member_blocks = |member_port: u16| {
format!(
"limit_conn_zone \"idunn_service_route_member\" zone=idunn_service_route_member:64k;\nupstream idunn_service_route_member {{\n server 127.0.0.1:{member_port};\n}}\nserver {{\n listen 127.0.0.1:4120;\n limit_conn idunn_service_route_member 200;\n proxy_pass idunn_service_route_member;\n}}\n"
)
};
let rendered = String::from_utf8(driver.render(&candidate)?)?;
assert_eq!(rendered, base_fragment(&sha, 4104) + &member_blocks(4121));
assert_eq!(rendered.matches("# Idunn Expected").count(), 1);
assert_eq!(rendered.matches("server {").count(), 2);
candidate.route.as_mut().unwrap().candidate_endpoint = "http://127.0.0.1:4106".into();
let sha = candidate.canonical_sha256()?;
assert_eq!(
String::from_utf8(driver.render(&candidate)?)?,
base_fragment(&sha, 4106) + &member_blocks(4123)
);
Ok(())
}
/// Both stable sockets are admitted on the firewall, and both withdrawn.
#[cfg(unix)]
#[test]
fn both_route_endpoints_are_admitted_and_withdrawn() -> Result<()> {
let temp = tempfile::tempdir()?;
let (mut driver, _candidate, calls) = stubbed_route_driver(temp.path())?;
with_member_listener(&mut driver);
driver.admit_endpoint()?;
driver.withdraw_endpoint()?;
let log = fs::read_to_string(&calls)?;
let ufw: Vec<&str> = log.lines().filter(|line| line.starts_with("ufw ")).collect();
assert_eq!(
ufw,
vec![
"ufw allow in to 127.0.0.1 port 4103 proto tcp comment Idunn route service-route",
"ufw allow in to 127.0.0.1 port 4120 proto tcp comment Idunn route service-route",
"ufw delete allow in to 127.0.0.1 port 4103 proto tcp",
"ufw delete allow in to 127.0.0.1 port 4120 proto tcp",
]
);
Ok(())
}
/// Real nginx over the rendered two-block fragment, a real presence
/// acceptor behind the presence block and a flood of idle members on the
/// member block. Needs an `nginx` on PATH and the fixed loopback ports
/// 4103, 4104, 4120 and 4121, so it runs on demand in a container:
/// `cargo test --lib real_nginx -- --ignored`.
#[cfg(unix)]
fn challenges_answered_during_a_member_flood(max_connections: u16) -> Result<usize> {
use std::net::{TcpListener, TcpStream};
use std::process::{Child, Command};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
/// Stopped through nginx itself: killing the master would orphan its
/// workers, which keep the test's output pipe open.
struct Nginx(Child, PathBuf, PathBuf);
impl Drop for Nginx {
fn drop(&mut self) {
let _ = Command::new("nginx")
.arg("-p")
.arg(&self.1)
.arg("-c")
.arg(&self.2)
.args(["-s", "stop"])
.status();
let _ = self.0.wait();
}
}
let temp = tempfile::tempdir()?;
let (mut driver, candidate, _calls) = stubbed_route_driver(temp.path())?;
with_member_listener(&mut driver);
driver.binding.member.as_mut().unwrap().max_connections = max_connections;
let fragment = temp.path().join("route.conf");
fs::write(&fragment, driver.render(&candidate)?)?;
// Debian builds the stream module dynamic.
let module = Path::new("/usr/lib/nginx/modules/ngx_stream_module.so");
let load = if module.exists() {
format!("load_module {};
", module.display())
} else {
String::new()
};
let conf = temp.path().join("nginx.conf");
fs::write(
&conf,
format!(
"{load}worker_processes 1;\npid {dir}/nginx.pid;\nerror_log {dir}/error.log;\nevents {{ worker_connections 512; }}\nstream {{ include {fragment}; }}\n",
dir = temp.path().display(),
fragment = fragment.display()
),
)?;
let halt = Arc::new(AtomicBool::new(false));
let presence = crate::launcher::PresenceAcceptor::new(
TcpListener::bind("127.0.0.1:4104")?,
Arc::new(|request| match request {
CultNetMessage::SnapshotRequest { message_id, .. } => {
Ok(CultNetMessage::SnapshotResponseRaw { message_id, documents: vec![] })
}
_ => bail!("not a snapshot request"),
}),
);
let presence_halt = halt.clone();
let presence = thread::spawn(move || presence.serve(presence_halt));
// The member backend accepts and holds every connection.
let members = TcpListener::bind("127.0.0.1:4121")?;
members.set_nonblocking(true)?;
let member_halt = halt.clone();
let members = thread::spawn(move || {
let mut held = Vec::new();
while !member_halt.load(Ordering::SeqCst) {
match members.accept() {
Ok((stream, _)) => held.push(stream),
Err(_) => thread::sleep(Duration::from_millis(5)),
}
}
});
let _nginx = Nginx(
Command::new("nginx")
.args(["-p"])
.arg(temp.path())
.arg("-c")
.arg(&conf)
.args(["-g", "daemon off;"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?,
temp.path().to_path_buf(),
conf.clone(),
);
let stable: SocketAddr = "127.0.0.1:4103".parse()?;
let ready = Instant::now();
while TcpStream::connect(stable).is_err() {
ensure!(ready.elapsed() < Duration::from_secs(10), "nginx never listened");
thread::sleep(Duration::from_millis(50));
}
// The probe connection above reached the presence acceptor as an idle
// peer; let its deadline lapse so only the flood is measured.
thread::sleep(Duration::from_millis(1200));
let mut flood = Vec::new();
for _ in 0..300 {
if let Ok(mut stream) = TcpStream::connect("127.0.0.1:4120") {
let _ = stream.write_all(b"x");
flood.push(stream);
}
}
thread::sleep(Duration::from_millis(500));
let payload = encode_cultnet_message_to_vec(
&CultNetMessage::SnapshotRequest {
message_id: "flood".into(),
schema_ids: None,
record_keys: None,
},
CultNetWireContract::CultNetSchemaV0,
)?;
let mut answered = 0;
for _ in 0..20 {
let started = Instant::now();
if request_tcp_snapshot(stable, &payload).is_ok()
&& started.elapsed() < Duration::from_secs(3)
{
answered += 1;
}
}
eprintln!("answered {answered} of 20 challenges with a member limit of {max_connections}");
drop(flood);
halt.store(true, Ordering::SeqCst);
presence.join().unwrap();
members.join().unwrap();
Ok(answered)
}
#[cfg(unix)]
#[test]
#[ignore = "needs nginx on PATH and fixed loopback ports; run in a container"]
fn real_nginx_keeps_presence_answerable_under_a_member_flood() -> Result<()> {
// The rendered default limit keeps the route's members out of the
// presence block's share of a 512-connection pool.
assert_eq!(challenges_answered_during_a_member_flood(128)?, 20);
Ok(())
}
#[cfg(unix)]
#[test]
#[ignore = "needs nginx on PATH and fixed loopback ports; run in a container"]
fn real_nginx_flood_starves_presence_without_a_member_limit() -> Result<()> {
// The control: the same flood against a limit too high to bind (nginx allows up to 65535) starves
// presence, so the test above measures the limit and not the load.
assert!(challenges_answered_during_a_member_flood(60_000)? < 20);
Ok(())
}
/// Idunn sets the member bind for a bound member listener, derived from the
/// candidate, and for no other workload.
#[test]
fn the_member_bind_is_set_only_for_a_bound_member_listener() -> Result<()> {
use crate::deployment::tests::{BINDING, RECIPE, member_listener_pair};
let driver = SystemdTransientWorkloadDriver::default();
let mut routed = expected();
routed.route = Some(cultnet_rs::IdunnExpectedRoute {
route_id: "test-service-public".into(),
transport: "http".into(),
stable_endpoint: "http://127.0.0.1:18830".into(),
candidate_endpoint: "http://127.0.0.1:18833".into(),
});
let bundle = Path::new("/etc/gamecult/test-service/runtime/bundle");
let unit = "idunn-test-service-abc";
let (recipe, binding) = member_listener_pair();
let binding = OperatorBinding::parse(&binding)?;
binding.admit(&TargetDeclaration::parse(&recipe)?)?;
let environment = driver.launch_environment(&binding, bundle, &routed, unit)?;
assert_eq!(
environment.get(IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT).map(String::as_str),
Some("127.0.0.1:18843")
);
assert_eq!(
environment.get(IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT).map(String::as_str),
Some("127.0.0.1:18833")
);
let plain = OperatorBinding::parse(BINDING)?;
plain.admit(&TargetDeclaration::parse(RECIPE)?)?;
let environment = driver.launch_environment(&plain, bundle, &routed, unit)?;
assert!(!environment.contains_key(IDUNN_RUNTIME_MEMBER_BIND_ENVIRONMENT));
assert!(environment.contains_key(IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT));
Ok(())
}
/// A refused forward change stops before any host mutation: no program
/// runs, no fragment is written, and no private-mount unit starts.
#[cfg(unix)]
#[test]
fn a_refused_forward_actuation_runs_no_program_and_writes_nothing() -> Result<()> {
let temp = tempfile::tempdir()?;
let (driver, candidate, calls) = stubbed_route_driver(temp.path())?;
let gate = ForwardRefusing(Default::default());
let receipt = preflight_receipt(&driver, &candidate)?;
let refused = driver
.preflight(&candidate, &digest('a'), None, &gate)
.unwrap_err();
assert!(refused.downcast_ref::<RouteActuationRefused>().is_some());
let refused = driver
.install(&candidate, &digest('a'), &receipt, true, &gate)
.unwrap_err();
assert!(refused.downcast_ref::<RouteActuationRefused>().is_some());
assert!(!calls.exists(), "a refused actuation must run no program");
assert!(!driver.binding.config_path.exists());
assert_eq!(
*gate.0.borrow(),
[RouteActuation::Forward, RouteActuation::Forward]
);
Ok(())
}
/// Restoring the admitted or incumbent route is never refused, whatever
/// the gate says about forward changes, and is always asked as survival.
#[cfg(unix)]
#[test]
fn survival_actuations_run_under_a_gate_that_refuses_forward_changes() -> Result<()> {
let temp = tempfile::tempdir()?;
let (driver, candidate, calls) = stubbed_route_driver(temp.path())?;
let gate = ForwardRefusing(Default::default());
let receipt = preflight_receipt(&driver, &candidate)?;
let rendered = driver.render(&candidate)?;
let membership = sha256_id(&rendered);
let reloads = || -> Result<usize> {
Ok(fs::read_to_string(&calls)?
.lines()
.filter(|line| line.starts_with("systemctl reload"))
.count())
};
fs::write(&driver.binding.config_path, b"drifted bytes\n")?;
driver.restore_admitted_membership(&candidate, &membership, &gate)?;
assert_eq!(fs::read(&driver.binding.config_path)?, rendered);
assert_eq!(reloads()?, 1);
driver.withdraw_candidate_membership(&receipt, &gate)?;
assert!(!driver.binding.config_path.exists());
assert_eq!(reloads()?, 2);
fs::write(&driver.binding.config_path, &rendered)?;
driver.rollback(&candidate, &digest('a'), &receipt, &gate)?;
assert_eq!(reloads()?, 3);
assert_eq!(
*gate.0.borrow(),
[RouteActuation::Survival, RouteActuation::Survival, RouteActuation::Survival]
);
Ok(())
}
/// Stub host programs that log every call. `systemctl reload` fails while
/// `<dir>/systemctl.fail` exists, `ufw delete` while `<dir>/ufw.fail` does.
#[cfg(unix)]
fn failable_route_driver(
temp: &Path,
) -> Result<(NginxRouteDriver, IdunnExpectedIncarnationRecord, PathBuf)> {
use std::os::unix::fs::PermissionsExt;
let (mut driver, candidate, calls) = stubbed_route_driver(temp)?;
let failable = |name: &str, verb: &str| -> Result<PathBuf> {
let path = temp.join(name);
fs::write(
&path,
format!(
"#!/bin/sh\necho \"{name} $*\" >> '{}'\nif [ \"$1\" = {verb} ] && [ -e \"$0.fail\" ]; then exit 1; fi\nexit 0\n",
calls.display()
),
)?;
fs::set_permissions(&path, fs::Permissions::from_mode(0o755))?;
Ok(path)
};
driver.systemctl_program = failable("systemctl", "reload")?;
driver.ufw_program = failable("ufw", "delete")?;
// `<dir>/ufw.absent`: the rule is already gone, as ufw reports it.
let ufw = fs::read_to_string(&driver.ufw_program)?.replace(
"exit 0\n",
"if [ \"$1\" = delete ] && [ -e \"$0.absent\" ]; then echo 'Could not delete non-existent rule' >&2; exit 1; fi\nexit 0\n",
);
fs::write(&driver.ufw_program, ufw)?;
Ok((driver, candidate, calls))
}
#[cfg(unix)]
fn logged(calls: &Path, prefix: &str) -> usize {
fs::read_to_string(calls)
.unwrap_or_default()
.lines()
.filter(|line| line.starts_with(prefix))
.count()
}
/// Admits everything and records what it was asked.
struct Recording(std::cell::RefCell<Vec<RouteActuation>>);
impl RouteActuationGate for Recording {
fn admit(&self, kind: RouteActuation) -> Result<()> {
self.0.borrow_mut().push(kind);
Ok(())
}
}
/// On a transaction path, bytes already on disk prove nothing: a crash may
/// have cut off the reload after the write. The install past the fence and
/// the withdrawal both write and reload whatever the disk shows, and the
/// install admits its firewall allow again.
#[cfg(unix)]
#[test]
fn a_crash_between_a_write_and_its_reload_is_reloaded_on_retry() -> Result<()> {
let temp = tempfile::tempdir()?;
let (driver, candidate, calls) = failable_route_driver(temp.path())?;
let receipt = preflight_receipt(&driver, &candidate)?;
let rendered = driver.render(&candidate)?;
// The install wrote its fragment and Idunn died before the reload.
fs::write(&driver.binding.config_path, &rendered)?;
driver.install(&candidate, &digest('a'), &receipt, false, &Unmetered)?;
assert_eq!(logged(&calls, "systemctl reload"), 1, "the unloaded install was not reloaded");
assert_eq!(logged(&calls, "ufw allow"), 1, "the first deploy's allow was skipped");
// The withdrawal removed it and Idunn died before the reload.
fs::remove_file(&driver.binding.config_path)?;
driver.withdraw_candidate_membership(&receipt, &Unmetered)?;
assert_eq!(logged(&calls, "systemctl reload"), 2, "the unloaded withdrawal was not reloaded");
assert_eq!(logged(&calls, "ufw delete"), 1);
Ok(())
}
/// A redeploy over an existing route changes it: the candidate differs
/// from the incumbent's fragment, so it installs and reloads, and its
/// rollback and withdrawal put the incumbent back and reload.
#[cfg(unix)]
#[test]
fn a_deploy_over_an_incumbent_route_installs_and_reloads() -> Result<()> {
let temp = tempfile::tempdir()?;
let (driver, candidate, calls) = failable_route_driver(temp.path())?;
let incumbent = b"# incumbent membership\n".to_vec();
let mut receipt = preflight_receipt(&driver, &candidate)?;
receipt.incumbent_runtime_instance_id = Some(digest('b'));
receipt.incumbent_membership_sha256 = Some(sha256_id(&incumbent));
receipt.incumbent_configuration = Some(incumbent.clone());
assert!(receipt.changes_route());
fs::write(&driver.binding.config_path, &incumbent)?;
driver.install(&candidate, &digest('a'), &receipt, true, &Unmetered)?;
assert_eq!(logged(&calls, "systemctl reload"), 1, "the redeploy did not install");
assert_eq!(fs::read(&driver.binding.config_path)?, driver.render(&candidate)?);
driver.rollback(&candidate, &digest('a'), &receipt, &Unmetered)?;
assert_eq!(logged(&calls, "systemctl reload"), 2, "the rollback did not reload");
assert_eq!(fs::read(&driver.binding.config_path)?, incumbent);
// The incumbent still routes: its endpoint's firewall allow stays.
assert_eq!(logged(&calls, "ufw delete"), 0, "the rollback withdrew a live endpoint");
driver.install(&candidate, &digest('a'), &receipt, true, &Unmetered)?;
driver.withdraw_candidate_membership(&receipt, &Unmetered)?;
assert_eq!(logged(&calls, "systemctl reload"), 4, "the withdrawal did not reload");
assert_eq!(fs::read(&driver.binding.config_path)?, incumbent);
assert_eq!(logged(&calls, "ufw delete"), 0, "the withdrawal withdrew a live endpoint");
Ok(())
}
/// With nothing left to route, the withdrawal takes the firewall allow
/// first. A `ufw delete` that keeps failing retries without reloading
/// nginx; once it goes through, the fragment is removed and reloaded.
#[cfg(unix)]
#[test]
fn a_failing_firewall_withdrawal_retries_without_reloading() -> Result<()> {
let temp = tempfile::tempdir()?;
let (driver, candidate, calls) = failable_route_driver(temp.path())?;
let receipt = preflight_receipt(&driver, &candidate)?;
driver.install(&candidate, &digest('a'), &receipt, false, &Unmetered)?;
let installed = logged(&calls, "systemctl reload");
fs::write(temp.path().join("ufw.fail"), b"x")?;
for _ in 0..5 {
assert!(driver.withdraw_candidate_membership(&receipt, &Unmetered).is_err());
}
assert_eq!(logged(&calls, "systemctl reload"), installed, "a failing ufw delete reloaded nginx");
assert_eq!(logged(&calls, "ufw delete"), 5);
fs::remove_file(temp.path().join("ufw.fail"))?;
driver.withdraw_candidate_membership(&receipt, &Unmetered)?;
assert_eq!(logged(&calls, "systemctl reload"), installed + 1);
assert!(!driver.binding.config_path.exists());
Ok(())
}
/// A crash after the firewall rule went and before the fragment did: the
/// retry reads the gone rule as withdrawn and finishes the withdrawal.
#[cfg(unix)]
#[test]
fn a_withdrawal_resumed_after_its_firewall_rule_went_finishes() -> Result<()> {
let temp = tempfile::tempdir()?;
let (driver, candidate, calls) = failable_route_driver(temp.path())?;
let receipt = preflight_receipt(&driver, &candidate)?;
driver.install(&candidate, &digest('a'), &receipt, false, &Unmetered)?;
let installed = logged(&calls, "systemctl reload");
fs::write(temp.path().join("ufw.absent"), b"x")?;
driver.withdraw_candidate_membership(&receipt, &Unmetered)?;
assert!(!driver.binding.config_path.exists(), "the fragment outlived the withdrawal");
assert_eq!(logged(&calls, "systemctl reload"), installed + 1, "nginx was not reloaded");
Ok(())
}
/// A continuity over its admitted route has the baseline's membership: its
/// install, rollback and withdrawal change nothing and run nothing.
#[cfg(unix)]
#[test]
fn a_transaction_that_does_not_change_the_route_runs_nothing() -> Result<()> {
let temp = tempfile::tempdir()?;
let (driver, candidate, calls) = failable_route_driver(temp.path())?;
let rendered = driver.render(&candidate)?;
let mut receipt = preflight_receipt(&driver, &candidate)?;
receipt.incumbent_runtime_instance_id = Some(digest('b'));
receipt.incumbent_membership_sha256 = Some(sha256_id(&rendered));
receipt.incumbent_configuration = Some(rendered.clone());
assert!(!receipt.changes_route());
fs::write(&driver.binding.config_path, &rendered)?;
let gate = Recording(Default::default());
driver.install(&candidate, &digest('a'), &receipt, false, &gate)?;
driver.rollback(&candidate, &digest('a'), &receipt, &gate)?;
driver.withdraw_candidate_membership(&receipt, &gate)?;
assert_eq!(logged(&calls, ""), 0, "an unchanged route ran a program");
assert!(gate.0.borrow().is_empty(), "an unchanged route was charged");
assert_eq!(fs::read(&driver.binding.config_path)?, rendered);
// The first-route receipt does change it.
assert!(preflight_receipt(&driver, &candidate)?.changes_route());
Ok(())
}
/// Bytes on disk are what nginx loaded: a write whose reload fails is
/// undone, so the retry writes and reloads again instead of taking the
/// unloaded bytes for a live route.
#[cfg(unix)]
#[test]
fn a_failed_reload_puts_the_fragment_back_so_the_retry_reloads() -> Result<()> {
let temp = tempfile::tempdir()?;
let (driver, candidate, calls) = failable_route_driver(temp.path())?;
let receipt = preflight_receipt(&driver, &candidate)?;
let rendered = driver.render(&candidate)?;
let fail = temp.path().join("systemctl.fail");
// Install, fenced (no rollback) and not.
for rollback_allowed in [false, true] {
fs::write(&fail, b"x")?;
assert!(
driver
.install(&candidate, &digest('a'), &receipt, rollback_allowed, &Unmetered)
.is_err()
);
assert!(!driver.binding.config_path.exists(), "an unloaded candidate stayed on disk");
fs::remove_file(&fail)?;
let before = logged(&calls, "systemctl reload");
driver.install(&candidate, &digest('a'), &receipt, rollback_allowed, &Unmetered)?;
assert_eq!(logged(&calls, "systemctl reload"), before + 1, "the retry did not reload");
assert_eq!(fs::read(&driver.binding.config_path)?, rendered);
if !rollback_allowed {
fs::remove_file(&driver.binding.config_path)?;
}
}
// Restore: the candidate is live; withdrawing it fails at the reload.
fs::write(&fail, b"x")?;
assert!(driver.withdraw_candidate_membership(&receipt, &Unmetered).is_err());
assert_eq!(fs::read(&driver.binding.config_path)?, rendered, "the unloaded removal stayed");
fs::remove_file(&fail)?;
let before = logged(&calls, "systemctl reload");
driver.withdraw_candidate_membership(&receipt, &Unmetered)?;
assert_eq!(logged(&calls, "systemctl reload"), before + 1, "the retry did not reload");
assert!(!driver.binding.config_path.exists());
Ok(())
}
/// Observation observes: restoring a membership whose fragment is already
/// exact touches nothing, and drifted bytes are restored with one reload.
#[cfg(unix)]
#[test]
fn restoring_an_exact_admitted_membership_actuates_nothing_and_drift_reloads_once()
-> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
let calls = temp.path().join("calls");
let program = |name: &str| -> Result<PathBuf> {
let path = temp.path().join(name);
fs::write(
&path,
format!("#!/bin/sh\necho \"{name} $*\" >> '{}'\nexit 0\n", calls.display()),
)?;
fs::set_permissions(&path, fs::Permissions::from_mode(0o755))?;
Ok(path)
};
let config = temp.path().join("service.conf");
let driver = NginxRouteDriver {
binding: RouteBinding {
driver: RouteDriver::NginxStreamTcp,
route_id: "service-route".into(),
stable_endpoint: "http://127.0.0.1:4103".into(),
private_host: "127.0.0.1".into(),
private_port_start: 4104,
private_port_end: 4109,
config_path: config.clone(),
reload_unit: "nginx.service".into(),
member: None,
},
nginx_program: program("nginx")?,
systemd_run_program: program("systemd-run")?,
systemctl_program: program("systemctl")?,
ufw_program: program("ufw")?,
preflight_root: temp.path().join("preflight"),
};
let mut candidate = expected();
candidate.route = Some(cultnet_rs::IdunnExpectedRoute {
route_id: "service-route".into(),
transport: "http".into(),
stable_endpoint: "http://127.0.0.1:4103".into(),
candidate_endpoint: "http://127.0.0.1:4104".into(),
});
let rendered = driver.render(&candidate)?;
let membership_sha256 = sha256_id(&rendered);
let count = |prefix: &str| -> Result<usize> {
Ok(fs::read_to_string(&calls)
.unwrap_or_default()
.lines()
.filter(|line| line.starts_with(prefix))
.count())
};
fs::write(&config, &rendered)?;
let modified = fs::metadata(&config)?.modified()?;
driver.restore_admitted_membership(&candidate, &membership_sha256, &Unmetered)?;
driver.restore_admitted_membership(&candidate, &membership_sha256, &Unmetered)?;
assert!(!calls.exists(), "an exact fragment must run no program");
assert!(!driver.preflight_root.exists());
assert_eq!(fs::read(&config)?, rendered);
assert_eq!(fs::metadata(&config)?.modified()?, modified);
fs::write(&config, b"drifted bytes\n")?;
driver.restore_admitted_membership(&candidate, &membership_sha256, &Unmetered)?;
assert_eq!(fs::read(&config)?, rendered);
assert_eq!(count("systemctl reload")?, 1);
assert_eq!(count("systemd-run")?, 1);
assert_eq!(count("ufw allow")?, 1);
assert_eq!(count("nginx -t")?, 1);
driver.restore_admitted_membership(&candidate, &membership_sha256, &Unmetered)?;
assert_eq!(count("systemctl reload")?, 1, "restored bytes never reload again");
assert_eq!(count("ufw")?, 1);
Ok(())
}
#[test]
fn an_empty_route_fragment_is_absence_not_a_membership() -> Result<()> {
let temp = tempfile::tempdir()?;
let config_path = temp.path().join("odin.conf");
let driver = NginxRouteDriver::new(RouteBinding {
driver: RouteDriver::NginxStreamUdp,
route_id: "odin-rudp".into(),
stable_endpoint: "rudp://127.0.0.1:17872".into(),
private_host: "127.0.0.1".into(),
private_port_start: 27872,
private_port_end: 27879,
config_path: config_path.clone(),
reload_unit: "nginx.service".into(),
member: None,
});
assert_eq!(driver.current_configuration()?, None);
// What systemd leaves behind when it materializes the preflight's bind
// mount point on a target's first deployment.
std::fs::write(&config_path, b"")?;
assert_eq!(
driver.current_configuration()?,
None,
"an empty fragment must not read as an unadmitted incumbent"
);
std::fs::write(&config_path, b"server {}")?;
assert_eq!(driver.current_configuration()?, Some(b"server {}".to_vec()));
Ok(())
}
#[test]
fn nginx_rudp_route_is_rendered_as_a_udp_stream_proxy() -> Result<()> {
let driver = NginxRouteDriver::new(RouteBinding {
driver: RouteDriver::NginxStreamUdp,
route_id: "odin-rudp".into(),
stable_endpoint: "rudp://127.0.0.1:17872".into(),
private_host: "127.0.0.1".into(),
private_port_start: 27872,
private_port_end: 27879,
config_path: "/etc/nginx/idunn-stream-routes/odin-rudp.conf".into(),
reload_unit: "nginx.service".into(),
member: None,
});
let mut candidate = expected();
candidate.route = Some(cultnet_rs::IdunnExpectedRoute {
route_id: "odin-rudp".into(),
transport: "rudp".into(),
stable_endpoint: "rudp://127.0.0.1:17872".into(),
candidate_endpoint: "rudp://127.0.0.1:27872".into(),
});
let rendered = String::from_utf8(driver.render(&candidate)?)?;
assert!(rendered.contains("listen 127.0.0.1:17872 udp reuseport;"));
assert!(rendered.contains("server 127.0.0.1:27872;"));
Ok(())
}
#[cfg(unix)]
#[test]
fn nginx_preflight_binds_exact_incumbent_configuration_without_claiming_live_route()
-> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
let config = temp.path().join("route.conf");
let nginx = temp.path().join("nginx");
let systemd_run = temp.path().join("systemd-run");
let systemctl = temp.path().join("systemctl");
fs::write(
&nginx,
format!(
"#!/bin/sh\nconfig='{}'\nif [ \"$1\" = \"-t\" ]; then\n candidate=${{IDUNN_TEST_SHADOW:-$config}}\n /bin/grep -q 'server 127.0.0.1:4104' \"$candidate\"\n exit $?\nfi\nexit 64\n",
config.display(),
),
)?;
fs::write(
&systemd_run,
"#!/bin/sh\nshadow=\nwhile [ \"$#\" -gt 0 ]; do\n case \"$1\" in\n --property=BindReadOnlyPaths=*) binding=${1#--property=BindReadOnlyPaths=}; shadow=${binding%%:*} ;;
--) shift; break ;;
esac\n shift\ndone\nIDUNN_TEST_SHADOW=\"$shadow\"; export IDUNN_TEST_SHADOW\nexec \"$@\"\n",
)?;
fs::write(
&systemctl,
"#!/bin/sh\nif [ -e \"$0.fail\" ]; then exit 65; fi\nif [ \"$1\" = \"reload\" ]; then echo reload >> \"$0.calls\"; exit 0; fi\nexit 64\n",
)?;
fs::set_permissions(&nginx, fs::Permissions::from_mode(0o755))?;
fs::set_permissions(&systemd_run, fs::Permissions::from_mode(0o755))?;
fs::set_permissions(&systemctl, fs::Permissions::from_mode(0o755))?;
let ufw = temp.path().join("ufw");
fs::write(
&ufw,
"#!/bin/sh\nif [ \"$1\" = \"delete\" ] && [ -e \"$0.absent\" ]; then echo 'Could not delete non-existent rule' >&2; exit 1; fi\necho \"$*\" >> \"$0.calls\"\nexit 0\n",
)?;
fs::set_permissions(&ufw, fs::Permissions::from_mode(0o755))?;
let binding = RouteBinding {
driver: RouteDriver::NginxStreamTcp,
route_id: "service-route".into(),
stable_endpoint: "http://127.0.0.1:4103".into(),
private_host: "127.0.0.1".into(),
private_port_start: 4104,
private_port_end: 4109,
config_path: config.clone(),
reload_unit: "nginx.service".into(),
member: None,
};
let driver = NginxRouteDriver {
binding,
nginx_program: nginx,
systemd_run_program: systemd_run,
systemctl_program: systemctl.clone(),
ufw_program: ufw.clone(),
preflight_root: temp.path().join("preflight"),
};
let mut candidate = expected();
candidate.route = Some(cultnet_rs::IdunnExpectedRoute {
route_id: "service-route".into(),
transport: "http".into(),
stable_endpoint: "http://127.0.0.1:4103".into(),
candidate_endpoint: "http://127.0.0.1:4104".into(),
});
let preflight = driver.preflight(&candidate, &digest('a'), None, &Unmetered)?;
assert!(!config.exists());
assert_eq!(fs::read_dir(&driver.preflight_root)?.count(), 0);
let membership_sha256 = driver.install(&candidate, &digest('a'), &preflight, true, &Unmetered)?;
assert_eq!(
fs::read_to_string(systemctl.with_extension("calls"))?,
"reload\n"
);
// The stable endpoint's allow is admitted with the fragment, before
// the reload that makes the listener live.
assert_eq!(
fs::read_to_string(ufw.with_extension("calls"))?,
"allow in to 127.0.0.1 port 4103 proto tcp comment Idunn route service-route\n"
);
assert!(driver.observe_membership(&candidate, &membership_sha256)?);
let admitted_bytes = fs::read(&config)?;
assert!(std::str::from_utf8(&admitted_bytes)?.contains("listen 127.0.0.1:4103;"));
let admitted = RouteObservation {
route_id: "service-route".into(),
runtime_instance_id: digest('a'),
membership_sha256,
signed_presence_sha256: digest('c'),
observed_at_unix_millis: 1,
};
let next_preflight = driver.preflight(&candidate, &digest('b'), Some(&admitted), &Unmetered)?;
assert_eq!(fs::read(&config)?, admitted_bytes);
// The next candidate renders the bytes already live: installing it
// runs nothing, even while a reload would fail.
fs::write(systemctl.with_extension("fail"), b"fail\n")?;
driver.install(&candidate, &digest('b'), &next_preflight, false, &Unmetered)?;
assert_eq!(fs::read(&config)?, admitted_bytes);
assert_eq!(
fs::read_to_string(systemctl.with_extension("calls"))?,
"reload\n"
);
fs::remove_file(systemctl.with_extension("fail"))?;
fs::write(&config, b"foreign route\n")?;
assert!(
driver
.install(&candidate, &digest('b'), &next_preflight, true, &Unmetered)
.is_err()
);
driver.restore_admitted_membership(&candidate, &admitted.membership_sha256, &Unmetered)?;
assert_eq!(fs::read(&config)?, admitted_bytes);
assert!(driver.observe_membership(&candidate, &admitted.membership_sha256)?);
fs::write(&config, b"foreign route after admission\n")?;
fs::write(systemctl.with_extension("fail"), b"fail\n")?;
assert!(
driver
.restore_admitted_membership(&candidate, &admitted.membership_sha256, &Unmetered)
.is_err()
);
// The failed reload puts back what it replaced: the disk shows what
// nginx still serves.
assert_eq!(fs::read(&config)?, b"foreign route after admission\n");
fs::remove_file(systemctl.with_extension("fail"))?;
driver.restore_admitted_membership(&candidate, &admitted.membership_sha256, &Unmetered)?;
assert_eq!(fs::read(&config)?, admitted_bytes);
Ok(())
}
#[test]
fn process_environment_witness_is_sorted_exact_and_secret_free() -> Result<()> {
let entries = vec![
b"B=two".to_vec(),
b"A=one".to_vec(),
b"UNRELATED=ignored".to_vec(),
];
let selected = select_process_environment(&entries, &["A".into(), "B".into()])?;
assert_eq!(selected.get("A").map(String::as_str), Some("one"));
assert!(!selected.contains_key("UNRELATED"));
assert!(select_process_environment(&entries, &["B".into(), "A".into()]).is_err());
Ok(())
}
#[test]
fn topology_transport_returns_opaque_odin_bytes_without_deciding_ready() -> Result<()> {
let temp = tempfile::tempdir()?;
let correlation_store = temp.path().join("correlation.cc");
let opaque = b"not a topology receipt".to_vec();
upsert_record(
&correlation_store,
CultCacheEnvelope {
key: incarnation_key_of("service", &digest('1')),
r#type: OdinRuntimeTopologyCorrelationRecord::TYPE.into(),
payload: opaque.clone(),
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(ODIN_RUNTIME_TOPOLOGY_CORRELATION_SCHEMA.into()),
},
)?;
let driver = CultCacheTopologyDriver {
projection_store: temp.path().join("projection.cc"),
correlation_store,
};
assert_eq!(driver.receive("service", &digest('2'))?, None);
assert_eq!(
driver.receive("service", &digest('1'))?,
Some(ReceivedOdinTopologyCorrelation {
target: "service".into(),
expected_sha256: digest('1'),
canonical_bytes: opaque,
})
);
Ok(())
}
#[test]
fn expected_withdrawal_is_exact_atomic_and_idempotent() -> Result<()> {
let temp = tempfile::tempdir()?;
let (expected, activation, warming, provider_anchor) = authenticated_warming(temp.path())?;
let lease = lease(&expected, &activation, &warming);
let driver = topology_driver(temp.path(), "projection");
driver.publish_expected(&expected, &provider_anchor)?;
let published = SingleFileMessagePackBackingStore::new(&driver.projection_store)
.pull_all_read_only_snapshot()?;
assert_eq!(published.len(), 2);
assert!(published.iter().any(|entry| {
entry.key == incarnation_key(&expected).unwrap()
&& entry.r#type == IdunnExpectedIncarnationRecord::TYPE
}));
let published_anchor = published
.iter()
.find(|entry| entry.r#type == GameCultServiceTrustAnchorRecord::TYPE)
.context("atomic Expected set has no provider trust anchor")?;
assert_eq!(
service_trust_anchor_from_envelope(published_anchor)?,
runtime_presence_trust_anchor(&expected, &provider_anchor)?
);
project_activation(&driver, &expected, &activation)?;
driver.publish_process_write_lease(&expected, &activation, &lease)?;
let mut substituted_expected = expected.clone();
substituted_expected.incarnation_id = "another-incarnation".into();
upsert_record(
&driver.projection_store,
CultCacheEnvelope {
key: incarnation_key(&expected)?,
r#type: IdunnExpectedIncarnationRecord::TYPE.into(),
payload: substituted_expected.canonical_bytes()?,
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(IDUNN_EXPECTED_INCARNATION_SCHEMA.into()),
},
)?;
assert!(
driver
.withdraw_incarnation(&expected, &provider_anchor, Some(&activation), Some(&lease))
.is_err()
);
driver.publish_expected(&expected, &provider_anchor)?;
let mut substituted_anchor = runtime_presence_trust_anchor(&expected, &provider_anchor)?;
substituted_anchor.runtime_id = "another-runtime".into();
substituted_anchor.validate()?;
upsert_record(
&driver.projection_store,
CultCacheEnvelope {
key: substituted_anchor.trust_anchor_id.clone(),
r#type: GameCultServiceTrustAnchorRecord::TYPE.into(),
payload: rmp_serde::to_vec(&substituted_anchor)?,
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(GAMECULT_SERVICE_TRUST_ANCHOR_SCHEMA.into()),
},
)?;
assert!(
driver
.withdraw_incarnation(&expected, &provider_anchor, Some(&activation), Some(&lease))
.is_err()
);
driver.publish_expected(&expected, &provider_anchor)?;
let mut substituted_activation = activation.clone();
substituted_activation.expected_projection_sha256 = digest('a');
upsert_record(
&driver.projection_store,
CultCacheEnvelope {
key: incarnation_key(&expected)?,
r#type: IdunnRuntimeActivationRecord::TYPE.into(),
payload: substituted_activation.canonical_bytes()?,
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(IDUNN_RUNTIME_ACTIVATION_SCHEMA.into()),
},
)?;
assert!(
driver
.withdraw_incarnation(&expected, &provider_anchor, Some(&activation), None)
.is_err()
);
project_activation(&driver, &expected, &activation)?;
let mut substituted_lease = lease.clone();
substituted_lease.lease_epoch += 1;
assert!(
driver
.publish_process_write_lease(&expected, &activation, &substituted_lease)
.is_err()
);
assert!(
driver
.withdraw_incarnation(
&expected,
&provider_anchor,
Some(&activation),
Some(&substituted_lease),
)
.is_err()
);
upsert_record(
&driver.projection_store,
CultCacheEnvelope {
key: "unrelated".into(),
r#type: "test.unrelated".into(),
payload: vec![0x90],
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some("test.unrelated.v1".into()),
},
)?;
driver.withdraw_incarnation(
&expected,
&provider_anchor,
Some(&activation),
Some(&lease),
)?;
let remaining = SingleFileMessagePackBackingStore::new(&driver.projection_store)
.pull_all_read_only_snapshot()?;
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].key, "unrelated");
driver.withdraw_incarnation(
&expected,
&provider_anchor,
Some(&activation),
Some(&lease),
)?;
Ok(())
}
#[test]
fn incumbent_projection_restoration_demotes_to_expected_only_and_rejects_unknown_authority()
-> Result<()> {
let temp = tempfile::tempdir()?;
let (incumbent, activation, warming, provider_anchor) = authenticated_warming(temp.path())?;
let lease = lease(&incumbent, &activation, &warming);
let driver = topology_driver(temp.path(), "restoration");
driver.publish_expected(&incumbent, &provider_anchor)?;
project_activation(&driver, &incumbent, &activation)?;
driver.publish_process_write_lease(&incumbent, &activation, &lease)?;
let incumbent_key = incarnation_key(&incumbent)?;
let mut candidate = incumbent.clone();
candidate.plan_id = digest('a');
candidate.incarnation_id = "candidate-incarnation".into();
candidate.sealed_release_id = digest('b');
candidate.artifact_sha256 = digest('c');
let mut candidate_activation = activation.clone();
candidate_activation.expected_projection_sha256 = candidate.canonical_sha256()?;
candidate_activation.runtime_instance_id = digest('d');
let candidate_key = incarnation_key(&candidate)?;
// Sealing the candidate beside the incumbent changes nothing the
// incumbent owns: its Expected, activation and lease are still there
// under its own key, byte for byte.
let before_candidate = SingleFileMessagePackBackingStore::new(&driver.projection_store)
.pull_all_read_only_snapshot()?;
driver.publish_expected(&candidate, &provider_anchor)?;
project_activation(&driver, &candidate, &candidate_activation)?;
let with_candidate = SingleFileMessagePackBackingStore::new(&driver.projection_store)
.pull_all_read_only_snapshot()?;
for envelope in &before_candidate {
if envelope.key == incumbent_key {
assert!(with_candidate.contains(envelope));
}
}
assert!(driver.admitted_runtime_projection_is_exact(
&incumbent,
&provider_anchor,
&activation,
Some(&lease),
)?);
assert_eq!(
with_candidate
.iter()
.filter(|entry| entry.r#type == IdunnExpectedIncarnationRecord::TYPE)
.count(),
2
);
upsert_record(
&driver.projection_store,
CultCacheEnvelope {
key: "unrelated".into(),
r#type: "test.unrelated".into(),
payload: vec![0x90],
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some("test.unrelated.v1".into()),
},
)?;
// A pre-fencing abort withdraws the candidate and only the candidate.
driver.withdraw_incarnation(
&candidate,
&provider_anchor,
Some(&candidate_activation),
None,
)?;
let after_withdrawal = SingleFileMessagePackBackingStore::new(&driver.projection_store)
.pull_all_read_only_snapshot()?;
assert!(
after_withdrawal
.iter()
.all(|entry| entry.key != candidate_key)
);
assert!(driver.admitted_runtime_projection_is_exact(
&incumbent,
&provider_anchor,
&activation,
Some(&lease),
)?);
assert!(
after_withdrawal
.iter()
.any(|entry| entry.r#type == GameCultServiceTrustAnchorRecord::TYPE)
);
// Demotion is for an incumbent whose process is gone: activation and
// lease go, Expected and anchor stay.
assert_eq!(
driver.demote_to_expected_only(
&incumbent,
&provider_anchor,
&activation,
Some(&lease),
)?,
incumbent.canonical_sha256()?
);
let restored = SingleFileMessagePackBackingStore::new(&driver.projection_store)
.pull_all_read_only_snapshot()?;
assert_eq!(restored.len(), 3);
assert_eq!(
IdunnExpectedIncarnationRecord::decode_canonical(
&restored
.iter()
.find(|entry| entry.r#type == IdunnExpectedIncarnationRecord::TYPE)
.unwrap()
.payload,
)?,
incumbent
);
assert_eq!(
service_trust_anchor_from_envelope(
restored
.iter()
.find(|entry| entry.r#type == GameCultServiceTrustAnchorRecord::TYPE)
.unwrap(),
)?,
runtime_presence_trust_anchor(&incumbent, &provider_anchor)?
);
assert!(
restored
.iter()
.all(|entry| entry.r#type != IdunnRuntimeActivationRecord::TYPE
&& entry.r#type != IdunnProcessWriteLeaseRecord::TYPE)
);
assert!(
restored
.iter()
.any(|entry| entry.key == "unrelated" && entry.r#type == "test.unrelated")
);
assert!(!driver.projected_activation_is_present(&incumbent)?);
let idempotent_bytes = fs::read(&driver.projection_store)?;
driver.demote_to_expected_only(&incumbent, &provider_anchor, &activation, Some(&lease))?;
assert_eq!(fs::read(&driver.projection_store)?, idempotent_bytes);
// A record under the incumbent's key that is not the incumbent's is a
// substitution; demotion refuses it and leaves the store as found.
let admitted_anchor = runtime_presence_trust_anchor(&incumbent, &provider_anchor)?;
let mut unknown_expected = incumbent.clone();
unknown_expected.incarnation_id = "unknown-incarnation".into();
let mut unknown_anchor = admitted_anchor.clone();
unknown_anchor.runtime_id = "unknown-runtime".into();
unknown_anchor.validate()?;
let mut unknown_activation = activation.clone();
unknown_activation.runtime_instance_id = digest('e');
let mut unknown_lease = lease.clone();
unknown_lease.lease_epoch += 1;
let substitutions = [
CultCacheEnvelope {
key: incumbent_key.clone(),
r#type: IdunnExpectedIncarnationRecord::TYPE.into(),
payload: unknown_expected.canonical_bytes()?,
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(IDUNN_EXPECTED_INCARNATION_SCHEMA.into()),
},
CultCacheEnvelope {
key: admitted_anchor.trust_anchor_id.clone(),
r#type: GameCultServiceTrustAnchorRecord::TYPE.into(),
payload: rmp_serde::to_vec(&unknown_anchor)?,
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(GAMECULT_SERVICE_TRUST_ANCHOR_SCHEMA.into()),
},
CultCacheEnvelope {
key: incumbent_key.clone(),
r#type: IdunnRuntimeActivationRecord::TYPE.into(),
payload: unknown_activation.canonical_bytes()?,
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(IDUNN_RUNTIME_ACTIVATION_SCHEMA.into()),
},
CultCacheEnvelope {
key: incumbent_key.clone(),
r#type: IdunnProcessWriteLeaseRecord::TYPE.into(),
payload: unknown_lease.canonical_bytes()?,
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(IDUNN_PROCESS_WRITE_LEASE_SCHEMA.into()),
},
];
for substitution in substitutions {
fs::write(&driver.projection_store, &idempotent_bytes)?;
upsert_record(&driver.projection_store, substitution)?;
let before = fs::read(&driver.projection_store)?;
assert!(
driver
.demote_to_expected_only(
&incumbent,
&provider_anchor,
&activation,
Some(&lease),
)
.is_err()
);
assert_eq!(fs::read(&driver.projection_store)?, before);
}
Ok(())
}
/// A replaced incarnation nothing owns is withdrawn whole; the one still
/// admitted, and the target's anchor, are untouched.
#[test]
fn a_stale_incarnation_is_withdrawn_whole_beside_the_admitted_one() -> Result<()> {
let temp = tempfile::tempdir()?;
let (admitted, activation, warming, provider_anchor) = authenticated_warming(temp.path())?;
let lease = lease(&admitted, &activation, &warming);
let driver = topology_driver(temp.path(), "stale");
let mut stale = admitted.clone();
stale.plan_id = digest('a');
stale.incarnation_id = "stale-incarnation".into();
stale.sealed_release_id = digest('b');
stale.artifact_sha256 = digest('c');
let mut stale_activation = activation.clone();
stale_activation.expected_projection_sha256 = stale.canonical_sha256()?;
stale_activation.runtime_instance_id = digest('d');
driver.publish_expected(&stale, &provider_anchor)?;
project_activation(&driver, &stale, &stale_activation)?;
driver.publish_expected(&admitted, &provider_anchor)?;
project_activation(&driver, &admitted, &activation)?;
driver.publish_process_write_lease(&admitted, &activation, &lease)?;
let mut projected = driver.projected_incarnations(&admitted.target)?;
projected.sort_by_key(|expected| expected.incarnation_id.clone());
assert_eq!(projected.len(), 2);
assert!(projected.contains(&stale) && projected.contains(&admitted));
driver.withdraw_stale_incarnation(&stale, &provider_anchor)?;
assert_eq!(
driver.projected_incarnations(&admitted.target)?,
vec![admitted.clone()]
);
assert!(driver.admitted_runtime_projection_is_exact(
&admitted,
&provider_anchor,
&activation,
Some(&lease),
)?);
let idempotent = fs::read(&driver.projection_store)?;
driver.withdraw_stale_incarnation(&stale, &provider_anchor)?;
assert_eq!(fs::read(&driver.projection_store)?, idempotent);
Ok(())
}
/// The single-slot projection an older Idunn wrote is retired the first
/// time this one publishes for the target, and never read.
#[test]
fn a_legacy_target_keyed_slot_is_retired_on_publish() -> Result<()> {
let temp = tempfile::tempdir()?;
let (expected, activation, _warming, provider_anchor) = authenticated_warming(temp.path())?;
let driver = topology_driver(temp.path(), "legacy");
upsert_record(
&driver.projection_store,
CultCacheEnvelope {
key: expected.target.clone(),
r#type: IdunnExpectedIncarnationRecord::TYPE.into(),
payload: expected.canonical_bytes()?,
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(IDUNN_EXPECTED_INCARNATION_SCHEMA.into()),
},
)?;
upsert_record(
&driver.projection_store,
CultCacheEnvelope {
key: expected.target.clone(),
r#type: IdunnRuntimeActivationRecord::TYPE.into(),
payload: activation.canonical_bytes()?,
stored_at: "2026-09-03T00:00:00Z".into(),
schema_id: Some(IDUNN_RUNTIME_ACTIVATION_SCHEMA.into()),
},
)?;
assert!(!driver.admitted_expected_projection_is_exact(&expected, &provider_anchor)?);
assert!(!driver.projected_activation_is_present(&expected)?);
driver.publish_expected(&expected, &provider_anchor)?;
let published = SingleFileMessagePackBackingStore::new(&driver.projection_store)
.pull_all_read_only_snapshot()?;
assert!(published.iter().all(|entry| entry.key != expected.target));
assert_eq!(published.len(), 2);
assert!(driver.admitted_expected_projection_is_exact(&expected, &provider_anchor)?);
Ok(())
}
#[cfg(unix)]
#[test]
fn service_credential_sources_require_one_root_owned_0400_inode() -> Result<()> {
use std::os::unix::fs::{PermissionsExt, symlink};
let temp = tempfile::tempdir()?;
let root = temp.path().join("credentials");
fs::create_dir(&root)?;
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))?;
let source = root.join("token");
fs::write(&source, b"secret")?;
fs::set_permissions(&source, fs::Permissions::from_mode(0o400))?;
let mut sources = BTreeMap::from([("SERVICE_TOKEN".into(), source.clone())]);
validate_service_credential_sources(&sources)?;
fs::set_permissions(&source, fs::Permissions::from_mode(0o600))?;
assert!(validate_service_credential_sources(&sources).is_err());
fs::set_permissions(&source, fs::Permissions::from_mode(0o400))?;
let hardlink = root.join("token-hardlink");
fs::hard_link(&source, &hardlink)?;
assert!(validate_service_credential_sources(&sources).is_err());
fs::remove_file(&hardlink)?;
let symlink_path = root.join("token-symlink");
symlink(&source, &symlink_path)?;
sources.insert("SERVICE_TOKEN".into(), symlink_path);
assert!(validate_service_credential_sources(&sources).is_err());
Ok(())
}
#[cfg(unix)]
#[test]
fn write_lease_revoke_is_exact_and_never_deletes_a_surprise() -> Result<()> {
let temp = tempfile::tempdir()?;
let path = temp.path().join("lease.cc");
let driver = CultCacheWriteLeaseDriver::new("service", &path);
let (expected, activation, warming, provider_anchor) = authenticated_warming(temp.path())?;
let lease = lease(&expected, &activation, &warming);
let topology = topology_driver(temp.path(), "lease-projection");
topology.publish_expected(&expected, &provider_anchor)?;
project_activation(&topology, &expected, &activation)?;
let lease_sha256 = driver.grant(&expected, &activation, &warming, &lease)?;
assert_eq!(lease_sha256, lease.canonical_sha256()?);
assert!(driver.observe(&expected, &activation, &warming, &lease)?);
assert!(
!SingleFileMessagePackBackingStore::new(&topology.projection_store)
.pull_all_read_only_snapshot()?
.iter()
.any(|entry| entry.r#type == IdunnProcessWriteLeaseRecord::TYPE)
);
assert_eq!(
topology.publish_process_write_lease(&expected, &activation, &lease)?,
lease_sha256
);
let mut surprise = lease.clone();
surprise.lease_epoch = 2;
assert!(driver.revoke_exact(Some(&surprise)).is_err());
assert!(driver.observe(&expected, &activation, &warming, &lease)?);
SingleFileMessagePackBackingStore::new(&path).with_read_only_shared_snapshot(|_| {
let denial = driver
.revoke_exact(Some(&lease))
.expect_err("shared lifetime holder must deny without blocking");
assert!(
denial
.to_string()
.contains("held by a shared-lock consumer")
);
Ok(())
})?;
assert!(driver.observe(&expected, &activation, &warming, &lease)?);
driver.revoke_exact(Some(&lease))?;
assert!(driver.observe_empty()?);
topology.withdraw_process_write_lease(&expected, &activation, Some(&lease))?;
assert!(
!SingleFileMessagePackBackingStore::new(&topology.projection_store)
.pull_all_read_only_snapshot()?
.iter()
.any(|entry| entry.r#type == IdunnProcessWriteLeaseRecord::TYPE)
);
topology.withdraw_process_write_lease(&expected, &activation, Some(&lease))?;
driver.revoke_exact(Some(&lease))?;
SingleFileMessagePackBackingStore::new(&path).with_read_only_shared_snapshot(
|snapshot| {
assert!(snapshot.is_empty());
let denial = driver
.grant(&expected, &activation, &warming, &lease)
.expect_err("shared lifetime holder must deny grant without blocking");
assert!(
denial
.to_string()
.contains("held by a shared-lock consumer")
);
Ok(())
},
)?;
assert!(driver.observe_empty()?);
assert_eq!(
driver.grant(&expected, &activation, &warming, &lease)?,
lease.canonical_sha256()?
);
driver.revoke_exact(Some(&lease))?;
Ok(())
}
#[cfg(unix)]
#[test]
fn write_lease_grant_binds_the_exact_warming_candidate_and_the_holder_read_keeps_custody()
-> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
let path = temp.path().join("lease.cc");
let driver = CultCacheWriteLeaseDriver::new("service", &path);
let (expected, activation, warming, _) = authenticated_warming(temp.path())?;
let good = lease(&expected, &activation, &warming);
let mut foreign = good.clone();
foreign.warming_presence_sha256 = digest('8');
assert!(
driver
.grant(&expected, &activation, &warming, &foreign)
.is_err()
);
let other = CultCacheWriteLeaseDriver::new("other", temp.path().join("other.cc"));
assert!(other.grant(&expected, &activation, &warming, &good).is_err());
assert!(driver.current_for_holder()?.is_none());
driver.grant(&expected, &activation, &warming, &good)?;
assert_eq!(
driver.current_for_holder()?.map(|(_, held)| held),
Some(good)
);
fs::set_permissions(&path, fs::Permissions::from_mode(0o666))?;
assert!(
driver.current_for_holder().is_err(),
"a service-writable lease store is not authority"
);
Ok(())
}
#[cfg(unix)]
#[test]
fn installed_release_is_root_owned_and_nonwritable() -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let temp = tempfile::tempdir()?;
let root = temp.path().join("release");
fs::create_dir(&root)?;
let artifact = root.join("service");
fs::write(&artifact, b"sealed")?;
fs::set_permissions(&artifact, fs::Permissions::from_mode(0o777))?;
harden_installed_release(
&root,
&[ArtifactReceipt {
artifact_id: "service".into(),
destination: PathBuf::from("service"),
sha256: sha256_id(b"sealed"),
size_bytes: 6,
executable: true,
executable_members: BTreeSet::new(),
}],
)?;
let root_metadata = fs::symlink_metadata(&root)?;
let artifact_metadata = fs::symlink_metadata(&artifact)?;
assert_eq!(root_metadata.uid(), 0);
assert_eq!(root_metadata.permissions().mode() & 0o777, 0o555);
assert_eq!(artifact_metadata.uid(), 0);
assert_eq!(artifact_metadata.permissions().mode() & 0o777, 0o555);
Ok(())
}
#[cfg(unix)]
const MEMBER_CANARY: &str = "zzcanary";
#[cfg(unix)]
fn tree_output(executable: bool, members: &[&str]) -> ArtifactOutput {
ArtifactOutput {
id: "upstream".into(),
source_kind: ArtifactSource::WorktreeTree,
runner: None,
source: PathBuf::from("upstream"),
destination: PathBuf::from("upstream"),
expected_sha256: None,
executable,
executable_members: members.iter().map(PathBuf::from).collect(),
}
}
#[cfg(unix)]
fn write_file(path: &Path, bytes: &[u8]) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
fs::create_dir_all(path.parent().context("file has no parent")?)?;
fs::write(path, bytes)?;
// Builds and git leave arbitrary exec bits; the seal must not inherit them.
fs::set_permissions(path, fs::Permissions::from_mode(0o777))?;
Ok(())
}
#[cfg(unix)]
fn sealed_mode(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
fs::symlink_metadata(path).unwrap().permissions().mode() & 0o777
}
/// `<root>/src/upstream` is the worktree tree; `<root>/staging` the release.
#[cfg(unix)]
fn sample_tree(root: &Path) -> Result<(PathBuf, PathBuf)> {
let source = root.join("src");
let staging = root.join("staging");
fs::create_dir_all(&staging)?;
let tree = source.join("upstream");
write_file(&tree.join("bin/daemon"), b"daemon")?;
write_file(&tree.join("bin/helper"), b"helper")?;
write_file(&tree.join("lib/tool"), b"tool")?;
write_file(&tree.join("lib/data.txt"), b"data")?;
Ok((source, staging))
}
#[cfg(unix)]
fn collect_tree(
source: &Path,
staging: &Path,
output: ArtifactOutput,
) -> Result<ArtifactReceipt> {
let mut declaration =
TargetDeclaration::parse(crate::deployment::tests::RECIPE)?;
let id = output.id.clone();
declaration.artifacts = vec![output];
let frozen = FrozenSource {
receipt: FrozenSourceReceipt {
transaction_id: "tx".into(),
plan_id: "plan".into(),
snapshot_sha256: "snapshot".into(),
},
facts: crate::deployment_plan::tests::source(crate::deployment::tests::RECIPE),
recipe_bytes: Vec::new(),
root: source.to_path_buf(),
};
DockerRunnerDriver::default().collect_artifact(
&declaration,
&frozen,
&BTreeMap::new(),
staging,
&id,
)
}
#[cfg(unix)]
fn refusal_text<T>(result: Result<T>) -> String {
let Err(error) = result else {
panic!("the release was not refused");
};
let text = format!("{error:#}");
assert!(
!text.contains(MEMBER_CANARY),
"the refusal echoed a member path: {text}"
);
text
}
#[cfg(unix)]
#[test]
fn tree_member_sealed_0555_and_siblings_0444() -> Result<()> {
let temp = tempfile::tempdir()?;
let (source, staging) = sample_tree(temp.path())?;
let receipt = collect_tree(
&source,
&staging,
tree_output(false, &["bin/daemon", "lib/tool"]),
)?;
assert_eq!(
receipt.executable_members,
BTreeSet::from([PathBuf::from("bin/daemon"), PathBuf::from("lib/tool")])
);
harden_installed_release(&staging, &[receipt])?;
let tree = staging.join("upstream");
assert_eq!(sealed_mode(&tree.join("bin/daemon")), 0o555);
assert_eq!(sealed_mode(&tree.join("lib/tool")), 0o555);
assert_eq!(sealed_mode(&tree.join("bin/helper")), 0o444);
assert_eq!(sealed_mode(&tree.join("lib/data.txt")), 0o444);
for directory in [&staging, &tree, &tree.join("bin"), &tree.join("lib")] {
assert_eq!(sealed_mode(directory), 0o555);
}
Ok(())
}
#[cfg(unix)]
#[test]
fn missing_member_refuses_the_release() -> Result<()> {
// The bad member sorts after the good one in one case and before it in
// the other, so a check that stops at the first (or last) member fails.
for members in [
&["bin/daemon", "lib/zzcanary-missing"][..],
&["aaa-zzcanary-missing", "bin/daemon"][..],
&["zzcanary-dir/tool"][..],
] {
let temp = tempfile::tempdir()?;
let (source, staging) = sample_tree(temp.path())?;
let text = refusal_text(collect_tree(&source, &staging, tree_output(false, members)));
assert!(text.contains("executable member is missing"), "{text}");
}
Ok(())
}
#[cfg(unix)]
#[test]
fn symlink_member_refuses_the_release() -> Result<()> {
for link_target in ["daemon", "../zzcanary-nowhere"] {
let temp = tempfile::tempdir()?;
let (source, staging) = sample_tree(temp.path())?;
std::os::unix::fs::symlink(
link_target,
source.join("upstream/bin/zzcanary-link"),
)?;
let text = refusal_text(collect_tree(
&source,
&staging,
tree_output(false, &["bin/daemon", "bin/zzcanary-link"]),
));
assert!(text.contains("not a regular file"), "{text}");
}
Ok(())
}
#[cfg(unix)]
#[test]
fn member_through_symlinked_directory_refuses_the_release() -> Result<()> {
let temp = tempfile::tempdir()?;
let (source, staging) = sample_tree(temp.path())?;
let tree = source.join("upstream");
write_file(&tree.join("real-bin/zzcanary-tool"), b"tool")?;
std::os::unix::fs::symlink("real-bin", tree.join("linked-bin"))?;
let text = refusal_text(collect_tree(
&source,
&staging,
tree_output(false, &["linked-bin/zzcanary-tool"]),
));
assert!(text.contains("symlink or non-directory"), "{text}");
// A member below a regular file is refused the same way.
let second_staging = temp.path().join("second-staging");
fs::create_dir_all(&second_staging)?;
let text = refusal_text(collect_tree(
&source,
&second_staging,
tree_output(false, &["bin/daemon/zzcanary-below-file"]),
));
assert!(text.contains("symlink or non-directory"), "{text}");
Ok(())
}
#[cfg(unix)]
#[test]
fn directory_member_refuses_the_release() -> Result<()> {
let temp = tempfile::tempdir()?;
let (source, staging) = sample_tree(temp.path())?;
let text = refusal_text(collect_tree(
&source,
&staging,
tree_output(false, &["bin/daemon", "lib"]),
));
assert!(text.contains("not a regular file"), "{text}");
Ok(())
}
#[cfg(unix)]
#[test]
fn executable_flag_on_tree_output_refuses() -> Result<()> {
let temp = tempfile::tempdir()?;
let (source, staging) = sample_tree(temp.path())?;
let text = refusal_text(collect_tree(&source, &staging, tree_output(true, &[])));
assert!(text.contains("cannot be executable"), "{text}");
// The same flag on a file output stays admitted.
let file = temp.path().join("file-artifact");
fs::write(&file, b"x")?;
let mut output = tree_output(true, &[]);
output.id = "file".into();
check_artifact_modes(&output, &file)?;
Ok(())
}
#[cfg(unix)]
#[test]
fn members_on_file_output_refuse() -> Result<()> {
let temp = tempfile::tempdir()?;
let file = temp.path().join("file-artifact");
fs::write(&file, b"x")?;
check_artifact_modes(&tree_output(false, &[]), &file)?;
let text = refusal_text(check_artifact_modes(
&tree_output(false, &["zzcanary"]),
&file,
));
assert!(text.contains("cannot declare executable_members"), "{text}");
Ok(())
}
#[cfg(unix)]
#[test]
fn check_artifact_modes_refuses_a_member_that_is_not_a_clean_relative_path() -> Result<()> {
let temp = tempfile::tempdir()?;
let (source, _) = sample_tree(temp.path())?;
let tree = source.join("upstream");
write_file(&temp.path().join("zzcanary-outside"), b"x")?;
for member in ["../../zzcanary-outside", "bin/../bin/daemon", "/zzcanary", ""] {
refusal_text(check_artifact_modes(&tree_output(false, &[member]), &tree));
}
check_artifact_modes(&tree_output(false, &["bin/daemon"]), &tree)?;
Ok(())
}
#[cfg(unix)]
#[test]
fn harden_fails_when_a_declared_member_is_absent() -> Result<()> {
let receipt = |members: &[&str]| ArtifactReceipt {
artifact_id: "upstream".into(),
destination: PathBuf::from("upstream"),
sha256: sha256_id(b"x"),
size_bytes: 1,
executable: false,
executable_members: members.iter().map(PathBuf::from).collect(),
};
let installed = |root: &Path| -> Result<()> {
write_file(&root.join("upstream/bin/daemon"), b"daemon")?;
write_file(&root.join("upstream/lib/tool"), b"tool")?;
fs::create_dir_all(root.join("upstream/zzcanary-dir"))?;
std::os::unix::fs::symlink("daemon", root.join("upstream/bin/zzcanary-link"))?;
Ok(())
};
let temp = tempfile::tempdir()?;
installed(temp.path())?;
harden_installed_release(temp.path(), &[receipt(&["bin/daemon", "lib/tool"])])?;
for members in [
&["bin/daemon", "lib/zzcanary-gone"][..],
&["zzcanary-gone", "bin/daemon"][..],
&["zzcanary-dir"][..],
&["bin/zzcanary-link"][..],
&["bin/daemon/zzcanary-below-file"][..],
] {
let temp = tempfile::tempdir()?;
installed(temp.path())?;
let text = refusal_text(harden_installed_release(temp.path(), &[receipt(members)]));
assert!(text.contains("declared executable is missing"), "{text}");
}
Ok(())
}
#[cfg(unix)]
#[test]
fn relative_in_tree_symlink_survives_the_seal() -> Result<()> {
let temp = tempfile::tempdir()?;
let (source, staging) = sample_tree(temp.path())?;
let tree = source.join("upstream");
write_file(&tree.join("bin/python3.12"), b"python")?;
std::os::unix::fs::symlink("python3.12", tree.join("bin/python3"))?;
let receipt = collect_tree(&source, &staging, tree_output(false, &["bin/python3.12"]))?;
let (before, _) = digest_artifact(&staging.join("upstream"))?;
harden_installed_release(&staging, &[receipt])?;
let link = staging.join("upstream/bin/python3");
assert!(fs::symlink_metadata(&link)?.file_type().is_symlink());
assert_eq!(fs::read_link(&link)?, PathBuf::from("python3.12"));
assert_eq!(sealed_mode(&staging.join("upstream/bin/python3.12")), 0o555);
assert_eq!(digest_artifact(&staging.join("upstream"))?.0, before);
// A link out of the release, by `..` or by absolute target, still refuses.
for (target, rule) in [
("../../zzcanary-up", "parent traversal"),
("/zzcanary/absolute", "absolute"),
] {
let temp = tempfile::tempdir()?;
let (source, staging) = sample_tree(temp.path())?;
std::os::unix::fs::symlink(target, source.join("upstream/lib/zzcanary-link"))?;
let receipt = collect_tree(&source, &staging, tree_output(false, &["bin/daemon"]))?;
let text = refusal_text(harden_installed_release(&staging, &[receipt]));
assert!(text.contains(rule), "{text}");
}
Ok(())
}
#[cfg(unix)]
#[test]
fn streamed_digest_is_byte_compatible() -> Result<()> {
// Not a multiple of the 64 KiB read buffer, so a short last chunk
// matters; the aligned and empty files are the other boundaries.
let big = (0..3 * 65536 + 17).map(|i| (i % 251) as u8).collect::<Vec<u8>>();
let aligned = vec![7_u8; 65536];
let temp = tempfile::tempdir()?;
let tree = temp.path().join("tree");
write_file(&tree.join("a.txt"), b"alpha")?;
write_file(&tree.join("empty"), b"")?;
write_file(&tree.join("sub/aligned.bin"), &aligned)?;
write_file(&tree.join("sub/big.bin"), &big)?;
std::os::unix::fs::symlink("a.txt", tree.join("zlink"))?;
// The encoding digest_tree had before it streamed, written out by hand.
fn add_file(hasher: &mut Sha256, name: &str, bytes: &[u8]) {
hasher.update(b"file\0");
hasher.update(name.as_bytes());
hasher.update(b"\0");
hasher.update((bytes.len() as u64).to_le_bytes());
hasher.update(bytes);
}
let mut expected = Sha256::new();
add_file(&mut expected, "a.txt", b"alpha");
add_file(&mut expected, "empty", b"");
let mut expected_size = 5_u64;
expected.update(b"dir\0sub\0");
add_file(&mut expected, "sub/aligned.bin", &aligned);
add_file(&mut expected, "sub/big.bin", &big);
expected_size += (aligned.len() + big.len()) as u64;
expected.update(b"link\0zlink\0a.txt");
expected_size += "a.txt".len() as u64;
assert_eq!(
digest_artifact(&tree)?,
(format!("{:x}", expected.finalize()), expected_size)
);
let single = tree.join("sub/big.bin");
assert_eq!(
digest_artifact(&single)?,
(raw_sha256(&big), big.len() as u64)
);
let mut hasher = Sha256::new();
assert_eq!(hash_file_streamed(&single, &mut hasher)?, big.len() as u64);
assert_eq!(format!("{:x}", hasher.finalize()), raw_sha256(&big));
Ok(())
}
/// Acceptance on the real trees: `IDUNN_BIG_TREE` holds `python/` and
/// `blender/`. Run on Yggdrasil as its own job (see the cut spec).
#[cfg(unix)]
#[test]
#[ignore = "needs IDUNN_BIG_TREE: a CPython+torch tree and a Blender tree"]
fn big_tree_seals_from_env() -> Result<()> {
use std::time::Instant;
fn symlinks(path: &Path) -> Result<usize> {
let mut count = 0;
for entry in fs::read_dir(path)? {
let entry = entry?;
let metadata = fs::symlink_metadata(entry.path())?;
if metadata.is_dir() {
count += symlinks(&entry.path())?;
} else if metadata.file_type().is_symlink() {
count += 1;
}
}
Ok(count)
}
fn peak_rss() -> String {
fs::read_to_string("/proc/self/status")
.ok()
.and_then(|status| {
status
.lines()
.find(|line| line.starts_with("VmHWM"))
.map(str::to_owned)
})
.unwrap_or_default()
}
let tree = PathBuf::from(
std::env::var_os("IDUNN_BIG_TREE").context("IDUNN_BIG_TREE is not set")?,
);
let members = ["python/bin/python3.12", "blender/blender"];
let temp = tempfile::tempdir()?;
let staging = temp.path().join("staging");
fs::create_dir_all(&staging)?;
let destination = staging.join("tree");
let output = ArtifactOutput {
destination: PathBuf::from("tree"),
..tree_output(false, &members)
};
let started = Instant::now();
copy_artifact(&tree, &tree, &destination)?;
println!("copy {:?} {}", started.elapsed(), peak_rss());
let started = Instant::now();
let first = digest_artifact(&destination)?;
let second = digest_artifact(&destination)?;
assert_eq!(first, second);
println!("digest x2 {:?} {}", started.elapsed(), peak_rss());
check_artifact_modes(&output, &destination)?;
let links = symlinks(&destination)?;
let started = Instant::now();
harden_installed_release(
&staging,
&[ArtifactReceipt {
artifact_id: "tree".into(),
destination: PathBuf::from("tree"),
sha256: format!("sha256-{}", first.0),
size_bytes: first.1,
executable: false,
executable_members: output.executable_members.clone(),
}],
)?;
println!("harden {:?} {}", started.elapsed(), peak_rss());
for member in members {
assert_eq!(sealed_mode(&destination.join(member)), 0o555);
}
let sample = fs::read_dir(&destination)?
.map(|entry| entry.map(|entry| entry.path()))
.collect::<std::io::Result<Vec<_>>>()?;
for path in sample {
if fs::symlink_metadata(&path)?.is_file() {
assert_eq!(sealed_mode(&path), 0o444);
}
}
assert_eq!(symlinks(&destination)?, links);
println!("bytes {} symlinks {links}", first.1);
Ok(())
}
#[cfg(unix)]
#[test]
fn runner_cache_is_created_once_for_one_exact_identity() -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o700))?;
let cache = temp.path().join("cache");
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
ensure_runner_cache_root(&cache, identity)?;
let metadata = fs::symlink_metadata(&cache)?;
assert_eq!((metadata.uid(), metadata.gid()), (1000, 1000));
assert_eq!(metadata.permissions().mode() & 0o777, 0o700);
assert!(ensure_runner_cache_root(&cache, identity).is_ok());
assert!(
ensure_runner_cache_root(
&cache,
ProcessIdentity {
uid: 1001,
gid: 1001
}
)
.is_err()
);
Ok(())
}
#[cfg(unix)]
#[test]
fn runner_secret_is_root_owned_and_bound_to_the_runner_group() -> Result<()> {
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o700))?;
let secret = temp.path().join("token");
fs::write(&secret, b"secret")?;
let secret_c = std::ffi::CString::new(secret.as_os_str().as_bytes())?;
ensure!(unsafe { libc::lchown(secret_c.as_ptr(), 0, 1000) } == 0);
fs::set_permissions(&secret, fs::Permissions::from_mode(0o440))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
validate_runner_secret(&secret, identity)?;
fs::set_permissions(&secret, fs::Permissions::from_mode(0o444))?;
assert!(validate_runner_secret(&secret, identity).is_err());
Ok(())
}
#[cfg(unix)]
#[test]
fn exact_git_blobs_become_root_owned_immutable_source_without_git_metadata() -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
let temp = tempfile::tempdir()?;
let repository = temp.path().join("repository");
fs::create_dir(&repository)?;
git_at(&repository, &["init", "--initial-branch=main"])?;
git_at(&repository, &["config", "user.name", "Idunn Test"])?;
git_at(
&repository,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(repository.join("deployment.toml"), b"target = 'test'\n")?;
let script = repository.join("build.sh");
fs::write(&script, b"#!/bin/sh\nexit 0\n")?;
fs::set_permissions(&script, fs::Permissions::from_mode(0o755))?;
fs::create_dir(repository.join("links"))?;
symlink("../deployment.toml", repository.join("links/deployment"))?;
git_at(&repository, &["add", "--all"])?;
git_at(&repository, &["commit", "-m", "fixture"])?;
let revision = git_at(&repository, &["rev-parse", "HEAD"])?;
let parent = temp.path().join("transaction");
fs::create_dir(&parent)?;
fs::set_permissions(&parent, fs::Permissions::from_mode(0o700))?;
let destination = parent.join("source");
prepare_frozen_source_destination(&destination)?;
let driver = GitSourceDriver::new(
temp.path().join("source-cache"),
temp.path().join("frozen-source"),
None,
);
let mut created_dirs = std::collections::HashSet::new();
created_dirs.insert(destination.clone());
driver.materialize_tree_raw(&repository, &revision, &destination, &destination, &mut created_dirs)?;
driver.materialize_tree_raw(
&repository,
&revision,
&destination.join("vendor/fixture"),
&destination,
&mut created_dirs,
)?;
harden_frozen_source(&destination)?;
assert!(!destination.join(".git").exists());
assert_eq!(
fs::symlink_metadata(destination.join("deployment.toml"))?
.permissions()
.mode()
& 0o777,
0o444
);
let script_metadata = fs::symlink_metadata(destination.join("build.sh"))?;
assert_eq!(script_metadata.uid(), 0);
assert_eq!(script_metadata.permissions().mode() & 0o777, 0o555);
assert!(destination.join("vendor/fixture/deployment.toml").is_file());
validate_frozen_source_symlink(&destination, &destination.join("links/deployment"))?;
validate_frozen_source(&destination)?;
let digest = frozen_source_sha256(&destination)?;
let recipe = destination.join("deployment.toml");
fs::set_permissions(&recipe, fs::Permissions::from_mode(0o644))?;
assert!(validate_frozen_source(&destination).is_err());
fs::set_permissions(&recipe, fs::Permissions::from_mode(0o444))?;
fs::write(&recipe, b"target = 'changed'\n")?;
assert_ne!(frozen_source_sha256(&destination)?, digest);
Ok(())
}
#[cfg(unix)]
#[test]
fn frozen_source_rejects_a_symlink_that_escapes_the_transaction_tree() -> Result<()> {
use std::os::unix::fs::{PermissionsExt, symlink};
let temp = tempfile::tempdir()?;
let parent = temp.path().join("transaction");
fs::create_dir(&parent)?;
fs::set_permissions(&parent, fs::Permissions::from_mode(0o700))?;
let destination = parent.join("source");
prepare_frozen_source_destination(&destination)?;
symlink("../../outside", destination.join("escape"))?;
// S6: `harden_frozen_source` no longer resolves symlink chains --
// only `target.is_relative()` -- because the chain must be judged
// against the tree's real, final published path, which this
// temporary destination already is (no `.partial` rename is
// involved here). `validate_frozen_source_symlinks` is what now
// owns the escape check.
harden_frozen_source(&destination)?;
assert!(validate_frozen_source_symlinks(&destination).is_err());
Ok(())
}
// ---------------------------------------------------------------------
// S5 (Self's ruling, third Cut 1 fix batch): a behavioural test for each
// guard Soul's third pass found unpinned, through `freeze_exact` or its
// own primitives wherever the guard is reachable from there.
// ---------------------------------------------------------------------
/// S5, root ownership: `harden_frozen_source` refuses any entry not
/// owned by root, not only the top-level directory. A file left behind
/// by a step that ran as the unprivileged Git identity, and never
/// re-owned, must be refused rather than silently hardened.
#[cfg(unix)]
#[test]
fn harden_frozen_source_refuses_a_non_root_owned_entry() -> Result<()> {
let temp = tempfile::tempdir()?;
let root = temp.path().join("root");
fs::create_dir(&root)?;
fs::write(root.join("f"), b"x")?;
ensure!(
Command::new("/bin/chown")
.args(["1000:1000"])
.arg(root.join("f"))
.status()?
.success(),
"chowning the fixture file"
);
assert!(
harden_frozen_source(&root).is_err(),
"a non-root-owned entry must be refused"
);
Ok(())
}
/// S5, exact duplicate path: two raw tree entries that literally share
/// one leaf path (not merely alias each other through a directory) must
/// be refused before anything is written. `git ls-tree -r` emits the
/// path twice; only `git_tree_entries`'s own "emits a path twice" check
/// -- not fsck, which is content-agnostic about which of the two blobs a
/// reader would see -- stands between this tree and a freeze that
/// silently picks one of the two blobs.
#[cfg(unix)]
#[test]
fn freeze_exact_refuses_an_exact_duplicate_leaf_path() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "seed"])?;
let hash_object = |content: &[u8]| -> Result<String> {
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&origin_repo)
.args(["hash-object", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(content)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object failed");
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let recipe = hash_object(b"target = 'test'\n")?;
let a = hash_object(b"a\n")?;
let b = hash_object(b"b\n")?;
let hex = |s: &str| -> Vec<u8> {
(0..20)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
.collect()
};
let mut raw = Vec::new();
for (mode, name, sha) in [
("100644", "deployment.toml", &recipe),
("100644", "same.txt", &a),
("100644", "same.txt", &b),
] {
raw.extend_from_slice(format!("{mode} {name}\0").as_bytes());
raw.extend(hex(sha));
}
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&origin_repo)
.args(["hash-object", "-t", "tree", "--literally", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(&raw)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object --literally failed");
let tree = String::from_utf8(output.stdout)?.trim().to_owned();
let git_owned = |args: &[&str]| -> Result<String> {
let output = Command::new("git")
.args([
"-c", "safe.directory=*",
"-c", "user.name=Idunn Test",
"-c", "user.email=idunn-test@example.invalid",
"-C",
])
.arg(&origin_repo)
.args(args)
.output()?;
ensure!(output.status.success(), "git {:?} failed: {}", args, String::from_utf8_lossy(&output.stderr));
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let commit = git_owned(&["commit-tree", &tree, "-m", "dup-leaf"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: format!("file://{}", origin_repo.display()),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let result = driver.freeze_exact(&source, &commit, "txn-dup-leaf", &frozen_source_root);
assert!(
result.is_err(),
"a raw tree with the same leaf path recorded twice must be refused"
);
Ok(())
}
/// S5, `ensure_frozen_directory` containment: its own top-of-function
/// check that `path` starts with `root` must be pinned directly, not
/// only observed indirectly through a symlink escape. A path that is
/// simply outside the root -- no symlink involved at all -- must be
/// refused, and nothing may be created along the way.
#[cfg(unix)]
#[test]
fn ensure_frozen_directory_refuses_a_path_outside_its_root() -> Result<()> {
let temp = tempfile::tempdir()?;
let root = temp.path().join("root");
fs::create_dir(&root)?;
let outside = temp.path().join("outside-dir");
let mut created_dirs = std::collections::HashSet::new();
created_dirs.insert(root.clone());
let result = ensure_frozen_directory(&root, &mut created_dirs, &outside);
assert!(result.is_err(), "a path outside the root must be refused");
assert!(!outside.exists(), "nothing outside the root may be created");
Ok(())
}
/// S5, the writer's own layer: pass 1's creation call is `fs::create_dir`,
/// which errors whenever something is already at the path, never
/// `fs::create_dir_all`, which treats an already-existing directory as
/// success. A second, independent `created_dirs` bookkeeping set --
/// exactly what a later pass or a second freeze would carry -- must
/// still refuse to reuse a directory an earlier call already made,
/// rather than silently accept it as already there.
#[cfg(unix)]
#[test]
fn ensure_frozen_directory_does_not_silently_reuse_a_directory_it_did_not_track() -> Result<()> {
let temp = tempfile::tempdir()?;
let root = temp.path().join("root");
fs::create_dir(&root)?;
let target = root.join("fresh");
let mut created_dirs = std::collections::HashSet::new();
created_dirs.insert(root.clone());
ensure_frozen_directory(&root, &mut created_dirs, &target)?;
assert!(target.is_dir());
let mut other_created_dirs = std::collections::HashSet::new();
other_created_dirs.insert(root.clone());
assert!(
ensure_frozen_directory(&root, &mut other_created_dirs, &target).is_err(),
"a directory this call did not itself create in this pass must be refused, not silently reused"
);
Ok(())
}
/// S5, `copy_artifact` containment: a source reached only by resolving a
/// symlink that leaves the artifact root must be refused before anything
/// is read from it or copied.
#[cfg(unix)]
#[test]
fn copy_artifact_refuses_a_source_that_escapes_its_root() -> Result<()> {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir()?;
let root = temp.path().join("root");
fs::create_dir(&root)?;
let outside = temp.path().join("outside.txt");
fs::write(&outside, b"secret")?;
let link = root.join("escape");
symlink(&outside, &link)?;
let destination = temp.path().join("dest.txt");
let result = copy_artifact(&root, &link, &destination);
assert!(
result.is_err(),
"an artifact source reached only by escaping the root must be refused"
);
assert!(!destination.exists(), "nothing may be copied on refusal");
Ok(())
}
// ---------------------------------------------------------------------
// S6 (Self's ruling, third Cut 1 fix batch): symlinks are judged against
// the tree's real, final published path, not `.partial`.
// ---------------------------------------------------------------------
/// A symlink whose target does not exist anywhere, but whose target path
/// stays inside the root once every real component along the way is
/// resolved, must freeze -- not be refused merely for being dangling.
#[cfg(unix)]
#[test]
fn freeze_exact_accepts_a_dangling_symlink_whose_target_stays_inside_the_root() -> Result<()> {
use std::os::unix::fs::{PermissionsExt, symlink};
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
symlink("not-there.txt", origin_repo.join("dangling"))?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: format!("file://{}", origin_repo.display()),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let (frozen_root, ..) =
driver.freeze_exact(&source, &revision, "txn-dangling", &frozen_source_root)?;
assert!(
fs::symlink_metadata(frozen_root.join("dangling"))?
.file_type()
.is_symlink(),
"a dangling in-root symlink must freeze, not be refused"
);
// Consistency with observe-time, the other half of S6's fix.
validate_frozen_source(&frozen_root)?;
Ok(())
}
/// The published-location regression Soul found: a relative symlink
/// naming `.partial`, the temporary directory the tree is built under
/// before the atomic rename that publishes it. Judged against `.partial`
/// it resolves back inside (a coincidence of the temporary name), but
/// judged against the tree's real, final name -- what every later reader
/// actually opens -- it resolves outside the root and must be refused at
/// freeze time, not accepted then found broken later by `observe_frozen`.
#[cfg(unix)]
#[test]
fn freeze_exact_refuses_a_symlink_that_only_resolves_inside_the_partial_name() -> Result<()> {
use std::os::unix::fs::{PermissionsExt, symlink};
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(
&origin_repo,
&["config", "user.email", "idunn-test@example.invalid"],
)?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
symlink("../.partial/deployment.toml", origin_repo.join("pl"))?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "fixture"])?;
let revision = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: format!("file://{}", origin_repo.display()),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
let result = driver.freeze_exact(&source, &revision, "txn-partial-name", &frozen_source_root);
assert!(
result.is_err(),
"a symlink that resolves inside the root only under the temporary `.partial` name, and outside it under the real published name, must be refused"
);
Ok(())
}
// ---------------------------------------------------------------------
// S1 (Self's ruling, third Cut 1 fix batch): fsck guards the fetch in
// `resolve()` as well as the one in `freeze_exact`, and `freeze_exact`
// also fscks the selected tree so that objects already local get
// checked too.
//
// Discrepancy: `OperatorBinding::validate()` (`deployment.rs:1027-1040`)
// requires `repository.origin` to start with `https://`, unconditionally
// -- `resolve()` calls it first thing, and `freeze()` calls it through
// `ResolvedSource::validate_against`. No test in this suite stands up a
// real HTTPS Git origin (every existing fixture uses `file://`), and
// adding one is out of this fix batch's scope. This fixture instead
// reproduces `resolve()`'s exact fetch invocation -- literally the same
// arguments `resolve()` passes to `self.git(...)`, this fix included --
// against the checkout, the same approximation Soul's own probe used
// (`resolve_like_fetch` in `probe4_soul3.rs`), followed by the real,
// unmodified `freeze_exact`. That is "resolve() then freeze()" in every
// way this suite can exercise without a live HTTPS remote: it proves the
// fetch line S1 changed, not a hand-built substitute for it, refuses the
// hostile object, and that `freeze_exact`'s own explicit fsck refuses it
// independently even if the fetch line's guard were ever weakened.
#[cfg(unix)]
#[test]
fn resolve_style_fetch_then_freeze_exact_refuses_hostile_trees() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
fn hash_object(repo: &Path, content: &[u8]) -> Result<String> {
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(repo)
.args(["hash-object", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(content)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object failed");
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}
fn literal_tree(repo: &Path, entries: &[(&str, &str, &str)]) -> Result<String> {
let hex = |s: &str| -> Vec<u8> {
(0..20)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
.collect()
};
let mut raw = Vec::new();
for (mode, name, sha) in entries {
raw.extend_from_slice(format!("{mode} {name}\0").as_bytes());
raw.extend(hex(sha));
}
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(repo)
.args(["hash-object", "-t", "tree", "--literally", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(&raw)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object --literally failed");
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}
fn dotgit_lookalike(repo: &Path, recipe: &str, name: &str) -> Result<Vec<(String, String, String)>> {
let cfg = hash_object(repo, b"[core]\n\tfsmonitor = touch /tmp/soul3-pwned\n")?;
let inner = literal_tree(repo, &[("100644", "config", &cfg)])?;
let mut entries = vec![
("100644".to_owned(), "deployment.toml".to_owned(), recipe.to_owned()),
("40000".to_owned(), name.to_owned(), inner),
];
entries.sort_by(|a, b| a.1.cmp(&b.1));
Ok(entries)
}
fn chown_to_source_identity(repo: &Path) -> Result<()> {
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
Ok(())
}
#[allow(clippy::type_complexity)]
let fixtures: Vec<(&str, Box<dyn Fn(&Path, &str) -> Result<Vec<(String, String, String)>>>)> = vec![
(
"dup-trees",
Box::new(|repo: &Path, recipe: &str| {
let x = hash_object(repo, b"x\n")?;
let y = hash_object(repo, b"y\n")?;
let tx = literal_tree(repo, &[("100644", "x", &x)])?;
let ty = literal_tree(repo, &[("100644", "y", &y)])?;
Ok(vec![
("40000".into(), "d".into(), tx),
("40000".into(), "d".into(), ty),
("100644".into(), "deployment.toml".into(), recipe.into()),
])
}) as Box<dyn Fn(&Path, &str) -> Result<Vec<(String, String, String)>>>,
),
("dotGIT", Box::new(|repo: &Path, recipe: &str| dotgit_lookalike(repo, recipe, ".GIT"))),
("dotGit", Box::new(|repo: &Path, recipe: &str| dotgit_lookalike(repo, recipe, ".Git"))),
("git-short-name", Box::new(|repo: &Path, recipe: &str| dotgit_lookalike(repo, recipe, "git~1"))),
("dotgit-trailing-dot", Box::new(|repo: &Path, recipe: &str| dotgit_lookalike(repo, recipe, ".git."))),
("dotgit-zwnj", Box::new(|repo: &Path, recipe: &str| dotgit_lookalike(repo, recipe, ".git\u{200c}"))),
("dotgit-trailing-space", Box::new(|repo: &Path, recipe: &str| dotgit_lookalike(repo, recipe, ".git "))),
];
for (label, build) in fixtures {
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(&origin_repo, &["config", "user.email", "idunn-test@example.invalid"])?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "root"])?;
let root_commit = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
let recipe_blob = git_at(&origin_repo, &["rev-parse", "HEAD:deployment.toml"])?;
chown_to_source_identity(&origin_repo)?;
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity { uid: 1000, gid: 1000 };
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: format!("file://{}", origin_repo.display()),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
// The checkout is established on the good root commit first,
// exactly as a real deploy target's checkout already exists by
// the time a later push lands: this is what makes the objects
// this fixture cares about arrive only through the *next*
// fetch, not the initial clone.
driver.prepare_source_root(&source)?;
let entries = build(&origin_repo, &recipe_blob)
.with_context(|| format!("building the {label} fixture"))?;
let entry_refs: Vec<(&str, &str, &str)> = entries
.iter()
.map(|(mode, name, sha)| (mode.as_str(), name.as_str(), sha.as_str()))
.collect();
let hostile_tree = literal_tree(&origin_repo, &entry_refs)?;
// Not `git_at`: the repository is already chowned to the
// unprivileged identity above, and this process is root, so
// plain `git` here refuses it as "dubious ownership" without
// `safe.directory` (the same reason `freeze_exact_refuses_a_
// duplicate_named_tree...` above uses its own `git_owned`).
let git_owned = |args: &[&str]| -> Result<String> {
let output = Command::new("git")
.args([
"-c", "safe.directory=*",
"-c", "user.name=Idunn Test",
"-c", "user.email=idunn-test@example.invalid",
"-C",
])
.arg(&origin_repo)
.args(args)
.output()?;
ensure!(output.status.success(), "git {:?} failed: {}", args, String::from_utf8_lossy(&output.stderr));
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let hostile_commit = git_owned(&["commit-tree", &hostile_tree, "-p", &root_commit, "-m", "hostile"])?;
git_owned(&["update-ref", "refs/heads/main", &hostile_commit])?;
// The exact fetch `resolve()` runs (`drivers.rs`, `fn resolve`),
// S1's fix included: `-c transfer.fsckObjects=true`, `--force`,
// `--no-tags`, fetching `refs/heads/main` into a resolution ref.
let resolve_style_fetch = driver.git([
OsString::from("-c"),
OsString::from("transfer.fsckObjects=true"),
OsString::from("-C"),
source.checkout.as_os_str().to_owned(),
OsString::from("fetch"),
OsString::from("--force"),
OsString::from("--no-tags"),
OsString::from("origin"),
OsString::from("+refs/heads/main:refs/idunn/resolutions/s1-fixture/r1"),
]);
match resolve_style_fetch {
Err(_) => {
// Refused at the fetch itself: the strongest outcome.
}
Ok(_) => {
// The fetch admitted the ref; `freeze_exact`'s own
// explicit fsck (defense in depth, for objects a fetch
// never re-checks) must still refuse it.
let result =
driver.freeze_exact(&source, &hostile_commit, "txn-hostile", &frozen_source_root);
assert!(
result.is_err(),
"{label}: a hostile tree that a resolve()-style fetch admitted must still be refused by freeze_exact's own fsck"
);
}
}
}
Ok(())
}
// -----------------------------------------------------------------
// S4-1 (Self's ruling, fourth Cut 1 fix batch): the symlink resolver
// used to recurse per path component and cost 2^n real filesystem
// resolutions for an n-link self-doubling chain. Rewritten iteratively
// with memoised per-symlink resolution and a single shared traversal
// budget.
// -----------------------------------------------------------------
/// Soul's `a0 -> .`, `ak -> a{k-1}/a{k-1}` fixture at n=20 took 43.7 s
/// under the old recursive resolver. Every `ak` ultimately resolves to
/// the frozen root itself (never dangling) and nothing in the chain
/// ever leaves the root (never escaping), so per S6 -- "a dangling
/// in-root link freezes, an escape refuses" -- this chain must FREEZE
/// (accept), and the memoised iterative walk must do it in under a
/// second.
#[cfg(unix)]
#[test]
fn validate_frozen_source_symlinks_resolves_an_n20_doubling_chain_in_under_a_second() -> Result<()>
{
use std::os::unix::fs::symlink;
use std::time::Instant;
let temp = tempfile::tempdir()?;
let root = temp.path().join("root");
fs::create_dir(&root)?;
symlink(".", root.join("a0"))?;
for k in 1..=20usize {
symlink(format!("a{}/a{}", k - 1, k - 1), root.join(format!("a{k}")))?;
}
let start = Instant::now();
let result = validate_frozen_source_symlinks(&root);
let elapsed = start.elapsed();
assert!(
result.is_ok(),
"an n=20 self-doubling chain that never dangles or escapes the root must freeze, \
per S6, not refuse: {result:?}"
);
assert!(
elapsed.as_secs() < 1,
"an n=20 chain must resolve in under a second with a memoised iterative walk, took {elapsed:?}"
);
Ok(())
}
/// S4-5 (Self's ruling, fourth Cut 1 fix batch): every existing S6 test
/// naming `..` is a refusal (an escape). `cargo-mutants` found that
/// deleting the `Component::ParentDir` match arm entirely -- so it falls
/// through to the `_ => bail!` catch-all -- still leaves the suite
/// green, because nothing proves a `..` that stays inside the root once
/// resolved is actually accepted, not merely never exercised.
#[cfg(unix)]
#[test]
fn validate_frozen_source_symlink_accepts_a_legitimate_in_root_parent_reference() -> Result<()> {
use std::os::unix::fs::symlink;
let temp = tempfile::tempdir()?;
let root = temp.path().join("root");
fs::create_dir(&root)?;
fs::write(root.join("deployment.toml"), b"target = 'test'\n")?;
fs::create_dir_all(root.join("sub/deep"))?;
let link = root.join("sub/deep/l");
symlink("../../deployment.toml", &link)?;
validate_frozen_source_symlink(&root, &link)?;
let canonical_root = root.canonicalize()?;
let resolved = resolve_frozen_source_symlink(&canonical_root, &link)?;
assert_eq!(
resolved,
canonical_root.join("deployment.toml"),
"a `..` chain that stays inside the root must resolve to the real in-root target"
);
Ok(())
}
// -----------------------------------------------------------------
// S4-2 (Self's ruling, fourth Cut 1 fix batch): the S9 streamed hash had
// no behavioural pin. Deleting the read buffer's size (hashing zero
// bytes of every file) or flipping the loop's exit test (hashing only
// the first 64 KiB) both left the suite green.
// -----------------------------------------------------------------
/// Two trees whose only difference is the trailing byte past the first
/// 64 KiB chunk must hash differently: this kills both the
/// zero-length-buffer mutant (hashes nothing, so the two digests
/// collide) and the first-chunk-only mutant (stops after the first
/// read, so the differing tail is never hashed).
#[cfg(unix)]
#[test]
fn frozen_source_sha256_differs_when_only_content_past_the_first_chunk_differs() -> Result<()> {
fn build(temp: &Path, byte: u8) -> Result<PathBuf> {
let root = temp.join(format!("root-{byte}"));
fs::create_dir(&root)?;
// One 64 KiB chunk of identical bytes, plus a tail that only
// this build's caller-chosen byte ever touches.
let mut content = vec![b'a'; (1 << 16) + 4096];
*content.last_mut().expect("content is non-empty") = byte;
fs::write(root.join("big.bin"), &content)?;
Ok(root)
}
let temp = tempfile::tempdir()?;
let root_a = build(temp.path(), b'A')?;
let root_b = build(temp.path(), b'B')?;
let digest_a = frozen_source_sha256(&root_a)?;
let digest_b = frozen_source_sha256(&root_b)?;
assert_ne!(
digest_a, digest_b,
"two trees differing only past the first 64 KiB chunk must hash differently"
);
Ok(())
}
// -----------------------------------------------------------------
// S4-3 (Self's ruling, fourth Cut 1 fix batch): three of the five S5
// tests did not kill their own guard, because another refusal fired
// first along the same call path. Each of these isolates its guard so
// that only deleting *that* guard changes the outcome.
// -----------------------------------------------------------------
/// S4-3, the duplicate-path guard: `git_tree_entries`'s own
/// `entries.windows(2).all(|pair| pair[0].path != pair[1].path)` sits
/// behind `freeze_exact`'s explicit `fsck --strict`, whose
/// `duplicateEntries` check refuses the identical raw tree first --
/// which is why the shipped `freeze_exact_refuses_an_exact_duplicate_
/// leaf_path` test survives deleting this guard. Calling
/// `git_tree_entries` directly, the one call site that runs before any
/// fsck exists, leaves only this guard able to refuse it.
#[cfg(unix)]
#[test]
fn git_tree_entries_refuses_a_tree_that_emits_a_path_twice() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
fn hash_object(repo: &Path, content: &[u8]) -> Result<String> {
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(repo)
.args(["hash-object", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(content)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object failed");
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}
fn literal_tree(repo: &Path, entries: &[(&str, &str, &str)]) -> Result<String> {
let hex = |s: &str| -> Vec<u8> {
(0..20)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
.collect()
};
let mut raw = Vec::new();
for (mode, name, sha) in entries {
raw.extend_from_slice(format!("{mode} {name}\0").as_bytes());
raw.extend(hex(sha));
}
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(repo)
.args(["hash-object", "-t", "tree", "--literally", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(&raw)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object --literally failed");
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(&origin_repo, &["config", "user.email", "idunn-test@example.invalid"])?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "root"])?;
let root_commit = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
let recipe_blob = git_at(&origin_repo, &["rev-parse", "HEAD:deployment.toml"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: format!("file://{}", origin_repo.display()),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
// Established on the clean root commit first, exactly like a real
// deploy target's checkout by the time a hostile push lands.
driver.prepare_source_root(&source)?;
// Two literal blob entries sharing the leaf path "same.txt": exactly
// what the windows(2) check pins, and exactly the shape
// `freeze_exact_refuses_an_exact_duplicate_leaf_path` uses. Two
// same-named TREE entries with different children would not do: Git
// `ls-tree -r` flattens into that tree, so it never emits one leaf
// path twice -- it would just list both trees' distinct contents.
let a = hash_object(&origin_repo, b"a\n")?;
let b = hash_object(&origin_repo, b"b\n")?;
let hostile_tree = literal_tree(
&origin_repo,
&[
("100644", "deployment.toml", &recipe_blob),
("100644", "same.txt", &a),
("100644", "same.txt", &b),
],
)?;
let git_owned = |args: &[&str]| -> Result<String> {
let output = Command::new("git")
.args([
"-c", "safe.directory=*",
"-c", "user.name=Idunn Test",
"-c", "user.email=idunn-test@example.invalid",
"-C",
])
.arg(&origin_repo)
.args(args)
.output()?;
ensure!(
output.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let hostile_commit =
git_owned(&["commit-tree", &hostile_tree, "-p", &root_commit, "-m", "hostile"])?;
git_owned(&["update-ref", "refs/heads/main", &hostile_commit])?;
// The hostile objects land already-local through an UNGUARDED fetch
// (no `transfer.fsckObjects=true`), exactly like Soul's probe A: no
// fsck of any kind has touched them by the time `git_tree_entries`
// runs.
driver.git([
OsString::from("-C"),
source.checkout.as_os_str().to_owned(),
OsString::from("fetch"),
OsString::from("--force"),
OsString::from("--no-tags"),
OsString::from("origin"),
OsString::from("+refs/heads/main:refs/idunn/resolutions/s4-3-x06/r1"),
])?;
let result = driver.git_tree_entries(&source.checkout, &hostile_commit);
assert!(
result.is_err(),
"a raw tree that emits the same path twice must be refused by git_tree_entries \
itself, with no fsck anywhere in this call path"
);
Ok(())
}
/// S4-3, `ensure_frozen_directory`'s own containment check
/// (`path.starts_with(root)`): the existing test only reaches a path
/// whose full ancestor chain terminates at a real, untracked directory
/// (the temp dir), so deleting the check still bails on "an existing
/// directory this freeze did not create" -- a different guard catches
/// it, and the mutant survives. Pre-seeding `created_dirs` with a real
/// directory outside the root -- exactly what a second, independent
/// bookkeeping pass could look like -- lets the walk short-circuit past
/// that other guard, so only the containment check stands between this
/// call and actually creating a directory outside the frozen root.
#[cfg(unix)]
#[test]
fn ensure_frozen_directory_containment_guard_is_load_bearing_on_its_own() -> Result<()> {
let temp = tempfile::tempdir()?;
let root = temp.path().join("root");
fs::create_dir(&root)?;
let outside_parent = temp.path().join("outside");
fs::create_dir(&outside_parent)?;
let target = outside_parent.join("child");
let mut created_dirs = std::collections::HashSet::new();
created_dirs.insert(root.clone());
created_dirs.insert(outside_parent.clone());
let result = ensure_frozen_directory(&root, &mut created_dirs, &target);
assert!(
result.is_err(),
"a path outside the root must be refused even when its parent is already tracked"
);
assert!(!target.exists(), "nothing outside the root may be created");
Ok(())
}
/// S4-3, the writer's creation call: pass 1 must use `fs::create_dir`,
/// which requires its immediate parent to already exist, never
/// `fs::create_dir_all`, which silently manufactures every missing
/// ancestor. The existing test only reaches a path whose parent
/// `ensure_frozen_directory` itself just created moments earlier, so the
/// two calls behave identically there. Pre-seeding `created_dirs` with a
/// parent bookkeeping claims already exists but that was never actually
/// created on disk makes the two calls diverge: `create_dir` must fail
/// closed; `create_dir_all` would silently manufacture it and
/// everything under it.
#[cfg(unix)]
#[test]
fn ensure_frozen_directory_create_call_does_not_silently_backfill_a_missing_parent() -> Result<()>
{
let temp = tempfile::tempdir()?;
let root = temp.path().join("root");
fs::create_dir(&root)?;
let phantom_parent = root.join("phantom");
let target = phantom_parent.join("child");
let mut created_dirs = std::collections::HashSet::new();
created_dirs.insert(root.clone());
// Bookkeeping lies: claims this parent already exists, but nothing
// ever created it on disk.
created_dirs.insert(phantom_parent.clone());
let result = ensure_frozen_directory(&root, &mut created_dirs, &target);
assert!(
result.is_err(),
"creating a directory whose tracked parent does not actually exist on disk must \
fail closed, not silently backfill it"
);
assert!(
!phantom_parent.exists(),
"a missing tracked parent must not be silently created"
);
Ok(())
}
// -----------------------------------------------------------------
// S4-4 (Self's ruling, fourth Cut 1 fix batch): all three S1 fsck
// guards were unpinned. `freeze_exact`'s explicit fsck, `resolve()`'s
// fetch fsck flag, and `freeze_exact`'s own fetch fsck flag each need a
// fixture where deleting exactly that guard -- and no other -- changes
// the outcome.
// -----------------------------------------------------------------
/// Builds an origin repository with a clean root commit, then a second,
/// hostile commit on top of it, without ever touching the driver.
/// Mirrors Soul's probe A fixtures exactly: dup-trees, six `.git`
/// look-alikes, an unsorted tree, mode 120000 on a tree, mode 040000 on
/// a blob, and a zero-padded mode.
#[cfg(unix)]
#[allow(clippy::type_complexity)]
fn s4_4_hostile_fixtures()
-> Vec<(&'static str, Box<dyn Fn(&Path, &str) -> Result<Vec<(String, String, String)>>>)> {
fn hash_object(repo: &Path, content: &[u8]) -> Result<String> {
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(repo)
.args(["hash-object", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(content)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object failed");
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}
fn literal_tree(repo: &Path, entries: &[(&str, &str, &str)]) -> Result<String> {
let hex = |s: &str| -> Vec<u8> {
(0..20)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
.collect()
};
let mut raw = Vec::new();
for (mode, name, sha) in entries {
raw.extend_from_slice(format!("{mode} {name}\0").as_bytes());
raw.extend(hex(sha));
}
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(repo)
.args(["hash-object", "-t", "tree", "--literally", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(&raw)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object --literally failed");
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
}
fn dotgit_lookalike(repo: &Path, recipe: &str, name: &str) -> Result<Vec<(String, String, String)>> {
let cfg = hash_object(repo, b"[core]\n\tfsmonitor = touch /tmp/s4-4-pwned\n")?;
let inner = literal_tree(repo, &[("100644", "config", &cfg)])?;
let mut entries = vec![
("100644".to_owned(), "deployment.toml".to_owned(), recipe.to_owned()),
("40000".to_owned(), name.to_owned(), inner),
];
entries.sort_by(|a, b| a.1.cmp(&b.1));
Ok(entries)
}
vec![
(
"dup-trees",
Box::new(|repo: &Path, recipe: &str| {
let x = hash_object(repo, b"x\n")?;
let y = hash_object(repo, b"y\n")?;
let tx = literal_tree(repo, &[("100644", "x", &x)])?;
let ty = literal_tree(repo, &[("100644", "y", &y)])?;
Ok(vec![
("40000".into(), "d".into(), tx),
("40000".into(), "d".into(), ty),
("100644".into(), "deployment.toml".into(), recipe.into()),
])
}) as Box<dyn Fn(&Path, &str) -> Result<Vec<(String, String, String)>>>,
),
(".GIT", Box::new(|r: &Path, c: &str| dotgit_lookalike(r, c, ".GIT"))),
(".Git", Box::new(|r: &Path, c: &str| dotgit_lookalike(r, c, ".Git"))),
("git~1", Box::new(|r: &Path, c: &str| dotgit_lookalike(r, c, "git~1"))),
(".git.", Box::new(|r: &Path, c: &str| dotgit_lookalike(r, c, ".git."))),
(".git-zwnj", Box::new(|r: &Path, c: &str| dotgit_lookalike(r, c, ".git\u{200c}"))),
(".git-space", Box::new(|r: &Path, c: &str| dotgit_lookalike(r, c, ".git "))),
(
"unsorted-tree",
Box::new(|repo: &Path, recipe: &str| {
let z = hash_object(repo, b"z\n")?;
let a = hash_object(repo, b"a\n")?;
Ok(vec![
("100644".into(), "z.txt".into(), z),
("100644".into(), "a.txt".into(), a),
("100644".into(), "deployment.toml".into(), recipe.into()),
])
}),
),
(
"symlink-mode-on-tree",
Box::new(|repo: &Path, recipe: &str| {
let blob = hash_object(repo, b"inner\n")?;
let inner = literal_tree(repo, &[("100644", "f", &blob)])?;
let mut entries = vec![
("100644".to_owned(), "deployment.toml".to_owned(), recipe.to_owned()),
("120000".to_owned(), "lnk".to_owned(), inner),
];
entries.sort_by(|a, b| a.1.cmp(&b.1));
Ok(entries)
}),
),
(
"dir-mode-on-blob",
Box::new(|repo: &Path, recipe: &str| {
let blob = hash_object(repo, b"notatree\n")?;
let mut entries = vec![
("100644".to_owned(), "deployment.toml".to_owned(), recipe.to_owned()),
("40000".to_owned(), "d".to_owned(), blob),
];
entries.sort_by(|a, b| a.1.cmp(&b.1));
Ok(entries)
}),
),
(
"mode-0100644-zero-padded",
Box::new(|repo: &Path, recipe: &str| {
let blob = hash_object(repo, b"pad\n")?;
let mut entries = vec![
("100644".to_owned(), "deployment.toml".to_owned(), recipe.to_owned()),
("0100644".to_owned(), "padded.txt".to_owned(), blob),
];
entries.sort_by(|a, b| a.1.cmp(&b.1));
Ok(entries)
}),
),
]
}
/// S4-4, `freeze_exact`'s explicit fsck (the guard S1 added because a
/// fetch by exact revision never re-transfers, and so never re-checks,
/// an object the checkout already holds). Each fixture's checkout is
/// established on the clean root commit first, then the hostile objects
/// are brought in with an UNGUARDED fetch -- already local, exactly as
/// Soul's probe A found them -- before `freeze_exact` itself runs.
/// Deleting the explicit fsck block must fail every one of these.
#[cfg(unix)]
#[test]
fn freeze_exact_refuses_already_local_hostile_objects_via_its_own_fsck() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut exercised = 0;
for (index, (label, build)) in s4_4_hostile_fixtures().into_iter().enumerate() {
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(&origin_repo, &["config", "user.email", "idunn-test@example.invalid"])?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "root"])?;
let root_commit = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
let recipe_blob = git_at(&origin_repo, &["rev-parse", "HEAD:deployment.toml"])?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: format!("file://{}", origin_repo.display()),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
// The checkout is established on the good root commit first, so
// the hostile objects only ever arrive through the next fetch,
// never the initial clone.
driver.prepare_source_root(&source)?;
let entries = build(&origin_repo, &recipe_blob)
.with_context(|| format!("building the {label} fixture"))?;
let entry_refs: Vec<(&str, &str, &str)> = entries
.iter()
.map(|(mode, name, sha)| (mode.as_str(), name.as_str(), sha.as_str()))
.collect();
let hex = |s: &str| -> Vec<u8> {
(0..20)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
.collect()
};
let mut raw = Vec::new();
for (mode, name, sha) in &entry_refs {
raw.extend_from_slice(format!("{mode} {name}\0").as_bytes());
raw.extend(hex(sha));
}
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&origin_repo)
.args(["hash-object", "-t", "tree", "--literally", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(&raw)?;
let tree_output = child.wait_with_output()?;
ensure!(tree_output.status.success(), "git hash-object --literally failed");
let hostile_tree = String::from_utf8(tree_output.stdout)?.trim().to_owned();
let git_owned = |args: &[&str]| -> Result<String> {
let output = Command::new("git")
.args([
"-c", "safe.directory=*",
"-c", "user.name=Idunn Test",
"-c", "user.email=idunn-test@example.invalid",
"-C",
])
.arg(&origin_repo)
.args(args)
.output()?;
ensure!(
output.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let hostile_commit =
git_owned(&["commit-tree", &hostile_tree, "-p", &root_commit, "-m", "hostile"])?;
git_owned(&["update-ref", "refs/heads/main", &hostile_commit])?;
// Unguarded fetch: no `transfer.fsckObjects=true`. Most of
// Soul's fixtures land already local this way, exactly like her
// probe A; a few (an object whose mode disagrees with its own
// type, e.g. mode 040000 naming a blob) make the server's own
// pack-objects refuse to serve them at all, guarded or not --
// Soul recorded this too ("refused earlier, at the fetch"). That
// is a stronger outcome than this test needs to prove, so it is
// treated the same as an already-local refusal, not a test
// failure: only a fixture that actually lands gets to exercise
// freeze_exact's own explicit fsck.
let landed = driver
.git([
OsString::from("-C"),
source.checkout.as_os_str().to_owned(),
OsString::from("fetch"),
OsString::from("--force"),
OsString::from("--no-tags"),
OsString::from("origin"),
OsString::from(format!("+refs/heads/main:refs/idunn/resolutions/s4-4a/r{index}")),
])
.is_ok();
if landed {
exercised += 1;
let result =
driver.freeze_exact(&source, &hostile_commit, "txn-s4-4a", &frozen_source_root);
assert!(
result.is_err(),
"{label}: already-local hostile objects must still be refused by \
freeze_exact's own explicit fsck"
);
}
}
assert!(
exercised > 0,
"at least one fixture must actually land already-local and exercise freeze_exact's \
own explicit fsck, or this test proves nothing"
);
Ok(())
}
/// S4-4, `freeze_exact`'s own fetch fsck flag (`-c
/// transfer.fsckObjects=true` on the fetch immediately before the
/// explicit fsck block). Here the hostile revision is fetched for the
/// first time by `freeze_exact` itself, so if this flag is what refuses
/// it, the error surfaces at the fetch -- distinct from the explicit
/// fsck's own error, which never gets a chance to run. Asserting on
/// which context wraps the error is what makes this guard, and not the
/// explicit fsck sitting right after it, the one this test kills.
#[cfg(unix)]
#[test]
fn freeze_exact_refuses_a_never_local_hostile_revision_at_its_own_fetch() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755))?;
let origin_repo = temp.path().join("origin");
fs::create_dir(&origin_repo)?;
git_at(&origin_repo, &["init", "--initial-branch=main"])?;
git_at(&origin_repo, &["config", "user.name", "Idunn Test"])?;
git_at(&origin_repo, &["config", "user.email", "idunn-test@example.invalid"])?;
fs::write(origin_repo.join("deployment.toml"), b"target = 'test'\n")?;
git_at(&origin_repo, &["add", "--all"])?;
git_at(&origin_repo, &["commit", "-m", "root"])?;
let root_commit = git_at(&origin_repo, &["rev-parse", "HEAD"])?;
let recipe_blob = git_at(&origin_repo, &["rev-parse", "HEAD:deployment.toml"])?;
let cfg = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&origin_repo)
.args(["hash-object", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.and_then(|mut child| {
child
.stdin
.take()
.unwrap()
.write_all(b"[core]\n\tfsmonitor = touch /tmp/s4-4c-pwned\n")?;
child.wait_with_output()
})?;
ensure!(cfg.status.success(), "git hash-object failed");
let cfg_blob = String::from_utf8(cfg.stdout)?.trim().to_owned();
let hex = |s: &str| -> Vec<u8> {
(0..20)
.map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap())
.collect()
};
let write_tree = |entries: &[(&str, &str, &str)]| -> Result<String> {
let mut raw = Vec::new();
for (mode, name, sha) in entries {
raw.extend_from_slice(format!("{mode} {name}\0").as_bytes());
raw.extend(hex(sha));
}
let mut child = Command::new("git")
.args(["-c", "safe.directory=*", "-C"])
.arg(&origin_repo)
.args(["hash-object", "-t", "tree", "--literally", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child.stdin.take().unwrap().write_all(&raw)?;
let output = child.wait_with_output()?;
ensure!(output.status.success(), "git hash-object --literally failed");
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let inner = write_tree(&[("100644", "config", &cfg_blob)])?;
let mut top_entries = vec![
("100644".to_owned(), "deployment.toml".to_owned(), recipe_blob.clone()),
("40000".to_owned(), ".git".to_owned(), inner),
];
top_entries.sort_by(|a, b| a.1.cmp(&b.1));
let top_refs: Vec<(&str, &str, &str)> = top_entries
.iter()
.map(|(m, n, s)| (m.as_str(), n.as_str(), s.as_str()))
.collect();
let hostile_tree = write_tree(&top_refs)?;
ensure!(
Command::new("/bin/chown")
.args(["-R", "1000:1000"])
.arg(&origin_repo)
.status()?
.success(),
"chowning the fixture origin repository"
);
let git_owned = |args: &[&str]| -> Result<String> {
let output = Command::new("git")
.args([
"-c", "safe.directory=*",
"-c", "user.name=Idunn Test",
"-c", "user.email=idunn-test@example.invalid",
"-C",
])
.arg(&origin_repo)
.args(args)
.output()?;
ensure!(
output.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let hostile_commit =
git_owned(&["commit-tree", &hostile_tree, "-p", &root_commit, "-m", "hostile"])?;
let source_cache_root = temp.path().join("source-cache");
let frozen_source_root = temp.path().join("frozen-source");
fs::create_dir(&frozen_source_root)?;
fs::set_permissions(&frozen_source_root, fs::Permissions::from_mode(0o700))?;
let identity = ProcessIdentity {
uid: 1000,
gid: 1000,
};
let driver = GitSourceDriver::new(
source_cache_root.clone(),
frozen_source_root.clone(),
Some(identity),
);
let source = ExactSource {
origin: format!("file://{}", origin_repo.display()),
checkout: source_cache_root.join("checkout"),
gitlinks: BTreeMap::new(),
recipe_path: PathBuf::from("deployment.toml"),
};
// A fresh checkout on the clean root commit -- `refs/heads/main`
// still points at it, not at the hostile commit -- established
// before the hostile commit is admitted anywhere. Only after this
// clone succeeds does `main` move, so the hostile commit has never
// been fetched by anything, guarded or not, and `freeze_exact`'s own
// fetch is the first thing to ever see it.
driver.prepare_source_root(&source)?;
git_owned(&["update-ref", "refs/heads/main", &hostile_commit])?;
let result =
driver.freeze_exact(&source, &hostile_commit, "txn-s4-4c", &frozen_source_root);
let error = result.expect_err("a never-local hostile revision must be refused");
let rendered = format!("{error:#}");
assert!(
rendered.contains("fetching the durable exact source revision"),
"expected the refusal to come from freeze_exact's own guarded fetch, got: {rendered}"
);
Ok(())
}
}