Compose cold-start resources under Runtime Supervisor ownership
This commit is contained in:
parent
510dfccf5a
commit
33ac0ba4ee
3 changed files with 1827 additions and 139 deletions
|
|
@ -388,7 +388,223 @@ pub struct StateAnchorAcquisition<'path> {
|
|||
pub ownership: ProcessOwnershipContext,
|
||||
}
|
||||
|
||||
pub struct StateAnchorLaunchAuthority {
|
||||
lower: OwnedFd,
|
||||
persistent_anchor: OwnedFd,
|
||||
runtime_anchor: OwnedFd,
|
||||
expected_lower: (u64, u64),
|
||||
}
|
||||
|
||||
impl StateAnchorLaunchAuthority {
|
||||
/// Captures exact directory authorities for inheritance by the Runtime Supervisor.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a changed layout or prepared lower.
|
||||
pub fn capture(
|
||||
layout: &StateAnchorLayout<'_>,
|
||||
artifact: &PreparedArtifact,
|
||||
) -> Result<Self, StateAnchorError> {
|
||||
layout.revalidate()?;
|
||||
let expected_lower = (artifact.device.get(), artifact.inode.get());
|
||||
let lower = open_exact_directory_path(&path_from_host(&artifact.path), expected_lower)?;
|
||||
Ok(Self {
|
||||
lower: inherit_for_exec(lower)?,
|
||||
persistent_anchor: inherit_for_exec(&layout.persistent_anchor)?,
|
||||
runtime_anchor: inherit_for_exec(&layout.runtime_anchor)?,
|
||||
expected_lower,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn lower_fd(&self) -> i32 {
|
||||
self.lower.as_raw_fd()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn persistent_anchor_fd(&self) -> i32 {
|
||||
self.persistent_anchor.as_raw_fd()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn runtime_anchor_fd(&self) -> i32 {
|
||||
self.runtime_anchor.as_raw_fd()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn expected_lower(&self) -> (u64, u64) {
|
||||
self.expected_lower
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RuntimeStateAnchorAuthority {
|
||||
lower: OwnedFd,
|
||||
persistent_anchor: OwnedFd,
|
||||
runtime_anchor: OwnedFd,
|
||||
merged_descriptor: OwnedFd,
|
||||
}
|
||||
|
||||
impl RuntimeStateAnchorAuthority {
|
||||
/// Reopens inherited authorities without falling back to ambient configured paths.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses missing, replaced, or incorrectly typed descriptors and fixed children.
|
||||
pub fn from_inherited(
|
||||
lower_fd: i32,
|
||||
persistent_anchor_fd: i32,
|
||||
runtime_anchor_fd: i32,
|
||||
expected_lower: (u64, u64),
|
||||
) -> Result<Self, StateAnchorError> {
|
||||
let lower = reopen_inherited_directory(lower_fd)?;
|
||||
require_directory_descriptor_identity(&lower, expected_lower)?;
|
||||
let persistent_anchor = reopen_inherited_directory(persistent_anchor_fd)?;
|
||||
let runtime_anchor = reopen_inherited_directory(runtime_anchor_fd)?;
|
||||
for name in ["upper", "work"] {
|
||||
drop(open_directory(&persistent_anchor, Path::new(name))?);
|
||||
}
|
||||
let merged_descriptor = open_directory(&runtime_anchor, Path::new("merged"))?;
|
||||
Ok(Self {
|
||||
lower,
|
||||
persistent_anchor,
|
||||
runtime_anchor,
|
||||
merged_descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
/// Mounts the exact inherited prepared lower and anchors under RS child ownership.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses acquisition, acknowledgment, readiness, or authority failures.
|
||||
pub fn acquire<Stage, Acknowledge, Ready>(
|
||||
self,
|
||||
expected_parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||
ownership: ProcessOwnershipContext,
|
||||
stage: Stage,
|
||||
acknowledge: Acknowledge,
|
||||
ready: Ready,
|
||||
) -> Result<RuntimeStateAnchorMount, StateAnchorError>
|
||||
where
|
||||
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
|
||||
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
|
||||
Ready: FnOnce(VerifiedStateAnchorReadiness) -> io::Result<()>,
|
||||
{
|
||||
if ownership.slot != HostProcessOwnershipSlot::StateAnchorDaemon {
|
||||
return Err(StateAnchorError::InvalidLayout(
|
||||
"process slot is not State Anchor daemon",
|
||||
));
|
||||
}
|
||||
let baseline = mount_identity(&self.merged_descriptor)?;
|
||||
let lower_launch = inherit_for_exec(&self.lower)?;
|
||||
let persistent_launch = inherit_for_exec(&self.persistent_anchor)?;
|
||||
let runtime_launch = inherit_for_exec(&self.runtime_anchor)?;
|
||||
let target = TargetCommand::new("fuse-overlayfs")
|
||||
.argument("-f")
|
||||
.argument("-o")
|
||||
.argument(overlay_options(
|
||||
&descriptor_path(&lower_launch),
|
||||
&descriptor_child_path(&persistent_launch, "upper"),
|
||||
&descriptor_child_path(&persistent_launch, "work"),
|
||||
))
|
||||
.argument(descriptor_child_path(&runtime_launch, "merged"));
|
||||
let process =
|
||||
child_broker().acquire(expected_parent, ownership, &target, stage, acknowledge)?;
|
||||
let staged = StagedProcessOwnership::v1(
|
||||
ownership.domain_id,
|
||||
ownership.generation_id,
|
||||
ownership.coordinator_id,
|
||||
ownership.slot,
|
||||
process.identity(),
|
||||
);
|
||||
let deadline = Instant::now() + MOUNT_TIMEOUT;
|
||||
loop {
|
||||
if process.has_exited()? {
|
||||
child_broker().reap(process)?;
|
||||
return Err(StateAnchorError::DaemonExited);
|
||||
}
|
||||
let observed = mount_identity(&open_mounted_directory(
|
||||
&self.runtime_anchor,
|
||||
Path::new("merged"),
|
||||
)?)?;
|
||||
if observed.mount_id != baseline.mount_id {
|
||||
let readiness = VerifiedStateAnchorReadiness {
|
||||
staged,
|
||||
mount: observed,
|
||||
};
|
||||
if let Err(error) = ready(readiness) {
|
||||
process.send_signal(rustix::process::Signal::KILL)?;
|
||||
while !process.has_exited()? {
|
||||
thread::yield_now();
|
||||
}
|
||||
let _ = rootless_unmount_anchor(&self.runtime_anchor);
|
||||
child_broker().reap(process)?;
|
||||
return Err(error.into());
|
||||
}
|
||||
return Ok(RuntimeStateAnchorMount {
|
||||
authority: self,
|
||||
process,
|
||||
mount: observed,
|
||||
});
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
process.send_signal(rustix::process::Signal::KILL)?;
|
||||
while !process.has_exited()? {
|
||||
thread::yield_now();
|
||||
}
|
||||
let merged = open_mounted_directory(&self.runtime_anchor, Path::new("merged"))?;
|
||||
let observed = mount_identity(&merged)?;
|
||||
if observed.mount_id != baseline.mount_id {
|
||||
let _ = rootless_unmount_anchor(&self.runtime_anchor);
|
||||
}
|
||||
child_broker().reap(process)?;
|
||||
return Err(StateAnchorError::MountTimeout);
|
||||
}
|
||||
thread::sleep(MOUNT_POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RuntimeStateAnchorMount {
|
||||
authority: RuntimeStateAnchorAuthority,
|
||||
process: RetainedProcess,
|
||||
mount: MountIdentity,
|
||||
}
|
||||
|
||||
impl RuntimeStateAnchorMount {
|
||||
#[must_use]
|
||||
pub const fn mount_identity(&self) -> MountIdentity {
|
||||
self.mount
|
||||
}
|
||||
|
||||
/// Unmounts and reaps only the retained exact RS-owned State Anchor.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses mount replacement, unmount failure, or exact child cleanup failure.
|
||||
pub fn unmount(self) -> Result<(), StateAnchorError> {
|
||||
let observed = mount_identity(&open_mounted_directory(
|
||||
&self.authority.runtime_anchor,
|
||||
Path::new("merged"),
|
||||
)?)?;
|
||||
if observed != self.mount {
|
||||
return Err(StateAnchorError::OwnershipMismatch);
|
||||
}
|
||||
rootless_unmount_anchor(&self.authority.runtime_anchor)?;
|
||||
let after = mount_identity(&open_directory(
|
||||
&self.authority.runtime_anchor,
|
||||
Path::new("merged"),
|
||||
)?)?;
|
||||
if after == self.mount {
|
||||
return Err(StateAnchorError::OwnershipMismatch);
|
||||
}
|
||||
child_broker().reap(self.process)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl VerifiedStateAnchorReadiness {
|
||||
#[must_use]
|
||||
pub const fn from_parts(staged: StagedProcessOwnership, mount: MountIdentity) -> Self {
|
||||
Self { staged, mount }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn staged(self) -> StagedProcessOwnership {
|
||||
self.staged
|
||||
|
|
@ -757,6 +973,24 @@ fn inherit_for_exec(descriptor: impl AsFd) -> Result<OwnedFd, StateAnchorError>
|
|||
dup(descriptor).map_err(io::Error::from).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn reopen_inherited_directory(descriptor: i32) -> Result<OwnedFd, StateAnchorError> {
|
||||
if descriptor < 0 {
|
||||
return Err(StateAnchorError::InvalidLayout(
|
||||
"inherited directory descriptor is invalid",
|
||||
));
|
||||
}
|
||||
let file = File::open(format!("/proc/self/fd/{descriptor}"))?;
|
||||
let descriptor: OwnedFd = file.into();
|
||||
let metadata = rustix::fs::fstat(&descriptor).map_err(io::Error::from)?;
|
||||
if FileType::from_raw_mode(metadata.st_mode).is_dir() {
|
||||
Ok(descriptor)
|
||||
} else {
|
||||
Err(StateAnchorError::InvalidLayout(
|
||||
"inherited authority is not a directory",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay_options(lower: &Path, upper: &Path, work: &Path) -> OsString {
|
||||
let mut bytes = b"lowerdir=".to_vec();
|
||||
append_fuse_option_path(&mut bytes, lower);
|
||||
|
|
@ -781,7 +1015,12 @@ fn append_fuse_option_path(target: &mut Vec<u8>, path: &Path) {
|
|||
|
||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||
fn rootless_unmount(layout: &StateAnchorLayout<'_>) -> Result<(), StateAnchorError> {
|
||||
let inherited_anchor = inherit_for_exec(&layout.runtime_anchor)?;
|
||||
rootless_unmount_anchor(&layout.runtime_anchor)
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||
fn rootless_unmount_anchor(runtime_anchor: impl AsFd) -> Result<(), StateAnchorError> {
|
||||
let inherited_anchor = inherit_for_exec(runtime_anchor)?;
|
||||
let path = descriptor_child_path(&inherited_anchor, "merged");
|
||||
let status = std::process::Command::new("fusermount3")
|
||||
.arg("-u")
|
||||
|
|
|
|||
|
|
@ -2,13 +2,22 @@ use std::collections::BTreeMap;
|
|||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use lazaret_core::configuration::{
|
||||
ConfigurationLoadRequest, SandboxLayoutV1, load_trusted_configuration,
|
||||
};
|
||||
use lazaret_state_anchor::{ProtectedRuntimeFiles, StateAnchorLayout};
|
||||
use lazaret_core::identity::{
|
||||
DeviceId, HostPath, IdMapDigest, InodeId, PolicyId, PreparedArtifactId, SelectedManifestDigest,
|
||||
SourceIdentity,
|
||||
};
|
||||
use lazaret_core::records::{ArtifactState, PreparedArtifact};
|
||||
use lazaret_state_anchor::{
|
||||
ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, StateAnchorLaunchAuthority,
|
||||
StateAnchorLayout,
|
||||
};
|
||||
|
||||
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
|
|
@ -48,6 +57,45 @@ fn replaced_runtime_root_is_refused_before_runtime_file_write() {
|
|||
assert!(!replacement_files.join("hosts").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inherited_runtime_authority_accepts_only_the_exact_prepared_lower() {
|
||||
let tree = TestTree::new();
|
||||
let configuration = configuration(&tree);
|
||||
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
||||
let lower = tree.path.join("prepared-lower");
|
||||
fs::create_dir(&lower).unwrap();
|
||||
let metadata = lower.metadata().unwrap();
|
||||
let artifact = PreparedArtifact {
|
||||
prepared_artifact_id: PreparedArtifactId::new([8; 32]),
|
||||
source_identity: SourceIdentity::new(b"fixture".to_vec()),
|
||||
selected_manifest_digest: SelectedManifestDigest::new([9; 32]),
|
||||
policy_id: PolicyId::new([10; 32]),
|
||||
id_map_digest: IdMapDigest::new([11; 32]),
|
||||
path: HostPath::new(lower.as_os_str().as_encoded_bytes().to_vec()),
|
||||
device: DeviceId::new(metadata.dev()),
|
||||
inode: InodeId::new(metadata.ino()),
|
||||
state: ArtifactState::Available,
|
||||
};
|
||||
let launch = StateAnchorLaunchAuthority::capture(&layout, &artifact).unwrap();
|
||||
let expected = launch.expected_lower();
|
||||
RuntimeStateAnchorAuthority::from_inherited(
|
||||
launch.lower_fd(),
|
||||
launch.persistent_anchor_fd(),
|
||||
launch.runtime_anchor_fd(),
|
||||
expected,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
RuntimeStateAnchorAuthority::from_inherited(
|
||||
launch.lower_fd(),
|
||||
launch.persistent_anchor_fd(),
|
||||
launch.runtime_anchor_fd(),
|
||||
(expected.0, expected.1 + 1),
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
struct TestTree {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ use lazaret_linux::process::{
|
|||
ProcessOwnershipContext, current_process_identity, dispatch_private_process_role,
|
||||
};
|
||||
use lazaret_state_anchor::{
|
||||
FuseOverlayfsMount, ProtectedRuntimeFiles, StateAnchorAcquisition, StateAnchorLayout,
|
||||
StateAnchorSandboxView, unmount_after_coordinator_loss,
|
||||
FuseOverlayfsMount, ProtectedRuntimeFiles, RuntimeStateAnchorAuthority, StateAnchorAcquisition,
|
||||
StateAnchorLaunchAuthority, StateAnchorLayout, StateAnchorSandboxView,
|
||||
unmount_after_coordinator_loss,
|
||||
};
|
||||
use rustix::process::Signal;
|
||||
|
||||
|
|
@ -77,6 +78,45 @@ fn run() -> io::Result<()> {
|
|||
};
|
||||
let layout = StateAnchorLayout::create(&configuration).map_err(io::Error::other)?;
|
||||
|
||||
let launch =
|
||||
StateAnchorLaunchAuthority::capture(&layout, &artifact).map_err(io::Error::other)?;
|
||||
let runtime_authority = RuntimeStateAnchorAuthority::from_inherited(
|
||||
launch.lower_fd(),
|
||||
launch.persistent_anchor_fd(),
|
||||
launch.runtime_anchor_fd(),
|
||||
launch.expected_lower(),
|
||||
)
|
||||
.map_err(io::Error::other)?;
|
||||
let expected = std::cell::Cell::new(None);
|
||||
let runtime_mount = runtime_authority
|
||||
.acquire(
|
||||
current_process_identity()?,
|
||||
ProcessOwnershipContext {
|
||||
domain_id: DomainId::new([1; 32]),
|
||||
generation_id: GenerationId::new(1),
|
||||
coordinator_id: CoordinatorId::new([3; 32]),
|
||||
slot: HostProcessOwnershipSlot::StateAnchorDaemon,
|
||||
},
|
||||
|staged| {
|
||||
expected.set(Some(staged));
|
||||
Ok(())
|
||||
},
|
||||
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
|
||||
|readiness| {
|
||||
if expected.get() != Some(readiness.staged())
|
||||
|| lazaret_linux::filesystem::current_mount_identity(layout.merged())?
|
||||
!= readiness.mount()
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"inherited State Anchor readiness changed authority",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.map_err(io::Error::other)?;
|
||||
runtime_mount.unmount().map_err(io::Error::other)?;
|
||||
|
||||
let first = acquire(&layout, &artifact)?;
|
||||
fs::write(
|
||||
layout.merged().join("usr/bin/installed-tool"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue