Confine runtime staging to retained authority

This commit is contained in:
ms2k 2026-07-16 18:53:20 +09:00
commit fc5b2b6ca0

View file

@ -1,6 +1,6 @@
use std::ffi::OsString;
use std::fmt;
use std::fs::{self, File, OpenOptions};
use std::fs::File;
use std::io::Write;
use std::os::fd::{AsFd, OwnedFd};
use std::os::unix::ffi::OsStringExt;
@ -8,7 +8,7 @@ use std::os::unix::fs::{FileExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use lazaret_core::configuration::ResolvedDomainConfiguration;
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration};
use lazaret_core::identity::{BundleDigest, ContainerId};
use lazaret_core::network::{ReachabilityExpectation, derive_ipv4_policy};
use lazaret_core::records::{
@ -24,7 +24,10 @@ use lazaret_runtime_supervisor::supervision::{RuntimeNetworkLaunch, RuntimeSuper
use lazaret_state_anchor::{StateAnchorLaunchAuthority, StateAnchorLayout};
use lazaret_storage_catalog::Catalog;
use rustix::event::{PollFd, PollFlags, Timespec, poll};
use rustix::fs::{FileType, fstat};
use rustix::fs::{
AtFlags, FileType, Mode, OFlags, RawDir, ResolveFlags, fchmod, fstat, mkdirat, openat2, statat,
unlinkat,
};
use rustix::io::{FdFlags, fcntl_setfd};
use sha2::{Digest, Sha256};
@ -132,9 +135,10 @@ impl<'a> ProductionColdStart<'a> {
let state_anchor_layout = StateAnchorLayout::create(inputs.configuration)
.map_err(|error| failure(format!("State Anchor layout: {error}")))?;
let staging = RuntimeStaging::create(
&runtime_domain,
inputs.configuration.runtime_domain_root(),
inputs.container_id,
inputs.escalation_client,
inputs.invoking_uid,
)
.map_err(|error| failure(format!("runtime staging: {error}")))?;
let expected_network_namespace = namespace_identity(Path::new(NETWORK_NAMESPACE_PATH))
@ -265,6 +269,7 @@ fn require_inputs(inputs: &ProductionColdStartInputs<'_>) -> Result<(), Producti
|| inputs.network_probe.domain_id != inputs.generation.domain_id
|| inputs.network_probe.generation_id != inputs.generation.generation_id
|| inputs.resolver_addresses.is_empty()
|| inputs.invoking_uid != rustix::process::getuid().as_raw()
{
return Err(ProductionColdStartError(
"production cold-start inputs disagree".into(),
@ -274,56 +279,79 @@ fn require_inputs(inputs: &ProductionColdStartInputs<'_>) -> Result<(), Producti
}
struct RuntimeStaging {
bundle: PathBuf,
control: PathBuf,
bundles: OwnedFd,
bundle: OwnedFd,
controls: OwnedFd,
control: OwnedFd,
name: OsString,
bundle_identity: FileIdentity,
control_identity: FileIdentity,
}
impl RuntimeStaging {
fn create(
runtime_domain: &Path,
runtime_domain: &ConfinedPathPlan,
container_id: ContainerId,
escalation_client: &File,
invoking_uid: u32,
) -> Result<Self, ProductionColdStartError> {
let name = hex(container_id.as_bytes());
let bundles = runtime_domain.join("bundles");
let controls = runtime_domain.join("sandbox-control");
let runsc = runtime_domain.join("runsc");
for directory in [&bundles, &controls, &runsc] {
ensure_directory(directory)
.map_err(|error| failure(format!("creating {}: {error}", directory.display())))?;
}
let bundle = bundles.join(&name);
let control = controls.join(name);
fs::create_dir(&bundle)
.map_err(|error| failure(format!("creating bundle leaf: {error}")))?;
set_private_directory(&bundle)?;
if let Err(error) = fs::create_dir(&control) {
let _ = fs::remove_dir(&bundle);
return Err(failure(error));
}
if let Err(error) = set_private_directory(&control) {
let _ = fs::remove_dir(&control);
let _ = fs::remove_dir(&bundle);
return Err(error);
}
let staging = Self { bundle, control };
let root = runtime_domain.open_authority().map_err(failure)?;
let bundles = open_or_create_private_directory(&root, "bundles", invoking_uid, false)?;
let controls =
open_or_create_private_directory(&root, "sandbox-control", invoking_uid, false)?;
drop(open_or_create_private_directory(
&root,
"runsc",
invoking_uid,
false,
)?);
let name = OsString::from(hex(container_id.as_bytes()));
let bundle = open_or_create_private_directory(&bundles, &name, invoking_uid, true)?;
let bundle_identity = FileIdentity::capture(&bundle)?;
let control = match open_or_create_private_directory(&controls, &name, invoking_uid, true) {
Ok(control) => control,
Err(error) => {
remove_staging_directory(&bundles, &name, &bundle, bundle_identity);
return Err(error);
}
};
let control_identity = FileIdentity::capture(&control)?;
let staging = Self {
bundles,
bundle,
controls,
control,
name,
bundle_identity,
control_identity,
};
staging.populate_control(escalation_client)?;
Ok(staging)
}
fn populate_control(&self, escalation_client: &File) -> Result<(), ProductionColdStartError> {
for relative in [
let invoking_uid = fstat(&self.control).map_err(failure)?.st_uid;
drop(open_or_create_private_directory(
&self.control,
"credentials",
"mailbox",
"mailbox/requests",
"mailbox/results",
"bin",
] {
let directory = self.control.join(relative);
fs::create_dir(&directory)
.map_err(|error| failure(format!("creating control {relative}: {error}")))?;
set_private_directory(&directory)?;
}
invoking_uid,
true,
)?);
let mailbox =
open_or_create_private_directory(&self.control, "mailbox", invoking_uid, true)?;
drop(open_or_create_private_directory(
&mailbox,
"requests",
invoking_uid,
true,
)?);
drop(open_or_create_private_directory(
&mailbox,
"results",
invoking_uid,
true,
)?);
let bin = open_or_create_private_directory(&self.control, "bin", invoking_uid, true)?;
let source = escalation_client
.metadata()
.map_err(|error| failure(format!("inspecting escalation client: {error}")))?;
@ -332,37 +360,44 @@ impl RuntimeStaging {
"escalation client is not an executable regular file".into(),
));
}
copy_descriptor(
escalation_client,
&self.control.join("bin/lazaret-escalate"),
)?;
copy_descriptor(escalation_client, &bin, "lazaret-escalate", invoking_uid)?;
Ok(())
}
fn write_bundle(&self, bytes: &[u8]) -> Result<(), ProductionColdStartError> {
let path = self.bundle.join("config.json");
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(failure)?;
let descriptor = openat2(
&self.bundle,
"config.json",
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::RUSR | Mode::WUSR,
confined_resolve_flags(),
)
.map_err(failure)?;
let mut file = File::from(descriptor);
file.write_all(bytes).map_err(failure)?;
file.set_permissions(fs::Permissions::from_mode(0o600))
.map_err(failure)?;
fchmod(file.as_fd(), Mode::RUSR | Mode::WUSR).map_err(failure)?;
file.sync_all().map_err(failure)?;
require_private_file(&file, fstat(&self.bundle).map_err(failure)?.st_uid, 0o600)?;
Ok(())
}
}
fn copy_descriptor(source: &File, destination: &Path) -> Result<(), ProductionColdStartError> {
let mut destination = OpenOptions::new()
.write(true)
.create_new(true)
.open(destination)
.map_err(failure)?;
destination
.set_permissions(fs::Permissions::from_mode(0o700))
.map_err(failure)?;
fn copy_descriptor(
source: &File,
parent: &OwnedFd,
name: &str,
invoking_uid: u32,
) -> Result<(), ProductionColdStartError> {
let descriptor = openat2(
parent,
name,
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::RWXU,
confined_resolve_flags(),
)
.map_err(failure)?;
let mut destination = File::from(descriptor);
fchmod(destination.as_fd(), Mode::RWXU).map_err(failure)?;
let mut offset = 0_u64;
let mut buffer = [0_u8; 16 * 1024];
loop {
@ -375,35 +410,171 @@ fn copy_descriptor(source: &File, destination: &Path) -> Result<(), ProductionCo
.checked_add(read as u64)
.ok_or_else(|| ProductionColdStartError("escalation client is too large".into()))?;
}
destination.sync_all().map_err(failure)
destination.sync_all().map_err(failure)?;
require_private_file(&destination, invoking_uid, 0o700)
}
fn set_private_directory(path: &Path) -> Result<(), ProductionColdStartError> {
fs::set_permissions(path, fs::Permissions::from_mode(0o700)).map_err(failure)
#[derive(Clone, Copy)]
struct FileIdentity {
device: u64,
inode: u64,
}
impl FileIdentity {
fn capture(descriptor: impl AsFd) -> Result<Self, ProductionColdStartError> {
let metadata = fstat(descriptor).map_err(failure)?;
Ok(Self {
device: metadata.st_dev,
inode: metadata.st_ino,
})
}
fn matches(self, metadata: &rustix::fs::Stat) -> bool {
metadata.st_dev == self.device && metadata.st_ino == self.inode
}
}
impl Drop for RuntimeStaging {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.control);
let _ = fs::remove_dir_all(&self.bundle);
remove_staging_directory(
&self.controls,
&self.name,
&self.control,
self.control_identity,
);
remove_staging_directory(
&self.bundles,
&self.name,
&self.bundle,
self.bundle_identity,
);
}
}
fn ensure_directory(path: &Path) -> Result<(), ProductionColdStartError> {
match fs::create_dir(path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
let metadata = fs::symlink_metadata(path).map_err(failure)?;
if metadata.file_type().is_dir() {
Ok(())
} else {
Err(ProductionColdStartError(
"production staging path is not a directory".into(),
))
fn open_or_create_private_directory(
parent: impl AsFd,
name: impl AsRef<std::ffi::OsStr>,
invoking_uid: u32,
require_new: bool,
) -> Result<OwnedFd, ProductionColdStartError> {
let name = name.as_ref();
let created = match mkdirat(parent.as_fd(), name, Mode::RWXU) {
Ok(()) => true,
Err(rustix::io::Errno::EXIST) if !require_new => false,
Err(error) => return Err(failure(error)),
};
let directory = openat2(
parent.as_fd(),
name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
confined_resolve_flags(),
)
.map_err(failure)?;
if created {
fchmod(&directory, Mode::RWXU).map_err(failure)?;
}
require_private_directory(&directory, invoking_uid)?;
Ok(directory)
}
fn require_private_directory(
descriptor: impl AsFd,
invoking_uid: u32,
) -> Result<(), ProductionColdStartError> {
let metadata = fstat(descriptor).map_err(failure)?;
if !FileType::from_raw_mode(metadata.st_mode).is_dir()
|| metadata.st_uid != invoking_uid
|| metadata.st_mode & 0o7777 != 0o700
{
return Err(ProductionColdStartError(
"runtime staging directory is not invoking-user-owned mode 0700".into(),
));
}
Ok(())
}
fn require_private_file(
descriptor: impl AsFd,
invoking_uid: u32,
mode: u32,
) -> Result<(), ProductionColdStartError> {
let metadata = fstat(descriptor).map_err(failure)?;
if !FileType::from_raw_mode(metadata.st_mode).is_file()
|| metadata.st_uid != invoking_uid
|| metadata.st_mode & 0o7777 != mode
|| metadata.st_nlink != 1
{
return Err(ProductionColdStartError(
"runtime staging file has unsafe type, ownership, mode, or link count".into(),
));
}
Ok(())
}
fn remove_staging_directory(
parent: impl AsFd,
name: &std::ffi::OsStr,
directory: impl AsFd,
expected: FileIdentity,
) {
let retained = fstat(directory.as_fd());
if !retained
.as_ref()
.is_ok_and(|metadata| expected.matches(metadata))
{
return;
}
if remove_children(directory.as_fd()).is_err() {
return;
}
let Ok(current) = statat(parent.as_fd(), name, AtFlags::SYMLINK_NOFOLLOW) else {
return;
};
if expected.matches(&current) && FileType::from_raw_mode(current.st_mode).is_dir() {
let _ = unlinkat(parent.as_fd(), name, AtFlags::REMOVEDIR);
}
}
fn remove_children(directory: impl AsFd) -> std::io::Result<()> {
let duplicate = rustix::io::dup(directory.as_fd())?;
let mut buffer = [std::mem::MaybeUninit::uninit(); 8192];
let names = {
let mut names = Vec::new();
let mut entries = RawDir::new(duplicate, &mut buffer);
while let Some(entry) = entries.next() {
let entry = entry?;
let bytes = entry.file_name().to_bytes();
if bytes != b"." && bytes != b".." {
names.push(OsString::from_vec(bytes.to_vec()));
}
}
Err(error) => Err(failure(error)),
names
};
for name in names {
let metadata = statat(directory.as_fd(), &name, AtFlags::SYMLINK_NOFOLLOW)?;
if FileType::from_raw_mode(metadata.st_mode).is_dir() {
let child = openat2(
directory.as_fd(),
&name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
Mode::empty(),
confined_resolve_flags(),
)?;
remove_children(&child)?;
unlinkat(directory.as_fd(), &name, AtFlags::REMOVEDIR)?;
} else {
unlinkat(directory.as_fd(), &name, AtFlags::empty())?;
}
}
Ok(())
}
fn confined_resolve_flags() -> ResolveFlags {
ResolveFlags::BENEATH
| ResolveFlags::NO_MAGICLINKS
| ResolveFlags::NO_SYMLINKS
| ResolveFlags::NO_XDEV
}
fn host_path(bytes: &[u8]) -> PathBuf {