Confine image preparation beneath retained storage authority
This commit is contained in:
parent
fbd332c31f
commit
c0b139256e
1 changed files with 694 additions and 103 deletions
|
|
@ -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" => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue