Confine runtime staging to retained authority
This commit is contained in:
parent
b2f178d25e
commit
fc5b2b6ca0
1 changed files with 434 additions and 173 deletions
|
|
@ -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(¤t) && 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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue