Close prepared-lower identity and reclaim races

This commit is contained in:
ms2k 2026-07-15 15:39:22 +09:00
commit 0dfd7767b5
3 changed files with 142 additions and 30 deletions

View file

@ -95,6 +95,7 @@ struct Descriptor {
}
pub(crate) struct VerifiedLayout {
pub(crate) manifest_digest: [u8; 32],
layers: Vec<Descriptor>,
diff_ids: Vec<String>,
}
@ -130,6 +131,7 @@ pub(crate) fn verify_layout(layout: &Path, limits: PreparationLimits) -> Result<
verify_blob_digest(layout, layer, limits.max_layer_blob_bytes)?;
}
Ok(VerifiedLayout {
manifest_digest: parse_sha256(&descriptor.digest)?,
layers: manifest.layers,
diff_ids: config.rootfs.diff_ids,
})
@ -147,41 +149,24 @@ pub(crate) fn apply_layout(
let mut usage = Usage::default();
for (index, (descriptor, diff_id)) in verified.layers.iter().zip(&verified.diff_ids).enumerate()
{
let spool_name = format!(".layer-{index}.tar");
let spool_output = File::from(openat2(
let spool = File::from(openat2(
rootfs,
&spool_name,
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
".",
OFlags::RDWR | OFlags::TMPFILE | OFlags::CLOEXEC,
Mode::from_raw_mode(0o600),
secure_resolve(),
)?);
let observed = decompress_layer(
&blob_path(layout, &descriptor.digest)?,
&descriptor.media_type,
spool_output,
spool.try_clone()?,
limits.max_uncompressed_layer_bytes,
)?;
if &observed != diff_id {
let _ = unlinkat(rootfs, &spool_name, AtFlags::empty());
bail!("layer {index} diff-ID mismatch");
}
let spool = File::from(openat2(
rootfs,
&spool_name,
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
secure_resolve(),
)?);
let result = validate_tar(&spool, id_map, limits, &mut usage)
.and_then(|()| state.apply(&spool, rootfs));
if let Err(error) = result {
let _ = unlinkat(rootfs, &spool_name, AtFlags::empty());
return Err(error);
}
match unlinkat(rootfs, &spool_name, AtFlags::empty()) {
Ok(()) | Err(rustix::io::Errno::NOENT) => {}
Err(error) => return Err(error.into()),
}
validate_tar(&spool, id_map, limits, &mut usage)?;
state.apply(&spool, rootfs)?;
}
state.finish(rootfs)
}

View file

@ -14,7 +14,10 @@ use lazaret_core::identity::{
use lazaret_core::records::{ArtifactState, PrepareOperation, PrepareStageState, PreparedArtifact};
use lazaret_linux::filesystem::{ExclusiveFileLock, try_exclusive_flock};
use lazaret_storage_catalog::{Catalog, CatalogError, CatalogRow};
use rustix::fs::{AtFlags, Mode, OFlags, ResolveFlags, mkdirat, openat2, renameat, unlinkat};
use rustix::fs::{
AtFlags, Mode, OFlags, RenameFlags, ResolveFlags, mkdirat, openat2, renameat, renameat_with,
unlinkat,
};
use sha2::{Digest, Sha256};
use crate::layer::{
@ -94,6 +97,9 @@ impl PreparedLowerStore {
bail!("source identity is empty or exceeds 4096 bytes");
}
let verified = verify_layout(&request.layout_path, self.limits)?;
if request.selected_manifest_digest != verified.manifest_digest {
bail!("selected manifest identity does not match verified OCI layout");
}
let identities = Identities::new(request)?;
if let Some(existing) = self.available_artifact(identities.artifact_id)? {
identities.require_equivalent(&existing)?;
@ -218,10 +224,20 @@ impl PreparedLowerStore {
{
let _lock = self.physical_lock()?;
if let Some(canonical) = self.open_relative(&expected_artifact)? {
require_identity(&canonical, reclaiming.device, reclaiming.inode)?;
self.rename_relative(&expected_artifact, &quarantine)?;
self.sync_fixed_directory("artifacts")?;
self.sync_fixed_directory("quarantine")?;
if file_identity(&canonical)? == (reclaiming.device, reclaiming.inode) {
self.rename_relative_noreplace(&expected_artifact, &quarantine)?;
self.sync_fixed_directory("artifacts")?;
self.sync_fixed_directory("quarantine")?;
} else {
let quarantined = self
.open_relative(&quarantine)?
.context("canonical replacement exists without quarantined old artifact")?;
require_identity(&quarantined, reclaiming.device, reclaiming.inode)?;
let replacement = self
.available_artifact(reclaiming.prepared_artifact_id)?
.context("canonical replacement exists without available ownership")?;
require_identity(&canonical, replacement.device, replacement.inode)?;
}
} else {
let existing = self
.open_relative(&quarantine)?
@ -275,7 +291,7 @@ impl PreparedLowerStore {
.context("canonical artifact path exists without matching catalog ownership")?;
require_identity(&existing, reclaiming.device, reclaiming.inode)?;
let quarantine = quarantine_path(&reclaiming);
self.rename_relative(&artifact_path, &quarantine)?;
self.rename_relative_noreplace(&artifact_path, &quarantine)?;
self.sync_fixed_directory("quarantine")?;
}
self.rename_relative(&stage, &artifact_path)?;
@ -360,6 +376,12 @@ impl PreparedLowerStore {
Ok(())
}
fn rename_relative_noreplace(&self, from: &Path, to: &Path) -> Result<()> {
let root = File::open(&self.root)?;
renameat_with(&root, from, &root, to, RenameFlags::NOREPLACE)?;
Ok(())
}
fn remove_exact_path(&self, relative: &Path, device: DeviceId, inode: InodeId) -> Result<()> {
let root = File::open(&self.root)?;
let file = open_directory_at(&root, relative)?;

View file

@ -164,7 +164,7 @@ impl OciRegistryTransport {
let local_digest = format!("sha256:{}", hex(&Sha256::digest(&local_manifest)));
write_verified_blob(&blobs, &local_digest, &local_manifest)?;
write_layout_index(layout_path, &local_digest, local_manifest.len())?;
let selected_manifest_digest = parse_sha256(&manifest_digest)?;
let selected_manifest_digest = parse_sha256(&local_digest)?;
let source_identity = format!(
"oci-v1\0{}\0{}\0{manifest_digest}",
reference.registry(),