Require fresh State Anchor absence before lease release

This commit is contained in:
ms2k 2026-07-17 04:40:02 +09:00
commit 3b76b59bdf
2 changed files with 543 additions and 194 deletions

View file

@ -9,7 +9,7 @@ use std::time::{Duration, Instant};
use lazaret_core::codec::{decode, encode};
use lazaret_core::identity::{
ConfigurationFingerprint, CoordinatorId, DerivedPolicyDigest, DomainId, GenerationId,
NetworkSidecarId,
MountIdentity, NamespaceIdentity, NetworkSidecarId,
};
use lazaret_core::network::CanonicalDerivedNetworkPolicy;
use lazaret_core::records::{
@ -23,7 +23,8 @@ use lazaret_linux::process::{
child_broker, current_process_identity,
};
use lazaret_state_anchor::{
RuntimeStateAnchorAuthority, RuntimeStateAnchorMount, StateAnchorLaunchAuthority,
RuntimeStateAnchorAbsenceAuthority, RuntimeStateAnchorAuthority, RuntimeStateAnchorInherited,
RuntimeStateAnchorMount, StateAnchorLaunchAuthority,
};
use rustix::event::{PollFd, PollFlags, Timespec, poll};
use rustix::fs::{MemfdFlags, memfd_create};
@ -86,6 +87,12 @@ pub enum SupervisorEvent {
StateAnchorUnmounted {
mount: lazaret_core::identity::MountIdentity,
},
StateAnchorAbsenceObserved {
user_namespace: NamespaceIdentity,
mount_namespace: NamespaceIdentity,
mount: MountIdentity,
observed: MountIdentity,
},
Exited,
AttachedRunscExited,
Stopped,
@ -106,6 +113,11 @@ enum CoordinatorMessage {
StopSandbox,
StopSidecar,
UnmountStateAnchor,
ObserveStateAnchorAbsence {
user_namespace: NamespaceIdentity,
mount_namespace: NamespaceIdentity,
mount: MountIdentity,
},
Exit,
Stop,
}
@ -225,6 +237,24 @@ impl CoordinatorChannel {
send_message(&self.descriptor, &CoordinatorMessage::UnmountStateAnchor)
}
/// # Errors
/// Returns an error when exact namespace-local absence observation cannot be requested.
pub fn observe_state_anchor_absence(
&self,
user_namespace: NamespaceIdentity,
mount_namespace: NamespaceIdentity,
mount: MountIdentity,
) -> io::Result<()> {
send_message(
&self.descriptor,
&CoordinatorMessage::ObserveStateAnchorAbsence {
user_namespace,
mount_namespace,
mount,
},
)
}
/// # Errors
/// Returns an error when graceful Runtime Supervisor exit cannot be requested.
pub fn exit(&self) -> io::Result<()> {
@ -657,8 +687,19 @@ fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
let control = std::io::stdout();
let parent = current_process_identity()?;
let mut state_anchor = prepare_state_anchor(arguments, parent, namespaces, &control)?;
let result = run_supervisor_with_state(arguments, parent, &control, &mut state_anchor);
let cleanup = state_anchor.take().map(RuntimeStateAnchorMount::unmount);
let mut state_anchor_absence = None;
let result = run_supervisor_with_state(
arguments,
parent,
&control,
namespaces,
&mut state_anchor,
&mut state_anchor_absence,
);
let cleanup = state_anchor
.take()
.map(RuntimeStateAnchorMount::unmount)
.map(|result| result.map(drop));
match (result, cleanup) {
(Err(error), Some(Err(cleanup))) => Err(io::Error::new(
error.kind(),
@ -688,17 +729,17 @@ fn prepare_state_anchor(
"Runtime Supervisor received no State Anchor mount command",
));
}
let authority = RuntimeStateAnchorAuthority::from_inherited(
state.lower_fd,
&state.lower_label,
state.persistent_root_fd,
state.persistent_anchor_fd,
&state.persistent_anchor_label,
state.runtime_root_fd,
state.runtime_anchor_fd,
&state.runtime_anchor_label,
state.expected_lower,
)
let authority = RuntimeStateAnchorAuthority::from_inherited(RuntimeStateAnchorInherited {
lower_fd: state.lower_fd,
lower_label: &state.lower_label,
persistent_root_fd: state.persistent_root_fd,
persistent_anchor_fd: state.persistent_anchor_fd,
persistent_anchor_label: &state.persistent_anchor_label,
runtime_root_fd: state.runtime_root_fd,
runtime_anchor_fd: state.runtime_anchor_fd,
runtime_anchor_label: &state.runtime_anchor_label,
expected_lower: state.expected_lower,
})
.map_err(io::Error::other)?;
let ownership = ProcessOwnershipContext {
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
@ -742,7 +783,9 @@ fn run_supervisor_with_state(
arguments: &RuntimeArguments,
parent: lazaret_core::identity::PidfsProcessIdentityV1,
control: &std::io::Stdout,
namespaces: (NamespaceIdentity, NamespaceIdentity),
state_anchor: &mut Option<RuntimeStateAnchorMount>,
state_anchor_absence: &mut Option<RuntimeStateAnchorAbsenceAuthority>,
) -> io::Result<()> {
if arguments.state_anchor.is_some() {
let message: CoordinatorMessage = receive_message(control)?;
@ -827,7 +870,15 @@ fn run_supervisor_with_state(
io::Error::other("sandbox commit acknowledgment was not received"),
);
}
supervise(control, process, network, state_anchor, &delete)
supervise(
control,
process,
network,
namespaces,
state_anchor,
state_anchor_absence,
&delete,
)
}
fn prepare_runtime_network(
@ -983,10 +1034,17 @@ fn supervise(
control: &std::io::Stdout,
process: RetainedProcess,
mut network: Option<PreparedNetwork>,
namespaces: (NamespaceIdentity, NamespaceIdentity),
state_anchor: &mut Option<RuntimeStateAnchorMount>,
state_anchor_absence: &mut Option<RuntimeStateAnchorAbsenceAuthority>,
delete: &TargetCommand,
) -> io::Result<()> {
let mut process = Some(process);
let mut state_anchor = StateAnchorSupervision {
namespaces,
mount: state_anchor,
absence: state_anchor_absence,
};
loop {
let exited = match process
.as_ref()
@ -1059,7 +1117,7 @@ fn supervise(
&message,
&mut process,
&mut network,
state_anchor,
&mut state_anchor,
delete,
) {
Ok(true) => return Ok(()),
@ -1070,12 +1128,18 @@ fn supervise(
}
}
struct StateAnchorSupervision<'a> {
namespaces: (NamespaceIdentity, NamespaceIdentity),
mount: &'a mut Option<RuntimeStateAnchorMount>,
absence: &'a mut Option<RuntimeStateAnchorAbsenceAuthority>,
}
fn handle_supervisor_message(
control: &std::io::Stdout,
message: &CoordinatorMessage,
process: &mut Option<RetainedProcess>,
network: &mut Option<PreparedNetwork>,
state_anchor: &mut Option<RuntimeStateAnchorMount>,
state_anchor: &mut StateAnchorSupervision<'_>,
delete: &TargetCommand,
) -> io::Result<bool> {
match message {
@ -1102,25 +1166,17 @@ fn handle_supervisor_message(
stop_network(network.take())?;
send_message(control, &SupervisorEvent::SidecarStopped)?;
}
CoordinatorMessage::UnmountStateAnchor => {
if process.is_some() || network.is_some() {
return Err(io::Error::other(
"State Anchor unmount preceded runtime resource stop",
));
}
let mount = state_anchor
.as_ref()
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
.mount_identity();
state_anchor
.take()
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
.unmount()
.map_err(io::Error::other)?;
send_message(control, &SupervisorEvent::StateAnchorUnmounted { mount })?;
CoordinatorMessage::UnmountStateAnchor
| CoordinatorMessage::ObserveStateAnchorAbsence { .. } => {
handle_state_anchor_message(
control,
message,
process.is_some() || network.is_some(),
state_anchor,
)?;
}
CoordinatorMessage::Exit => {
if process.is_some() || network.is_some() || state_anchor.is_some() {
if process.is_some() || network.is_some() || state_anchor.mount.is_some() {
return Err(io::Error::other(
"Runtime Supervisor exit preceded resource cleanup",
));
@ -1150,6 +1206,70 @@ fn handle_supervisor_message(
Ok(false)
}
fn handle_state_anchor_message(
control: &std::io::Stdout,
message: &CoordinatorMessage,
runtime_resources_present: bool,
state: &mut StateAnchorSupervision<'_>,
) -> io::Result<()> {
match message {
CoordinatorMessage::UnmountStateAnchor => {
if runtime_resources_present {
return Err(io::Error::other(
"State Anchor unmount preceded runtime resource stop",
));
}
let mount = state
.mount
.as_ref()
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
.mount_identity();
let absence = state
.mount
.take()
.ok_or_else(|| io::Error::other("State Anchor is absent"))?
.unmount()
.map_err(io::Error::other)?;
*state.absence = Some(absence);
send_message(control, &SupervisorEvent::StateAnchorUnmounted { mount })
}
CoordinatorMessage::ObserveStateAnchorAbsence {
user_namespace,
mount_namespace,
mount,
} => {
if (*user_namespace, *mount_namespace) != state.namespaces {
return send_message(
control,
&SupervisorEvent::Refused(
"State Anchor absence request namespace authority mismatched".into(),
),
);
}
match state.absence.as_ref().map_or_else(
|| {
Err(lazaret_state_anchor::StateAnchorError::InvalidLayout(
"State Anchor absence authority is absent",
))
},
|authority| authority.observe_absence(*mount),
) {
Ok(observed) => send_message(
control,
&SupervisorEvent::StateAnchorAbsenceObserved {
user_namespace: state.namespaces.0,
mount_namespace: state.namespaces.1,
mount: *mount,
observed,
},
),
Err(error) => send_message(control, &SupervisorEvent::Refused(error.to_string())),
}
}
_ => Err(io::Error::other("message is not a State Anchor command")),
}
}
fn fail_active_runtime(
process: Option<RetainedProcess>,
network: Option<PreparedNetwork>,
@ -1347,6 +1467,30 @@ mod tests {
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
CoordinatorMessage::UnmountStateAnchor
);
let user_namespace = NamespaceIdentity {
device: lazaret_core::identity::DeviceId::new(11),
inode: lazaret_core::identity::InodeId::new(12),
};
let mount_namespace = NamespaceIdentity {
device: lazaret_core::identity::DeviceId::new(13),
inode: lazaret_core::identity::InodeId::new(14),
};
let mount = MountIdentity {
mount_id: lazaret_core::identity::MountId::new(15),
device: lazaret_core::identity::DeviceId::new(16),
inode: lazaret_core::identity::InodeId::new(17),
};
channel
.observe_state_anchor_absence(user_namespace, mount_namespace, mount)
.unwrap();
assert_eq!(
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
CoordinatorMessage::ObserveStateAnchorAbsence {
user_namespace,
mount_namespace,
mount,
}
);
channel.exit().unwrap();
assert_eq!(
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),

View file

@ -11,7 +11,7 @@ use lazaret_core::identity::{ContainerId, DeviceId, InodeId, NamespaceIdentity};
use lazaret_runtime_supervisor::sandbox::{
CREDENTIALS_DESTINATION, ESCALATION_CLIENT_DESTINATION, HostTerminal,
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH, RootProcess,
SANDBOX_INIT_DESTINATION, SandboxBuildErrorKind, SandboxBuildRequest,
SANDBOX_INIT_DESTINATION, SandboxBuildErrorKind, SandboxBuildRequest, SandboxBundle,
TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION, build_sandbox_bundle,
production_sandbox_layout, stage_host_terminal,
};
@ -312,7 +312,20 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
}])
);
assert_eq!(first.command().program(), OsStr::new("runsc"));
assert_runtime_command(&first, &fixture);
assert_eq!(
specification["root"]["path"],
fixture
.tree
.path()
.join("prepared-root")
.display()
.to_string()
);
}
fn assert_runtime_command(bundle: &SandboxBundle, fixture: &Fixture) {
assert_eq!(bundle.command().program(), OsStr::new("runsc"));
let runtime_domain = String::from_utf8(
fixture
.configuration
@ -323,7 +336,7 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
)
.unwrap();
assert_eq!(
first
bundle
.command()
.arguments()
.iter()
@ -355,16 +368,7 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
"11".repeat(32),
]
);
assert_eq!(
specification["root"]["path"],
fixture
.tree
.path()
.join("prepared-root")
.display()
.to_string()
);
assert!(!first.command().arguments().iter().any(|argument| {
assert!(!bundle.command().arguments().iter().any(|argument| {
matches!(
argument.to_str(),
Some("-detach" | "-console-socket" | "-c")