Fix rootless launches with subordinate ID maps

Resolve and verify canonical caller subordinate ranges, launch preparation and runtime workers through mapped causal acquisition, and keep runsc in the existing user namespace. Repair descriptor-relative OCI cleanup and absent-mount recovery so failed launches can progress to the corrected path.
This commit is contained in:
ms2k 2026-07-19 19:51:47 +09:00
commit 73e3e9fdee

View file

@ -20,6 +20,7 @@ use rustix::fs::{
AtFlags, Mode, OFlags, RenameFlags, ResolveFlags, mkdirat, openat2, renameat, renameat_with,
unlinkat,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::layer::{
@ -33,7 +34,7 @@ use crate::{PreparedLowerError, PreparedLowerResult};
const PHYSICAL_LOCK_WAIT: Duration = Duration::from_secs(5);
const LOCK_RETRY: Duration = Duration::from_millis(2);
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PrepareRequest {
pub operation_id: PrepareOperationId,
pub layout_path: PathBuf,
@ -103,11 +104,27 @@ impl PreparedLowerStore {
.open_authority()
.context("opening validated Storage Root")?,
);
match mkdirat(&storage_root, "prepared-lower", Mode::from_raw_mode(0o700)) {
Self::under_storage_root_authority_inner(&storage_root, catalog)
}
/// Creates the prepared-lower store from an exact inherited Storage Root authority.
///
/// # Errors
/// Refuses fixed-child conflicts, changed ownership, or catalog initialization failure.
pub fn under_storage_root_authority(
storage_root: &File,
catalog: Catalog,
) -> PreparedLowerResult<Self> {
Self::under_storage_root_authority_inner(storage_root, catalog)
.map_err(PreparedLowerError::confinement)
}
fn under_storage_root_authority_inner(storage_root: &File, catalog: Catalog) -> Result<Self> {
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"))?;
let root = open_directory_at(storage_root, Path::new("prepared-lower"))?;
require_owned_directory(&root)?;
Self::initialize_retained(root, catalog)
}
@ -167,7 +184,32 @@ impl PreparedLowerStore {
/// Refuses corrupt or unsupported OCI input, forged physical ownership, catalog conflicts,
/// resource limits, and publication failures.
pub fn prepare(&self, request: &PrepareRequest) -> PreparedLowerResult<PreparedArtifact> {
self.prepare_from_source(request, &LayoutSource::Ambient(&request.layout_path))
self.prepare_from_source(request, &LayoutSource::Ambient(&request.layout_path), false)
}
/// Materializes from an exact inherited OCI-layout directory authority.
///
/// # Errors
/// Refuses invalid layout content, mapped ownership, catalog state, or publication.
pub fn prepare_from_layout_authority(
&self,
request: &PrepareRequest,
layout: &File,
) -> PreparedLowerResult<PreparedArtifact> {
self.prepare_from_source(request, &LayoutSource::Retained(layout), false)
}
/// Materializes from an exact OCI layout while executing inside the target ID-map namespace.
///
/// # Errors
/// Refuses invalid layout content, logical ownership outside the target map, catalog state,
/// or publication.
pub fn prepare_from_layout_authority_in_target_namespace(
&self,
request: &PrepareRequest,
layout: &File,
) -> PreparedLowerResult<PreparedArtifact> {
self.prepare_from_source(request, &LayoutSource::Retained(layout), true)
}
/// Verifies and prepares an image through its retained transport-layout authority.
@ -203,13 +245,14 @@ impl PreparedLowerStore {
id_map,
destinations,
};
self.prepare_from_source(&request, &LayoutSource::Retained(layout.root()))
self.prepare_from_source(&request, &LayoutSource::Retained(layout.root()), false)
}
fn prepare_from_source(
&self,
request: &PrepareRequest,
layout: &LayoutSource<'_>,
materialize_logical_owners: bool,
) -> PreparedLowerResult<PreparedArtifact> {
if request.source_identity.is_empty() || request.source_identity.len() > 4096 {
return Err(PreparedLowerError::layer_validation(anyhow::anyhow!(
@ -265,9 +308,17 @@ impl PreparedLowerStore {
}
return Err(PreparedLowerError::catalog(error));
}
if let Err(error) = apply_layout(layout, &stage, &verified, &request.id_map, self.limits)
.and_then(|()| materialize_destinations(&stage, &request.destinations, &request.id_map))
.and_then(|()| sync_tree(&stage))
let materialization_map = if materialize_logical_owners {
logical_id_map(&request.id_map)
} else {
request.id_map.clone()
};
if let Err(error) =
apply_layout(layout, &stage, &verified, &materialization_map, self.limits)
.and_then(|()| {
materialize_destinations(&stage, &request.destinations, &materialization_map)
})
.and_then(|()| sync_tree(&stage))
{
if self
.remove_exact_path(&stage_relative, device, inode)
@ -596,12 +647,21 @@ impl PreparedLowerStore {
}
fn rename_relative(&self, from: &Path, to: &Path) -> Result<()> {
renameat(&self.root, from, &self.root, to)?;
renameat(&self.root, from, &self.root, to)
.with_context(|| format!("renaming {} to {}", from.display(), to.display()))?;
Ok(())
}
fn rename_relative_noreplace(&self, from: &Path, to: &Path) -> Result<()> {
renameat_with(&self.root, from, &self.root, to, RenameFlags::NOREPLACE)?;
renameat_with(&self.root, from, &self.root, to, RenameFlags::NOREPLACE).with_context(
|| {
format!(
"renaming {} to {} without replacement",
from.display(),
to.display()
)
},
)?;
Ok(())
}
@ -639,6 +699,29 @@ impl PreparedLowerStore {
}
}
fn logical_id_map(target: &IdMap) -> IdMap {
IdMap {
uid: target
.uid
.iter()
.map(|range| crate::layer::IdMapRange {
container_id: range.container_id,
namespace_id: range.container_id,
size: range.size,
})
.collect(),
gid: target
.gid
.iter()
.map(|range| crate::layer::IdMapRange {
container_id: range.container_id,
namespace_id: range.container_id,
size: range.size,
})
.collect(),
}
}
fn require_owned_directory(directory: &File) -> Result<()> {
let stat = rustix::fs::fstat(directory)?;
if stat.st_uid != rustix::process::getuid().as_raw() || stat.st_mode & 0o7777 != 0o700 {