Replace runtime argv handoff with a sealed typed record

This commit is contained in:
ms2k 2026-07-19 03:56:49 +09:00
commit d70f483844
5 changed files with 725 additions and 465 deletions

View file

@ -227,13 +227,16 @@ impl<'a> ColdStart<'a> {
},
)
.map_err(|error| failure(format!("Runtime Supervisor launch: {error}")))?
.with_state_anchor(state_anchor);
.with_state_anchor(state_anchor)
.map_err(|error| failure(format!("Runtime Supervisor handoff: {error}")))?;
if let Some(git_routing) = &git_routing {
runtime_launch = runtime_launch.with_git_routing(
runtime_launch = runtime_launch
.with_git_routing(
git_routing
.launch_authority(inputs.configuration)
.map_err(|error| failure(format!("Git-routing authority: {error}")))?,
);
)
.map_err(|error| failure(format!("Runtime Supervisor handoff: {error}")))?;
}
let bundle_digest =
BundleDigest::new(Sha256::digest(sandbox_bundle.specification_json()).into());

View file

@ -2,6 +2,7 @@ use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::{self, Read};
use std::os::fd::{AsFd, AsRawFd, OwnedFd};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::os::unix::fs::FileExt;
use std::path::PathBuf;
use std::time::{Duration, Instant};
@ -18,7 +19,7 @@ use lazaret_core::records::{
};
use lazaret_ignored_workspace::{GitSentry, PRIVATE_ROUTER_ROLE};
use lazaret_linux::capability;
use lazaret_linux::filesystem::{mount_identity, sealed_memfd};
use lazaret_linux::filesystem::{mount_identity, require_write_sealed, sealed_memfd};
use lazaret_linux::process::{
ChildStandardIo, ParentEdge, ProcessOwnershipContext, RetainedProcess, TargetCommand,
child_broker, current_process_identity,
@ -44,6 +45,7 @@ use crate::sandbox::RunscCommand;
pub const PRIVATE_RUNTIME_ROLE: &str = "--lazaret-runtime-supervisor-role";
const MAX_CONTROL_MESSAGE: usize = 16 * 1024;
const MAX_RUNTIME_HANDOFF_BYTES: usize = 64 * 1024;
const READINESS_TIMEOUT: Duration = Duration::from_secs(30);
const TERMINATION_GRACE: Duration = Duration::from_secs(2);
@ -302,6 +304,7 @@ impl CoordinatorChannel {
pub struct RuntimeSupervisorLaunch {
target: TargetCommand,
handoff_file: Option<File>,
ownership: ProcessOwnershipContext,
policy_file: Option<File>,
policy_digest: Option<DerivedPolicyDigest>,
@ -414,21 +417,9 @@ impl RuntimeSupervisorLaunch {
"Runtime Supervisor requires foreground attached runsc",
));
}
let mut target = TargetCommand::new("/proc/self/exe")
.argument(PRIVATE_RUNTIME_ROLE)
.argument("--domain")
.argument(hex(ownership.domain_id.as_bytes()))
.argument("--generation")
.argument(ownership.generation_id.get().to_string())
.argument("--coordinator")
.argument(hex(ownership.coordinator_id.as_bytes()))
.argument("--runsc")
.argument(command.program());
for argument in command.arguments() {
target = target.argument(argument);
}
Ok(Self {
target: target.with_lifetime_pipe_parent_edge(),
let mut launch = Self {
target: TargetCommand::new("/usr/bin/false").with_lifetime_pipe_parent_edge(),
handoff_file: None,
ownership,
policy_file: None,
policy_digest: None,
@ -437,7 +428,9 @@ impl RuntimeSupervisorLaunch {
runsc_command: command.clone(),
state_anchor: None,
git_routing: None,
})
};
launch.refresh_target()?;
Ok(launch)
}
/// Builds the RS-owned network path with one inherited sealed policy document.
@ -449,161 +442,44 @@ impl RuntimeSupervisorLaunch {
command: &RunscCommand,
network: RuntimeNetworkLaunch<'_>,
) -> io::Result<Self> {
let runtime_supervisor_executable = std::env::current_exe()?;
let mut launch = Self::from_runsc_command(ownership, command)?;
let policy_file = sealed_memfd("lazaret-network-policy", network.policy.canonical_bytes())?;
fcntl_setfd(policy_file.as_fd(), FdFlags::empty())?;
let policy_fd = policy_file.as_raw_fd();
let target = std::mem::replace(
&mut launch.target,
TargetCommand::new("/usr/bin/false").with_lifetime_pipe_parent_edge(),
);
launch.target = TargetCommand::new(runtime_supervisor_executable)
.argument(PRIVATE_RUNTIME_ROLE)
.argument("--domain")
.argument(hex(ownership.domain_id.as_bytes()))
.argument("--generation")
.argument(ownership.generation_id.get().to_string())
.argument("--coordinator")
.argument(hex(ownership.coordinator_id.as_bytes()))
.argument("--network-policy-fd")
.argument(policy_fd.to_string())
.argument("--network-policy-digest")
.argument(hex(network.policy.digest().as_bytes()))
.argument("--configuration-fingerprint")
.argument(hex(network.configuration_fingerprint.as_bytes()))
.argument("--network-sidecar")
.argument(hex(network.network_sidecar_id.as_bytes()))
.argument("--runsc")
.argument(command.program());
for argument in command.arguments() {
launch.target = launch.target.argument(argument);
}
launch.target = launch.target.with_lifetime_pipe_parent_edge();
drop(target);
launch.policy_file = Some(policy_file);
launch.policy_digest = Some(network.policy.digest());
launch.configuration_fingerprint = Some(network.configuration_fingerprint);
launch.network_sidecar_id = Some(network.network_sidecar_id);
launch.refresh_target()?;
Ok(launch)
}
/// Adds exact inherited State Anchor authorities without beginning physical acquisition.
#[must_use]
pub fn with_state_anchor(mut self, authority: StateAnchorLaunchAuthority) -> Self {
/// # Errors
/// Refuses an invalid or unsealable State Anchor handoff.
pub fn with_state_anchor(mut self, authority: StateAnchorLaunchAuthority) -> io::Result<Self> {
self.state_anchor = Some(authority);
self.rebuild_target();
self
self.refresh_target()?;
Ok(self)
}
#[must_use]
pub fn with_git_routing(mut self, authority: RuntimeGitRoutingLaunch) -> Self {
/// # Errors
/// Refuses an invalid or unsealable Git-routing handoff.
pub fn with_git_routing(mut self, authority: RuntimeGitRoutingLaunch) -> io::Result<Self> {
self.git_routing = Some(authority);
self.rebuild_target();
self
self.refresh_target()?;
Ok(self)
}
fn rebuild_target(&mut self) {
let mut target = TargetCommand::new("/proc/self/exe")
fn refresh_target(&mut self) -> io::Result<()> {
let encoded = build_runtime_handoff(self)?;
let handoff = sealed_memfd("lazaret-runtime-handoff", &encoded)?;
fcntl_setfd(&handoff, FdFlags::empty())?;
self.target = TargetCommand::new("/proc/self/exe")
.argument(PRIVATE_RUNTIME_ROLE)
.argument("--domain")
.argument(hex(self.ownership.domain_id.as_bytes()))
.argument("--generation")
.argument(self.ownership.generation_id.get().to_string())
.argument("--coordinator")
.argument(hex(self.ownership.coordinator_id.as_bytes()));
if let Some(authority) = &self.state_anchor {
let (device, inode) = authority.expected_lower();
target = target
.argument("--state-lower-fd")
.argument(authority.lower_fd().to_string())
.argument("--state-lower-label")
.argument(authority.lower_label())
.argument("--state-persistent-root-fd")
.argument(authority.persistent_root_fd().to_string())
.argument("--state-persistent-fd")
.argument(authority.persistent_anchor_fd().to_string())
.argument("--state-persistent-label")
.argument(authority.persistent_anchor_label())
.argument("--state-runtime-root-fd")
.argument(authority.runtime_root_fd().to_string())
.argument("--state-runtime-fd")
.argument(authority.runtime_anchor_fd().to_string())
.argument("--state-runtime-label")
.argument(authority.runtime_anchor_label())
.argument("--state-lower-device")
.argument(device.to_string())
.argument("--state-lower-inode")
.argument(inode.to_string())
.argument("--state-persistent-count")
.argument(authority.persistent_destinations().len().to_string());
for destination in authority.persistent_destinations() {
target = target
.argument("--state-persistent-destination")
.argument(destination);
}
}
if let Some(authority) = &self.git_routing {
target = target
.argument("--git-workspace-fd")
.argument(authority.workspace.as_raw_fd().to_string())
.argument("--git-workspace-label")
.argument(&authority.workspace_label)
.argument("--git-isolated-fd")
.argument(authority.isolated.as_raw_fd().to_string())
.argument("--git-mountpoint-fd")
.argument(authority.mountpoint.as_raw_fd().to_string())
.argument("--git-mountpoint-label")
.argument(&authority.mountpoint_label)
.argument("--git-common-fd")
.argument(authority.git_common.as_raw_fd().to_string())
.argument("--git-common-label")
.argument(&authority.git_common_label)
.argument("--git-hooks-fd")
.argument(authority.transient_hooks.as_raw_fd().to_string())
.argument("--git-hooks-label")
.argument(&authority.transient_hooks_label)
.argument("--git-config-fd")
.argument(authority.transient_config.as_raw_fd().to_string())
.argument("--git-config-label")
.argument(&authority.transient_config_label)
.argument("--git-global-excludes")
.argument(
authority
.global_excludes
.as_deref()
.unwrap_or_else(|| std::path::Path::new("")),
)
.argument("--git-isolate-ignored")
.argument(if authority.isolate_ignored {
"true"
} else {
"false"
});
}
if let (Some(policy_file), Some(digest), Some(fingerprint), Some(sidecar)) = (
&self.policy_file,
self.policy_digest,
self.configuration_fingerprint,
self.network_sidecar_id,
) {
target = target
.argument("--network-policy-fd")
.argument(policy_file.as_raw_fd().to_string())
.argument("--network-policy-digest")
.argument(hex(digest.as_bytes()))
.argument("--configuration-fingerprint")
.argument(hex(fingerprint.as_bytes()))
.argument("--network-sidecar")
.argument(hex(sidecar.as_bytes()));
}
target = target
.argument("--runsc")
.argument(self.runsc_command.program());
for argument in self.runsc_command.arguments() {
target = target.argument(argument);
}
self.target = target.with_lifetime_pipe_parent_edge();
.argument(handoff.as_raw_fd().to_string())
.with_lifetime_pipe_parent_edge();
self.handoff_file = Some(handoff);
Ok(())
}
#[must_use]
@ -662,8 +538,7 @@ pub fn dispatch_private_runtime_role(arguments: &[OsString]) -> Option<io::Resul
{
return None;
}
let result =
parse_runtime_arguments(&arguments[1..]).and_then(|parsed| run_supervisor(&parsed));
let result = parse_runtime_handoff(&arguments[1..]).and_then(|parsed| run_supervisor(&parsed));
if let Err(error) = &result {
let _ = send_message(
std::io::stdout(),
@ -718,204 +593,7 @@ struct RuntimeNetworkArguments {
network_sidecar_id: NetworkSidecarId,
}
#[allow(clippy::too_many_lines)]
fn parse_runtime_arguments(arguments: &[OsString]) -> io::Result<RuntimeArguments> {
let mut arguments = arguments.iter().peekable();
let domain = parse_fixed_hex(named(&mut arguments, "--domain")?)?;
let generation = named(&mut arguments, "--generation")?
.to_str()
.and_then(|value| value.parse().ok())
.ok_or_else(|| io::Error::other("Runtime Supervisor generation is invalid"))?;
let coordinator = parse_fixed_hex(named(&mut arguments, "--coordinator")?)?;
let state_anchor = if arguments
.peek()
.is_some_and(|value| *value == "--state-lower-fd")
{
let lower_fd = parse_number(named(&mut arguments, "--state-lower-fd")?)?;
let lower_label = PathBuf::from(named(&mut arguments, "--state-lower-label")?);
let persistent_root_fd =
parse_number(named(&mut arguments, "--state-persistent-root-fd")?)?;
let persistent_anchor_fd = parse_number(named(&mut arguments, "--state-persistent-fd")?)?;
let persistent_anchor_label =
PathBuf::from(named(&mut arguments, "--state-persistent-label")?);
let runtime_root_fd = parse_number(named(&mut arguments, "--state-runtime-root-fd")?)?;
let runtime_anchor_fd = parse_number(named(&mut arguments, "--state-runtime-fd")?)?;
let runtime_anchor_label = PathBuf::from(named(&mut arguments, "--state-runtime-label")?);
let expected_lower = (
parse_number(named(&mut arguments, "--state-lower-device")?)?,
parse_number(named(&mut arguments, "--state-lower-inode")?)?,
);
let count: usize = parse_number(named(&mut arguments, "--state-persistent-count")?)?;
let persistent_destinations = (0..count)
.map(|_| named(&mut arguments, "--state-persistent-destination").map(PathBuf::from))
.collect::<io::Result<Vec<_>>>()?;
Some(RuntimeStateAnchorArguments {
lower_fd,
lower_label,
persistent_root_fd,
persistent_anchor_fd,
persistent_anchor_label,
runtime_root_fd,
runtime_anchor_fd,
runtime_anchor_label,
expected_lower,
persistent_destinations,
})
} else {
None
};
let git_routing = if arguments
.peek()
.is_some_and(|value| *value == "--git-workspace-fd")
{
let workspace_fd = parse_descriptor(named(&mut arguments, "--git-workspace-fd")?)?;
let workspace_label = PathBuf::from(named(&mut arguments, "--git-workspace-label")?);
let isolated_fd = parse_descriptor(named(&mut arguments, "--git-isolated-fd")?)?;
let mountpoint_fd = parse_descriptor(named(&mut arguments, "--git-mountpoint-fd")?)?;
let mountpoint_label = PathBuf::from(named(&mut arguments, "--git-mountpoint-label")?);
let git_common_fd = parse_descriptor(named(&mut arguments, "--git-common-fd")?)?;
let git_common_label = PathBuf::from(named(&mut arguments, "--git-common-label")?);
let transient_hooks_fd = parse_descriptor(named(&mut arguments, "--git-hooks-fd")?)?;
let transient_hooks_label = PathBuf::from(named(&mut arguments, "--git-hooks-label")?);
let transient_config_fd = parse_descriptor(named(&mut arguments, "--git-config-fd")?)?;
let transient_config_label = PathBuf::from(named(&mut arguments, "--git-config-label")?);
let global_excludes = match named(&mut arguments, "--git-global-excludes")? {
value if value.is_empty() => None,
value => Some(PathBuf::from(value)),
};
let isolate_ignored = match named(&mut arguments, "--git-isolate-ignored")?.to_str() {
Some("true") => true,
Some("false") => false,
_ => {
return Err(io::Error::other(
"Runtime Supervisor Git isolation mode is invalid",
));
}
};
Some(RuntimeGitRoutingArguments {
workspace_fd,
workspace_label,
isolated_fd,
mountpoint_fd,
mountpoint_label,
git_common_fd,
git_common_label,
transient_hooks_fd,
transient_hooks_label,
transient_config_fd,
transient_config_label,
global_excludes,
isolate_ignored,
})
} else {
None
};
let network = if arguments
.peek()
.is_some_and(|value| *value == "--network-policy-fd")
{
Some(parse_network_arguments(&mut arguments)?)
} else {
None
};
if arguments.next().is_none_or(|value| value != "--runsc") {
return Err(io::Error::other(
"Runtime Supervisor runsc separator is missing",
));
}
let program = arguments
.next()
.ok_or_else(|| io::Error::other("Runtime Supervisor runsc program is missing"))?;
if program != "runsc" {
return Err(io::Error::other("Runtime Supervisor target is not runsc"));
}
Ok(RuntimeArguments {
ownership: ProcessOwnershipContext {
domain_id: DomainId::new(domain),
generation_id: GenerationId::new(generation),
coordinator_id: CoordinatorId::new(coordinator),
slot: HostProcessOwnershipSlot::SandboxAttachedRunsc,
},
state_anchor,
git_routing,
network,
runsc_program: (*program).clone(),
runsc_arguments: arguments.cloned().collect(),
})
}
fn parse_number<T>(value: &OsStr) -> io::Result<T>
where
T: std::str::FromStr,
{
value
.to_str()
.and_then(|value| value.parse().ok())
.ok_or_else(|| io::Error::other("Runtime Supervisor numeric argument is invalid"))
}
fn parse_descriptor(value: &OsStr) -> io::Result<i32> {
parse_number(value)
.ok()
.filter(|descriptor| *descriptor >= 0)
.ok_or_else(|| io::Error::other("Runtime Supervisor descriptor is invalid"))
}
fn parse_network_arguments<'a>(
arguments: &mut impl Iterator<Item = &'a OsString>,
) -> io::Result<RuntimeNetworkArguments> {
let policy_fd = named(arguments, "--network-policy-fd")?
.to_str()
.and_then(|value| value.parse().ok())
.filter(|value| *value >= 0)
.ok_or_else(|| io::Error::other("Runtime Supervisor policy descriptor is invalid"))?;
let policy_digest = DerivedPolicyDigest::new(parse_fixed_hex(named(
arguments,
"--network-policy-digest",
)?)?);
let configuration_fingerprint = ConfigurationFingerprint::new(parse_fixed_hex(named(
arguments,
"--configuration-fingerprint",
)?)?);
let network_sidecar_id =
NetworkSidecarId::new(parse_fixed_hex(named(arguments, "--network-sidecar")?)?);
Ok(RuntimeNetworkArguments {
policy_fd,
policy_digest,
configuration_fingerprint,
network_sidecar_id,
})
}
fn named<'a>(
arguments: &mut impl Iterator<Item = &'a OsString>,
name: &str,
) -> io::Result<&'a OsString> {
if arguments.next().is_none_or(|value| value != name) {
return Err(io::Error::other(format!(
"Runtime Supervisor argument {name} is missing"
)));
}
arguments
.next()
.ok_or_else(|| io::Error::other(format!("Runtime Supervisor argument {name} has no value")))
}
fn parse_fixed_hex(value: &OsStr) -> io::Result<[u8; 32]> {
let value = value
.to_str()
.ok_or_else(|| io::Error::other("Runtime Supervisor identity is not UTF-8"))?;
if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(io::Error::other("Runtime Supervisor identity is invalid"));
}
let mut bytes = [0_u8; 32];
for (index, byte) in bytes.iter_mut().enumerate() {
*byte = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16)
.map_err(|_| io::Error::other("Runtime Supervisor identity is invalid"))?;
}
Ok(bytes)
}
include!("supervision/handoff.rs");
include!("supervision/startup.rs");
include!("supervision/runtime.rs");
include!("supervision/tests.rs");

View file

@ -0,0 +1,656 @@
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
enum RuntimeDescriptorKindV1 {
Directory,
RegularFile,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
struct RuntimeDescriptorV1 {
number: i32,
device: u64,
inode: u64,
kind: RuntimeDescriptorKindV1,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
struct RuntimeStateAnchorHandoffV1 {
lower: RuntimeDescriptorV1,
lower_label: Vec<u8>,
persistent_root: RuntimeDescriptorV1,
persistent_anchor: RuntimeDescriptorV1,
persistent_anchor_label: Vec<u8>,
runtime_root: RuntimeDescriptorV1,
runtime_anchor: RuntimeDescriptorV1,
runtime_anchor_label: Vec<u8>,
expected_lower: (u64, u64),
persistent_destinations: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
struct RuntimeGitRoutingHandoffV1 {
workspace: RuntimeDescriptorV1,
workspace_label: Vec<u8>,
isolated: RuntimeDescriptorV1,
mountpoint: RuntimeDescriptorV1,
mountpoint_label: Vec<u8>,
git_common: RuntimeDescriptorV1,
git_common_label: Vec<u8>,
transient_hooks: RuntimeDescriptorV1,
transient_hooks_label: Vec<u8>,
transient_config: RuntimeDescriptorV1,
transient_config_label: Vec<u8>,
global_excludes: Option<Vec<u8>>,
isolate_ignored: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
struct RuntimeNetworkHandoffV1 {
policy: RuntimeDescriptorV1,
policy_digest: DerivedPolicyDigest,
configuration_fingerprint: ConfigurationFingerprint,
network_sidecar_id: NetworkSidecarId,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
struct RuntimeSupervisorHandoffV1 {
domain_id: DomainId,
generation_id: GenerationId,
coordinator_id: CoordinatorId,
state_anchor: Option<RuntimeStateAnchorHandoffV1>,
git_routing: Option<RuntimeGitRoutingHandoffV1>,
network: Option<RuntimeNetworkHandoffV1>,
runsc_program: Vec<u8>,
runsc_arguments: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
enum RuntimeSupervisorHandoff {
V1(RuntimeSupervisorHandoffV1),
}
fn descriptor(
number: i32,
kind: RuntimeDescriptorKindV1,
) -> io::Result<RuntimeDescriptorV1> {
if number < 0 {
return Err(io::Error::other("Runtime Supervisor descriptor is invalid"));
}
let file = File::open(format!("/proc/self/fd/{number}"))?;
let status = fstat(&file)?;
let actual = FileType::from_raw_mode(status.st_mode);
let expected = match kind {
RuntimeDescriptorKindV1::Directory => actual.is_dir(),
RuntimeDescriptorKindV1::RegularFile => actual.is_file(),
};
if !expected {
return Err(io::Error::other(
"Runtime Supervisor descriptor has the wrong role type",
));
}
Ok(RuntimeDescriptorV1 {
number,
device: status.st_dev,
inode: status.st_ino,
kind,
})
}
fn build_runtime_handoff(launch: &RuntimeSupervisorLaunch) -> io::Result<Vec<u8>> {
let state_anchor = launch
.state_anchor
.as_ref()
.map(build_state_anchor_handoff)
.transpose()?;
let git_routing = launch
.git_routing
.as_ref()
.map(build_git_routing_handoff)
.transpose()?;
let network = build_network_handoff(launch)?;
let handoff = RuntimeSupervisorHandoff::V1(RuntimeSupervisorHandoffV1 {
domain_id: launch.ownership.domain_id,
generation_id: launch.ownership.generation_id,
coordinator_id: launch.ownership.coordinator_id,
state_anchor,
git_routing,
network,
runsc_program: launch.runsc_command.program().as_bytes().to_vec(),
runsc_arguments: launch
.runsc_command
.arguments()
.iter()
.map(|argument| argument.as_bytes().to_vec())
.collect(),
});
let encoded = encode(&handoff).map_err(io::Error::other)?;
if encoded.len() > MAX_RUNTIME_HANDOFF_BYTES {
return Err(io::Error::other(
"Runtime Supervisor handoff is oversized",
));
}
Ok(encoded)
}
fn build_state_anchor_handoff(
authority: &StateAnchorLaunchAuthority,
) -> io::Result<RuntimeStateAnchorHandoffV1> {
Ok(RuntimeStateAnchorHandoffV1 {
lower: descriptor(
authority.lower_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
lower_label: authority.lower_label().as_os_str().as_bytes().to_vec(),
persistent_root: descriptor(
authority.persistent_root_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
persistent_anchor: descriptor(
authority.persistent_anchor_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
persistent_anchor_label: authority
.persistent_anchor_label()
.as_os_str()
.as_bytes()
.to_vec(),
runtime_root: descriptor(
authority.runtime_root_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
runtime_anchor: descriptor(
authority.runtime_anchor_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
runtime_anchor_label: authority
.runtime_anchor_label()
.as_os_str()
.as_bytes()
.to_vec(),
expected_lower: authority.expected_lower(),
persistent_destinations: authority
.persistent_destinations()
.iter()
.map(|path| path.as_os_str().as_bytes().to_vec())
.collect(),
})
}
fn build_git_routing_handoff(
authority: &RuntimeGitRoutingLaunch,
) -> io::Result<RuntimeGitRoutingHandoffV1> {
Ok(RuntimeGitRoutingHandoffV1 {
workspace: descriptor(
authority.workspace.as_raw_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
workspace_label: authority.workspace_label.as_os_str().as_bytes().to_vec(),
isolated: descriptor(
authority.isolated.as_raw_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
mountpoint: descriptor(
authority.mountpoint.as_raw_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
mountpoint_label: authority.mountpoint_label.as_os_str().as_bytes().to_vec(),
git_common: descriptor(
authority.git_common.as_raw_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
git_common_label: authority.git_common_label.as_os_str().as_bytes().to_vec(),
transient_hooks: descriptor(
authority.transient_hooks.as_raw_fd(),
RuntimeDescriptorKindV1::Directory,
)?,
transient_hooks_label: authority
.transient_hooks_label
.as_os_str()
.as_bytes()
.to_vec(),
transient_config: descriptor(
authority.transient_config.as_raw_fd(),
RuntimeDescriptorKindV1::RegularFile,
)?,
transient_config_label: authority
.transient_config_label
.as_os_str()
.as_bytes()
.to_vec(),
global_excludes: authority
.global_excludes
.as_ref()
.map(|path| path.as_os_str().as_bytes().to_vec()),
isolate_ignored: authority.isolate_ignored,
})
}
fn build_network_handoff(
launch: &RuntimeSupervisorLaunch,
) -> io::Result<Option<RuntimeNetworkHandoffV1>> {
let network = launch
.policy_file
.as_ref()
.map(|policy| -> io::Result<RuntimeNetworkHandoffV1> {
Ok(RuntimeNetworkHandoffV1 {
policy: descriptor(
policy.as_raw_fd(),
RuntimeDescriptorKindV1::RegularFile,
)?,
policy_digest: launch
.policy_digest
.ok_or_else(|| io::Error::other("network policy digest is missing"))?,
configuration_fingerprint: launch.configuration_fingerprint.ok_or_else(|| {
io::Error::other("network configuration fingerprint is missing")
})?,
network_sidecar_id: launch
.network_sidecar_id
.ok_or_else(|| io::Error::other("network sidecar identity is missing"))?,
})
})
.transpose()?;
if network.is_none()
&& (launch.policy_digest.is_some()
|| launch.configuration_fingerprint.is_some()
|| launch.network_sidecar_id.is_some())
{
return Err(io::Error::other(
"Runtime Supervisor network handoff is partial",
));
}
Ok(network)
}
fn parse_runtime_handoff(arguments: &[OsString]) -> io::Result<RuntimeArguments> {
let [descriptor] = arguments else {
return Err(io::Error::other(
"Runtime Supervisor requires one handoff descriptor",
));
};
let handoff_descriptor = parse_descriptor(descriptor)?;
let mut file = File::open(format!("/proc/self/fd/{handoff_descriptor}"))?;
require_write_sealed(&file)?;
let length = usize::try_from(file.metadata()?.len())
.ok()
.filter(|length| *length > 0 && *length <= MAX_RUNTIME_HANDOFF_BYTES)
.ok_or_else(|| io::Error::other("Runtime Supervisor handoff size is invalid"))?;
let mut encoded = vec![0_u8; length];
file.read_exact(&mut encoded)?;
let RuntimeSupervisorHandoff::V1(handoff) =
decode::<RuntimeSupervisorHandoff>(&encoded).map_err(io::Error::other)?;
validate_runtime_handoff(handoff_descriptor, &handoff)?;
if handoff.runsc_program.as_slice() != b"runsc" {
return Err(io::Error::other("Runtime Supervisor target is not runsc"));
}
if !handoff
.runsc_arguments
.iter()
.any(|argument| argument.as_slice() == b"run")
|| handoff.runsc_arguments.iter().any(|argument| {
matches!(
argument.as_slice(),
b"-detach" | b"--detach" | b"-d"
)
})
{
return Err(io::Error::other(
"Runtime Supervisor requires foreground attached runsc",
));
}
Ok(RuntimeArguments {
ownership: ProcessOwnershipContext {
domain_id: handoff.domain_id,
generation_id: handoff.generation_id,
coordinator_id: handoff.coordinator_id,
slot: HostProcessOwnershipSlot::SandboxAttachedRunsc,
},
state_anchor: handoff.state_anchor.map(runtime_state_anchor_arguments),
git_routing: handoff.git_routing.map(runtime_git_routing_arguments),
network: handoff.network.map(|network| RuntimeNetworkArguments {
policy_fd: network.policy.number,
policy_digest: network.policy_digest,
configuration_fingerprint: network.configuration_fingerprint,
network_sidecar_id: network.network_sidecar_id,
}),
runsc_program: OsString::from_vec(handoff.runsc_program),
runsc_arguments: handoff
.runsc_arguments
.into_iter()
.map(OsString::from_vec)
.collect(),
})
}
fn parse_descriptor(value: &OsStr) -> io::Result<i32> {
value
.to_str()
.and_then(|value| value.parse().ok())
.filter(|descriptor| *descriptor >= 0)
.ok_or_else(|| io::Error::other("Runtime Supervisor descriptor is invalid"))
}
fn validate_runtime_handoff(
handoff_descriptor: i32,
handoff: &RuntimeSupervisorHandoffV1,
) -> io::Result<()> {
let mut numbers = std::collections::BTreeSet::from([handoff_descriptor]);
let mut descriptors = Vec::new();
if let Some(state) = &handoff.state_anchor {
descriptors.extend([
state.lower,
state.persistent_root,
state.persistent_anchor,
state.runtime_root,
state.runtime_anchor,
]);
if state.expected_lower != (state.lower.device, state.lower.inode) {
return Err(io::Error::other(
"State Anchor lower identity differs from its descriptor slot",
));
}
}
if let Some(git) = &handoff.git_routing {
descriptors.extend([
git.workspace,
git.isolated,
git.mountpoint,
git.git_common,
git.transient_hooks,
git.transient_config,
]);
}
if let Some(network) = &handoff.network {
descriptors.push(network.policy);
}
for expected in descriptors {
if !numbers.insert(expected.number) {
return Err(io::Error::other(
"Runtime Supervisor descriptor roles are duplicated",
));
}
let actual = descriptor(expected.number, expected.kind)?;
if actual != expected {
return Err(io::Error::other(
"Runtime Supervisor descriptor role identity changed",
));
}
if handoff
.network
.as_ref()
.is_some_and(|network| network.policy == expected)
{
let file = File::open(format!("/proc/self/fd/{}", expected.number))?;
require_write_sealed(file)?;
}
}
Ok(())
}
fn runtime_state_anchor_arguments(
state: RuntimeStateAnchorHandoffV1,
) -> RuntimeStateAnchorArguments {
RuntimeStateAnchorArguments {
lower_fd: state.lower.number,
lower_label: PathBuf::from(OsString::from_vec(state.lower_label)),
persistent_root_fd: state.persistent_root.number,
persistent_anchor_fd: state.persistent_anchor.number,
persistent_anchor_label: PathBuf::from(OsString::from_vec(
state.persistent_anchor_label,
)),
runtime_root_fd: state.runtime_root.number,
runtime_anchor_fd: state.runtime_anchor.number,
runtime_anchor_label: PathBuf::from(OsString::from_vec(state.runtime_anchor_label)),
expected_lower: state.expected_lower,
persistent_destinations: state
.persistent_destinations
.into_iter()
.map(|path| PathBuf::from(OsString::from_vec(path)))
.collect(),
}
}
fn runtime_git_routing_arguments(
routing: RuntimeGitRoutingHandoffV1,
) -> RuntimeGitRoutingArguments {
RuntimeGitRoutingArguments {
workspace_fd: routing.workspace.number,
workspace_label: PathBuf::from(OsString::from_vec(routing.workspace_label)),
isolated_fd: routing.isolated.number,
mountpoint_fd: routing.mountpoint.number,
mountpoint_label: PathBuf::from(OsString::from_vec(routing.mountpoint_label)),
git_common_fd: routing.git_common.number,
git_common_label: PathBuf::from(OsString::from_vec(routing.git_common_label)),
transient_hooks_fd: routing.transient_hooks.number,
transient_hooks_label: PathBuf::from(OsString::from_vec(routing.transient_hooks_label)),
transient_config_fd: routing.transient_config.number,
transient_config_label: PathBuf::from(OsString::from_vec(routing.transient_config_label)),
global_excludes: routing
.global_excludes
.map(|path| PathBuf::from(OsString::from_vec(path))),
isolate_ignored: routing.isolate_ignored,
}
}
#[cfg(test)]
mod handoff_tests {
use std::io::{Seek, SeekFrom, Write};
use std::os::unix::ffi::OsStrExt;
use rustix::fs::{MemfdFlags, memfd_create};
use sha2::{Digest, Sha256};
use super::*;
fn minimal_handoff(arguments: Vec<Vec<u8>>) -> RuntimeSupervisorHandoff {
RuntimeSupervisorHandoff::V1(RuntimeSupervisorHandoffV1 {
domain_id: DomainId::new([1; 32]),
generation_id: GenerationId::new(2),
coordinator_id: CoordinatorId::new([3; 32]),
state_anchor: None,
git_routing: None,
network: None,
runsc_program: b"runsc".to_vec(),
runsc_arguments: arguments,
})
}
fn sealed_arguments(bytes: &[u8]) -> (File, [OsString; 1]) {
let file = sealed_memfd("runtime-handoff-test", bytes).unwrap();
let arguments = [OsString::from(file.as_raw_fd().to_string())];
(file, arguments)
}
fn parse_record(record: &RuntimeSupervisorHandoff) -> io::Result<RuntimeArguments> {
let encoded = encode(record).unwrap();
let (_file, arguments) = sealed_arguments(&encoded);
parse_runtime_handoff(&arguments)
}
#[test]
fn v1_vector_and_non_utf8_runsc_arguments_are_stable() {
let directory = |number, identity| RuntimeDescriptorV1 {
number,
device: identity,
inode: identity + 1,
kind: RuntimeDescriptorKindV1::Directory,
};
let regular = |number, identity| RuntimeDescriptorV1 {
kind: RuntimeDescriptorKindV1::RegularFile,
..directory(number, identity)
};
let record = RuntimeSupervisorHandoff::V1(RuntimeSupervisorHandoffV1 {
domain_id: DomainId::new([1; 32]),
generation_id: GenerationId::new(2),
coordinator_id: CoordinatorId::new([3; 32]),
state_anchor: Some(RuntimeStateAnchorHandoffV1 {
lower: directory(10, 100),
lower_label: b"/lower".to_vec(),
persistent_root: directory(11, 110),
persistent_anchor: directory(12, 120),
persistent_anchor_label: b"/persistent/state-anchor".to_vec(),
runtime_root: directory(13, 130),
runtime_anchor: directory(14, 140),
runtime_anchor_label: b"/runtime/state-anchor".to_vec(),
expected_lower: (100, 101),
persistent_destinations: vec![b"/var/lib/tool".to_vec()],
}),
git_routing: Some(RuntimeGitRoutingHandoffV1 {
workspace: directory(15, 150),
workspace_label: b"/workspace".to_vec(),
isolated: directory(16, 160),
mountpoint: directory(17, 170),
mountpoint_label: b"/runtime/workspace".to_vec(),
git_common: directory(18, 180),
git_common_label: b"/workspace/.git".to_vec(),
transient_hooks: directory(19, 190),
transient_hooks_label: b"/runtime/hooks".to_vec(),
transient_config: regular(20, 200),
transient_config_label: b"/runtime/config".to_vec(),
global_excludes: Some(b"/home/user/.config/git/ignore".to_vec()),
isolate_ignored: true,
}),
network: Some(RuntimeNetworkHandoffV1 {
policy: regular(21, 210),
policy_digest: DerivedPolicyDigest::new([4; 32]),
configuration_fingerprint: ConfigurationFingerprint::new([5; 32]),
network_sidecar_id: NetworkSidecarId::new([6; 32]),
}),
runsc_program: b"runsc".to_vec(),
runsc_arguments: vec![
b"-root=/runtime".to_vec(),
b"run".to_vec(),
vec![b'c', 0xff],
],
});
let encoded = encode(&record).unwrap();
assert_eq!(
Sha256::digest(&encoded).as_slice(),
[
123, 0, 18, 227, 164, 11, 96, 15, 4, 201, 99, 25, 198, 122, 175, 149, 170,
165, 12, 40, 209, 64, 119, 67, 10, 109, 128, 44, 201, 103, 14, 182,
]
);
assert_eq!(
decode::<RuntimeSupervisorHandoff>(&encoded).unwrap(),
record
);
let parsed = parse_record(&minimal_handoff(vec![
b"-root=/runtime".to_vec(),
b"run".to_vec(),
vec![b'c', 0xff],
]))
.unwrap();
assert_eq!(parsed.runsc_arguments[2].as_bytes(), [b'c', 0xff]);
}
#[test]
fn unknown_trailing_truncated_unsealed_and_oversize_handoffs_refuse() {
let record = minimal_handoff(vec![b"run".to_vec(), b"container".to_vec()]);
let encoded = encode(&record).unwrap();
for malformed in [
vec![1],
{
let mut trailing = encoded.clone();
trailing.push(0);
trailing
},
encoded[..encoded.len() - 1].to_vec(),
] {
let (_file, arguments) = sealed_arguments(&malformed);
assert!(parse_runtime_handoff(&arguments).is_err());
}
let descriptor = memfd_create("unsealed-handoff", MemfdFlags::ALLOW_SEALING).unwrap();
let mut unsealed = File::from(descriptor);
unsealed.write_all(&encoded).unwrap();
unsealed.seek(SeekFrom::Start(0)).unwrap();
let arguments = [OsString::from(unsealed.as_raw_fd().to_string())];
assert!(parse_runtime_handoff(&arguments).is_err());
let oversized = vec![0_u8; MAX_RUNTIME_HANDOFF_BYTES + 1];
let (_file, arguments) = sealed_arguments(&oversized);
assert!(parse_runtime_handoff(&arguments).is_err());
}
#[test]
fn descriptor_identity_role_substitution_and_duplication_refuse() {
let first_tree = tempfile::tempdir().unwrap();
let second_tree = tempfile::tempdir().unwrap();
let first = File::open(first_tree.path()).unwrap();
let second = File::open(second_tree.path()).unwrap();
let first_slot =
descriptor(first.as_raw_fd(), RuntimeDescriptorKindV1::Directory).unwrap();
let second_slot =
descriptor(second.as_raw_fd(), RuntimeDescriptorKindV1::Directory).unwrap();
let mut role_mismatch = state_record(first_slot);
role_mismatch.lower.kind = RuntimeDescriptorKindV1::RegularFile;
assert!(parse_record(&with_state(role_mismatch)).is_err());
let mut identity_drift = state_record(first_slot);
identity_drift.lower.inode = identity_drift.lower.inode.wrapping_add(1);
identity_drift.expected_lower = (
identity_drift.lower.device,
identity_drift.lower.inode,
);
assert!(parse_record(&with_state(identity_drift)).is_err());
let mut substitution = state_record(first_slot);
substitution.lower.number = second_slot.number;
assert!(parse_record(&with_state(substitution)).is_err());
assert!(parse_record(&with_state(state_record(first_slot))).is_err());
}
#[test]
fn partial_network_group_and_nonexact_private_argv_refuse() {
let ownership = ProcessOwnershipContext {
domain_id: DomainId::new([4; 32]),
generation_id: GenerationId::new(5),
coordinator_id: CoordinatorId::new([6; 32]),
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
};
let launch = RuntimeSupervisorLaunch {
target: TargetCommand::new("/usr/bin/false").with_lifetime_pipe_parent_edge(),
handoff_file: None,
ownership,
policy_file: None,
policy_digest: Some(DerivedPolicyDigest::new([7; 32])),
configuration_fingerprint: None,
network_sidecar_id: None,
runsc_command: RunscCommand::new(
"runsc",
[OsString::from("run"), OsString::from("container")],
[8; 32],
),
state_anchor: None,
git_routing: None,
};
assert!(build_runtime_handoff(&launch).is_err());
assert!(parse_runtime_handoff(&[]).is_err());
assert!(
parse_runtime_handoff(&[OsString::from("1"), OsString::from("2")]).is_err()
);
}
fn state_record(slot: RuntimeDescriptorV1) -> RuntimeStateAnchorHandoffV1 {
RuntimeStateAnchorHandoffV1 {
lower: slot,
lower_label: b"/lower".to_vec(),
persistent_root: slot,
persistent_anchor: slot,
persistent_anchor_label: b"/persistent".to_vec(),
runtime_root: slot,
runtime_anchor: slot,
runtime_anchor_label: b"/runtime".to_vec(),
expected_lower: (slot.device, slot.inode),
persistent_destinations: vec![b"/state".to_vec()],
}
}
fn with_state(state_anchor: RuntimeStateAnchorHandoffV1) -> RuntimeSupervisorHandoff {
let RuntimeSupervisorHandoff::V1(mut handoff) =
minimal_handoff(vec![b"run".to_vec(), b"container".to_vec()]);
handoff.state_anchor = Some(state_anchor);
RuntimeSupervisorHandoff::V1(handoff)
}
}

View file

@ -598,12 +598,3 @@ fn receive_exact(descriptor: impl AsFd + Copy, mut bytes: &mut [u8]) -> io::Resu
}
Ok(())
}
fn hex(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes.iter().fold(String::new(), |mut output, byte| {
write!(output, "{byte:02x}").expect("writing to a String cannot fail");
output
})
}

View file

@ -101,6 +101,7 @@ mod tests {
};
let launch = RuntimeSupervisorLaunch {
target: TargetCommand::new("/proc/self/exe").with_lifetime_pipe_parent_edge(),
handoff_file: None,
ownership,
policy_file: None,
policy_digest: None,
@ -155,107 +156,38 @@ mod tests {
}
#[test]
fn runtime_role_parser_preserves_exact_ownership_and_runsc_arguments() {
let domain = [1_u8; 32];
let coordinator = [2_u8; 32];
let arguments = [
"--domain".into(),
hex(&domain).into(),
"--generation".into(),
"7".into(),
"--coordinator".into(),
hex(&coordinator).into(),
"--runsc".into(),
"runsc".into(),
"-root=/runtime".into(),
"run".into(),
"container".into(),
];
let parsed = parse_runtime_arguments(&arguments).unwrap();
assert_eq!(parsed.ownership.domain_id, DomainId::new(domain));
assert_eq!(parsed.ownership.generation_id, GenerationId::new(7));
fn runtime_handoff_preserves_exact_ownership_and_runsc_arguments() {
let ownership = ProcessOwnershipContext {
domain_id: DomainId::new([1; 32]),
generation_id: GenerationId::new(7),
coordinator_id: CoordinatorId::new([2; 32]),
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
};
let launch = RuntimeSupervisorLaunch::from_runsc_command(
ownership,
&RunscCommand::new(
"runsc",
[
OsString::from("-root=/runtime"),
OsString::from("run"),
OsString::from("container"),
],
[0; 32],
),
)
.unwrap();
assert_eq!(
parsed.ownership.coordinator_id,
CoordinatorId::new(coordinator)
launch.target().arguments().first(),
Some(&OsString::from(PRIVATE_RUNTIME_ROLE))
);
assert_eq!(
parsed.ownership.slot,
HostProcessOwnershipSlot::SandboxAttachedRunsc
);
assert_eq!(parsed.runsc_program, OsString::from("runsc"));
assert_eq!(launch.target().arguments().len(), 2);
let parsed = parse_runtime_handoff(&launch.target().arguments()[1..]).unwrap();
assert_eq!(parsed.ownership.domain_id, ownership.domain_id);
assert_eq!(parsed.ownership.generation_id, ownership.generation_id);
assert_eq!(parsed.ownership.coordinator_id, ownership.coordinator_id);
assert_eq!(
parsed.runsc_arguments,
["-root=/runtime", "run", "container"].map(OsString::from)
);
}
#[test]
fn runtime_role_parser_refuses_non_runsc_target() {
let arguments = [
"--domain".into(),
hex(&[1_u8; 32]).into(),
"--generation".into(),
"7".into(),
"--coordinator".into(),
hex(&[2_u8; 32]).into(),
"--runsc".into(),
"foreign-runtime".into(),
];
assert!(parse_runtime_arguments(&arguments).is_err());
}
#[test]
fn runtime_role_parser_preserves_git_routing_authorities() {
let arguments = [
"--domain".into(),
hex(&[1_u8; 32]).into(),
"--generation".into(),
"7".into(),
"--coordinator".into(),
hex(&[2_u8; 32]).into(),
"--git-workspace-fd".into(),
"3".into(),
"--git-workspace-label".into(),
"/workspace".into(),
"--git-isolated-fd".into(),
"4".into(),
"--git-mountpoint-fd".into(),
"5".into(),
"--git-mountpoint-label".into(),
"/runtime/workspace".into(),
"--git-common-fd".into(),
"6".into(),
"--git-common-label".into(),
"/workspace/.git".into(),
"--git-hooks-fd".into(),
"7".into(),
"--git-hooks-label".into(),
"/runtime/hooks".into(),
"--git-config-fd".into(),
"8".into(),
"--git-config-label".into(),
"/runtime/config".into(),
"--git-global-excludes".into(),
"/home/developer/.config/git/ignore".into(),
"--git-isolate-ignored".into(),
"true".into(),
"--runsc".into(),
"runsc".into(),
"run".into(),
"container".into(),
];
let parsed = parse_runtime_arguments(&arguments).unwrap();
let routing = parsed.git_routing.unwrap();
assert_eq!(routing.workspace_fd, 3);
assert_eq!(routing.isolated_fd, 4);
assert_eq!(routing.mountpoint_fd, 5);
assert_eq!(routing.git_common_fd, 6);
assert_eq!(routing.transient_hooks_fd, 7);
assert_eq!(routing.transient_config_fd, 8);
assert!(routing.isolate_ignored);
let mut negative = arguments;
negative[7] = "-1".into();
assert!(parse_runtime_arguments(&negative).is_err());
}
}