Confine image preparation beneath retained storage authority

This commit is contained in:
ms2k 2026-07-16 20:41:26 +09:00
commit c0b139256e

View file

@ -3,10 +3,12 @@ use std::io;
use std::os::fd::AsFd;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use anyhow::{Context, Result, bail};
use lazaret_core::configuration::ConfinedPathPlan;
use lazaret_core::identity::{
DeviceId, HostPath, IdMapDigest, InodeId, PolicyId, PrepareOperationId, PreparedArtifactId,
SelectedManifestDigest, SourceIdentity,
@ -21,8 +23,10 @@ use rustix::fs::{
use sha2::{Digest, Sha256};
use crate::layer::{
IdMap, POLICY_NAME, PreparationLimits, apply_layout, remove_children, verify_layout,
IdMap, LayoutSource, POLICY_NAME, PreparationLimits, apply_layout, remove_children,
verify_layout,
};
use crate::transport::TransportedImage;
const PHYSICAL_LOCK_WAIT: Duration = Duration::from_secs(5);
const LOCK_RETRY: Duration = Duration::from_millis(2);
@ -51,7 +55,7 @@ pub enum ReclaimOutcome {
#[derive(Clone, Debug)]
pub struct PreparedLowerStore {
root: PathBuf,
root: Arc<File>,
catalog: Catalog,
limits: PreparationLimits,
}
@ -63,20 +67,43 @@ impl PreparedLowerStore {
///
/// Returns an error if the root cannot be opened or any fixed path is not a real directory.
pub fn initialize(root: impl Into<PathBuf>, catalog: Catalog) -> Result<Self> {
let root = root.into();
let root = File::open(&root).context("opening Storage Root")?;
Self::initialize_retained(root, catalog)
}
/// Retains the exact validated Storage Root and creates only fixed children beneath it.
///
/// # Errors
/// Returns an error if Storage Root authority drifts or a fixed child is not a real directory.
pub fn under_storage_root(storage_root: &ConfinedPathPlan, catalog: Catalog) -> Result<Self> {
let storage_root = File::from(
storage_root
.open_authority()
.context("opening validated Storage Root")?,
);
match mkdirat(&storage_root, "prepared-lower", Mode::from_raw_mode(0o700)) {
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
Err(error) => return Err(error.into()),
}
let root = open_directory_at(&storage_root, Path::new("prepared-lower"))?;
Self::initialize_retained(root, catalog)
}
fn initialize_retained(root: File, catalog: Catalog) -> Result<Self> {
let store = Self {
root: root.into(),
root: Arc::new(root),
catalog,
limits: PreparationLimits::default(),
};
let root = File::open(&store.root).context("opening Storage Root")?;
for name in ["artifacts", "stages", "quarantine"] {
match mkdirat(&root, name, Mode::from_raw_mode(0o700)) {
match mkdirat(&store.root, name, Mode::from_raw_mode(0o700)) {
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
Err(error) => return Err(error.into()),
}
open_directory_at(&root, Path::new(name))?;
open_directory_at(&store.root, Path::new(name))?;
}
open_lock_at(&root)?;
open_lock_at(&store.root)?;
Ok(store)
}
@ -93,10 +120,41 @@ impl PreparedLowerStore {
/// Refuses corrupt or unsupported OCI input, forged physical ownership, catalog conflicts,
/// resource limits, and publication failures.
pub fn prepare(&self, request: &PrepareRequest) -> Result<PreparedArtifact> {
self.prepare_from_source(request, &LayoutSource::Ambient(&request.layout_path))
}
/// Verifies and prepares an image through its retained transport-layout authority.
///
/// # Errors
/// Refuses ambient-only transported images and all normal preparation validation failures.
pub fn prepare_transported(
&self,
operation_id: PrepareOperationId,
image: &TransportedImage,
id_map: IdMap,
) -> Result<PreparedArtifact> {
let layout = image
.retained_layout()
.context("transported image has no retained layout authority")?;
let request = PrepareRequest {
operation_id,
layout_path: image.layout_path.clone(),
source_identity: image.source_identity.clone(),
selected_manifest_digest: image.selected_manifest_digest,
id_map,
};
self.prepare_from_source(&request, &LayoutSource::Retained(layout.root()))
}
fn prepare_from_source(
&self,
request: &PrepareRequest,
layout: &LayoutSource<'_>,
) -> Result<PreparedArtifact> {
if request.source_identity.is_empty() || request.source_identity.len() > 4096 {
bail!("source identity is empty or exceeds 4096 bytes");
}
let verified = verify_layout(&request.layout_path, self.limits)?;
let verified = verify_layout(layout, self.limits)?;
if request.selected_manifest_digest != verified.manifest_digest {
bail!("selected manifest identity does not match verified OCI layout");
}
@ -133,14 +191,8 @@ impl PreparedLowerStore {
}
return Err(error.into());
}
if let Err(error) = apply_layout(
&request.layout_path,
&stage,
&verified,
&request.id_map,
self.limits,
)
.and_then(|()| sync_tree(&stage))
if let Err(error) = apply_layout(layout, &stage, &verified, &request.id_map, self.limits)
.and_then(|()| sync_tree(&stage))
{
if self
.remove_exact_path(&stage_relative, device, inode)
@ -358,16 +410,15 @@ impl PreparedLowerStore {
}
fn create_stage(&self, relative: &Path) -> Result<File> {
let stages = open_directory_at(&File::open(&self.root)?, Path::new("stages"))?;
let stages = open_directory_at(&self.root, Path::new("stages"))?;
let name = relative.file_name().context("stage has no name")?;
mkdirat(&stages, name, Mode::from_raw_mode(0o700))?;
open_directory_at(&stages, Path::new(name))
}
fn open_relative(&self, relative: &Path) -> Result<Option<File>> {
let root = File::open(&self.root)?;
match openat2(
&root,
&self.root,
relative,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
@ -380,30 +431,27 @@ impl PreparedLowerStore {
}
fn rename_relative(&self, from: &Path, to: &Path) -> Result<()> {
let root = File::open(&self.root)?;
renameat(&root, from, &root, to)?;
renameat(&self.root, from, &self.root, to)?;
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)?;
renameat_with(&self.root, from, &self.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)?;
let file = open_directory_at(&self.root, relative)?;
require_identity(&file, device, inode)?;
remove_children(&file)?;
unlinkat(&root, relative, AtFlags::REMOVEDIR)?;
unlinkat(&self.root, relative, AtFlags::REMOVEDIR)?;
Ok(())
}
fn physical_lock(&self) -> Result<ExclusiveFileLock> {
let deadline = Instant::now() + PHYSICAL_LOCK_WAIT;
loop {
let file = open_lock_at(&File::open(&self.root)?)?;
let file = open_lock_at(&self.root)?;
match try_exclusive_flock(file) {
Ok(lock) => return Ok(lock),
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
@ -419,7 +467,7 @@ impl PreparedLowerStore {
}
fn sync_fixed_directory(&self, name: &str) -> Result<()> {
open_directory_at(&File::open(&self.root)?, Path::new(name))?.sync_all()?;
open_directory_at(&self.root, Path::new(name))?.sync_all()?;
Ok(())
}
}