Compose selected State Anchor paths over prepared roots

This commit is contained in:
ms2k 2026-07-17 03:58:27 +09:00
commit 4dd2b150d2

View file

@ -32,6 +32,18 @@ pub struct IdMap {
pub gid: Vec<IdMapRange>,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum PreparedDestinationKind {
Directory,
RegularFile,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct PreparedDestination {
pub path: PathBuf,
pub kind: PreparedDestinationKind,
}
#[derive(Clone, Copy, Debug)]
pub struct PreparationLimits {
pub max_layers: usize,
@ -189,11 +201,91 @@ pub(crate) fn apply_layout(
bail!("layer {index} diff-ID mismatch");
}
validate_tar(&spool, id_map, limits, &mut usage)?;
state.apply(&spool, rootfs)?;
state.apply(&spool, rootfs, id_map)?;
}
state.finish(rootfs)
}
pub(crate) fn materialize_destinations(
root: &File,
destinations: &[PreparedDestination],
id_map: &IdMap,
) -> Result<()> {
if destinations.is_empty() {
return Ok(());
}
let uid = map_owner(0, &id_map.uid, "uid")?;
let gid = map_owner(0, &id_map.gid, "gid")?;
for destination in destinations {
let relative = destination
.path
.strip_prefix("/")
.context("prepared destination is not absolute")?;
let relative = normalize_relative(relative)?;
let (parent, leaf) = materialize_parent(root, &relative, uid, gid)?;
let expected = match destination.kind {
PreparedDestinationKind::Directory => FileType::Directory,
PreparedDestinationKind::RegularFile => FileType::RegularFile,
};
match path_type(&parent, Path::new(&leaf))? {
Some(actual) if actual == expected => {}
Some(_) => bail!("prepared destination conflicts with the image layout"),
None => match destination.kind {
PreparedDestinationKind::Directory => {
mkdirat(&parent, &leaf, Mode::from_raw_mode(0o755))?;
set_materialized_metadata(open_component(&parent, &leaf)?, uid, gid)?;
}
PreparedDestinationKind::RegularFile => {
let file = openat2(
&parent,
&leaf,
OFlags::WRONLY
| OFlags::CREATE
| OFlags::EXCL
| OFlags::CLOEXEC
| OFlags::NOFOLLOW,
Mode::from_raw_mode(0o755),
secure_resolve(),
)?;
set_materialized_metadata(file, uid, gid)?;
}
},
}
}
Ok(())
}
fn materialize_parent(root: &File, path: &Path, uid: u32, gid: u32) -> Result<(OwnedFd, OsString)> {
let leaf = path
.file_name()
.context("prepared destination has no final component")?
.to_os_string();
let mut current = dup(root)?;
for component in path.parent().into_iter().flat_map(Path::components) {
let Component::Normal(name) = component else {
bail!("prepared destination is not normalized");
};
match mkdirat(&current, name, Mode::from_raw_mode(0o755)) {
Ok(()) => set_materialized_metadata(open_component(&current, name)?, uid, gid)?,
Err(Errno::EXIST) => {}
Err(error) => return Err(error.into()),
}
current = open_component(&current, name)?;
}
Ok((current, leaf))
}
fn set_materialized_metadata(descriptor: impl AsFd, uid: u32, gid: u32) -> Result<()> {
fchown(
&descriptor,
Some(Uid::from_raw(uid)),
Some(Gid::from_raw(gid)),
)?;
fchmod(&descriptor, Mode::from_raw_mode(0o755))?;
futimens(&descriptor, &timestamps(0))?;
Ok(())
}
fn read_json<T: serde::de::DeserializeOwned>(
layout: &LayoutSource<'_>,
path: &Path,
@ -315,7 +407,8 @@ fn validate_tar(
let mut paths = HashSet::new();
for entry in archive.entries()? {
let mut entry = entry?;
let path = normalize_relative(&entry.path()?)?;
let entry_type = entry.header().entry_type();
let path = normalize_layer_path(&entry.path()?, entry_type)?;
if !paths.insert(path.clone()) {
bail!("policy {POLICY_NAME} rejects duplicate layer path");
}
@ -353,9 +446,8 @@ fn validate_tar(
bail!("policy {POLICY_NAME} rejects PAX metadata");
}
}
validate_owner(entry.header().uid()?, &id_map.uid, "uid")?;
validate_owner(entry.header().gid()?, &id_map.gid, "gid")?;
let entry_type = entry.header().entry_type();
map_owner(entry.header().uid()?, &id_map.uid, "uid")?;
map_owner(entry.header().gid()?, &id_map.gid, "gid")?;
if !matches!(
entry_type,
EntryType::Regular
@ -407,17 +499,35 @@ fn normalize_relative(path: &Path) -> Result<PathBuf> {
Ok(normalized)
}
fn validate_owner(value: u64, ranges: &[IdMapRange], name: &str) -> Result<()> {
fn normalize_layer_path(path: &Path, entry_type: EntryType) -> Result<PathBuf> {
match normalize_relative(path) {
Ok(path) => Ok(path),
Err(_)
if entry_type == EntryType::Directory
&& path.components().next().is_some()
&& path
.components()
.all(|component| component == Component::CurDir) =>
{
Ok(PathBuf::from("."))
}
Err(error) => Err(error),
}
}
fn map_owner(value: u64, ranges: &[IdMapRange], name: &str) -> Result<u32> {
let Ok(value) = u32::try_from(value) else {
bail!("{name} {value} is outside the target ID map");
};
if !ranges
.iter()
.any(|range| value >= range.container_id && value - range.container_id < range.size)
{
bail!("{name} {value} is outside the target ID map");
for range in ranges {
if value >= range.container_id && value - range.container_id < range.size {
return range
.namespace_id
.checked_add(value - range.container_id)
.context("mapped owner overflows");
}
}
Ok(())
bail!("{name} {value} is outside the target ID map")
}
fn validate_id_map(id_map: &IdMap) -> Result<()> {
@ -483,7 +593,7 @@ struct LayerState {
}
impl LayerState {
fn apply(&mut self, tar_source: &File, root: &File) -> Result<()> {
fn apply(&mut self, tar_source: &File, root: &File, id_map: &IdMap) -> Result<()> {
apply_whiteouts(tar_source, root, &mut self.directories)?;
let mut archive = tar::Archive::new(rewound(tar_source)?);
let mut hardlinks = Vec::new();
@ -497,7 +607,7 @@ impl LayerState {
if name == ".wh..wh..opq" || name.starts_with(".wh.") {
continue;
}
let metadata = EntryMetadata::from_header(path.clone(), entry.header())?;
let metadata = EntryMetadata::from_header(path.clone(), entry.header(), id_map)?;
match entry.header().entry_type() {
EntryType::Regular | EntryType::Continuous => {
forget_directory_tree(&mut self.directories, &path);
@ -557,11 +667,11 @@ struct EntryMetadata {
}
impl EntryMetadata {
fn from_header(path: PathBuf, header: &Header) -> Result<Self> {
fn from_header(path: PathBuf, header: &Header, id_map: &IdMap) -> Result<Self> {
Ok(Self {
path,
uid: u32::try_from(header.uid()?).context("entry uid exceeds u32")?,
gid: u32::try_from(header.gid()?).context("entry gid exceeds u32")?,
uid: map_owner(header.uid()?, &id_map.uid, "uid")?,
gid: map_owner(header.gid()?, &id_map.gid, "gid")?,
mode: header.mode()?,
mtime: header.mtime()?,
})