Replace external reachability with an owned canary

This commit is contained in:
ms2k 2026-07-19 22:39:31 +09:00
commit 31e9ab0f2d
29 changed files with 1409 additions and 1972 deletions

View file

@ -5,14 +5,13 @@ use std::sync::Mutex;
use lazaret_core::configuration::ResolvedDomainConfiguration;
use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity};
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
use lazaret_core::network::CanonicalDerivedNetworkPolicy;
use lazaret_core::records::Session;
use lazaret_core::records::{
Generation, GenerationIdentity, Participant, ParticipantOwnership, ParticipantRole,
ParticipantState, PreparedArtifact,
};
use lazaret_linux::process::{ProcessOwnershipContext, current_process_identity};
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
use lazaret_runtime_supervisor::sandbox::SandboxBundle;
use lazaret_runtime_supervisor::supervision::RuntimeSupervisorLaunch;
use lazaret_state_anchor::StateAnchorLayout;
@ -70,8 +69,6 @@ pub struct BootstrapPlan<'a> {
pub runtime_launch: RuntimeSupervisorLaunch,
pub sandbox_bundle: &'a SandboxBundle,
pub network_policy: &'a CanonicalDerivedNetworkPolicy,
pub network_probe: NetworkProbeRequest,
pub reachability: &'a ReachabilityExpectation,
pub container_id: ContainerId,
pub runtime_root: PathBuf,
pub bundle_digest: BundleDigest,
@ -98,6 +95,7 @@ pub struct CoordinatorLifecycleAdapter<'a> {
escalation: Option<EscalationService<'a>>,
escalation_handle: Option<EscalationServiceHandle>,
awaiting_runtime: Option<AwaitingAttachedRunsc>,
awaiting_sandbox: Option<crate::runtime::AwaitingSandboxReadiness>,
runtime: Option<CommittedSandboxRuntime>,
state_anchor_mount: Option<MountIdentity>,
git_routing_mount: Option<MountIdentity>,
@ -197,6 +195,7 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
escalation: None,
escalation_handle: None,
awaiting_runtime: None,
awaiting_sandbox: None,
runtime: None,
state_anchor_mount: None,
git_routing_mount: None,
@ -330,12 +329,16 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
fn start_connectivity_helper(&mut self) -> Result<(), LifecycleServiceError> {
let store = Arc::clone(&self.plan.store);
let network = NetworkCoordinator::new(&store);
self.awaiting_runtime
let awaiting = self
.awaiting_runtime
.as_mut()
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?;
awaiting
.receive_helper_staged(&network)
.map(|_| ())
.map_err(|error| failure(format!("starting connectivity helper: {error}")))
.map_err(|error| failure(format!("starting connectivity helper: {error}")))?;
awaiting
.receive_network_attestation(&network, self.plan.network_policy)
.map_err(|error| failure(format!("attesting prepared network: {error}")))
}
fn start_sidecar(&mut self) -> Result<(), LifecycleServiceError> {
@ -349,21 +352,16 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
fn mark_network_ready(&mut self) -> Result<(), LifecycleServiceError> {
let store = Arc::clone(&self.plan.store);
let network = NetworkCoordinator::new(&store);
let mut probe = self.plan.network_probe.clone();
self.awaiting_runtime
.as_mut()
.ok_or_else(|| LifecycleServiceError("Runtime Supervisor is absent".into()))?
.receive_network_readiness(
&network,
self.plan.network_policy,
|namespace| {
probe.enforcement_namespace = namespace;
probe
},
self.plan.reachability,
)
.map(|_| ())
.map_err(|error| failure(format!("marking network ready: {error}")))
let sandbox = SandboxCoordinator::new(&store);
let pending = self
.awaiting_sandbox
.take()
.ok_or_else(|| LifecycleServiceError("Sandbox readiness is absent".into()))?;
let (runtime, _) = pending
.commit_networked(&sandbox, &network, self.plan.network_policy)
.map_err(|error| failure(format!("marking network ready: {error}")))?;
self.runtime = Some(runtime);
Ok(())
}
fn start_sandbox(&mut self) -> Result<(), LifecycleServiceError> {
@ -381,12 +379,10 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
.sandbox_bundle
.capture_handoff_authority(self.plan.configuration)
.map_err(|error| failure(format!("capturing sandbox handoff: {error}")))?;
let (runtime, _) = pending
let awaiting = pending
.acknowledge(staged, &handoff)
.map_err(|error| failure(format!("releasing sandbox handoff: {error}")))?
.commit_networked(&sandbox)
.map_err(|error| failure(format!("committing sandbox readiness: {error}")))?;
self.runtime = Some(runtime);
.map_err(|error| failure(format!("releasing sandbox handoff: {error}")))?;
self.awaiting_sandbox = Some(awaiting);
Ok(())
}

View file

@ -138,7 +138,7 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
Arc::clone(&store),
InstalledProductPaths {
escalation_client: &installed.join("lazaret-escalation-client"),
sandbox_init: &executable,
sandbox_init: &installed.join("lazaret-sandbox-init"),
},
))
.map_err(failure)?;

View file

@ -11,9 +11,9 @@ use std::sync::{Arc, Mutex};
use lazaret_core::configuration::{ConfinedPathPlan, ResolvedDomainConfiguration};
use lazaret_core::identity::{BundleDigest, ContainerId, HostPath, MountIdentity};
use lazaret_core::network::{ReachabilityExpectation, derive_ipv4_policy};
use lazaret_core::network::derive_ipv4_policy;
use lazaret_core::records::{
Generation, GenerationIdentity, HostProcessOwnershipSlot, IpAddress, PreparedArtifact, Session,
Generation, GenerationIdentity, HostProcessOwnershipSlot, PreparedArtifact, Session,
};
use lazaret_ignored_workspace::git_common_directory;
use lazaret_linux::filesystem::{RegularFileMailbox, mount_identity, namespace_identity};
@ -21,7 +21,6 @@ use lazaret_linux::process::{
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity,
};
use lazaret_linux::user_namespace::UserNamespaceMap;
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
use lazaret_runtime_supervisor::sandbox::{
RootProcess, SandboxBuildRequest, SandboxBundle, build_sandbox_bundle,
build_sandbox_bundle_with_workspace,
@ -79,9 +78,6 @@ pub struct ColdStartInputs<'a> {
pub prepared_artifact_authority: &'a File,
pub prepared_artifact_label: &'a Path,
pub root_process: RootProcess,
pub resolver_addresses: Vec<IpAddress>,
pub network_probe: NetworkProbeRequest,
pub reachability: ReachabilityExpectation,
pub container_id: ContainerId,
pub invoking_uid: u32,
pub escalation_client: &'a File,
@ -142,8 +138,6 @@ pub struct ColdStart<'a> {
runtime_launch: Option<RuntimeSupervisorLaunch>,
sandbox_bundle: SandboxBundle,
network_policy: lazaret_core::network::CanonicalDerivedNetworkPolicy,
network_probe: NetworkProbeRequest,
reachability: ReachabilityExpectation,
container_id: ContainerId,
runtime_root: PathBuf,
bundle_digest: BundleDigest,
@ -204,7 +198,7 @@ impl<'a> ColdStart<'a> {
staging
.write_bundle(sandbox_bundle.specification_json())
.map_err(|error| failure(format!("bundle staging: {error}")))?;
let network_policy = derive_ipv4_policy(inputs.configuration, inputs.resolver_addresses)
let network_policy = derive_ipv4_policy(inputs.configuration)
.map_err(|error| failure(format!("network policy: {error}")))?;
let ownership = ProcessOwnershipContext {
domain_id: inputs.generation.domain_id,
@ -263,8 +257,6 @@ impl<'a> ColdStart<'a> {
runtime_launch: Some(runtime_launch),
sandbox_bundle,
network_policy,
network_probe: inputs.network_probe,
reachability: inputs.reachability,
container_id: inputs.container_id,
runtime_root: runtime_domain.join("runsc"),
bundle_digest,
@ -366,6 +358,7 @@ impl<'a> ColdStart<'a> {
self.container_id,
current_process_identity().map_err(failure)?,
self.sandbox_bundle.command(),
self.configuration.ephemeral_environment().clone(),
self.runtime_root.clone(),
)
.map_err(failure)?;
@ -383,8 +376,6 @@ impl<'a> ColdStart<'a> {
.ok_or_else(|| ColdStartError("cold start was already consumed".into()))?,
sandbox_bundle: &self.sandbox_bundle,
network_policy: &self.network_policy,
network_probe: self.network_probe.clone(),
reachability: &self.reachability,
container_id: self.container_id,
runtime_root: self.runtime_root.clone(),
bundle_digest: self.bundle_digest,
@ -393,6 +384,7 @@ impl<'a> ColdStart<'a> {
RunscNonInteractiveExecutor::from_run_command(
self.sandbox_bundle.command(),
self.container_id,
self.configuration.ephemeral_environment().clone(),
)
.map_err(failure)?,
),
@ -414,9 +406,6 @@ fn require_inputs(inputs: &ColdStartInputs<'_>) -> Result<(), ColdStartError> {
if inputs.generation.domain_id != inputs.configuration.domain_id()
|| inputs.generation.configuration_fingerprint
!= inputs.configuration.configuration_fingerprint()
|| 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(ColdStartError("cold-start inputs disagree".into()));

View file

@ -130,6 +130,12 @@ impl RuntimeStaging {
invoking_uid,
true,
)?);
drop(open_or_create_private_directory(
&self.control,
"readiness",
invoking_uid,
true,
)?);
let mailbox =
open_or_create_private_directory(&self.control, "mailbox", invoking_uid, true)?;
drop(open_or_create_private_directory(

View file

@ -13,7 +13,7 @@ use ed25519_dalek::SigningKey;
pub use lazaret_control_client::{CONTROL_ALPN, CONTROL_SERVER_NAME, PendingClientEnrollment};
use lazaret_control_client::{ControlClient, ControlClientError};
use lazaret_core::codec::{CodecError, decode, encode};
use lazaret_core::configuration::ConfinedPathPlan;
use lazaret_core::configuration::{ConfinedPathPlan, EnvironmentValue};
use lazaret_core::identity::{
CertificateDigest, ClientId, ContainerId, DeviceId, HostPath, InodeId, MountIdentity,
SessionAnchorId, SessionId, TrustManifestDigest,

View file

@ -1,9 +1,9 @@
use super::{
Arc, AsFd, CONTROL_SOCKET_NAME, ContainerId, ControlError, File, FileIdentity,
InteractiveSessionBatch, InteractiveSessionEvent, InteractiveSessionRequest, MetadataExt, Mode,
NonInteractiveSessionMessage, NonInteractiveSessionRequest, OFlags, OpenOptions, OsStr,
OsString, OsStringExt, PUBLICATION_IDENTITY_NAME, Path, PathBuf, ProcessExit,
ProtocolPublicationIdentity, Read, RunscCommand, STAGED_CONTROL_SOCKET_NAME,
Arc, AsFd, BTreeMap, CONTROL_SOCKET_NAME, ContainerId, ControlError, EnvironmentValue, File,
FileIdentity, InteractiveSessionBatch, InteractiveSessionEvent, InteractiveSessionRequest,
MetadataExt, Mode, NonInteractiveSessionMessage, NonInteractiveSessionRequest, OFlags,
OpenOptions, OsStr, OsString, OsStringExt, PUBLICATION_IDENTITY_NAME, Path, PathBuf,
ProcessExit, ProtocolPublicationIdentity, Read, RunscCommand, STAGED_CONTROL_SOCKET_NAME,
STAGED_PUBLICATION_IDENTITY_NAME, ServerConfig, SessionExit, SessionId, SessionRegistry,
TargetCommand, TrustAnchor, Write, child_broker, fchmod, fmt, io, openat,
remove_if_identity_matches,
@ -221,6 +221,7 @@ impl InteractiveSessionExecutor for InteractiveExecutionUnavailable {
pub struct RunscNonInteractiveExecutor {
pub(super) program: OsString,
pub(super) global_arguments: Vec<OsString>,
pub(super) environment: Vec<(String, OsString)>,
pub(super) container: OsString,
pub(super) runner: Arc<dyn NonInteractiveCommandRunner>,
}
@ -268,6 +269,7 @@ impl RunscNonInteractiveExecutor {
pub fn from_run_command(
command: &RunscCommand,
container_id: ContainerId,
environment: BTreeMap<String, EnvironmentValue>,
) -> Result<Self, ControlError> {
if command.program() != OsStr::new("runsc") {
return Err(ControlError::InvalidRequest);
@ -291,6 +293,10 @@ impl RunscNonInteractiveExecutor {
Ok(Self {
program: command.program().to_owned(),
global_arguments: command.arguments()[..run].to_vec(),
environment: environment
.into_iter()
.map(|(name, value)| (name, value.expose().to_owned()))
.collect(),
container,
runner: Arc::new(ChildBrokerCommandRunner),
})
@ -307,8 +313,19 @@ impl NonInteractiveSessionExecutor for RunscNonInteractiveExecutor {
for argument in &self.global_arguments {
target = target.argument(argument.clone());
}
target = target.argument("exec");
if !self.environment.iter().any(|(name, _)| name == "PATH") {
target = target
.argument("-env")
.argument(format!("PATH={}", crate::sandbox_init::DEFAULT_AGENT_PATH));
}
for (name, value) in &self.environment {
let mut assignment = OsString::from(name);
assignment.push("=");
assignment.push(value);
target = target.argument("-env").argument(assignment);
}
target = target
.argument("exec")
.argument("-env")
.argument(format!(
"LAZARET_SESSION_ID={}",

View file

@ -94,7 +94,7 @@ impl NonInteractiveCommandRunner for RecordingRunner {
}
#[test]
fn runsc_exec_uses_the_command_as_the_first_post_container_argument() {
fn runsc_exec_places_profile_environment_before_session_binding_and_command() {
let runner = Arc::new(RecordingRunner {
observed: Mutex::new(None),
exit: ProcessExit {
@ -105,6 +105,7 @@ fn runsc_exec_uses_the_command_as_the_first_post_container_argument() {
let executor = RunscNonInteractiveExecutor {
program: "runsc".into(),
global_arguments: vec!["--root=/runtime".into()],
environment: vec![("PROFILE_SENTINEL".into(), "profile-value".into())],
container: "container-id".into(),
runner: runner.clone(),
};
@ -129,6 +130,10 @@ fn runsc_exec_uses_the_command_as_the_first_post_container_argument() {
OsString::from("--root=/runtime"),
OsString::from("exec"),
OsString::from("-env"),
OsString::from(format!("PATH={}", crate::sandbox_init::DEFAULT_AGENT_PATH)),
OsString::from("-env"),
OsString::from("PROFILE_SENTINEL=profile-value"),
OsString::from("-env"),
OsString::from(format!(
"LAZARET_SESSION_ID={}",
hex_lower(session_id.as_bytes())

View file

@ -180,12 +180,12 @@ where
checkpoint!();
self.start_connectivity_helper()?;
checkpoint!();
self.mark_network_ready()?;
checkpoint!();
self.arm_runtime_causal_edge()?;
checkpoint!();
self.start_sandbox()?;
checkpoint!();
self.mark_network_ready()?;
checkpoint!();
withdraw_waiter_admission(self).map_err(LifecycleError::Adapter)?;
if !owner_live() {
self.disconnect_session(session_id)?;
@ -419,9 +419,7 @@ where
pub fn mark_network_ready(&mut self) -> Result<(), LifecycleError<A::Error>> {
self.startup_action(
LifecycleAction::MarkNetworkReady,
self.state
.progress
.has(RuntimeMilestone::ConnectivityHelper)
self.state.progress.has(RuntimeMilestone::Sandbox)
&& !self.state.progress.has(RuntimeMilestone::NetworkReady),
|p| p.mark(RuntimeMilestone::NetworkReady),
)
@ -432,17 +430,22 @@ where
pub fn arm_runtime_causal_edge(&mut self) -> Result<(), LifecycleError<A::Error>> {
self.startup_action(
LifecycleAction::ArmRuntimeCausalEdge,
self.state.progress.has(RuntimeMilestone::NetworkReady)
self.state
.progress
.has(RuntimeMilestone::ConnectivityHelper)
&& !self.state.progress.has(RuntimeMilestone::CausalEdge),
|p| p.mark(RuntimeMilestone::CausalEdge),
)
}
/// # Errors
/// Refuses launch before network readiness and the causal edge are established.
/// Refuses launch before helper attestation and the causal edge are established.
pub fn start_sandbox(&mut self) -> Result<(), LifecycleError<A::Error>> {
let allowed = self.state.progress.has(RuntimeMilestone::CausalEdge)
&& self.state.progress.has(RuntimeMilestone::NetworkReady)
&& self
.state
.progress
.has(RuntimeMilestone::ConnectivityHelper)
&& !self.state.progress.has(RuntimeMilestone::Sandbox);
Self::require(
LifecycleAction::StartSandbox,
@ -692,6 +695,7 @@ where
LifecycleAction::StopSandbox,
self.state.progress.has(RuntimeMilestone::Sandbox),
|p| {
p.clear(RuntimeMilestone::NetworkReady);
p.clear(RuntimeMilestone::Sandbox);
p.clear(RuntimeMilestone::CausalEdge);
},

View file

@ -20,9 +20,9 @@ pub(super) enum RuntimeMilestone {
StateAnchor,
Sidecar,
ConnectivityHelper,
NetworkReady,
CausalEdge,
Sandbox,
NetworkReady,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
@ -51,9 +51,9 @@ impl RuntimeProgress {
RuntimeMilestone::StateAnchor,
RuntimeMilestone::Sidecar,
RuntimeMilestone::ConnectivityHelper,
RuntimeMilestone::NetworkReady,
RuntimeMilestone::CausalEdge,
RuntimeMilestone::Sandbox,
RuntimeMilestone::NetworkReady,
];
let completed = ORDER
.iter()
@ -92,7 +92,7 @@ impl LifecycleAggregate {
if !self.progress.validate_prefix() {
return Err("runtime milestones are not an ordered prefix");
}
if self.phase == DomainPhase::Ready && !self.progress.has(RuntimeMilestone::Sandbox) {
if self.phase == DomainPhase::Ready && !self.progress.has(RuntimeMilestone::NetworkReady) {
return Err("ready Domain has incomplete runtime progress");
}
if self.phase == DomainPhase::Starting {

View file

@ -5,11 +5,11 @@ use lazaret_core::identity::{
DerivedPolicyDigest, GenerationId, NamespaceIdentity, NetworkSidecarId, PidfsProcessIdentityV1,
};
use lazaret_core::network::{
CanonicalDerivedNetworkPolicy, ReachabilityExpectation, ReadinessExpectation,
accept_network_readiness, validate_network_attestation,
CanonicalDerivedNetworkPolicy, ReadinessExpectation, accept_network_readiness,
validate_network_attestation,
};
use lazaret_core::records::{
HelperOwnership, HostProcessOwnershipSlot, NetworkOwnership, NetworkReachabilityProbe,
HelperOwnership, HostProcessOwnershipSlot, NetworkCanaryObservation, NetworkOwnership,
NetworkReadinessAttestation, NetworkReadinessEvidence, Participant, ParticipantOwnership,
ParticipantRole, ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck,
StagedProcessOwnership,
@ -289,8 +289,7 @@ impl<'store> NetworkCoordinator<'store> {
&self,
policy: &CanonicalDerivedNetworkPolicy,
attestation: NetworkReadinessAttestation,
probe: NetworkReachabilityProbe,
expected_reachability: &ReachabilityExpectation,
canary: NetworkCanaryObservation,
) -> Result<NetworkReadinessEvidence, NetworkCoordinatorError> {
let generation_id = attestation.generation_id;
let snapshot = self.domain_store.snapshot()?;
@ -350,14 +349,8 @@ impl<'store> NetworkCoordinator<'store> {
runtime_supervisor: &runtime_supervisor,
connectivity_helper: &connectivity_helper,
};
let evidence = accept_network_readiness(
policy,
&expectation,
attestation,
probe,
expected_reachability,
)
.map_err(|error| NetworkCoordinatorError::InvalidEvidence(error.to_string()))?;
let evidence = accept_network_readiness(policy, &expectation, attestation, canary)
.map_err(|error| NetworkCoordinatorError::InvalidEvidence(error.to_string()))?;
ownership.readiness_fingerprint = Some(evidence.readiness_fingerprint);
ownership.readiness_evidence = Some(evidence.clone());
self.put(network_participant(

View file

@ -4,14 +4,13 @@ use lazaret_core::identity::{
CoordinatorId, DerivedPolicyDigest, DomainId, GenerationId, HostPath, NamespaceIdentity,
NetworkSidecarId, PidfsProcessIdentityV1, SandboxId,
};
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation, derive_policy};
use lazaret_core::network::derive_policy;
use lazaret_core::records::{
AddressFamily, AddressFamilyDisposition, AddressFamilyDispositionValue, DefaultAction,
DeniedDestinationObservation, DerivedNetworkPolicyFields, DnsObservation, Domain,
DurableDecision, Generation, HostProcessOwnershipSlot, IpAddress, IpNetwork,
NetworkObservation, NetworkReachabilityProbe, NetworkReadinessAttestation,
NetworkReadinessAttestationFields, ParticipantOwnership, ParticipantRole, ParticipantState,
PermittedDestinationObservation, PolicyAction, PolicyDelivery, PrefixRule, Resolver,
DerivedNetworkPolicyFields, Domain, DurableDecision, Generation, HostProcessOwnershipSlot,
IpAddress, IpNetwork, NetworkCanaryObservation, NetworkObservation,
NetworkReadinessAttestation, NetworkReadinessAttestationFields, ParticipantOwnership,
ParticipantRole, ParticipantState, PolicyAction, PolicyDelivery, PrefixRule, Resolver,
StagedProcessOwnership,
};
use lazaret_linux::filesystem::{namespace_identity, try_exclusive_flock};
@ -93,35 +92,15 @@ impl Fixture {
}
#[test]
fn failed_probe_leaves_network_intent_uncommitted() {
fn failed_canary_leaves_network_intent_uncommitted() {
let fixture = Fixture::new();
let (policy, runtime) = fixture.prepare_roles();
let namespace = namespace_identity(std::path::Path::new("/proc/self/ns/net")).unwrap();
let attestation = attestation(&fixture.generation, policy.digest(), namespace, runtime);
let probe = NetworkReachabilityProbe {
domain_id: fixture.generation.domain_id,
generation_id: fixture.generation.generation_id,
enforcement_namespace: namespace,
denied_destinations: vec![DeniedDestinationObservation {
destination: IpAddress::V4([10, 1, 2, 3]),
unreachable: false,
}],
permitted_destinations: vec![PermittedDestinationObservation {
destination: IpAddress::V4([1, 1, 1, 1]),
reachable: true,
}],
dns_observations: vec![DnsObservation {
resolver: IpAddress::V4([8, 8, 8, 8]),
query_name: "example.com".into(),
resolved_addresses: vec![IpAddress::V4([93, 184, 216, 34])],
}],
};
let result = NetworkCoordinator::new(&fixture.store).commit_readiness(
&policy,
attestation,
probe,
&reachability(),
);
let mut canary = canary(&fixture.generation, namespace);
canary.denied_endpoint_unreachable = false;
let result =
NetworkCoordinator::new(&fixture.store).commit_readiness(&policy, attestation, canary);
assert!(matches!(
result,
Err(NetworkCoordinatorError::InvalidEvidence(_))
@ -161,27 +140,9 @@ fn committed_network_roles_release_together_only_after_absence() {
let (policy, runtime) = fixture.prepare_roles();
let namespace = namespace_identity(std::path::Path::new("/proc/self/ns/net")).unwrap();
let attestation = attestation(&fixture.generation, policy.digest(), namespace, runtime);
let probe = NetworkReachabilityProbe {
domain_id: fixture.generation.domain_id,
generation_id: fixture.generation.generation_id,
enforcement_namespace: namespace,
denied_destinations: vec![DeniedDestinationObservation {
destination: IpAddress::V4([10, 1, 2, 3]),
unreachable: true,
}],
permitted_destinations: vec![PermittedDestinationObservation {
destination: IpAddress::V4([1, 1, 1, 1]),
reachable: true,
}],
dns_observations: vec![DnsObservation {
resolver: IpAddress::V4([8, 8, 8, 8]),
query_name: "example.com".into(),
resolved_addresses: vec![IpAddress::V4([93, 184, 216, 34])],
}],
};
let coordinator = NetworkCoordinator::new(&fixture.store);
coordinator
.commit_readiness(&policy, attestation, probe, &reachability())
.commit_readiness(&policy, attestation, canary(&fixture.generation, namespace))
.unwrap();
assert!(
coordinator
@ -275,14 +236,28 @@ fn attestation(
})
}
fn reachability() -> ReachabilityExpectation {
ReachabilityExpectation {
denied_destinations: vec![IpAddress::V4([10, 1, 2, 3])],
permitted_destinations: vec![IpAddress::V4([1, 1, 1, 1])],
dns_queries: vec![DnsQueryExpectation {
resolver: IpAddress::V4([8, 8, 8, 8]),
query_name: "example.com".into(),
}],
fn canary(generation: &Generation, namespace: NamespaceIdentity) -> NetworkCanaryObservation {
NetworkCanaryObservation {
domain_id: generation.domain_id,
generation_id: generation.generation_id,
enforcement_namespace: namespace,
linux_policy_matches: true,
endpoints_authenticated_before_policy: true,
allowed_endpoint_authenticated: true,
denied_endpoint_unreachable: true,
imported_interface_matches: true,
tap_present: true,
ipv6_absent: true,
network_capabilities_empty: true,
route_mutation_refused: true,
cleanup_shape_matched: true,
responder_closed: true,
peer_namespace_closed: true,
host_interface_deleted: true,
allowed_endpoint_unreachable_after_cleanup: true,
tap_present_after_cleanup: true,
ipv6_unchanged_after_cleanup: true,
network_admin_dropped: true,
}
}

View file

@ -904,7 +904,8 @@ fn test_configuration(root: &Path) -> ResolvedDomainConfiguration {
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"
",
root.join("data").display()
),
)

View file

@ -9,17 +9,16 @@ use lazaret_core::identity::{
BundleDigest, ContainerId, GenerationId, HostPath, MountIdentity, NamespaceIdentity,
PidfsProcessIdentityV1, PreparedArtifactId,
};
use lazaret_core::network::{CanonicalDerivedNetworkPolicy, ReachabilityExpectation};
use lazaret_core::network::CanonicalDerivedNetworkPolicy;
use lazaret_core::records::{
HostProcessOwnershipSlot, NetworkReadinessEvidence, Participant, ParticipantOwnership,
ParticipantRole, ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck,
SandboxOwnership, StagedProcessOwnership,
HostProcessOwnershipSlot, NetworkReadinessAttestation, NetworkReadinessEvidence, Participant,
ParticipantOwnership, ParticipantRole, ParticipantState, PersistedHostProcessOwnership,
ProcessOwnershipCommitAck, SandboxOwnership, StagedProcessOwnership,
};
use lazaret_linux::ipc::private_seqpacket_pair;
use lazaret_linux::process::{
AcknowledgedChildAcquisition, RetainedProcess, child_broker, current_process_identity,
};
use lazaret_runtime_supervisor::network::NetworkProbeRequest;
use lazaret_runtime_supervisor::sandbox::SandboxHandoffAuthority;
use lazaret_runtime_supervisor::supervision::{
AttachedRunscSignal, CoordinatorChannel, RuntimeSupervisorLaunch, SupervisorEvent,

View file

@ -1,12 +1,11 @@
use super::{
AcknowledgedChildAcquisition, CanonicalDerivedNetworkPolicy, CommittedSandboxRuntime,
CoordinatorChannel, GitRoutingCoordinator, MountIdentity, NamespaceIdentity,
NetworkCoordinator, NetworkProbeRequest, NetworkReadinessEvidence, PidfsProcessIdentityV1,
PipeFlags, PreparedArtifactId, ProcessOwnershipCommitAck, ReachabilityExpectation,
RetainedProcess, RuntimeSupervisorLaunch, SandboxCoordinator, SandboxCoordinatorError,
SandboxHandoffAuthority, StagedProcessOwnership, StateAnchorCoordinator, SupervisorEvent,
child_broker, cleanup_supervisor, current_process_identity, io, pipe_with,
private_seqpacket_pair,
CoordinatorChannel, GitRoutingCoordinator, MountIdentity, NetworkCoordinator,
NetworkReadinessAttestation, NetworkReadinessEvidence, PidfsProcessIdentityV1, PipeFlags,
PreparedArtifactId, ProcessOwnershipCommitAck, RetainedProcess, RuntimeSupervisorLaunch,
SandboxCoordinator, SandboxCoordinatorError, SandboxHandoffAuthority, StagedProcessOwnership,
StateAnchorCoordinator, SupervisorEvent, child_broker, cleanup_supervisor,
current_process_identity, io, pipe_with, private_seqpacket_pair,
};
pub struct AwaitingAttachedRunsc {
@ -18,6 +17,7 @@ pub struct AwaitingAttachedRunsc {
git_routing_required: bool,
git_routing_mount: Option<MountIdentity>,
network_evidence: Option<NetworkReadinessEvidence>,
network_attestation: Option<NetworkReadinessAttestation>,
runsc_command: lazaret_runtime_supervisor::sandbox::RunscCommand,
}
@ -263,6 +263,7 @@ impl AwaitingAttachedRunsc {
git_routing_required: launch.has_git_routing(),
git_routing_mount: None,
network_evidence: None,
network_attestation: None,
runsc_command: launch.runsc_command().clone(),
})
}
@ -315,16 +316,11 @@ impl AwaitingAttachedRunsc {
///
/// # Errors
/// Refuses event ordering, namespace mutation, or any attestation mismatch.
pub fn receive_network_readiness<BuildProbeRequest>(
pub fn receive_network_attestation(
&mut self,
network: &NetworkCoordinator<'_>,
policy: &CanonicalDerivedNetworkPolicy,
build_probe_request: BuildProbeRequest,
expected_reachability: &ReachabilityExpectation,
) -> Result<NetworkReadinessEvidence, SandboxCoordinatorError>
where
BuildProbeRequest: FnOnce(NamespaceIdentity) -> NetworkProbeRequest,
{
) -> Result<(), SandboxCoordinatorError> {
let staged = self
.helper_staged
.ok_or(SandboxCoordinatorError::InvalidState(
@ -359,32 +355,14 @@ impl AwaitingAttachedRunsc {
attestation.runtime_supervisor,
)?;
network.validate_attestation(policy, &attestation)?;
let probe_request = build_probe_request(namespace);
if probe_request.domain_id != attestation.domain_id
|| probe_request.generation_id != attestation.generation_id
|| probe_request.enforcement_namespace != attestation.enforcement_namespace
{
if namespace != attestation.enforcement_namespace {
return Err(SandboxCoordinatorError::InvalidState(
"network probe request mismatches attestation",
"network namespace changed during attestation",
));
}
control.acknowledge_network_attestation(probe_request)?;
let probe = match control.receive()? {
SupervisorEvent::NetworkProbe(probe) => probe,
SupervisorEvent::Refused(detail) => {
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
}
_ => {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor did not return the network probe",
));
}
};
let evidence =
network.commit_readiness(policy, attestation, probe, expected_reachability)?;
control.acknowledge_network_readiness()?;
self.network_evidence = Some(evidence.clone());
Ok(evidence)
control.acknowledge_network_attestation()?;
self.network_attestation = Some(attestation);
Ok(())
}
/// Receives and durably stages the exact RS child without releasing its barrier.
@ -400,7 +378,7 @@ impl AwaitingAttachedRunsc {
"attached runsc stage was already received",
));
}
if self.helper_staged.is_some() && self.network_evidence.is_none() {
if self.helper_staged.is_some() && self.network_attestation.is_none() {
return Err(SandboxCoordinatorError::InvalidState(
"attached runsc cannot start before network attestation",
));
@ -469,6 +447,7 @@ impl AwaitingAttachedRunsc {
lifetime: self.lifetime.take(),
staged,
network_evidence: self.network_evidence.take(),
network_attestation: self.network_attestation.take(),
})
}
@ -494,6 +473,7 @@ pub struct AwaitingSandboxReadiness {
lifetime: Option<std::os::fd::OwnedFd>,
staged: StagedProcessOwnership,
network_evidence: Option<NetworkReadinessEvidence>,
network_attestation: Option<NetworkReadinessAttestation>,
}
impl AwaitingSandboxReadiness {
@ -520,13 +500,35 @@ impl AwaitingSandboxReadiness {
pub fn commit_networked(
mut self,
coordinator: &SandboxCoordinator<'_>,
network: &NetworkCoordinator<'_>,
policy: &CanonicalDerivedNetworkPolicy,
) -> Result<(CommittedSandboxRuntime, NetworkReadinessEvidence), SandboxCoordinatorError> {
let evidence =
self.network_evidence
let attestation =
self.network_attestation
.take()
.ok_or(SandboxCoordinatorError::InvalidState(
"network readiness evidence is absent",
"network attestation is absent",
))?;
let control = self
.control
.as_ref()
.ok_or(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor control channel is absent",
))?;
let canary = match control.receive()? {
SupervisorEvent::NetworkCanary(canary) => canary,
SupervisorEvent::Refused(detail) => {
return Err(SandboxCoordinatorError::Io(io::Error::other(detail)));
}
_ => {
return Err(SandboxCoordinatorError::InvalidState(
"Runtime Supervisor did not return owned-canary evidence",
));
}
};
let evidence = network.commit_readiness(policy, attestation, canary)?;
control.acknowledge_network_readiness()?;
self.network_evidence = Some(evidence.clone());
let runtime = self.commit_after_probe(coordinator, Some(evidence.clone()))?;
Ok((runtime, evidence))
}

View file

@ -1,4 +1,3 @@
use std::collections::BTreeSet;
use std::fmt;
use std::fs::File;
use std::os::unix::ffi::OsStringExt;
@ -11,21 +10,17 @@ use lazaret_core::identity::{
ConfigurationFingerprint, ContainerId, CoordinatorId, DomainId, GenerationId, NetworkSidecarId,
PrepareOperationId, SandboxId,
};
use lazaret_core::network::{DnsQueryExpectation, ReachabilityExpectation};
use lazaret_core::records::{DurableDecision, Generation, IpAddress, PreparedArtifact, VolumeKind};
use lazaret_linux::filesystem::namespace_identity;
use lazaret_core::records::{DurableDecision, Generation, PreparedArtifact, VolumeKind};
use lazaret_linux::platform::{PlatformObservation, check_platform};
use lazaret_linux::user_namespace::UserNamespaceMap;
use lazaret_prepared_lower::{
OciRegistryTransport, Platform, PreparedDestination, PreparedDestinationKind,
PreparedLowerError, PreparedLowerStore, RegistryCredentials,
};
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
use lazaret_runtime_supervisor::sandbox::{
CREDENTIALS_DESTINATION, DEV_DESTINATION, ESCALATION_CLIENT_DESTINATION,
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH,
PROC_DESTINATION, RootProcess, SANDBOX_INIT_DESTINATION, SYS_DESTINATION,
TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION,
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, PROC_DESTINATION, RootProcess,
SANDBOX_INIT_DESTINATION, SYS_DESTINATION, TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION,
};
use lazaret_sandbox_contract::RUNTIME_FILE_DESTINATIONS;
use lazaret_storage_catalog::{Catalog, CatalogError};
@ -85,9 +80,6 @@ pub struct RuntimeSelection {
artifact_authority: File,
artifact_label: PathBuf,
generation: Generation,
resolver_addresses: Vec<IpAddress>,
network_probe: NetworkProbeRequest,
reachability: ReachabilityExpectation,
container_id: ContainerId,
escalation_client: File,
sandbox_init: File,
@ -121,9 +113,6 @@ impl RuntimeSelection {
[PRIVATE_SANDBOX_INIT_ROLE.to_owned()],
)
.map_err(failure)?,
resolver_addresses: self.resolver_addresses.clone(),
network_probe: self.network_probe.clone(),
reachability: self.reachability.clone(),
container_id: self.container_id,
invoking_uid: rustix::process::getuid().as_raw(),
escalation_client: &self.escalation_client,
@ -222,9 +211,6 @@ async fn select_runtime_for_platform(
sandbox_id,
durable_decision: DurableDecision::None,
};
let namespace = namespace_identity(Path::new(NETWORK_NAMESPACE_PATH)).map_err(failure)?;
let (resolver_addresses, network_probe, reachability) =
select_network_readiness(configuration, &generation, namespace);
Ok(RuntimeSelection {
store,
domain_id: configuration.domain_id(),
@ -234,9 +220,6 @@ async fn select_runtime_for_platform(
artifact_authority,
artifact_label,
generation,
resolver_addresses,
network_probe,
reachability,
container_id: ContainerId::new(random_id()?),
escalation_client,
sandbox_init,
@ -313,67 +296,6 @@ fn next_generation(store: &HostCoordinatorDomainStore) -> Result<u64, RuntimeSel
.ok_or_else(|| failure("generation identity exhausted"))
}
fn select_network_readiness(
configuration: &ResolvedDomainConfiguration,
generation: &Generation,
enforcement_namespace: lazaret_core::identity::NamespaceIdentity,
) -> (Vec<IpAddress>, NetworkProbeRequest, ReachabilityExpectation) {
let readiness = configuration.network_readiness();
let denied = readiness
.denied
.iter()
.map(|target| TcpProbeTarget {
address: IpAddress::V4(target.address),
port: target.port,
})
.collect::<Vec<_>>();
let permitted = readiness
.permitted
.iter()
.map(|target| TcpProbeTarget {
address: IpAddress::V4(target.address),
port: target.port,
})
.collect::<Vec<_>>();
let dns = readiness
.dns
.iter()
.map(|target| DnsProbeTarget {
resolver: IpAddress::V4(target.resolver),
query_name: target.query.clone(),
})
.collect::<Vec<_>>();
let reachability = ReachabilityExpectation {
denied_destinations: denied.iter().map(|target| target.address).collect(),
permitted_destinations: permitted.iter().map(|target| target.address).collect(),
dns_queries: dns
.iter()
.map(|target| DnsQueryExpectation {
resolver: target.resolver,
query_name: target.query_name.clone(),
})
.collect(),
};
let resolvers = dns
.iter()
.map(|target| target.resolver)
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
(
resolvers,
NetworkProbeRequest {
domain_id: generation.domain_id,
generation_id: generation.generation_id,
enforcement_namespace,
denied,
permitted,
dns,
},
reachability,
)
}
fn open_installed_executable(path: &Path) -> Result<File, RuntimeSelectionError> {
let descriptor = openat2(
rustix::fs::CWD,
@ -420,6 +342,7 @@ fn random_id() -> Result<[u8; 32], RuntimeSelectionError> {
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::fs::{self, OpenOptions};
use std::future::Future;
use std::os::unix::ffi::OsStringExt;
@ -427,7 +350,6 @@ mod tests {
use std::task::{Context, Poll, Waker};
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
use lazaret_core::identity::{DeviceId, InodeId, NamespaceIdentity};
use lazaret_core::records::Domain;
use lazaret_linux::filesystem::try_exclusive_flock;
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
@ -437,35 +359,9 @@ mod tests {
use super::*;
#[test]
fn resolved_readiness_maps_exactly_and_destinations_match_builder_policy() {
fn destinations_match_builder_policy() {
let tree = tempfile::tempdir().unwrap();
let configuration = configuration(tree.path());
let generation = Generation {
domain_id: configuration.domain_id(),
generation_id: GenerationId::new(9),
configuration_fingerprint: configuration.configuration_fingerprint(),
coordinator_id: CoordinatorId::new([1; 32]),
network_sidecar_id: NetworkSidecarId::new([2; 32]),
sandbox_id: SandboxId::new([3; 32]),
durable_decision: DurableDecision::None,
};
let namespace = NamespaceIdentity {
device: DeviceId::new(4),
inode: InodeId::new(5),
};
let (resolvers, request, expectation) =
select_network_readiness(&configuration, &generation, namespace);
assert_eq!(resolvers, [IpAddress::V4([198, 51, 100, 53])]);
assert_eq!(request.denied[0].port, 8443);
assert_eq!(request.permitted[0].port, 443);
assert_eq!(request.dns[0].query_name, "example.invalid");
assert_eq!(expectation.denied_destinations, [request.denied[0].address]);
assert_eq!(
expectation.permitted_destinations,
[request.permitted[0].address]
);
assert_eq!(expectation.dns_queries[0].resolver, request.dns[0].resolver);
let destinations = prepared_destinations(&configuration);
assert!(destinations.iter().any(|destination| {
destination.path == Path::new(WORKSPACE_DESTINATION)
@ -620,7 +516,8 @@ mod tests {
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"Example.Invalid.\" }}]\n",
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/factory:v1\"
",
root.join("data").display()
),
)

View file

@ -1,216 +1,7 @@
use std::ffi::{OsStr, OsString};
use std::io;
use std::os::unix::ffi::OsStrExt as _;
pub use lazaret_sandbox_init::{
PRIVATE_SANDBOX_INIT_ROLE, PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE,
dispatch_private_sandbox_init, is_private_sandbox_init_role,
};
use lazaret_linux::process::{TargetCommand, exec_target};
pub const PRIVATE_SANDBOX_INIT_ROLE: &str = "--lazaret-private-sandbox-init-v1";
pub const PRIVATE_SESSION_EXEC_ROLE: &str = "--lazaret-private-session-exec-v1";
pub const PRIVATE_SESSION_IDENTITY_ROLE: &str = "--lazaret-private-session-identity-v1";
#[must_use]
pub fn is_private_sandbox_init_role(arguments: &[OsString]) -> bool {
arguments.len() == 1 && arguments[0] == OsStr::new(PRIVATE_SANDBOX_INIT_ROLE)
}
#[must_use]
pub fn dispatch_private_sandbox_init(arguments: &[OsString]) -> Option<io::Result<()>> {
if is_private_sandbox_init_role(arguments) {
loop {
std::thread::park();
}
}
if arguments
.first()
.is_some_and(|argument| argument == PRIVATE_SESSION_EXEC_ROLE)
{
return Some(run_session_exec(&arguments[1..]));
}
if arguments
.first()
.is_some_and(|argument| argument == PRIVATE_SESSION_IDENTITY_ROLE)
{
return Some(report_session_identity(&arguments[1..]));
}
None
}
fn run_session_exec(arguments: &[OsString]) -> io::Result<()> {
let (separator, argv) = arguments
.split_first()
.filter(|(separator, argv)| {
separator == &OsStr::new("--")
&& !argv.is_empty()
&& !argv[0].is_empty()
&& argv
.iter()
.all(|argument| !argument.as_bytes().contains(&0))
})
.ok_or_else(|| io::Error::other("session exec requires -- and a nonempty argv"))?;
let _ = separator;
establish_process_group_leader()?;
let target = argv[1..]
.iter()
.cloned()
.fold(TargetCommand::new(argv[0].clone()), TargetCommand::argument);
Err(exec_target(target))
}
fn establish_process_group_leader() -> io::Result<()> {
let pid = rustix::process::getpid();
if rustix::process::getpgrp() != pid {
rustix::process::setsid()?;
}
require_process_group_leader(pid, rustix::process::getpgrp())
}
fn require_process_group_leader(
pid: rustix::process::Pid,
process_group: rustix::process::Pid,
) -> io::Result<()> {
if pid != process_group {
return Err(io::Error::other(
"session exec is not its process-group leader",
));
}
Ok(())
}
fn report_session_identity(arguments: &[OsString]) -> io::Result<()> {
match arguments {
[pid] => report_process_identity(parse_positive_pid(pid)?),
[mode, group] if mode == "--group" => report_group_presence(parse_positive_pid(group)?),
_ => Err(io::Error::other("session identity query is invalid")),
}
}
fn parse_positive_pid(value: &OsStr) -> io::Result<u32> {
value
.to_str()
.and_then(|value| value.parse::<u32>().ok())
.filter(|value| *value > 0)
.ok_or_else(|| io::Error::other("session identity number is invalid"))
}
fn report_process_identity(pid: u32) -> io::Result<()> {
let Some((observed_pid, start_ticks, group)) = read_process_identity(pid)? else {
println!("absent");
return Ok(());
};
println!("{observed_pid} {start_ticks} {group}");
Ok(())
}
fn report_group_presence(group: u32) -> io::Result<()> {
println!(
"{}",
if group_is_present_except(group, std::process::id())? {
"present"
} else {
"absent"
}
);
Ok(())
}
fn group_is_present_except(group: u32, observer: u32) -> io::Result<bool> {
for entry in std::fs::read_dir("/proc")? {
let entry = entry?;
let Some(pid) = entry
.file_name()
.to_str()
.and_then(|value| value.parse::<u32>().ok())
else {
continue;
};
if pid == observer {
continue;
}
if read_process_identity(pid)?.is_some_and(|(_, _, observed_group)| observed_group == group)
{
return Ok(true);
}
}
Ok(false)
}
fn read_process_identity(pid: u32) -> io::Result<Option<(u32, u64, u32)>> {
let stat = match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
Ok(stat) => stat,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
let close = stat
.rfind(')')
.ok_or_else(|| io::Error::other("guest process stat is malformed"))?;
let observed_pid = stat[..close]
.split_once(' ')
.and_then(|(pid, _)| pid.parse::<u32>().ok())
.ok_or_else(|| io::Error::other("guest process stat PID is malformed"))?;
let fields = stat[close + 1..].split_whitespace().collect::<Vec<_>>();
let group = fields
.get(2)
.and_then(|value| value.parse::<u32>().ok())
.ok_or_else(|| io::Error::other("guest process stat group is malformed"))?;
let start_ticks = fields
.get(19)
.and_then(|value| value.parse::<u64>().ok())
.ok_or_else(|| io::Error::other("guest process stat start is malformed"))?;
Ok(Some((observed_pid, start_ticks, group)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn private_role_dispatch_is_exact_and_closed() {
assert!(is_private_sandbox_init_role(&[OsString::from(
PRIVATE_SANDBOX_INIT_ROLE
)]));
assert!(!is_private_sandbox_init_role(&[]));
assert!(!is_private_sandbox_init_role(&[
OsString::from(PRIVATE_SANDBOX_INIT_ROLE),
OsString::from("extra"),
]));
assert!(!is_private_sandbox_init_role(&[OsString::from(
"--private-sandbox-init"
)]));
}
#[test]
fn private_session_roles_are_shape_separated() {
assert!(dispatch_private_sandbox_init(&[]).is_none());
assert!(
dispatch_private_sandbox_init(&[
OsString::from(PRIVATE_SESSION_EXEC_ROLE),
OsString::from("missing-separator"),
])
.unwrap()
.is_err()
);
assert!(
dispatch_private_sandbox_init(&[
OsString::from(PRIVATE_SESSION_IDENTITY_ROLE),
OsString::from("not-a-pid"),
])
.unwrap()
.is_err()
);
}
#[test]
fn process_group_leader_validation_is_fail_closed() {
let leader = rustix::process::Pid::from_raw(41).unwrap();
let other_group = rustix::process::Pid::from_raw(42).unwrap();
require_process_group_leader(leader, leader).unwrap();
assert!(require_process_group_leader(leader, other_group).is_err());
}
#[test]
fn group_presence_is_independent_of_a_designated_leader() {
let (_, _, group) = read_process_identity(std::process::id()).unwrap().unwrap();
assert!(group_is_present_except(group, u32::MAX).unwrap());
assert!(!group_is_present_except(u32::MAX, u32::MAX).unwrap());
}
}
pub(crate) const DEFAULT_AGENT_PATH: &str =
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";

View file

@ -699,7 +699,8 @@ fn configuration(tree: &TestTree) -> lazaret_core::configuration::ResolvedDomain
fs::write(
&configuration_path,
format!(
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/fixture:v1\"\npersistent_dirs = [\"/usr\"]
",
tree.path.join("data").display()
),
)

View file

@ -1,4 +1,4 @@
use std::collections::{HashMap, VecDeque};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::ffi::OsString;
use std::fs::File;
use std::io;
@ -6,6 +6,7 @@ use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use lazaret_core::configuration::EnvironmentValue;
use lazaret_core::identity::{
ContainerId, CoordinatorId, DeviceId, DomainId, FileDescriptor, GenerationId,
GuestProcessGroupId, GuestProcessId, GuestProcessStartTimeTicks, InodeId,
@ -467,6 +468,7 @@ pub struct TerminalAcquisition {
pub host_coordinator: PidfsProcessIdentityV1,
pub runsc_program: OsString,
pub runsc_global_arguments: Vec<OsString>,
pub environment: BTreeMap<String, EnvironmentValue>,
pub internal_pid_file: PathBuf,
pub argv: Vec<Vec<u8>>,
}

View file

@ -1,12 +1,12 @@
use super::{
Arc, ContainerId, ControlError, CoordinatorId, DomainId, GenerationId, HashMap,
HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder, InteractiveControlSession,
InteractiveSessionBatch, InteractiveSessionEvent, InteractiveSessionExecutor,
InteractiveSessionMessage, InteractiveSessionRequest, Mutex, OsString, PathBuf,
PidfsProcessIdentityV1, PromptRouteCancellation, ProtocolVersion, RunscSessionTerminalFactory,
SessionId, SessionPromptTransport, SessionTerminal, SessionTerminalEvent,
SessionTerminalFactory, SessionTerminalMessage, SessionTerminalProcess, TerminalAcquisition,
TerminalError, TerminalLoss, TerminalRoute, VecDeque,
Arc, BTreeMap, ContainerId, ControlError, CoordinatorId, DomainId, EnvironmentValue,
GenerationId, HashMap, HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder,
InteractiveControlSession, InteractiveSessionBatch, InteractiveSessionEvent,
InteractiveSessionExecutor, InteractiveSessionMessage, InteractiveSessionRequest, Mutex,
OsString, PathBuf, PidfsProcessIdentityV1, PromptRouteCancellation, ProtocolVersion,
RunscSessionTerminalFactory, SessionId, SessionPromptTransport, SessionTerminal,
SessionTerminalEvent, SessionTerminalFactory, SessionTerminalMessage, SessionTerminalProcess,
TerminalAcquisition, TerminalError, TerminalLoss, TerminalRoute, VecDeque,
};
pub(super) struct ActiveTerminal {
@ -36,6 +36,7 @@ pub struct RunscInteractiveSessionExecutor {
host_coordinator: PidfsProcessIdentityV1,
runsc_program: OsString,
runsc_global_arguments: Vec<OsString>,
environment: BTreeMap<String, EnvironmentValue>,
runtime_root: PathBuf,
}
@ -50,6 +51,7 @@ impl RunscInteractiveSessionExecutor {
container_id: ContainerId,
host_coordinator: PidfsProcessIdentityV1,
command: &lazaret_runtime_supervisor::sandbox::RunscCommand,
environment: BTreeMap<String, EnvironmentValue>,
runtime_root: PathBuf,
) -> Result<Self, TerminalError> {
let run = command
@ -75,6 +77,7 @@ impl RunscInteractiveSessionExecutor {
host_coordinator,
runsc_program: command.program().to_owned(),
runsc_global_arguments: command.arguments()[..run].to_vec(),
environment,
runtime_root,
})
}
@ -104,6 +107,7 @@ impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
host_coordinator: self.host_coordinator,
runsc_program: self.runsc_program.clone(),
runsc_global_arguments: self.runsc_global_arguments.clone(),
environment: self.environment.clone(),
internal_pid_file: self.runtime_root.join(format!(
"session-{}.pid",
hex_session(request.session_id.as_bytes())

View file

@ -45,8 +45,19 @@ impl SessionTerminalFactory for RunscSessionTerminalFactory {
for argument in &request.runsc_global_arguments {
target = target.argument(argument.clone());
}
target = target.argument("exec");
if !request.environment.contains_key("PATH") {
target = target
.argument("-env")
.argument(format!("PATH={}", crate::sandbox_init::DEFAULT_AGENT_PATH));
}
for (name, value) in &request.environment {
let mut assignment = OsString::from(name);
assignment.push("=");
assignment.push(value.expose());
target = target.argument("-env").argument(assignment);
}
target = target
.argument("exec")
.argument(format!(
"-internal-pid-file={}",
request.internal_pid_file.display()

Some files were not shown because too many files have changed in this diff Show more