diff --git a/crates/rust-release-manifest/src/candidate.rs b/crates/rust-release-manifest/src/candidate.rs index 5bd77fa..742496b 100644 --- a/crates/rust-release-manifest/src/candidate.rs +++ b/crates/rust-release-manifest/src/candidate.rs @@ -157,6 +157,16 @@ pub struct ResolvedImages { pub proof_tar: ImageIdentity, } +impl ResolvedImages { + pub fn proof_images(&self) -> [(&'static str, &ImageIdentity); 3] { + [ + (PROOF_SPECS[0].id, &self.proof_debian), + (PROOF_SPECS[1].id, &self.proof_rpm), + (PROOF_SPECS[2].id, &self.proof_tar), + ] + } +} + pub fn resolve_release_images( processes: &ProcessEnvironment, engine: ContainerEngine, @@ -919,8 +929,13 @@ fn command_evidence(program: &str, args: &[&str]) -> Result { output.status ))); } + let field = if program == "rustc" && args == ["--version", "--verbose"] { + "lane rustc verbose" + } else { + program + }; normalize_command_evidence( - program, + field, String::from_utf8(output.stdout).map_err(display_error)?, ) } @@ -1404,16 +1419,8 @@ pub fn reconcile_lanes( return Err(Error::new(format!("lane reconciliation {field} mismatch"))); } } - let deb_tar = deb - .artifacts - .iter() - .find(|item| artifact_kind(&item.path, None).ok() == Some("tar")) - .ok_or_else(|| Error::new("lane tar mismatch: expected deb tar, actual missing"))?; - let rpm_tar = rpm - .artifacts - .iter() - .find(|item| artifact_kind(&item.path, None).ok() == Some("tar")) - .ok_or_else(|| Error::new("lane tar mismatch: expected rpm tar, actual missing"))?; + let deb_tar = artifact_by_kind(&deb.artifacts, "tar")?; + let rpm_tar = artifact_by_kind(&rpm.artifacts, "tar")?; if deb_tar != rpm_tar || fs::read(deb_root.join(&deb_tar.path)).map_err(display_error)? != fs::read(rpm_root.join(&rpm_tar.path)).map_err(display_error)? diff --git a/crates/rust-release-manifest/src/candidate_tests.rs b/crates/rust-release-manifest/src/candidate_tests.rs index d63fd14..cc6f3ff 100644 --- a/crates/rust-release-manifest/src/candidate_tests.rs +++ b/crates/rust-release-manifest/src/candidate_tests.rs @@ -421,6 +421,68 @@ fn release_image_policy_is_digest_only_strict_and_immutable() { fs::write(&policy, valid).unwrap(); } +#[test] +fn typed_release_authorities_match_parallel_serialized_contracts() { + let repo = fixture(); + let destination = tempfile::tempdir().unwrap(); + let context = export_immutable_context(&repo.root, destination.path()).unwrap(); + let policy = ReleaseImages::from_context(&context).unwrap(); + assert_eq!( + policy.proof_policies().map(|(id, _, image)| (id, image)), + [ + (PROOF_SPECS[0].id, policy.proof_debian.as_str()), + (PROOF_SPECS[1].id, policy.proof_rpm.as_str()), + (PROOF_SPECS[2].id, policy.proof_tar.as_str()), + ] + ); + let identity = |byte: char| ImageIdentity { + configured_reference: format!("sha256:{}", byte.to_string().repeat(64)), + digest: format!("sha256:{}", byte.to_string().repeat(64)), + }; + let resolved = ResolvedImages { + build_ubuntu: identity('a'), + build_fedora: identity('b'), + proof_debian: identity('c'), + proof_rpm: identity('d'), + proof_tar: identity('e'), + }; + assert_eq!( + resolved + .proof_images() + .map(|(id, image)| (id, image.digest.as_str())), + [ + (PROOF_SPECS[0].id, resolved.proof_debian.digest.as_str()), + (PROOF_SPECS[1].id, resolved.proof_rpm.digest.as_str()), + (PROOF_SPECS[2].id, resolved.proof_tar.digest.as_str()), + ] + ); + + for spec in PROOF_SPECS { + let name = artifact_name(spec.artifact_kind, "1.0.0").unwrap(); + assert_eq!( + artifact_kind(&name, Some("1.0.0")).unwrap(), + spec.artifact_kind + ); + } + + let tool_keys = crate::tests::tools().into_keys().collect::>(); + assert_eq!( + tool_keys, + TOOL_SPECS + .iter() + .map(|spec| spec.key.to_owned()) + .collect::>() + ); + assert_eq!( + TOOL_SPECS + .iter() + .filter(|spec| spec.source == ToolSource::Host) + .map(|spec| spec.key) + .collect::>(), + BTreeSet::from(["container_engine", "manifest_validator"]) + ); +} + #[test] fn release_image_resolution_rejects_absent_and_mismatched_local_ids() { let proof_policy = |image_digest: String| ProofPlatformPolicy { @@ -824,7 +886,7 @@ pub(super) fn lane_fixture( target: TARGET_TRIPLE.into(), profile: "release".into(), features: vec![], - rustc_verbose: "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: 0123456789abcdef0123456789abcdef01234567\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0".into(), + rustc_verbose: "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: abcdef0123456789abcdef0123456789abcdef01\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0".into(), cargo: "cargo 1.97.1 (abcdef012 2026-06-30)".into(), baseline_executable_sha256: "d".repeat(64), image_digest: image.into(), @@ -1339,7 +1401,7 @@ fn lane_build_argv_is_offline_no_pull_and_uses_exported_context() { "0123456789abcdef0123456789abcdef", &ubuntu.digest, b"same tar", - "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: 0123456789abcdef0123456789abcdef01234567\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0\n", + "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: abcdef0123456789abcdef0123456789abcdef01\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0\n", ); fs::write( repo.root.path().join("packaging/Containerfile"), diff --git a/crates/rust-release-manifest/src/lib.rs b/crates/rust-release-manifest/src/lib.rs index 1a388f4..69a588c 100644 --- a/crates/rust-release-manifest/src/lib.rs +++ b/crates/rust-release-manifest/src/lib.rs @@ -1042,10 +1042,80 @@ fn validate_manifest_policy(root: &RepoRoot, manifest: &Manifest) -> Result<()> } fn validate_evidence_text(field: &str, value: &str) -> Result<()> { - validate_privacy(field, value, true) + let compiler_commit = if matches!(field, "rust.rustc_verbose" | "lane rustc verbose") { + Some(validate_rustc_verbose_banner(field, value)?) + } else { + None + }; + validate_privacy(field, value, true, compiler_commit.as_deref()) } -fn validate_privacy(field: &str, value: &str, allow_multiline: bool) -> Result<()> { +fn validate_rustc_verbose_banner(field: &str, value: &str) -> Result { + let mut lines = value.lines(); + let banner = lines.next().unwrap_or_default(); + let banner_parts = banner + .strip_prefix("rustc ") + .and_then(|body| body.strip_suffix(')')) + .and_then(|body| body.split_once(" (")); + let Some((banner_release, revision)) = banner_parts else { + return Err(Error::new(format!( + "evidence field {field} mismatch: expected complete rustc verbose banner" + ))); + }; + let Some((short_commit, banner_date)) = revision.split_once(' ') else { + return Err(Error::new(format!( + "evidence field {field} mismatch: expected complete rustc verbose banner" + ))); + }; + let mut fields = BTreeMap::new(); + for line in lines { + let (key, value) = line.split_once(": ").ok_or_else(|| { + Error::new(format!( + "evidence field {field} mismatch: expected rustc verbose key-value line" + )) + })?; + if fields.insert(key, value).is_some() { + return Err(Error::new(format!( + "evidence field {field} mismatch: expected unique rustc verbose lines" + ))); + } + } + let expected = BTreeSet::from([ + "LLVM version", + "binary", + "commit-date", + "commit-hash", + "host", + "release", + ]); + if fields.keys().copied().collect::>() != expected + || fields["binary"] != "rustc" + || !is_git_commit(fields["commit-hash"]) + || short_commit.len() != 9 + || !short_commit + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + || !fields["commit-hash"].starts_with(short_commit) + || banner_release != fields["release"] + || banner_date != fields["commit-date"] + || fields["host"].is_empty() + || fields["release"].is_empty() + || fields["LLVM version"].is_empty() + || DateTime::parse_from_rfc3339(&format!("{}T00:00:00Z", fields["commit-date"])).is_err() + { + return Err(Error::new(format!( + "evidence field {field} mismatch: expected complete rustc verbose banner" + ))); + } + Ok(fields["commit-hash"].to_owned()) +} + +fn validate_privacy( + field: &str, + value: &str, + allow_multiline: bool, + allowed_opaque: Option<&str>, +) -> Result<()> { let lower = value.to_ascii_lowercase(); let forbidden = [ "token", @@ -1084,15 +1154,11 @@ fn validate_privacy(field: &str, value: &str, allow_multiline: bool) -> Result<( }; ipv4 || ipv6 }); - let compiler_commit = value.lines().find_map(|line| { - line.strip_prefix("commit-hash: ") - .filter(|hash| is_git_commit(hash)) - }); let opaque_blob = tokens.iter().any(|token| { (token.len() >= 12 && token.chars().all(|character| character.is_ascii_digit())) || (token.len() >= 20 && *token != TARGET_TRIPLE - && Some(*token) != compiler_commit + && Some(*token) != allowed_opaque && token.chars().all(|character| { character.is_ascii_alphanumeric() || matches!(character, '+' | '/' | '=' | '_' | '-') @@ -1195,7 +1261,7 @@ fn exact_tool(tools: &BTreeMap, key: &str, expected: &str) -> Re } pub(crate) fn validate_identity(key: &str, value: &str) -> Result<()> { - validate_privacy(&format!("native tool {key}"), value, false) + validate_privacy(&format!("native tool {key}"), value, false, None) .map_err(|_| Error::new(format!("native tool {key} identity mismatch")))?; let approved_prefixes: &[&str] = match key { "container_engine" => &["podman ", "docker "], @@ -1297,6 +1363,13 @@ pub(crate) fn artifact_name(kind: &str, version: &str) -> Result { } } +pub(crate) fn artifact_by_kind<'a>(artifacts: &'a [Artifact], kind: &str) -> Result<&'a Artifact> { + artifacts + .iter() + .find(|artifact| artifact_kind(&artifact.path, None).ok() == Some(kind)) + .ok_or_else(|| Error::new(format!("{kind} artifact mismatch: expected present"))) +} + fn artifact_paths(root: &Path, version: &str) -> Result> { require_directory(root, "release root")?; let mut paths = Vec::new(); diff --git a/crates/rust-release-manifest/src/proof_tests.rs b/crates/rust-release-manifest/src/proof_tests.rs index 5f72494..076dd62 100644 --- a/crates/rust-release-manifest/src/proof_tests.rs +++ b/crates/rust-release-manifest/src/proof_tests.rs @@ -28,8 +28,10 @@ fn failed_proof_attempt_removes_only_owned_attempt_and_publication() { &attempt, (attempt_metadata.dev(), attempt_metadata.ino()), &published, - true, - Some((published_metadata.dev(), published_metadata.ino())), + Some(FileIdentity { + device: published_metadata.dev(), + inode: published_metadata.ino(), + }), ); assert_eq!(error.to_string(), "primary"); assert!(!attempt.exists()); @@ -42,6 +44,17 @@ fn candidate_schemas_are_digest_and_identity_pinned() { verify_candidate_schemas().unwrap(); assert_eq!(digest(ledger_schema_bytes()), LEDGER_SCHEMA_SHA256); assert_eq!(digest(proof_schema_bytes()), PROOF_SCHEMA_SHA256); + let ids = proof_ids().map(str::to_owned).to_vec(); + let proof_schema: Value = serde_json::from_slice(proof_schema_bytes()).unwrap(); + let ledger_schema: Value = serde_json::from_slice(ledger_schema_bytes()).unwrap(); + assert_eq!( + proof_schema["properties"]["platform"]["enum"], + serde_json::json!(ids) + ); + assert_eq!( + ledger_schema["properties"]["expected_proof_ids"]["const"], + serde_json::json!(ids) + ); } fn payload_vector() -> (tempfile::TempDir, Vec) { @@ -358,6 +371,66 @@ fn atomic_temp_creation_failure_does_not_mutate_read_only_parent() { assert!(!directory.path().join("ledger.json").exists()); } +#[test] +fn post_rename_sync_failure_reclaims_owned_proof_and_preserves_replacement() { + let directory = tempfile::tempdir().unwrap(); + let proof = directory.path().join("proof.json"); + let result = atomic_write_0644_with_parent_sync(&proof, b"owned proof\n", |_| { + Err(Error::new("injected parent fsync failure")) + }); + assert!(result.is_err()); + assert!(!proof.exists()); + + let result = atomic_write_0644_with_parent_sync(&proof, b"owned proof\n", |_| { + fs::remove_file(&proof).unwrap(); + fs::write(&proof, b"foreign replacement\n").unwrap(); + Err(Error::new("injected parent fsync failure")) + }); + assert!(result.is_err()); + assert_eq!(fs::read(&proof).unwrap(), b"foreign replacement\n"); + assert!(fs::read_dir(directory.path()).unwrap().all(|entry| { + !entry + .unwrap() + .file_name() + .to_string_lossy() + .ends_with(".tmp") + })); +} + +#[test] +fn post_rename_metadata_failure_uses_precomputed_identity_and_preserves_replacement() { + let directory = tempfile::tempdir().unwrap(); + let proof = directory.path().join("proof.json"); + let result = atomic_write_0644_with_post_rename( + &proof, + b"owned proof\n", + |_, _| Err(Error::new("injected post-rename metadata failure")), + |_| Ok(()), + ); + assert!(result.is_err()); + assert!(!proof.exists()); + + let result = atomic_write_0644_with_post_rename( + &proof, + b"owned proof\n", + |path, _| { + fs::remove_file(path).unwrap(); + fs::write(path, b"foreign replacement\n").unwrap(); + Err(Error::new("injected post-rename metadata failure")) + }, + |_| Ok(()), + ); + assert!(result.is_err()); + assert_eq!(fs::read(&proof).unwrap(), b"foreign replacement\n"); + assert!(fs::read_dir(directory.path()).unwrap().all(|entry| { + !entry + .unwrap() + .file_name() + .to_string_lossy() + .ends_with(".tmp") + })); +} + #[test] fn fixed_payload_and_ledger_serialization_are_reproducible() { let (_temp, mut payload) = payload_vector(); @@ -1449,7 +1522,7 @@ fn docker_create_templates(root: &RepoRoot, directory: &Path) { target: TARGET_TRIPLE.into(), profile: "release".into(), features: vec![], - rustc_verbose: "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: 0123456789abcdef0123456789abcdef01234567\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0".into(), + rustc_verbose: "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: abcdef0123456789abcdef0123456789abcdef01\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0".into(), cargo: "cargo 1.97.1 (abcdef012 2026-06-30)".into(), baseline_executable_sha256: "d".repeat(64), image_digest: "@IMAGE@".into(), diff --git a/crates/rust-release-manifest/src/tests.rs b/crates/rust-release-manifest/src/tests.rs index ae1609a..084f653 100644 --- a/crates/rust-release-manifest/src/tests.rs +++ b/crates/rust-release-manifest/src/tests.rs @@ -85,7 +85,7 @@ pub(super) fn evidence() -> Evidence { source_dirty: false, cargo_lock_sha256: digest(&fs::read(root.join("Cargo.lock")).unwrap()), rust: RustEvidence { - rustc_verbose: "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: 0123456789abcdef0123456789abcdef01234567\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0".into(), + rustc_verbose: "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: abcdef0123456789abcdef0123456789abcdef01\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0".into(), cargo_version: "cargo 1.97.1 (c980f4866 2026-06-30)".into(), }, target: TargetEvidence::Compiled { @@ -108,9 +108,26 @@ pub(super) fn evidence() -> Evidence { #[test] fn real_rustc_verbose_shape_is_privacy_safe() { - let banner = "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: 0123456789abcdef0123456789abcdef01234567\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0"; + let banner = "rustc 1.97.1 (abcdef012 2026-06-30)\nbinary: rustc\ncommit-hash: abcdef0123456789abcdef0123456789abcdef01\ncommit-date: 2026-06-30\nhost: x86_64-unknown-linux-gnu\nrelease: 1.97.1\nLLVM version: 18.1.0"; validate_evidence_text("rust.rustc_verbose", banner).unwrap(); assert!(validate_evidence_text("other evidence", &"a".repeat(40)).is_err()); + assert!( + validate_evidence_text( + "other evidence", + &format!("commit-hash: {}", "a".repeat(40)), + ) + .is_err() + ); + for malformed in [ + banner.replace("host: x86_64-unknown-linux-gnu\n", ""), + banner.replace("release: 1.97.1", "release: 1.97.1\nrelease: 1.97.1"), + banner.replace( + "commit-hash: abcdef0123456789abcdef0123456789abcdef01", + "commit-hash: short", + ), + ] { + assert!(validate_evidence_text("rust.rustc_verbose", &malformed).is_err()); + } } fn tarball(root: &Path, version: &str) -> PathBuf { diff --git a/crates/rust-release-manifest/src/transaction.rs b/crates/rust-release-manifest/src/transaction.rs index ab96e95..3ed611d 100644 --- a/crates/rust-release-manifest/src/transaction.rs +++ b/crates/rust-release-manifest/src/transaction.rs @@ -261,7 +261,34 @@ pub fn read_ledger(root: &RepoRoot, version: &str) -> Result<(CandidateLedger, V Ok((ledger, bytes)) } -pub fn atomic_write_0644(path: &Path, bytes: &[u8]) -> Result<()> { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FileIdentity { + pub(crate) device: u64, + pub(crate) inode: u64, +} + +pub fn atomic_write_0644(path: &Path, bytes: &[u8]) -> Result { + atomic_write_0644_with_parent_sync(path, bytes, |parent| { + File::open(parent) + .and_then(|file| file.sync_all()) + .map_err(display_error) + }) +} + +pub(crate) fn atomic_write_0644_with_parent_sync( + path: &Path, + bytes: &[u8], + parent_sync: impl FnOnce(&Path) -> Result<()>, +) -> Result { + atomic_write_0644_with_post_rename(path, bytes, |_, _| Ok(()), parent_sync) +} + +pub(crate) fn atomic_write_0644_with_post_rename( + path: &Path, + bytes: &[u8], + post_rename: impl FnOnce(&Path, FileIdentity) -> Result<()>, + parent_sync: impl FnOnce(&Path) -> Result<()>, +) -> Result { let parent = path .parent() .ok_or_else(|| Error::new("atomic output parent mismatch"))?; @@ -283,21 +310,41 @@ pub fn atomic_write_0644(path: &Path, bytes: &[u8]) -> Result<()> { .mode(0o644) .open(&temp) .map_err(display_error)?; + // Capture ownership from the open temp handle before publication. After rename, + // every failure path already has the token needed to reclaim only this inode. + let metadata = match file.metadata().map_err(display_error) { + Ok(metadata) => metadata, + Err(error) => { + drop(file); + finish_atomic_publish(&temp, Err(error))?; + unreachable!("failed metadata lookup cannot publish") + } + }; + let owned = FileIdentity { + device: metadata.dev(), + inode: metadata.ino(), + }; let publish = (|| { file.write_all(bytes).map_err(display_error)?; file.sync_all().map_err(display_error)?; fs::set_permissions(&temp, fs::Permissions::from_mode(0o644)).map_err(display_error)?; fs::rename(&temp, path).map_err(display_error) })(); - let owned = file.metadata().map_err(display_error)?; drop(file); finish_atomic_publish(&temp, publish)?; - let synced = File::open(parent) - .and_then(|file| file.sync_all()) - .map_err(display_error); - if let Err(error) = synced { - let same_file = fs::symlink_metadata(path) - .is_ok_and(|metadata| metadata.dev() == owned.dev() && metadata.ino() == owned.ino()); + reclaim_after_publish_failure(path, owned, post_rename(path, owned))?; + reclaim_after_publish_failure(path, owned, parent_sync(parent))?; + Ok(owned) +} + +fn reclaim_after_publish_failure( + path: &Path, + owned: FileIdentity, + result: Result<()>, +) -> Result<()> { + if let Err(error) = result { + let same_file = + fs::symlink_metadata(path).is_ok_and(|metadata| same_file_identity(&metadata, owned)); if same_file { fs::remove_file(path).map_err(|cleanup| { Error::new(format!( @@ -311,6 +358,10 @@ pub fn atomic_write_0644(path: &Path, bytes: &[u8]) -> Result<()> { Ok(()) } +fn same_file_identity(metadata: &fs::Metadata, identity: FileIdentity) -> bool { + metadata.dev() == identity.device && metadata.ino() == identity.inode +} + pub(crate) fn finish_atomic_publish(temp: &Path, publish: Result<()>) -> Result<()> { if let Err(error) = publish { if temp.symlink_metadata().is_ok() { @@ -432,24 +483,9 @@ fn stage_payload(input: &FinalizeInput<'_>) -> Result<()> { "staged payload mismatch: expected empty, actual populated", )); } - let tar = input - .deb - .artifacts - .iter() - .find(|item| artifact_kind(&item.path, None).ok() == Some("tar")) - .ok_or_else(|| Error::new("deb tar mismatch"))?; - let deb = input - .deb - .artifacts - .iter() - .find(|item| artifact_kind(&item.path, None).ok() == Some("deb")) - .ok_or_else(|| Error::new("deb artifact mismatch"))?; - let rpm = input - .rpm - .artifacts - .iter() - .find(|item| artifact_kind(&item.path, None).ok() == Some("rpm")) - .ok_or_else(|| Error::new("rpm artifact mismatch"))?; + let tar = artifact_by_kind(&input.deb.artifacts, "tar")?; + let deb = artifact_by_kind(&input.deb.artifacts, "deb")?; + let rpm = artifact_by_kind(&input.rpm.artifacts, "rpm")?; for (source_root, item) in [ (&input.staging.deb_lane, tar), (&input.staging.deb_lane, deb), @@ -676,7 +712,6 @@ pub fn produce_or_retain_proof(request: &ProofRequest<'_>) -> Result { )); fs::create_dir(&attempt).map_err(display_error)?; let attempt_metadata = fs::symlink_metadata(&attempt).map_err(display_error)?; - let mut published = false; let mut published_identity = None; let result = (|| { let artifact = proof_artifact(request.ledger, request.platform)?; @@ -747,10 +782,7 @@ pub fn produce_or_retain_proof(request: &ProofRequest<'_>) -> Result { validate_proof_file(request, &produced)?; let bytes = fs::read(&produced).map_err(display_error)?; fs::remove_dir_all(&attempt).map_err(display_error)?; - atomic_write_0644(&final_path, &bytes)?; - published = true; - let metadata = fs::symlink_metadata(&final_path).map_err(display_error)?; - published_identity = Some((metadata.dev(), metadata.ino())); + published_identity = Some(atomic_write_0644(&final_path, &bytes)?); validate_proof_file(request, &final_path)?; Ok(final_path.clone()) })(); @@ -760,7 +792,6 @@ pub fn produce_or_retain_proof(request: &ProofRequest<'_>) -> Result { &attempt, (attempt_metadata.dev(), attempt_metadata.ino()), &final_path, - published, published_identity, ) }) @@ -771,22 +802,19 @@ pub(crate) fn cleanup_proof_attempt( attempt: &Path, attempt_identity: (u64, u64), published: &Path, - was_published: bool, - published_identity: Option<(u64, u64)>, + published_identity: Option, ) -> Error { let mut residue = Vec::new(); if same_inode(attempt, attempt_identity) && fs::remove_dir_all(attempt).is_err() { residue.push(attempt.to_owned()); } if let Some(identity) = published_identity - && same_inode(published, identity) + && fs::symlink_metadata(published) + .is_ok_and(|metadata| same_file_identity(&metadata, identity)) && fs::remove_file(published).is_err() { residue.push(published.to_owned()); } - if was_published && published_identity.is_none() && published.symlink_metadata().is_ok() { - residue.push(published.to_owned()); - } if residue.is_empty() { error } else { @@ -810,13 +838,7 @@ pub(crate) fn proof_artifact<'a>( platform: &str, ) -> Result<&'a Artifact> { let kind = proof_spec(platform)?.artifact_kind; - ledger - .payload - .iter() - .find(|item| artifact_kind(&item.path, None).ok() == Some(kind)) - .ok_or_else(|| { - Error::new("proof artifact mismatch: expected ledger artifact, actual missing") - }) + artifact_by_kind(&ledger.payload, kind) } pub(crate) fn proof_member<'a>( @@ -1437,7 +1459,7 @@ fn create_candidate_locked( })?; let mut owned = Vec::new(); let proof_result = (|| { - for (id, image) in proof_images(&images) { + for (id, image) in images.proof_images() { owned.push( finalized .evidence_root @@ -1524,7 +1546,7 @@ fn prove_candidate_locked( cargo_lock_sha256: lock_digest, path: root.path().to_owned(), }; - for (id, image) in proof_images(&images) { + for (id, image) in images.proof_images() { produce_or_retain_proof(&ProofRequest { root, ledger: &ledger, @@ -1591,7 +1613,7 @@ fn preflight_existing_proofs( )); } } - for (id, image) in proof_images(images) { + for (id, image) in images.proof_images() { let path = proofs_root.join(format!("{id}.json")); if path.symlink_metadata().is_ok() { validate_proof_file( @@ -1611,13 +1633,6 @@ fn preflight_existing_proofs( Ok(()) } -fn proof_images(images: &ResolvedImages) -> [(&'static str, &ImageIdentity); 3] { - [ - (PROOF_SPECS[0].id, &images.proof_debian), - (PROOF_SPECS[1].id, &images.proof_rpm), - (PROOF_SPECS[2].id, &images.proof_tar), - ] -} fn recheck_all_images( processes: &ProcessEnvironment, engine: ContainerEngine,