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

@ -100,6 +100,26 @@ pub(crate) struct VerifiedLayout {
diff_ids: Vec<String>,
}
pub(crate) enum LayoutSource<'a> {
Ambient(&'a Path),
Retained(&'a File),
}
impl LayoutSource<'_> {
fn open(&self, relative: &Path) -> Result<File> {
match self {
Self::Ambient(root) => Ok(File::open(root.join(relative))?),
Self::Retained(root) => Ok(File::from(openat2(
root,
relative,
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
secure_resolve(),
)?)),
}
}
}
#[derive(Default)]
struct Usage {
entries: u64,
@ -107,8 +127,11 @@ struct Usage {
total_bytes: u64,
}
pub(crate) fn verify_layout(layout: &Path, limits: PreparationLimits) -> Result<VerifiedLayout> {
let index: LayoutIndex = read_json(&layout.join("index.json"), limits.max_json_bytes)?;
pub(crate) fn verify_layout(
layout: &LayoutSource<'_>,
limits: PreparationLimits,
) -> Result<VerifiedLayout> {
let index: LayoutIndex = read_json(layout, Path::new("index.json"), limits.max_json_bytes)?;
if index.manifests.len() != 1 {
bail!("selected OCI layout must contain exactly one manifest");
}
@ -138,7 +161,7 @@ pub(crate) fn verify_layout(layout: &Path, limits: PreparationLimits) -> Result<
}
pub(crate) fn apply_layout(
layout: &Path,
layout: &LayoutSource<'_>,
rootfs: &File,
verified: &VerifiedLayout,
id_map: &IdMap,
@ -157,7 +180,7 @@ pub(crate) fn apply_layout(
secure_resolve(),
)?);
let observed = decompress_layer(
&blob_path(layout, &descriptor.digest)?,
layout.open(&blob_path(&descriptor.digest)?)?,
&descriptor.media_type,
spool.try_clone()?,
limits.max_uncompressed_layer_bytes,
@ -171,48 +194,64 @@ pub(crate) fn apply_layout(
state.finish(rootfs)
}
fn read_json<T: serde::de::DeserializeOwned>(path: &Path, limit: u64) -> Result<T> {
let metadata = std::fs::metadata(path)?;
fn read_json<T: serde::de::DeserializeOwned>(
layout: &LayoutSource<'_>,
path: &Path,
limit: u64,
) -> Result<T> {
let mut file = layout.open(path)?;
let metadata = file.metadata()?;
if metadata.len() > limit {
bail!("OCI JSON exceeds {limit} bytes");
}
Ok(serde_json::from_slice(&std::fs::read(path)?)?)
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(serde_json::from_slice(&bytes)?)
}
fn verified_blob(layout: &Path, descriptor: &Descriptor, limit: u64) -> Result<Vec<u8>> {
let path = blob_path(layout, &descriptor.digest)?;
let metadata = std::fs::metadata(&path)?;
fn verified_blob(
layout: &LayoutSource<'_>,
descriptor: &Descriptor,
limit: u64,
) -> Result<Vec<u8>> {
let mut file = layout.open(&blob_path(&descriptor.digest)?)?;
let metadata = file.metadata()?;
if metadata.len() != descriptor.size || metadata.len() > limit {
bail!("OCI descriptor size or resource limit mismatch");
}
let bytes = std::fs::read(path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
if Sha256::digest(&bytes).as_slice() != parse_sha256(&descriptor.digest)? {
bail!("OCI descriptor digest mismatch");
}
Ok(bytes)
}
fn verify_blob_digest(layout: &Path, descriptor: &Descriptor, limit: u64) -> Result<()> {
let path = blob_path(layout, &descriptor.digest)?;
let metadata = std::fs::metadata(&path)?;
fn verify_blob_digest(
layout: &LayoutSource<'_>,
descriptor: &Descriptor,
limit: u64,
) -> Result<()> {
let file = layout.open(&blob_path(&descriptor.digest)?)?;
let metadata = file.metadata()?;
if metadata.len() != descriptor.size || metadata.len() > limit {
bail!("OCI layer size or resource limit mismatch");
}
let observed = digest_reader(File::open(path)?, limit, None)?;
let observed = digest_reader(file, limit, None)?;
if observed != descriptor.digest {
bail!("OCI layer digest mismatch");
}
Ok(())
}
fn blob_path(layout: &Path, digest: &str) -> Result<PathBuf> {
fn blob_path(digest: &str) -> Result<PathBuf> {
let component = digest
.strip_prefix("sha256:")
.context("only sha256 OCI descriptors are supported")?;
if component.len() != 64 || !component.bytes().all(|byte| byte.is_ascii_hexdigit()) {
bail!("invalid sha256 OCI descriptor");
}
Ok(layout.join("blobs/sha256").join(component))
Ok(Path::new("blobs/sha256").join(component))
}
fn parse_sha256(digest: &str) -> Result<[u8; 32]> {
@ -229,8 +268,7 @@ fn parse_sha256(digest: &str) -> Result<[u8; 32]> {
Ok(bytes)
}
fn decompress_layer(source: &Path, media_type: &str, output: File, limit: u64) -> Result<String> {
let input = File::open(source)?;
fn decompress_layer(input: File, media_type: &str, output: File, limit: u64) -> Result<String> {
let reader: Box<dyn Read> = match media_type {
"application/vnd.oci.image.layer.v1.tar" => Box::new(input),
"application/vnd.oci.image.layer.v1.tar+gzip" => {