Bind sandbox handoff authority and restore verifier gates
This commit is contained in:
parent
cf914bdf51
commit
49d5def1a9
5 changed files with 951 additions and 170 deletions
|
|
@ -16,6 +16,7 @@ oci-spec.workspace = true
|
|||
serde.workspace = true
|
||||
rustix.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "=3.27.0"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ use lazaret_core::records::{
|
|||
};
|
||||
use lazaret_linux::filesystem::{namespace_identity, require_write_sealed};
|
||||
use lazaret_linux::process::{
|
||||
ChildStandardIo, ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker,
|
||||
AcknowledgedChildAcquisition, ChildStandardIo, ProcessOwnershipContext, RetainedProcess,
|
||||
TargetCommand, child_broker,
|
||||
};
|
||||
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
||||
use rustix::io::{FdFlags, fcntl_setfd, read};
|
||||
|
|
@ -161,15 +162,17 @@ where
|
|||
helper_ready_signal.as_raw_fd(),
|
||||
);
|
||||
let helper = child_broker().acquire_with_stdio_after_ack(
|
||||
context.runtime_supervisor,
|
||||
ProcessOwnershipContext {
|
||||
domain_id: context.domain_id,
|
||||
generation_id: context.generation_id,
|
||||
coordinator_id: context.coordinator_id,
|
||||
slot: HostProcessOwnershipSlot::HelperConnectivityHelper,
|
||||
AcknowledgedChildAcquisition {
|
||||
expected_parent: context.runtime_supervisor,
|
||||
ownership: ProcessOwnershipContext {
|
||||
domain_id: context.domain_id,
|
||||
generation_id: context.generation_id,
|
||||
coordinator_id: context.coordinator_id,
|
||||
slot: HostProcessOwnershipSlot::HelperConnectivityHelper,
|
||||
},
|
||||
target: &target,
|
||||
standard_io: ChildStandardIo::silenced(),
|
||||
},
|
||||
&target,
|
||||
ChildStandardIo::silenced(),
|
||||
stage,
|
||||
acknowledge,
|
||||
enter_domain_namespaces,
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@ use std::os::fd::{AsFd, OwnedFd};
|
|||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use lazaret_core::configuration::{ResolvedDomainConfiguration, SandboxLayoutV1};
|
||||
use lazaret_core::identity::{ContainerId, NamespaceIdentity};
|
||||
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration, SandboxLayoutV1};
|
||||
use lazaret_core::identity::{ConfigurationFingerprint, ContainerId, DomainId, NamespaceIdentity};
|
||||
use lazaret_core::records::VolumeAccess;
|
||||
use oci_spec::runtime::{
|
||||
Capabilities, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxIdMappingBuilder,
|
||||
LinuxNamespaceBuilder, LinuxNamespaceType, Mount, MountBuilder, ProcessBuilder, RootBuilder,
|
||||
Spec, SpecBuilder, UserBuilder,
|
||||
};
|
||||
use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, openat2};
|
||||
use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, StatxFlags, openat2, statx};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub const WORKSPACE_DESTINATION: &str = "/workspace";
|
||||
pub const CONTROL_ROOT_DESTINATION: &str = "/run/lazaret";
|
||||
|
|
@ -122,11 +123,20 @@ impl RootProcess {
|
|||
struct FileIdentity {
|
||||
device: u64,
|
||||
inode: u64,
|
||||
mount_id: u64,
|
||||
}
|
||||
|
||||
impl FileIdentity {
|
||||
fn from_descriptor(descriptor: &OwnedFd) -> Result<Self, SandboxBuildError> {
|
||||
let stat = rustix::fs::fstat(descriptor).map_err(staging_error)?;
|
||||
let extended = statx(descriptor, "", AtFlags::EMPTY_PATH, StatxFlags::MNT_ID)
|
||||
.map_err(staging_error)?;
|
||||
if !StatxFlags::from_bits_retain(extended.stx_mask).contains(StatxFlags::MNT_ID) {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"host filesystem does not report mount identity",
|
||||
));
|
||||
}
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_file() {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
|
|
@ -136,10 +146,142 @@ impl FileIdentity {
|
|||
Ok(Self {
|
||||
device: stat.st_dev,
|
||||
inode: stat.st_ino,
|
||||
mount_id: extended.stx_mnt_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum HandoffPathType {
|
||||
Directory,
|
||||
RegularFile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct DerivedHandoffAuthority {
|
||||
relative_path: PathBuf,
|
||||
expected_type: HandoffPathType,
|
||||
identity: FileIdentity,
|
||||
expected_bytes: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct RuntimeHandoffSource {
|
||||
relative_path: PathBuf,
|
||||
expected_type: HandoffPathType,
|
||||
build_identity: Option<FileIdentity>,
|
||||
expected_bytes: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct PathAuthorityIdentity {
|
||||
device: u64,
|
||||
inode: u64,
|
||||
mount_id: u64,
|
||||
}
|
||||
|
||||
impl PathAuthorityIdentity {
|
||||
fn from_plan(plan: &ConfinedPathPlan) -> Self {
|
||||
Self {
|
||||
device: plan.target_device().get(),
|
||||
inode: plan.target_inode().get(),
|
||||
mount_id: plan.target_mount_id().get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct ConfigurationAuthorityIdentity {
|
||||
workspace: PathAuthorityIdentity,
|
||||
runtime_domain: PathAuthorityIdentity,
|
||||
volumes: Vec<PathAuthorityIdentity>,
|
||||
}
|
||||
|
||||
impl ConfigurationAuthorityIdentity {
|
||||
fn capture(configuration: &ResolvedDomainConfiguration) -> Self {
|
||||
Self {
|
||||
workspace: PathAuthorityIdentity::from_plan(configuration.workspace()),
|
||||
runtime_domain: PathAuthorityIdentity::from_plan(configuration.runtime_domain_root()),
|
||||
volumes: configuration
|
||||
.volumes()
|
||||
.iter()
|
||||
.map(|volume| PathAuthorityIdentity::from_plan(volume.source_plan()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SandboxHandoffAuthority<'configuration> {
|
||||
configuration: &'configuration ResolvedDomainConfiguration,
|
||||
domain_id: DomainId,
|
||||
configuration_fingerprint: ConfigurationFingerprint,
|
||||
configuration_authority: ConfigurationAuthorityIdentity,
|
||||
command: RunscCommand,
|
||||
derived: Vec<DerivedHandoffAuthority>,
|
||||
}
|
||||
|
||||
impl SandboxHandoffAuthority<'_> {
|
||||
/// Revalidates every pathname source immediately before attached runsc is released.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses changed workspace, volume, Runtime Domain, bundle, rootfs, control, or terminal
|
||||
/// authority.
|
||||
pub fn revalidate(&self) -> Result<(), SandboxBuildError> {
|
||||
if self.configuration.domain_id() != self.domain_id
|
||||
|| self.configuration.configuration_fingerprint() != self.configuration_fingerprint
|
||||
|| ConfigurationAuthorityIdentity::capture(self.configuration)
|
||||
!= self.configuration_authority
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"sandbox handoff configuration does not match its bundle",
|
||||
));
|
||||
}
|
||||
self.configuration
|
||||
.workspace()
|
||||
.open_authority()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
for volume in self.configuration.volumes() {
|
||||
volume
|
||||
.open_source()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
}
|
||||
let runtime_root = self
|
||||
.configuration
|
||||
.runtime_domain_root()
|
||||
.open_authority()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
for authority in &self.derived {
|
||||
let descriptor = open_runtime_source(
|
||||
&runtime_root,
|
||||
&authority.relative_path,
|
||||
authority.expected_type,
|
||||
authority.expected_bytes.is_some(),
|
||||
)?;
|
||||
if descriptor_identity(&descriptor)? != authority.identity {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"runtime handoff source changed identity",
|
||||
));
|
||||
}
|
||||
if let Some(expected) = &authority.expected_bytes {
|
||||
require_exact_bytes(descriptor, expected)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn domain_id(&self) -> DomainId {
|
||||
self.domain_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn matches_command(&self, command: &RunscCommand) -> bool {
|
||||
self.command == *command
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct HostTerminal {
|
||||
term: String,
|
||||
|
|
@ -279,9 +421,22 @@ pub struct SandboxBuildRequest<'a> {
|
|||
pub struct RunscCommand {
|
||||
program: OsString,
|
||||
arguments: Vec<OsString>,
|
||||
bundle_authorization: [u8; 32],
|
||||
}
|
||||
|
||||
impl RunscCommand {
|
||||
pub(crate) fn new(
|
||||
program: impl Into<OsString>,
|
||||
arguments: impl IntoIterator<Item = OsString>,
|
||||
bundle_authorization: [u8; 32],
|
||||
) -> Self {
|
||||
Self {
|
||||
program: program.into(),
|
||||
arguments: arguments.into_iter().collect(),
|
||||
bundle_authorization,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn program(&self) -> &OsStr {
|
||||
&self.program
|
||||
|
|
@ -298,6 +453,10 @@ pub struct SandboxBundle {
|
|||
specification: Spec,
|
||||
specification_json: Vec<u8>,
|
||||
command: RunscCommand,
|
||||
domain_id: DomainId,
|
||||
configuration_fingerprint: ConfigurationFingerprint,
|
||||
configuration_authority: ConfigurationAuthorityIdentity,
|
||||
runtime_sources: Vec<RuntimeHandoffSource>,
|
||||
}
|
||||
|
||||
impl SandboxBundle {
|
||||
|
|
@ -315,6 +474,78 @@ impl SandboxBundle {
|
|||
pub const fn command(&self) -> &RunscCommand {
|
||||
&self.command
|
||||
}
|
||||
|
||||
/// Captures the exact staged sources that must survive until attached runsc release.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses absent, changed, symlinked, or incorrectly typed handoff sources.
|
||||
pub fn capture_handoff_authority<'configuration>(
|
||||
&self,
|
||||
configuration: &'configuration ResolvedDomainConfiguration,
|
||||
) -> Result<SandboxHandoffAuthority<'configuration>, SandboxBuildError> {
|
||||
if configuration.domain_id() != self.domain_id
|
||||
|| configuration.configuration_fingerprint() != self.configuration_fingerprint
|
||||
|| ConfigurationAuthorityIdentity::capture(configuration)
|
||||
!= self.configuration_authority
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"sandbox handoff configuration does not match its bundle",
|
||||
));
|
||||
}
|
||||
configuration
|
||||
.workspace()
|
||||
.open_authority()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
for volume in configuration.volumes() {
|
||||
volume
|
||||
.open_source()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
}
|
||||
let runtime_root = configuration
|
||||
.runtime_domain_root()
|
||||
.open_authority()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
let derived = self
|
||||
.runtime_sources
|
||||
.iter()
|
||||
.map(|source| {
|
||||
let descriptor = open_runtime_source(
|
||||
&runtime_root,
|
||||
&source.relative_path,
|
||||
source.expected_type,
|
||||
source.expected_bytes.is_some(),
|
||||
)?;
|
||||
let identity = descriptor_identity(&descriptor)?;
|
||||
if source
|
||||
.build_identity
|
||||
.is_some_and(|expected| expected != identity)
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged handoff source changed identity after bundle construction",
|
||||
));
|
||||
}
|
||||
if let Some(expected) = &source.expected_bytes {
|
||||
require_exact_bytes(descriptor, expected)?;
|
||||
}
|
||||
Ok(DerivedHandoffAuthority {
|
||||
relative_path: source.relative_path.clone(),
|
||||
expected_type: source.expected_type,
|
||||
identity,
|
||||
expected_bytes: source.expected_bytes.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, SandboxBuildError>>()?;
|
||||
Ok(SandboxHandoffAuthority {
|
||||
configuration,
|
||||
domain_id: self.domain_id,
|
||||
configuration_fingerprint: self.configuration_fingerprint,
|
||||
configuration_authority: self.configuration_authority.clone(),
|
||||
command: self.command.clone(),
|
||||
derived,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the complete protected destination roots used by bundle construction.
|
||||
|
|
@ -371,9 +602,8 @@ pub fn build_sandbox_bundle(
|
|||
request.terminal.as_ref(),
|
||||
)?;
|
||||
let linux = build_linux()?;
|
||||
let command = build_runsc_command(&paths);
|
||||
let root = RootBuilder::default()
|
||||
.path(paths.rootfs)
|
||||
.path(paths.rootfs.clone())
|
||||
.readonly(false)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
|
|
@ -389,10 +619,18 @@ pub fn build_sandbox_bundle(
|
|||
let specification_json = serde_json::to_vec(&specification).map_err(|error| {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::JsonEncoding, error.to_string())
|
||||
})?;
|
||||
let bundle_authorization = Sha256::digest(&specification_json).into();
|
||||
let command = build_runsc_command(&paths, bundle_authorization);
|
||||
let runtime_sources =
|
||||
handoff_runtime_sources(&paths, request.terminal.as_ref(), &specification_json);
|
||||
Ok(SandboxBundle {
|
||||
specification,
|
||||
specification_json,
|
||||
command,
|
||||
domain_id: request.configuration.domain_id(),
|
||||
configuration_fingerprint: request.configuration.configuration_fingerprint(),
|
||||
configuration_authority: ConfigurationAuthorityIdentity::capture(request.configuration),
|
||||
runtime_sources,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -404,6 +642,143 @@ struct RuntimePaths {
|
|||
control_sources: PathBuf,
|
||||
}
|
||||
|
||||
fn handoff_runtime_sources(
|
||||
paths: &RuntimePaths,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
specification_json: &[u8],
|
||||
) -> Vec<RuntimeHandoffSource> {
|
||||
let container = Path::new("sandbox-control").join(&paths.container_name);
|
||||
let mut sources = [
|
||||
(
|
||||
Path::new("bundles").join(&paths.container_name),
|
||||
HandoffPathType::Directory,
|
||||
),
|
||||
(
|
||||
Path::new("bundles")
|
||||
.join(&paths.container_name)
|
||||
.join("config.json"),
|
||||
HandoffPathType::RegularFile,
|
||||
),
|
||||
(PathBuf::from("runsc"), HandoffPathType::Directory),
|
||||
(
|
||||
PathBuf::from("state-anchor/merged"),
|
||||
HandoffPathType::Directory,
|
||||
),
|
||||
(container.clone(), HandoffPathType::Directory),
|
||||
(container.join("credentials"), HandoffPathType::Directory),
|
||||
(
|
||||
container.join("mailbox/requests"),
|
||||
HandoffPathType::Directory,
|
||||
),
|
||||
(
|
||||
container.join("mailbox/results"),
|
||||
HandoffPathType::Directory,
|
||||
),
|
||||
(
|
||||
container.join("bin/lazaret-escalate"),
|
||||
HandoffPathType::RegularFile,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(relative_path, expected_type)| {
|
||||
let expected_bytes = (relative_path
|
||||
== Path::new("bundles")
|
||||
.join(&paths.container_name)
|
||||
.join("config.json"))
|
||||
.then(|| specification_json.to_vec());
|
||||
RuntimeHandoffSource {
|
||||
relative_path,
|
||||
expected_type,
|
||||
build_identity: None,
|
||||
expected_bytes,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(terminal) = terminal {
|
||||
sources.push(RuntimeHandoffSource {
|
||||
relative_path: terminal.relative_source.clone(),
|
||||
expected_type: HandoffPathType::RegularFile,
|
||||
build_identity: Some(terminal.identity),
|
||||
expected_bytes: None,
|
||||
});
|
||||
}
|
||||
sources
|
||||
}
|
||||
|
||||
fn open_runtime_source(
|
||||
runtime_root: &OwnedFd,
|
||||
relative_path: &Path,
|
||||
expected_type: HandoffPathType,
|
||||
readable: bool,
|
||||
) -> Result<OwnedFd, SandboxBuildError> {
|
||||
let descriptor = openat2(
|
||||
runtime_root,
|
||||
relative_path,
|
||||
if readable {
|
||||
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW
|
||||
} else {
|
||||
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW
|
||||
},
|
||||
Mode::empty(),
|
||||
ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||
)
|
||||
.map_err(staging_error)?;
|
||||
let stat = rustix::fs::fstat(&descriptor).map_err(staging_error)?;
|
||||
let actual = FileType::from_raw_mode(stat.st_mode);
|
||||
let matches = match expected_type {
|
||||
HandoffPathType::Directory => actual.is_dir(),
|
||||
HandoffPathType::RegularFile => actual.is_file(),
|
||||
};
|
||||
if !matches {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"runtime handoff source changed type",
|
||||
));
|
||||
}
|
||||
Ok(descriptor)
|
||||
}
|
||||
|
||||
fn require_exact_bytes(descriptor: OwnedFd, expected: &[u8]) -> Result<(), SandboxBuildError> {
|
||||
let limit = u64::try_from(expected.len())
|
||||
.ok()
|
||||
.and_then(|length| length.checked_add(1))
|
||||
.ok_or_else(|| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged OCI configuration length is invalid",
|
||||
)
|
||||
})?;
|
||||
let mut observed = Vec::new();
|
||||
File::from(descriptor)
|
||||
.take(limit)
|
||||
.read_to_end(&mut observed)
|
||||
.map_err(staging_error)?;
|
||||
if observed != expected {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged OCI configuration differs from its bundle",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn descriptor_identity(descriptor: &OwnedFd) -> Result<FileIdentity, SandboxBuildError> {
|
||||
let stat = rustix::fs::fstat(descriptor).map_err(staging_error)?;
|
||||
let extended =
|
||||
statx(descriptor, "", AtFlags::EMPTY_PATH, StatxFlags::MNT_ID).map_err(staging_error)?;
|
||||
if !StatxFlags::from_bits_retain(extended.stx_mask).contains(StatxFlags::MNT_ID) {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"host filesystem does not report mount identity",
|
||||
));
|
||||
}
|
||||
Ok(FileIdentity {
|
||||
device: stat.st_dev,
|
||||
inode: stat.st_ino,
|
||||
mount_id: extended.stx_mnt_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn runtime_paths(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
container_id: ContainerId,
|
||||
|
|
@ -491,10 +866,10 @@ fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildError> {
|
|||
.map_err(oci_error)
|
||||
}
|
||||
|
||||
fn build_runsc_command(paths: &RuntimePaths) -> RunscCommand {
|
||||
RunscCommand {
|
||||
program: OsString::from("runsc"),
|
||||
arguments: vec![
|
||||
fn build_runsc_command(paths: &RuntimePaths, bundle_authorization: [u8; 32]) -> RunscCommand {
|
||||
RunscCommand::new(
|
||||
"runsc",
|
||||
[
|
||||
OsString::from(format!(
|
||||
"-root={}",
|
||||
paths
|
||||
|
|
@ -515,7 +890,8 @@ fn build_runsc_command(paths: &RuntimePaths) -> RunscCommand {
|
|||
)),
|
||||
OsString::from(&paths.container_name),
|
||||
],
|
||||
}
|
||||
bundle_authorization,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_process(
|
||||
|
|
|
|||
|
|
@ -162,10 +162,12 @@ impl CoordinatorChannel {
|
|||
pub struct RuntimeSupervisorLaunch {
|
||||
target: TargetCommand,
|
||||
ownership: ProcessOwnershipContext,
|
||||
_policy_file: Option<File>,
|
||||
policy_file: Option<File>,
|
||||
network_sidecar_id: Option<NetworkSidecarId>,
|
||||
runsc_command: RunscCommand,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RuntimeNetworkLaunch<'policy> {
|
||||
pub policy: &'policy CanonicalDerivedNetworkPolicy,
|
||||
pub configuration_fingerprint: ConfigurationFingerprint,
|
||||
|
|
@ -210,8 +212,9 @@ impl RuntimeSupervisorLaunch {
|
|||
Ok(Self {
|
||||
target: target.with_lifetime_pipe_parent_edge(),
|
||||
ownership,
|
||||
_policy_file: None,
|
||||
policy_file: None,
|
||||
network_sidecar_id: None,
|
||||
runsc_command: command.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +259,7 @@ impl RuntimeSupervisorLaunch {
|
|||
}
|
||||
launch.target = launch.target.with_lifetime_pipe_parent_edge();
|
||||
drop(target);
|
||||
launch._policy_file = Some(policy_file);
|
||||
launch.policy_file = Some(policy_file);
|
||||
launch.network_sidecar_id = Some(network.network_sidecar_id);
|
||||
Ok(launch)
|
||||
}
|
||||
|
|
@ -276,6 +279,11 @@ impl RuntimeSupervisorLaunch {
|
|||
self.network_sidecar_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn runsc_command(&self) -> &RunscCommand {
|
||||
&self.runsc_command
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses a Runtime Supervisor target without the lifetime-pipe parent edge.
|
||||
pub fn child_standard_io(
|
||||
|
|
@ -557,14 +565,11 @@ fn prepare_runtime_network(
|
|||
Ok(message) => message,
|
||||
Err(error) => return fail_network(Some(prepared), error),
|
||||
};
|
||||
let request = match message {
|
||||
CoordinatorMessage::NetworkAttestationAccepted(request) => request,
|
||||
_ => {
|
||||
return fail_network(
|
||||
Some(prepared),
|
||||
io::Error::other("network attestation acceptance was not received"),
|
||||
);
|
||||
}
|
||||
let CoordinatorMessage::NetworkAttestationAccepted(request) = message else {
|
||||
return fail_network(
|
||||
Some(prepared),
|
||||
io::Error::other("network attestation acceptance was not received"),
|
||||
);
|
||||
};
|
||||
let probe = match probe_current_network(&request) {
|
||||
Ok(probe) => probe,
|
||||
|
|
@ -896,7 +901,12 @@ mod tests {
|
|||
let launch = RuntimeSupervisorLaunch {
|
||||
target: TargetCommand::new("/proc/self/exe").with_lifetime_pipe_parent_edge(),
|
||||
ownership,
|
||||
_policy_file: None,
|
||||
policy_file: None,
|
||||
runsc_command: RunscCommand::new(
|
||||
"runsc",
|
||||
[OsString::from("run"), OsString::from("container")],
|
||||
[0; 32],
|
||||
),
|
||||
network_sidecar_id: None,
|
||||
};
|
||||
let (_coordinator_control, supervisor_control) = private_seqpacket_pair().unwrap();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use serde_json::Value;
|
|||
use tempfile::TempDir;
|
||||
|
||||
struct Fixture {
|
||||
_tree: TempDir,
|
||||
tree: TempDir,
|
||||
configuration: ResolvedDomainConfiguration,
|
||||
terminfo_source: PathBuf,
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
|
|||
fs::create_dir_all(terminfo_source.parent().unwrap()).unwrap();
|
||||
fs::write(&terminfo_source, compiled_terminfo("xterm-lazaret")).unwrap();
|
||||
Self {
|
||||
_tree: tree,
|
||||
tree,
|
||||
configuration,
|
||||
terminfo_source,
|
||||
}
|
||||
|
|
@ -98,6 +98,25 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
|
|||
stage_host_terminal(&self.configuration, ContainerId::new([0x11; 32]), terminal).unwrap()
|
||||
}
|
||||
|
||||
fn reload_configuration(&self) -> ResolvedDomainConfiguration {
|
||||
let root = self.tree.path();
|
||||
load_trusted_configuration(ConfigurationLoadRequest {
|
||||
explicit_configuration_path: Some(root.join("configuration/lazaret.toml")),
|
||||
selected_profile: "default".to_owned(),
|
||||
workspace: root.join("workspace"),
|
||||
home: root.join("home"),
|
||||
xdg_config_home: None,
|
||||
xdg_data_home: None,
|
||||
xdg_runtime_dir: root.join("runtime"),
|
||||
environment: BTreeMap::from([
|
||||
("SELECTED_HOST_SECRET".to_owned(), "selected-value".into()),
|
||||
("UNSELECTED_HOST_SECRET".to_owned(), "must-not-enter".into()),
|
||||
]),
|
||||
sandbox_layout: production_sandbox_layout().unwrap(),
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn staged_terminfo_source(&self) -> PathBuf {
|
||||
PathBuf::from(
|
||||
String::from_utf8(
|
||||
|
|
@ -123,6 +142,53 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
|
|||
terminal: Some(self.terminal()),
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_handoff(&self, bundle: &lazaret_runtime_supervisor::sandbox::SandboxBundle) {
|
||||
let runtime = host_path(
|
||||
self.configuration
|
||||
.runtime_domain_root()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
);
|
||||
let container = "11".repeat(32);
|
||||
for directory in [
|
||||
format!("bundles/{container}"),
|
||||
"runsc".to_owned(),
|
||||
"state-anchor/merged".to_owned(),
|
||||
format!("sandbox-control/{container}/credentials"),
|
||||
format!("sandbox-control/{container}/mailbox/requests"),
|
||||
format!("sandbox-control/{container}/mailbox/results"),
|
||||
format!("sandbox-control/{container}/bin"),
|
||||
] {
|
||||
fs::create_dir_all(runtime.join(directory)).unwrap();
|
||||
}
|
||||
fs::write(
|
||||
runtime.join(format!("bundles/{container}/config.json")),
|
||||
bundle.specification_json(),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
runtime.join(format!("sandbox-control/{container}/bin/lazaret-escalate")),
|
||||
b"executable",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn bundle_config_path(&self) -> PathBuf {
|
||||
host_path(
|
||||
self.configuration
|
||||
.runtime_domain_root()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
)
|
||||
.join("bundles")
|
||||
.join("11".repeat(32))
|
||||
.join("config.json")
|
||||
}
|
||||
}
|
||||
|
||||
fn host_path(bytes: &[u8]) -> PathBuf {
|
||||
PathBuf::from(OsStr::from_bytes(bytes))
|
||||
}
|
||||
|
||||
fn compiled_terminfo(term: &str) -> Vec<u8> {
|
||||
|
|
@ -473,3 +539,157 @@ fn production_layout_reserves_every_mount_root() {
|
|||
);
|
||||
assert_eq!(TERMINFO_ROOT_DESTINATION, "/run/lazaret/terminfo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_handoff_refuses_replaced_workspace_and_runtime_sources() {
|
||||
let fixture = Fixture::new();
|
||||
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
|
||||
fixture.stage_handoff(&bundle);
|
||||
let handoff = bundle
|
||||
.capture_handoff_authority(&fixture.configuration)
|
||||
.unwrap();
|
||||
let workspace = host_path(
|
||||
fixture
|
||||
.configuration
|
||||
.workspace()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
);
|
||||
fs::rename(&workspace, workspace.with_extension("selected")).unwrap();
|
||||
fs::create_dir(&workspace).unwrap();
|
||||
fs::write(workspace.join("replacement-marker"), b"unchanged").unwrap();
|
||||
let error = handoff.revalidate().unwrap_err();
|
||||
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
|
||||
assert_eq!(
|
||||
fs::read(workspace.join("replacement-marker")).unwrap(),
|
||||
b"unchanged"
|
||||
);
|
||||
|
||||
let fixture = Fixture::new();
|
||||
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
|
||||
fixture.stage_handoff(&bundle);
|
||||
let handoff = bundle
|
||||
.capture_handoff_authority(&fixture.configuration)
|
||||
.unwrap();
|
||||
let runtime = host_path(
|
||||
fixture
|
||||
.configuration
|
||||
.runtime_domain_root()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
);
|
||||
let rootfs = runtime.join("state-anchor/merged");
|
||||
fs::rename(&rootfs, runtime.join("state-anchor/selected")).unwrap();
|
||||
fs::create_dir(&rootfs).unwrap();
|
||||
fs::write(rootfs.join("replacement-marker"), b"unchanged").unwrap();
|
||||
let error = handoff.revalidate().unwrap_err();
|
||||
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
|
||||
assert_eq!(
|
||||
fs::read(rootfs.join("replacement-marker")).unwrap(),
|
||||
b"unchanged"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reloaded_same_label_configuration_cannot_replace_bundle_authority() {
|
||||
for selected in ["workspace", "volume", "runtime-domain"] {
|
||||
let fixture = Fixture::new();
|
||||
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
|
||||
fixture.stage_handoff(&bundle);
|
||||
let path = match selected {
|
||||
"workspace" => host_path(
|
||||
fixture
|
||||
.configuration
|
||||
.workspace()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
),
|
||||
"volume" => host_path(
|
||||
fixture.configuration.volumes()[0]
|
||||
.record()
|
||||
.canonical_host_source
|
||||
.as_bytes(),
|
||||
),
|
||||
"runtime-domain" => host_path(
|
||||
fixture
|
||||
.configuration
|
||||
.runtime_domain_root()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let selected_path = path.with_extension(format!("{selected}-selected"));
|
||||
fs::rename(&path, &selected_path).unwrap();
|
||||
fs::create_dir(&path).unwrap();
|
||||
fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(0o700)).unwrap();
|
||||
let marker = path.join("replacement-marker");
|
||||
fs::write(&marker, b"unchanged").unwrap();
|
||||
let reloaded = fixture.reload_configuration();
|
||||
let Err(error) = bundle.capture_handoff_authority(&reloaded) else {
|
||||
panic!("reloaded {selected} replacement was accepted");
|
||||
};
|
||||
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
|
||||
assert_eq!(fs::read(marker).unwrap(), b"unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_build_terminfo_replacement_cannot_become_handoff_authority() {
|
||||
let fixture = Fixture::new();
|
||||
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
|
||||
let terminfo = fixture.staged_terminfo_source();
|
||||
fs::rename(&terminfo, terminfo.with_extension("selected")).unwrap();
|
||||
fs::write(&terminfo, b"replacement remains unchanged").unwrap();
|
||||
fixture.stage_handoff(&bundle);
|
||||
let Err(error) = bundle.capture_handoff_authority(&fixture.configuration) else {
|
||||
panic!("post-build terminfo replacement was accepted");
|
||||
};
|
||||
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
|
||||
assert_eq!(
|
||||
fs::read(terminfo).unwrap(),
|
||||
b"replacement remains unchanged"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_authorization_distinguishes_equal_runsc_argv() {
|
||||
let fixture = Fixture::new();
|
||||
let first = build_sandbox_bundle(fixture.request(root_process(["first".to_owned()]))).unwrap();
|
||||
let second =
|
||||
build_sandbox_bundle(fixture.request(root_process(["second".to_owned()]))).unwrap();
|
||||
assert_eq!(first.command().program(), second.command().program());
|
||||
assert_eq!(first.command().arguments(), second.command().arguments());
|
||||
assert_ne!(first.command(), second.command());
|
||||
fixture.stage_handoff(&first);
|
||||
let handoff = first
|
||||
.capture_handoff_authority(&fixture.configuration)
|
||||
.unwrap();
|
||||
assert!(!handoff.matches_command(second.command()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_oci_configuration_must_match_bundle_at_capture_and_release() {
|
||||
let fixture = Fixture::new();
|
||||
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
|
||||
fixture.stage_handoff(&bundle);
|
||||
let config = fixture.bundle_config_path();
|
||||
fs::write(&config, b"pre-capture replacement").unwrap();
|
||||
let Err(error) = bundle.capture_handoff_authority(&fixture.configuration) else {
|
||||
panic!("mutated pre-capture OCI configuration was accepted");
|
||||
};
|
||||
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
|
||||
assert_eq!(fs::read(&config).unwrap(), b"pre-capture replacement");
|
||||
|
||||
let fixture = Fixture::new();
|
||||
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
|
||||
fixture.stage_handoff(&bundle);
|
||||
let handoff = bundle
|
||||
.capture_handoff_authority(&fixture.configuration)
|
||||
.unwrap();
|
||||
let config = fixture.bundle_config_path();
|
||||
fs::write(&config, b"post-capture replacement").unwrap();
|
||||
let error = handoff.revalidate().unwrap_err();
|
||||
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
|
||||
assert_eq!(fs::read(config).unwrap(), b"post-capture replacement");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue