Decompose oversized Rust files by responsibility
This commit is contained in:
parent
03bfe92ca0
commit
ee9201b057
8 changed files with 18648 additions and 18513 deletions
|
|
@ -709,777 +709,6 @@ fn build_sandbox_bundle_from_source(
|
|||
})
|
||||
}
|
||||
|
||||
struct RuntimePaths {
|
||||
container_name: String,
|
||||
bundle: PathBuf,
|
||||
runsc_root: PathBuf,
|
||||
overlay: PathBuf,
|
||||
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("runsc-overlay").join(&paths.container_name),
|
||||
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,
|
||||
),
|
||||
(
|
||||
container.join("bin/lazaret-sandbox-init"),
|
||||
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<_>>();
|
||||
for name in ["resolv.conf", "hosts", "hostname"] {
|
||||
sources.push(RuntimeHandoffSource {
|
||||
relative_path: Path::new("state-anchor/runtime-files").join(name),
|
||||
expected_type: HandoffPathType::RegularFile,
|
||||
build_identity: None,
|
||||
expected_bytes: None,
|
||||
});
|
||||
}
|
||||
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(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!(
|
||||
"runtime handoff source {}: {error}",
|
||||
relative_path.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
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,
|
||||
) -> Result<RuntimePaths, SandboxBuildError> {
|
||||
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label())?;
|
||||
let container_name = hex_lower(container_id.as_bytes());
|
||||
Ok(RuntimePaths {
|
||||
bundle: runtime_domain.join("bundles").join(&container_name),
|
||||
runsc_root: runtime_domain.join("runsc"),
|
||||
overlay: runtime_domain.join("runsc-overlay").join(&container_name),
|
||||
control_sources: runtime_domain.join("sandbox-control").join(&container_name),
|
||||
container_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_mounts(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
control_sources: &Path,
|
||||
workspace_source: PathBuf,
|
||||
) -> Result<Vec<Mount>, SandboxBuildError> {
|
||||
let mut mounts = standard_mounts()?;
|
||||
mounts.push(bind_mount(
|
||||
workspace_source,
|
||||
PathBuf::from(WORKSPACE_DESTINATION),
|
||||
false,
|
||||
false,
|
||||
)?);
|
||||
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label())?;
|
||||
for destination in &configuration.fingerprint_record().persistent_dirs {
|
||||
mounts.push(bind_mount(
|
||||
runtime_domain
|
||||
.join("state-anchor/merged")
|
||||
.join(destination.strip_prefix('/').unwrap_or(destination)),
|
||||
PathBuf::from(destination),
|
||||
false,
|
||||
false,
|
||||
)?);
|
||||
}
|
||||
for volume in configuration.volumes() {
|
||||
let record = volume.record();
|
||||
mounts.push(bind_mount(
|
||||
host_path(&record.canonical_host_source)?,
|
||||
PathBuf::from(&record.container_path),
|
||||
record.access == VolumeAccess::ReadOnly,
|
||||
false,
|
||||
)?);
|
||||
}
|
||||
mounts.extend(control_mounts(control_sources)?);
|
||||
for name in ["resolv.conf", "hosts", "hostname"] {
|
||||
mounts.push(bind_mount(
|
||||
runtime_domain.join("state-anchor/runtime-files").join(name),
|
||||
Path::new("/etc").join(name),
|
||||
true,
|
||||
true,
|
||||
)?);
|
||||
}
|
||||
if let Some(terminal) = terminal {
|
||||
mounts.push(bind_mount(
|
||||
host_path(configuration.runtime_domain_root().canonical_label())?
|
||||
.join(&terminal.relative_source),
|
||||
terminal.destination(),
|
||||
true,
|
||||
true,
|
||||
)?);
|
||||
}
|
||||
Ok(mounts)
|
||||
}
|
||||
|
||||
fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildError> {
|
||||
let private_namespaces = [
|
||||
LinuxNamespaceType::Pid,
|
||||
LinuxNamespaceType::Ipc,
|
||||
LinuxNamespaceType::Uts,
|
||||
LinuxNamespaceType::Mount,
|
||||
LinuxNamespaceType::User,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|typ| {
|
||||
LinuxNamespaceBuilder::default()
|
||||
.typ(typ)
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
})
|
||||
.chain(std::iter::once(
|
||||
LinuxNamespaceBuilder::default()
|
||||
.typ(LinuxNamespaceType::Network)
|
||||
.path(PathBuf::from(NETWORK_NAMESPACE_PATH))
|
||||
.build()
|
||||
.map_err(oci_error),
|
||||
))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let uid_mapping = LinuxIdMappingBuilder::default()
|
||||
.container_id(0_u32)
|
||||
.host_id(0_u32)
|
||||
.size(1_u32)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
let gid_mapping = LinuxIdMappingBuilder::default()
|
||||
.container_id(0_u32)
|
||||
.host_id(0_u32)
|
||||
.size(1_u32)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
LinuxBuilder::default()
|
||||
.namespaces(private_namespaces)
|
||||
.uid_mappings(vec![uid_mapping])
|
||||
.gid_mappings(vec![gid_mapping])
|
||||
.rootfs_propagation("rprivate")
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
}
|
||||
|
||||
fn build_runsc_command(paths: &RuntimePaths, bundle_authorization: [u8; 32]) -> RunscCommand {
|
||||
RunscCommand::new(
|
||||
"runsc",
|
||||
[
|
||||
OsString::from(format!(
|
||||
"-root={}",
|
||||
paths
|
||||
.runsc_root
|
||||
.to_str()
|
||||
.expect("validated runtime path remains UTF-8")
|
||||
)),
|
||||
OsString::from("-rootless"),
|
||||
OsString::from("-network=host"),
|
||||
OsString::from("-ignore-cgroups"),
|
||||
OsString::from(format!(
|
||||
"-overlay2=root:dir={}",
|
||||
paths
|
||||
.overlay
|
||||
.to_str()
|
||||
.expect("validated runtime path remains UTF-8")
|
||||
)),
|
||||
OsString::from("run"),
|
||||
OsString::from(format!(
|
||||
"-bundle={}",
|
||||
paths
|
||||
.bundle
|
||||
.to_str()
|
||||
.expect("validated runtime path remains UTF-8")
|
||||
)),
|
||||
OsString::from(&paths.container_name),
|
||||
],
|
||||
bundle_authorization,
|
||||
)
|
||||
}
|
||||
|
||||
fn open_absolute_directory(path: &Path) -> Result<OwnedFd, SandboxBuildError> {
|
||||
validate_absolute_normalized(path)?;
|
||||
openat2(
|
||||
rustix::fs::CWD,
|
||||
path,
|
||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||
)
|
||||
.map_err(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!("prepared root {}: {error}", path.display()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_process(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
root_process: RootProcess,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
) -> Result<oci_spec::runtime::Process, SandboxBuildError> {
|
||||
let mut environment = BTreeMap::from([("PATH".to_owned(), DEFAULT_PATH.to_owned())]);
|
||||
for (name, value) in configuration.ephemeral_environment() {
|
||||
let value = value.expose().to_str().ok_or_else(|| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidProcess,
|
||||
"resolved environment value is not UTF-8",
|
||||
)
|
||||
})?;
|
||||
environment.insert(name.clone(), value.to_owned());
|
||||
}
|
||||
if let Some(terminal) = terminal {
|
||||
environment.insert("TERM".to_owned(), terminal.term.clone());
|
||||
if let Some(color_term) = &terminal.color_term {
|
||||
environment.insert("COLORTERM".to_owned(), color_term.clone());
|
||||
} else {
|
||||
environment.remove("COLORTERM");
|
||||
}
|
||||
environment.insert(
|
||||
"TERMINFO_DIRS".to_owned(),
|
||||
format!("{TERMINFO_ROOT_DESTINATION}:"),
|
||||
);
|
||||
}
|
||||
let environment = environment
|
||||
.into_iter()
|
||||
.map(|(name, value)| format!("{name}={value}"))
|
||||
.collect::<Vec<_>>();
|
||||
let empty = Capabilities::new();
|
||||
let capabilities = LinuxCapabilitiesBuilder::default()
|
||||
.bounding(empty.clone())
|
||||
.effective(empty.clone())
|
||||
.inheritable(empty.clone())
|
||||
.permitted(empty.clone())
|
||||
.ambient(empty)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
let user = UserBuilder::default()
|
||||
.uid(0_u32)
|
||||
.gid(0_u32)
|
||||
.umask(0o022_u32)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
ProcessBuilder::default()
|
||||
.terminal(false)
|
||||
.user(user)
|
||||
.args(root_process.arguments)
|
||||
.env(environment)
|
||||
.cwd(PathBuf::from(WORKSPACE_DESTINATION))
|
||||
.capabilities(capabilities)
|
||||
.no_new_privileges(true)
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
}
|
||||
|
||||
fn standard_mounts() -> Result<Vec<Mount>, SandboxBuildError> {
|
||||
[
|
||||
("/proc", "proc", "proc", vec!["nosuid", "noexec", "nodev"]),
|
||||
(
|
||||
"/dev",
|
||||
"tmpfs",
|
||||
"tmpfs",
|
||||
vec!["nosuid", "strictatime", "mode=755", "size=65536k"],
|
||||
),
|
||||
(
|
||||
"/dev/pts",
|
||||
"devpts",
|
||||
"devpts",
|
||||
vec![
|
||||
"nosuid",
|
||||
"noexec",
|
||||
"newinstance",
|
||||
"ptmxmode=0666",
|
||||
"mode=0620",
|
||||
],
|
||||
),
|
||||
(
|
||||
"/dev/shm",
|
||||
"tmpfs",
|
||||
"shm",
|
||||
vec!["nosuid", "noexec", "nodev", "mode=1777", "size=65536k"],
|
||||
),
|
||||
(
|
||||
"/dev/mqueue",
|
||||
"mqueue",
|
||||
"mqueue",
|
||||
vec!["nosuid", "noexec", "nodev"],
|
||||
),
|
||||
(
|
||||
"/sys",
|
||||
"sysfs",
|
||||
"sysfs",
|
||||
vec!["nosuid", "noexec", "nodev", "ro"],
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(destination, typ, source, options)| {
|
||||
MountBuilder::default()
|
||||
.destination(PathBuf::from(destination))
|
||||
.typ(typ)
|
||||
.source(PathBuf::from(source))
|
||||
.options(options.into_iter().map(str::to_owned).collect::<Vec<_>>())
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn control_mounts(control_sources: &Path) -> Result<Vec<Mount>, SandboxBuildError> {
|
||||
[
|
||||
("credentials", CREDENTIALS_DESTINATION, true, false),
|
||||
(
|
||||
"mailbox/requests",
|
||||
MAILBOX_REQUESTS_DESTINATION,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
("mailbox/results", MAILBOX_RESULTS_DESTINATION, true, false),
|
||||
(
|
||||
"bin/lazaret-escalate",
|
||||
ESCALATION_CLIENT_DESTINATION,
|
||||
true,
|
||||
false,
|
||||
),
|
||||
(
|
||||
"bin/lazaret-sandbox-init",
|
||||
SANDBOX_INIT_DESTINATION,
|
||||
true,
|
||||
false,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(source, destination, read_only, no_exec)| {
|
||||
bind_mount(
|
||||
control_sources.join(source),
|
||||
PathBuf::from(destination),
|
||||
read_only,
|
||||
no_exec,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bind_mount(
|
||||
source: PathBuf,
|
||||
destination: PathBuf,
|
||||
read_only: bool,
|
||||
no_exec: bool,
|
||||
) -> Result<Mount, SandboxBuildError> {
|
||||
validate_absolute_normalized(&source)?;
|
||||
validate_absolute_normalized(&destination)?;
|
||||
let mut options = vec!["bind".to_owned(), "nosuid".to_owned(), "nodev".to_owned()];
|
||||
options.push(if read_only { "ro" } else { "rw" }.to_owned());
|
||||
if no_exec {
|
||||
options.push("noexec".to_owned());
|
||||
}
|
||||
MountBuilder::default()
|
||||
.destination(destination)
|
||||
.typ("bind")
|
||||
.source(source)
|
||||
.options(options)
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
}
|
||||
|
||||
fn require_network_namespace(expected: NamespaceIdentity) -> Result<(), SandboxBuildError> {
|
||||
let observed = lazaret_linux::filesystem::namespace_identity(Path::new(NETWORK_NAMESPACE_PATH))
|
||||
.map_err(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::NetworkNamespaceMismatch,
|
||||
format!("cannot inspect Runtime Supervisor network namespace: {error}"),
|
||||
)
|
||||
})?;
|
||||
if observed != expected {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::NetworkNamespaceMismatch,
|
||||
"Runtime Supervisor network namespace identity differs from readiness evidence",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn changed_configuration_authority(error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
||||
}
|
||||
|
||||
fn changed_authority_at(label: &str, error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!("{label}: {error}"),
|
||||
)
|
||||
}
|
||||
|
||||
fn oci_error(error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::OciSpecification, error.to_string())
|
||||
}
|
||||
|
||||
fn validate_sandbox_executable(program: &str) -> Result<(), SandboxBuildError> {
|
||||
validate_absolute_normalized(Path::new(program)).map_err(|_| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidProcess,
|
||||
"program is not a normalized absolute sandbox path",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_terminal_name(term: &str) -> Result<(), SandboxBuildError> {
|
||||
let valid = !term.is_empty()
|
||||
&& term.len() <= 64
|
||||
&& term
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.' | b'_'));
|
||||
if valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidTerminal,
|
||||
"TERM violates the conservative terminal-name grammar",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_compiled_terminfo(term: &str, bytes: &[u8]) -> Result<(), SandboxBuildError> {
|
||||
if bytes.len() < 12 || bytes.len() > MAX_TERMINFO_BYTES {
|
||||
return Err(invalid_terminfo("compiled entry has an invalid size"));
|
||||
}
|
||||
let magic = little_u16(bytes, 0);
|
||||
let number_width = match magic {
|
||||
0x011a => 2,
|
||||
0x021e => 4,
|
||||
_ => return Err(invalid_terminfo("compiled entry has an unknown magic")),
|
||||
};
|
||||
let names_size = usize::from(little_u16(bytes, 2));
|
||||
let bool_count = usize::from(little_u16(bytes, 4));
|
||||
let number_count = usize::from(little_u16(bytes, 6));
|
||||
let string_count = usize::from(little_u16(bytes, 8));
|
||||
let string_table_size = usize::from(little_u16(bytes, 10));
|
||||
let names_end = 12_usize
|
||||
.checked_add(names_size)
|
||||
.filter(|end| *end <= bytes.len())
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry names exceed its length"))?;
|
||||
let names = &bytes[12..names_end];
|
||||
if names.last() != Some(&0)
|
||||
|| !names[..names.len().saturating_sub(1)]
|
||||
.split(|byte| *byte == b'|')
|
||||
.any(|name| name == term.as_bytes())
|
||||
{
|
||||
return Err(invalid_terminfo(
|
||||
"compiled entry does not name the requested terminal",
|
||||
));
|
||||
}
|
||||
let after_bools = names_end
|
||||
.checked_add(bool_count)
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
||||
let aligned = after_bools
|
||||
.checked_add(after_bools % 2)
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
||||
let required = aligned
|
||||
.checked_add(
|
||||
number_count
|
||||
.checked_mul(number_width)
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?,
|
||||
)
|
||||
.and_then(|size| size.checked_add(string_count.checked_mul(2)?))
|
||||
.and_then(|size| size.checked_add(string_table_size))
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
||||
if required > bytes.len() {
|
||||
return Err(invalid_terminfo("compiled entry sections are truncated"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn little_u16(bytes: &[u8], offset: usize) -> u16 {
|
||||
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
|
||||
}
|
||||
|
||||
fn invalid_terminfo(detail: &str) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::InvalidTerminal, detail)
|
||||
}
|
||||
|
||||
fn open_or_create_staging_directory(
|
||||
parent: OwnedFd,
|
||||
name: &str,
|
||||
) -> Result<OwnedFd, SandboxBuildError> {
|
||||
match rustix::fs::mkdirat(&parent, name, Mode::RWXU) {
|
||||
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
|
||||
Err(error) => return Err(staging_error(error)),
|
||||
}
|
||||
let directory = openat2(
|
||||
parent,
|
||||
name,
|
||||
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
confined_resolve_flags(),
|
||||
)
|
||||
.map_err(staging_error)?;
|
||||
let stat = rustix::fs::fstat(&directory).map_err(staging_error)?;
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_dir()
|
||||
|| stat.st_uid != rustix::process::getuid().as_raw()
|
||||
|| stat.st_mode & 0o7777 != 0o700
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"terminfo staging directory has unsafe ownership or mode",
|
||||
));
|
||||
}
|
||||
Ok(directory)
|
||||
}
|
||||
|
||||
fn stage_exact_file(
|
||||
directory: &OwnedFd,
|
||||
name: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<OwnedFd, SandboxBuildError> {
|
||||
match open_staged_file(directory, name) {
|
||||
Ok(descriptor) => {
|
||||
require_staged_file(&descriptor)?;
|
||||
if read_descriptor(descriptor)? != bytes {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"existing staged terminfo artifact has different bytes",
|
||||
));
|
||||
}
|
||||
open_staged_file(directory, name).map_err(staging_error)
|
||||
}
|
||||
Err(rustix::io::Errno::NOENT) => {
|
||||
let descriptor = rustix::fs::openat(
|
||||
directory,
|
||||
name,
|
||||
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::RUSR | Mode::WUSR,
|
||||
)
|
||||
.map_err(staging_error)?;
|
||||
let mut file = File::from(descriptor);
|
||||
let result = (|| {
|
||||
file.write_all(bytes).map_err(staging_error)?;
|
||||
rustix::fs::fchmod(file.as_fd(), Mode::RUSR).map_err(staging_error)?;
|
||||
file.sync_all().map_err(staging_error)
|
||||
})();
|
||||
if let Err(error) = result {
|
||||
drop(file);
|
||||
let _ = rustix::fs::unlinkat(directory, name, AtFlags::empty());
|
||||
return Err(error);
|
||||
}
|
||||
drop(file);
|
||||
rustix::fs::fsync(directory).map_err(staging_error)?;
|
||||
let descriptor = open_staged_file(directory, name).map_err(staging_error)?;
|
||||
require_staged_file(&descriptor)?;
|
||||
Ok(descriptor)
|
||||
}
|
||||
Err(error) => Err(staging_error(error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_staged_file(directory: &OwnedFd, name: &str) -> rustix::io::Result<OwnedFd> {
|
||||
openat2(
|
||||
directory,
|
||||
name,
|
||||
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
confined_resolve_flags(),
|
||||
)
|
||||
}
|
||||
|
||||
fn require_staged_file(descriptor: &OwnedFd) -> Result<(), SandboxBuildError> {
|
||||
let stat = rustix::fs::fstat(descriptor).map_err(staging_error)?;
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_file()
|
||||
|| stat.st_uid != rustix::process::getuid().as_raw()
|
||||
|| stat.st_mode & 0o7777 != 0o400
|
||||
|| stat.st_nlink != 1
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged terminfo artifact has unsafe type, ownership, mode, or link count",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_descriptor(descriptor: OwnedFd) -> Result<Vec<u8>, SandboxBuildError> {
|
||||
let mut bytes = Vec::new();
|
||||
File::from(descriptor)
|
||||
.take((MAX_TERMINFO_BYTES + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(staging_error)?;
|
||||
if bytes.len() > MAX_TERMINFO_BYTES {
|
||||
return Err(invalid_terminfo("staged terminfo artifact is oversized"));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn confined_resolve_flags() -> ResolveFlags {
|
||||
ResolveFlags::BENEATH
|
||||
| ResolveFlags::NO_MAGICLINKS
|
||||
| ResolveFlags::NO_SYMLINKS
|
||||
| ResolveFlags::NO_XDEV
|
||||
}
|
||||
|
||||
fn staging_error(error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
||||
}
|
||||
|
||||
fn validate_absolute_normalized(path: &Path) -> Result<(), SandboxBuildError> {
|
||||
if !path.is_absolute()
|
||||
|| path.as_os_str().as_bytes().contains(&0)
|
||||
|| path
|
||||
.components()
|
||||
.any(|component| !matches!(component, Component::RootDir | Component::Normal(_)))
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidPath,
|
||||
"path is not normalized and absolute",
|
||||
));
|
||||
}
|
||||
utf8_path(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn utf8_path(path: &Path) -> Result<&str, SandboxBuildError> {
|
||||
path.to_str().ok_or_else(|| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidPath,
|
||||
"OCI JSON path is not UTF-8",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn host_path(path: &lazaret_core::identity::HostPath) -> Result<PathBuf, SandboxBuildError> {
|
||||
let path = PathBuf::from(OsString::from_vec(path.as_bytes().to_vec()));
|
||||
validate_absolute_normalized(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn hex_lower(bytes: &[u8]) -> String {
|
||||
let mut output = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
use fmt::Write as _;
|
||||
write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
|
||||
}
|
||||
output
|
||||
}
|
||||
include!("sandbox/runtime_sources.rs");
|
||||
include!("sandbox/mounts.rs");
|
||||
include!("sandbox/validation.rs");
|
||||
|
|
|
|||
110
crates/lazaret-runtime-supervisor/src/sandbox/mounts.rs
Normal file
110
crates/lazaret-runtime-supervisor/src/sandbox/mounts.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
fn standard_mounts() -> Result<Vec<Mount>, SandboxBuildError> {
|
||||
[
|
||||
("/proc", "proc", "proc", vec!["nosuid", "noexec", "nodev"]),
|
||||
(
|
||||
"/dev",
|
||||
"tmpfs",
|
||||
"tmpfs",
|
||||
vec!["nosuid", "strictatime", "mode=755", "size=65536k"],
|
||||
),
|
||||
(
|
||||
"/dev/pts",
|
||||
"devpts",
|
||||
"devpts",
|
||||
vec![
|
||||
"nosuid",
|
||||
"noexec",
|
||||
"newinstance",
|
||||
"ptmxmode=0666",
|
||||
"mode=0620",
|
||||
],
|
||||
),
|
||||
(
|
||||
"/dev/shm",
|
||||
"tmpfs",
|
||||
"shm",
|
||||
vec!["nosuid", "noexec", "nodev", "mode=1777", "size=65536k"],
|
||||
),
|
||||
(
|
||||
"/dev/mqueue",
|
||||
"mqueue",
|
||||
"mqueue",
|
||||
vec!["nosuid", "noexec", "nodev"],
|
||||
),
|
||||
(
|
||||
"/sys",
|
||||
"sysfs",
|
||||
"sysfs",
|
||||
vec!["nosuid", "noexec", "nodev", "ro"],
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(destination, typ, source, options)| {
|
||||
MountBuilder::default()
|
||||
.destination(PathBuf::from(destination))
|
||||
.typ(typ)
|
||||
.source(PathBuf::from(source))
|
||||
.options(options.into_iter().map(str::to_owned).collect::<Vec<_>>())
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn control_mounts(control_sources: &Path) -> Result<Vec<Mount>, SandboxBuildError> {
|
||||
[
|
||||
("credentials", CREDENTIALS_DESTINATION, true, false),
|
||||
(
|
||||
"mailbox/requests",
|
||||
MAILBOX_REQUESTS_DESTINATION,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
("mailbox/results", MAILBOX_RESULTS_DESTINATION, true, false),
|
||||
(
|
||||
"bin/lazaret-escalate",
|
||||
ESCALATION_CLIENT_DESTINATION,
|
||||
true,
|
||||
false,
|
||||
),
|
||||
(
|
||||
"bin/lazaret-sandbox-init",
|
||||
SANDBOX_INIT_DESTINATION,
|
||||
true,
|
||||
false,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(source, destination, read_only, no_exec)| {
|
||||
bind_mount(
|
||||
control_sources.join(source),
|
||||
PathBuf::from(destination),
|
||||
read_only,
|
||||
no_exec,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bind_mount(
|
||||
source: PathBuf,
|
||||
destination: PathBuf,
|
||||
read_only: bool,
|
||||
no_exec: bool,
|
||||
) -> Result<Mount, SandboxBuildError> {
|
||||
validate_absolute_normalized(&source)?;
|
||||
validate_absolute_normalized(&destination)?;
|
||||
let mut options = vec!["bind".to_owned(), "nosuid".to_owned(), "nodev".to_owned()];
|
||||
options.push(if read_only { "ro" } else { "rw" }.to_owned());
|
||||
if no_exec {
|
||||
options.push("noexec".to_owned());
|
||||
}
|
||||
MountBuilder::default()
|
||||
.destination(destination)
|
||||
.typ("bind")
|
||||
.source(source)
|
||||
.options(options)
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
}
|
||||
|
||||
388
crates/lazaret-runtime-supervisor/src/sandbox/runtime_sources.rs
Normal file
388
crates/lazaret-runtime-supervisor/src/sandbox/runtime_sources.rs
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
struct RuntimePaths {
|
||||
container_name: String,
|
||||
bundle: PathBuf,
|
||||
runsc_root: PathBuf,
|
||||
overlay: PathBuf,
|
||||
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("runsc-overlay").join(&paths.container_name),
|
||||
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,
|
||||
),
|
||||
(
|
||||
container.join("bin/lazaret-sandbox-init"),
|
||||
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<_>>();
|
||||
for name in ["resolv.conf", "hosts", "hostname"] {
|
||||
sources.push(RuntimeHandoffSource {
|
||||
relative_path: Path::new("state-anchor/runtime-files").join(name),
|
||||
expected_type: HandoffPathType::RegularFile,
|
||||
build_identity: None,
|
||||
expected_bytes: None,
|
||||
});
|
||||
}
|
||||
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(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!(
|
||||
"runtime handoff source {}: {error}",
|
||||
relative_path.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
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,
|
||||
) -> Result<RuntimePaths, SandboxBuildError> {
|
||||
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label())?;
|
||||
let container_name = hex_lower(container_id.as_bytes());
|
||||
Ok(RuntimePaths {
|
||||
bundle: runtime_domain.join("bundles").join(&container_name),
|
||||
runsc_root: runtime_domain.join("runsc"),
|
||||
overlay: runtime_domain.join("runsc-overlay").join(&container_name),
|
||||
control_sources: runtime_domain.join("sandbox-control").join(&container_name),
|
||||
container_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_mounts(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
control_sources: &Path,
|
||||
workspace_source: PathBuf,
|
||||
) -> Result<Vec<Mount>, SandboxBuildError> {
|
||||
let mut mounts = standard_mounts()?;
|
||||
mounts.push(bind_mount(
|
||||
workspace_source,
|
||||
PathBuf::from(WORKSPACE_DESTINATION),
|
||||
false,
|
||||
false,
|
||||
)?);
|
||||
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label())?;
|
||||
for destination in &configuration.fingerprint_record().persistent_dirs {
|
||||
mounts.push(bind_mount(
|
||||
runtime_domain
|
||||
.join("state-anchor/merged")
|
||||
.join(destination.strip_prefix('/').unwrap_or(destination)),
|
||||
PathBuf::from(destination),
|
||||
false,
|
||||
false,
|
||||
)?);
|
||||
}
|
||||
for volume in configuration.volumes() {
|
||||
let record = volume.record();
|
||||
mounts.push(bind_mount(
|
||||
host_path(&record.canonical_host_source)?,
|
||||
PathBuf::from(&record.container_path),
|
||||
record.access == VolumeAccess::ReadOnly,
|
||||
false,
|
||||
)?);
|
||||
}
|
||||
mounts.extend(control_mounts(control_sources)?);
|
||||
for name in ["resolv.conf", "hosts", "hostname"] {
|
||||
mounts.push(bind_mount(
|
||||
runtime_domain.join("state-anchor/runtime-files").join(name),
|
||||
Path::new("/etc").join(name),
|
||||
true,
|
||||
true,
|
||||
)?);
|
||||
}
|
||||
if let Some(terminal) = terminal {
|
||||
mounts.push(bind_mount(
|
||||
host_path(configuration.runtime_domain_root().canonical_label())?
|
||||
.join(&terminal.relative_source),
|
||||
terminal.destination(),
|
||||
true,
|
||||
true,
|
||||
)?);
|
||||
}
|
||||
Ok(mounts)
|
||||
}
|
||||
|
||||
fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildError> {
|
||||
let private_namespaces = [
|
||||
LinuxNamespaceType::Pid,
|
||||
LinuxNamespaceType::Ipc,
|
||||
LinuxNamespaceType::Uts,
|
||||
LinuxNamespaceType::Mount,
|
||||
LinuxNamespaceType::User,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|typ| {
|
||||
LinuxNamespaceBuilder::default()
|
||||
.typ(typ)
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
})
|
||||
.chain(std::iter::once(
|
||||
LinuxNamespaceBuilder::default()
|
||||
.typ(LinuxNamespaceType::Network)
|
||||
.path(PathBuf::from(NETWORK_NAMESPACE_PATH))
|
||||
.build()
|
||||
.map_err(oci_error),
|
||||
))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let uid_mapping = LinuxIdMappingBuilder::default()
|
||||
.container_id(0_u32)
|
||||
.host_id(0_u32)
|
||||
.size(1_u32)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
let gid_mapping = LinuxIdMappingBuilder::default()
|
||||
.container_id(0_u32)
|
||||
.host_id(0_u32)
|
||||
.size(1_u32)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
LinuxBuilder::default()
|
||||
.namespaces(private_namespaces)
|
||||
.uid_mappings(vec![uid_mapping])
|
||||
.gid_mappings(vec![gid_mapping])
|
||||
.rootfs_propagation("rprivate")
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
}
|
||||
|
||||
fn build_runsc_command(paths: &RuntimePaths, bundle_authorization: [u8; 32]) -> RunscCommand {
|
||||
RunscCommand::new(
|
||||
"runsc",
|
||||
[
|
||||
OsString::from(format!(
|
||||
"-root={}",
|
||||
paths
|
||||
.runsc_root
|
||||
.to_str()
|
||||
.expect("validated runtime path remains UTF-8")
|
||||
)),
|
||||
OsString::from("-rootless"),
|
||||
OsString::from("-network=host"),
|
||||
OsString::from("-ignore-cgroups"),
|
||||
OsString::from(format!(
|
||||
"-overlay2=root:dir={}",
|
||||
paths
|
||||
.overlay
|
||||
.to_str()
|
||||
.expect("validated runtime path remains UTF-8")
|
||||
)),
|
||||
OsString::from("run"),
|
||||
OsString::from(format!(
|
||||
"-bundle={}",
|
||||
paths
|
||||
.bundle
|
||||
.to_str()
|
||||
.expect("validated runtime path remains UTF-8")
|
||||
)),
|
||||
OsString::from(&paths.container_name),
|
||||
],
|
||||
bundle_authorization,
|
||||
)
|
||||
}
|
||||
|
||||
fn open_absolute_directory(path: &Path) -> Result<OwnedFd, SandboxBuildError> {
|
||||
validate_absolute_normalized(path)?;
|
||||
openat2(
|
||||
rustix::fs::CWD,
|
||||
path,
|
||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||
)
|
||||
.map_err(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!("prepared root {}: {error}", path.display()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_process(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
root_process: RootProcess,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
) -> Result<oci_spec::runtime::Process, SandboxBuildError> {
|
||||
let mut environment = BTreeMap::from([("PATH".to_owned(), DEFAULT_PATH.to_owned())]);
|
||||
for (name, value) in configuration.ephemeral_environment() {
|
||||
let value = value.expose().to_str().ok_or_else(|| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidProcess,
|
||||
"resolved environment value is not UTF-8",
|
||||
)
|
||||
})?;
|
||||
environment.insert(name.clone(), value.to_owned());
|
||||
}
|
||||
if let Some(terminal) = terminal {
|
||||
environment.insert("TERM".to_owned(), terminal.term.clone());
|
||||
if let Some(color_term) = &terminal.color_term {
|
||||
environment.insert("COLORTERM".to_owned(), color_term.clone());
|
||||
} else {
|
||||
environment.remove("COLORTERM");
|
||||
}
|
||||
environment.insert(
|
||||
"TERMINFO_DIRS".to_owned(),
|
||||
format!("{TERMINFO_ROOT_DESTINATION}:"),
|
||||
);
|
||||
}
|
||||
let environment = environment
|
||||
.into_iter()
|
||||
.map(|(name, value)| format!("{name}={value}"))
|
||||
.collect::<Vec<_>>();
|
||||
let empty = Capabilities::new();
|
||||
let capabilities = LinuxCapabilitiesBuilder::default()
|
||||
.bounding(empty.clone())
|
||||
.effective(empty.clone())
|
||||
.inheritable(empty.clone())
|
||||
.permitted(empty.clone())
|
||||
.ambient(empty)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
let user = UserBuilder::default()
|
||||
.uid(0_u32)
|
||||
.gid(0_u32)
|
||||
.umask(0o022_u32)
|
||||
.build()
|
||||
.map_err(oci_error)?;
|
||||
ProcessBuilder::default()
|
||||
.terminal(false)
|
||||
.user(user)
|
||||
.args(root_process.arguments)
|
||||
.env(environment)
|
||||
.cwd(PathBuf::from(WORKSPACE_DESTINATION))
|
||||
.capabilities(capabilities)
|
||||
.no_new_privileges(true)
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
}
|
||||
|
||||
276
crates/lazaret-runtime-supervisor/src/sandbox/validation.rs
Normal file
276
crates/lazaret-runtime-supervisor/src/sandbox/validation.rs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
fn require_network_namespace(expected: NamespaceIdentity) -> Result<(), SandboxBuildError> {
|
||||
let observed = lazaret_linux::filesystem::namespace_identity(Path::new(NETWORK_NAMESPACE_PATH))
|
||||
.map_err(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::NetworkNamespaceMismatch,
|
||||
format!("cannot inspect Runtime Supervisor network namespace: {error}"),
|
||||
)
|
||||
})?;
|
||||
if observed != expected {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::NetworkNamespaceMismatch,
|
||||
"Runtime Supervisor network namespace identity differs from readiness evidence",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn changed_configuration_authority(error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
||||
}
|
||||
|
||||
fn changed_authority_at(label: &str, error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!("{label}: {error}"),
|
||||
)
|
||||
}
|
||||
|
||||
fn oci_error(error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::OciSpecification, error.to_string())
|
||||
}
|
||||
|
||||
fn validate_sandbox_executable(program: &str) -> Result<(), SandboxBuildError> {
|
||||
validate_absolute_normalized(Path::new(program)).map_err(|_| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidProcess,
|
||||
"program is not a normalized absolute sandbox path",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_terminal_name(term: &str) -> Result<(), SandboxBuildError> {
|
||||
let valid = !term.is_empty()
|
||||
&& term.len() <= 64
|
||||
&& term
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.' | b'_'));
|
||||
if valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidTerminal,
|
||||
"TERM violates the conservative terminal-name grammar",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_compiled_terminfo(term: &str, bytes: &[u8]) -> Result<(), SandboxBuildError> {
|
||||
if bytes.len() < 12 || bytes.len() > MAX_TERMINFO_BYTES {
|
||||
return Err(invalid_terminfo("compiled entry has an invalid size"));
|
||||
}
|
||||
let magic = little_u16(bytes, 0);
|
||||
let number_width = match magic {
|
||||
0x011a => 2,
|
||||
0x021e => 4,
|
||||
_ => return Err(invalid_terminfo("compiled entry has an unknown magic")),
|
||||
};
|
||||
let names_size = usize::from(little_u16(bytes, 2));
|
||||
let bool_count = usize::from(little_u16(bytes, 4));
|
||||
let number_count = usize::from(little_u16(bytes, 6));
|
||||
let string_count = usize::from(little_u16(bytes, 8));
|
||||
let string_table_size = usize::from(little_u16(bytes, 10));
|
||||
let names_end = 12_usize
|
||||
.checked_add(names_size)
|
||||
.filter(|end| *end <= bytes.len())
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry names exceed its length"))?;
|
||||
let names = &bytes[12..names_end];
|
||||
if names.last() != Some(&0)
|
||||
|| !names[..names.len().saturating_sub(1)]
|
||||
.split(|byte| *byte == b'|')
|
||||
.any(|name| name == term.as_bytes())
|
||||
{
|
||||
return Err(invalid_terminfo(
|
||||
"compiled entry does not name the requested terminal",
|
||||
));
|
||||
}
|
||||
let after_bools = names_end
|
||||
.checked_add(bool_count)
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
||||
let aligned = after_bools
|
||||
.checked_add(after_bools % 2)
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
||||
let required = aligned
|
||||
.checked_add(
|
||||
number_count
|
||||
.checked_mul(number_width)
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?,
|
||||
)
|
||||
.and_then(|size| size.checked_add(string_count.checked_mul(2)?))
|
||||
.and_then(|size| size.checked_add(string_table_size))
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
||||
if required > bytes.len() {
|
||||
return Err(invalid_terminfo("compiled entry sections are truncated"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn little_u16(bytes: &[u8], offset: usize) -> u16 {
|
||||
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
|
||||
}
|
||||
|
||||
fn invalid_terminfo(detail: &str) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::InvalidTerminal, detail)
|
||||
}
|
||||
|
||||
fn open_or_create_staging_directory(
|
||||
parent: OwnedFd,
|
||||
name: &str,
|
||||
) -> Result<OwnedFd, SandboxBuildError> {
|
||||
match rustix::fs::mkdirat(&parent, name, Mode::RWXU) {
|
||||
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
|
||||
Err(error) => return Err(staging_error(error)),
|
||||
}
|
||||
let directory = openat2(
|
||||
parent,
|
||||
name,
|
||||
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
confined_resolve_flags(),
|
||||
)
|
||||
.map_err(staging_error)?;
|
||||
let stat = rustix::fs::fstat(&directory).map_err(staging_error)?;
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_dir()
|
||||
|| stat.st_uid != rustix::process::getuid().as_raw()
|
||||
|| stat.st_mode & 0o7777 != 0o700
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"terminfo staging directory has unsafe ownership or mode",
|
||||
));
|
||||
}
|
||||
Ok(directory)
|
||||
}
|
||||
|
||||
fn stage_exact_file(
|
||||
directory: &OwnedFd,
|
||||
name: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<OwnedFd, SandboxBuildError> {
|
||||
match open_staged_file(directory, name) {
|
||||
Ok(descriptor) => {
|
||||
require_staged_file(&descriptor)?;
|
||||
if read_descriptor(descriptor)? != bytes {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"existing staged terminfo artifact has different bytes",
|
||||
));
|
||||
}
|
||||
open_staged_file(directory, name).map_err(staging_error)
|
||||
}
|
||||
Err(rustix::io::Errno::NOENT) => {
|
||||
let descriptor = rustix::fs::openat(
|
||||
directory,
|
||||
name,
|
||||
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::RUSR | Mode::WUSR,
|
||||
)
|
||||
.map_err(staging_error)?;
|
||||
let mut file = File::from(descriptor);
|
||||
let result = (|| {
|
||||
file.write_all(bytes).map_err(staging_error)?;
|
||||
rustix::fs::fchmod(file.as_fd(), Mode::RUSR).map_err(staging_error)?;
|
||||
file.sync_all().map_err(staging_error)
|
||||
})();
|
||||
if let Err(error) = result {
|
||||
drop(file);
|
||||
let _ = rustix::fs::unlinkat(directory, name, AtFlags::empty());
|
||||
return Err(error);
|
||||
}
|
||||
drop(file);
|
||||
rustix::fs::fsync(directory).map_err(staging_error)?;
|
||||
let descriptor = open_staged_file(directory, name).map_err(staging_error)?;
|
||||
require_staged_file(&descriptor)?;
|
||||
Ok(descriptor)
|
||||
}
|
||||
Err(error) => Err(staging_error(error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_staged_file(directory: &OwnedFd, name: &str) -> rustix::io::Result<OwnedFd> {
|
||||
openat2(
|
||||
directory,
|
||||
name,
|
||||
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
confined_resolve_flags(),
|
||||
)
|
||||
}
|
||||
|
||||
fn require_staged_file(descriptor: &OwnedFd) -> Result<(), SandboxBuildError> {
|
||||
let stat = rustix::fs::fstat(descriptor).map_err(staging_error)?;
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_file()
|
||||
|| stat.st_uid != rustix::process::getuid().as_raw()
|
||||
|| stat.st_mode & 0o7777 != 0o400
|
||||
|| stat.st_nlink != 1
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged terminfo artifact has unsafe type, ownership, mode, or link count",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_descriptor(descriptor: OwnedFd) -> Result<Vec<u8>, SandboxBuildError> {
|
||||
let mut bytes = Vec::new();
|
||||
File::from(descriptor)
|
||||
.take((MAX_TERMINFO_BYTES + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(staging_error)?;
|
||||
if bytes.len() > MAX_TERMINFO_BYTES {
|
||||
return Err(invalid_terminfo("staged terminfo artifact is oversized"));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn confined_resolve_flags() -> ResolveFlags {
|
||||
ResolveFlags::BENEATH
|
||||
| ResolveFlags::NO_MAGICLINKS
|
||||
| ResolveFlags::NO_SYMLINKS
|
||||
| ResolveFlags::NO_XDEV
|
||||
}
|
||||
|
||||
fn staging_error(error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
||||
}
|
||||
|
||||
fn validate_absolute_normalized(path: &Path) -> Result<(), SandboxBuildError> {
|
||||
if !path.is_absolute()
|
||||
|| path.as_os_str().as_bytes().contains(&0)
|
||||
|| path
|
||||
.components()
|
||||
.any(|component| !matches!(component, Component::RootDir | Component::Normal(_)))
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidPath,
|
||||
"path is not normalized and absolute",
|
||||
));
|
||||
}
|
||||
utf8_path(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn utf8_path(path: &Path) -> Result<&str, SandboxBuildError> {
|
||||
path.to_str().ok_or_else(|| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidPath,
|
||||
"OCI JSON path is not UTF-8",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn host_path(path: &lazaret_core::identity::HostPath) -> Result<PathBuf, SandboxBuildError> {
|
||||
let path = PathBuf::from(OsString::from_vec(path.as_bytes().to_vec()));
|
||||
validate_absolute_normalized(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn hex_lower(bytes: &[u8]) -> String {
|
||||
let mut output = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
use fmt::Write as _;
|
||||
write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
|
||||
}
|
||||
output
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
609
crates/lazaret-runtime-supervisor/src/supervision/runtime.rs
Normal file
609
crates/lazaret-runtime-supervisor/src/supervision/runtime.rs
Normal file
|
|
@ -0,0 +1,609 @@
|
|||
fn runsc_target(arguments: &RuntimeArguments, pid_file: i32) -> io::Result<TargetCommand> {
|
||||
let run = arguments
|
||||
.runsc_arguments
|
||||
.iter()
|
||||
.position(|argument| argument == "run")
|
||||
.ok_or_else(|| io::Error::other("attached runsc command has no run operation"))?;
|
||||
let mut target = TargetCommand::new(arguments.runsc_program.clone());
|
||||
for argument in &arguments.runsc_arguments[..=run] {
|
||||
target = target.argument(argument);
|
||||
}
|
||||
target = target.argument(format!("-pid-file=/proc/self/fd/{pid_file}"));
|
||||
for argument in &arguments.runsc_arguments[run + 1..] {
|
||||
target = target.argument(argument);
|
||||
}
|
||||
Ok(target.with_parent_death_kill_edge())
|
||||
}
|
||||
|
||||
fn runsc_delete_target(arguments: &RuntimeArguments) -> io::Result<TargetCommand> {
|
||||
let run = arguments
|
||||
.runsc_arguments
|
||||
.iter()
|
||||
.position(|argument| argument == "run")
|
||||
.ok_or_else(|| io::Error::other("attached runsc command has no run operation"))?;
|
||||
let container = arguments
|
||||
.runsc_arguments
|
||||
.last()
|
||||
.filter(|container| {
|
||||
!container.is_empty() && !container.as_encoded_bytes().starts_with(b"-")
|
||||
})
|
||||
.ok_or_else(|| io::Error::other("attached runsc command has no container ID"))?;
|
||||
let mut target = TargetCommand::new(arguments.runsc_program.clone());
|
||||
for argument in &arguments.runsc_arguments[..run] {
|
||||
target = target.argument(argument);
|
||||
}
|
||||
Ok(target
|
||||
.argument("delete")
|
||||
.argument("--force")
|
||||
.argument(container))
|
||||
}
|
||||
|
||||
fn attached_runsc_standard_io(target: &TargetCommand) -> io::Result<ChildStandardIo> {
|
||||
if target.parent_edge() != ParentEdge::ParentDeathKill {
|
||||
return Err(io::Error::other(
|
||||
"attached runsc requires the parent-death KILL edge",
|
||||
));
|
||||
}
|
||||
Ok(ChildStandardIo::silenced())
|
||||
}
|
||||
|
||||
fn wait_for_runsc_ready(pid_file: &File, process: &RetainedProcess) -> io::Result<u32> {
|
||||
let deadline = Instant::now() + READINESS_TIMEOUT;
|
||||
let mut bytes = [0_u8; 32];
|
||||
loop {
|
||||
require_lifetime_held()?;
|
||||
if process.has_exited()? {
|
||||
return Err(io::Error::other("attached runsc exited before readiness"));
|
||||
}
|
||||
let length = pid_file.read_at(&mut bytes, 0)?;
|
||||
if length > 0 {
|
||||
return std::str::from_utf8(&bytes[..length])
|
||||
.ok()
|
||||
.map(str::trim)
|
||||
.and_then(|value| value.parse().ok())
|
||||
.filter(|pid| *pid > 0)
|
||||
.ok_or_else(|| io::Error::other("runsc pid-file readiness is malformed"));
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return Err(io::Error::other("attached runsc readiness timed out"));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||
fn supervise(
|
||||
control: &std::io::Stdout,
|
||||
process: RetainedProcess,
|
||||
mut git_routing: Option<RuntimeGitRouting>,
|
||||
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()
|
||||
.map_or(Ok(false), RetainedProcess::has_exited)
|
||||
{
|
||||
Ok(exited) => exited,
|
||||
Err(error) => return fail_active_runtime(process, git_routing, network, error),
|
||||
};
|
||||
if exited {
|
||||
let runsc = process
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("attached runsc handle is absent"))?;
|
||||
cleanup_stopped_runtime(runsc, git_routing, network, delete, false)?;
|
||||
send_message(control, &SupervisorEvent::AttachedRunscExited)?;
|
||||
return Ok(());
|
||||
}
|
||||
if git_routing
|
||||
.as_ref()
|
||||
.is_some_and(|routing| routing.process.has_exited().unwrap_or(true))
|
||||
{
|
||||
return fail_active_runtime(
|
||||
process,
|
||||
git_routing,
|
||||
network,
|
||||
io::Error::other("Git router exited while the sandbox was active"),
|
||||
);
|
||||
}
|
||||
if network
|
||||
.as_ref()
|
||||
.is_some_and(|network| network.helper_exited().unwrap_or(true))
|
||||
{
|
||||
network
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("connectivity helper handle is absent"))?
|
||||
.reap_exited()?;
|
||||
send_message(control, &SupervisorEvent::HelperExited)?;
|
||||
}
|
||||
let lease_lost = match lifetime_lost() {
|
||||
Ok(lost) => lost,
|
||||
Err(error) => return fail_active_runtime(process, git_routing, network, error),
|
||||
};
|
||||
if lease_lost {
|
||||
if let Some(process) = process.take() {
|
||||
cleanup_stopped_runtime(process, git_routing, network, delete, true)?;
|
||||
} else {
|
||||
cleanup_runtime_components(git_routing, network)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let mut descriptors = [PollFd::new(control, PollFlags::IN)];
|
||||
if let Err(error) = poll(
|
||||
&mut descriptors,
|
||||
Some(&Timespec {
|
||||
tv_sec: 0,
|
||||
tv_nsec: 10_000_000,
|
||||
}),
|
||||
) {
|
||||
return fail_active_runtime(process, git_routing, network, error.into());
|
||||
}
|
||||
let events = descriptors[0].revents();
|
||||
if events.intersects(PollFlags::HUP | PollFlags::ERR) {
|
||||
return fail_active_runtime(
|
||||
process,
|
||||
git_routing,
|
||||
network,
|
||||
io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"runtime control channel closed",
|
||||
),
|
||||
);
|
||||
}
|
||||
if events.intersects(PollFlags::IN) {
|
||||
let message = match receive_message(control) {
|
||||
Ok(message) => message,
|
||||
Err(error) => {
|
||||
return fail_active_runtime(process, git_routing, network, error);
|
||||
}
|
||||
};
|
||||
match handle_supervisor_message(
|
||||
control,
|
||||
&message,
|
||||
&mut process,
|
||||
&mut git_routing,
|
||||
&mut network,
|
||||
&mut state_anchor,
|
||||
delete,
|
||||
) {
|
||||
Ok(true) => return Ok(()),
|
||||
Ok(false) => {}
|
||||
Err(error) => {
|
||||
return fail_active_runtime(process, git_routing, network, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
git_routing: &mut Option<RuntimeGitRouting>,
|
||||
network: &mut Option<PreparedNetwork>,
|
||||
state_anchor: &mut StateAnchorSupervision<'_>,
|
||||
delete: &TargetCommand,
|
||||
) -> io::Result<bool> {
|
||||
match message {
|
||||
CoordinatorMessage::SignalAttachedRunsc(signal) => {
|
||||
process
|
||||
.as_ref()
|
||||
.ok_or_else(|| io::Error::other("attached runsc is already stopped"))?
|
||||
.send_signal(signal.signal())?;
|
||||
send_message(control, &SupervisorEvent::SignalApplied(*signal))?;
|
||||
}
|
||||
CoordinatorMessage::StopSandbox => {
|
||||
terminate_and_reap(
|
||||
process
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("attached runsc is already stopped"))?,
|
||||
)?;
|
||||
child_broker().run_transient(delete)?;
|
||||
send_message(control, &SupervisorEvent::SandboxStopped)?;
|
||||
}
|
||||
CoordinatorMessage::StopGitRouting => {
|
||||
if process.is_some() {
|
||||
return Err(io::Error::other("Git-routing stop preceded sandbox stop"));
|
||||
}
|
||||
let routing = git_routing
|
||||
.take()
|
||||
.ok_or_else(|| io::Error::other("Git routing is already stopped"))?;
|
||||
let mount = routing.mount;
|
||||
stop_git_routing(Some(routing))?;
|
||||
send_message(control, &SupervisorEvent::GitRoutingStopped { mount })?;
|
||||
}
|
||||
CoordinatorMessage::StopSidecar => {
|
||||
if process.is_some() {
|
||||
return Err(io::Error::other("sidecar stop preceded sandbox stop"));
|
||||
}
|
||||
stop_network(network.take())?;
|
||||
send_message(control, &SupervisorEvent::SidecarStopped)?;
|
||||
}
|
||||
CoordinatorMessage::UnmountStateAnchor
|
||||
| CoordinatorMessage::ObserveStateAnchorAbsence { .. } => {
|
||||
handle_state_anchor_message(
|
||||
control,
|
||||
message,
|
||||
process.is_some() || git_routing.is_some() || network.is_some(),
|
||||
state_anchor,
|
||||
)?;
|
||||
}
|
||||
CoordinatorMessage::Exit => {
|
||||
if process.is_some()
|
||||
|| git_routing.is_some()
|
||||
|| network.is_some()
|
||||
|| state_anchor.mount.is_some()
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"Runtime Supervisor exit preceded resource cleanup",
|
||||
));
|
||||
}
|
||||
send_message(control, &SupervisorEvent::Exited)?;
|
||||
return Ok(true);
|
||||
}
|
||||
CoordinatorMessage::Stop => {
|
||||
if let Some(process) = process.take() {
|
||||
terminate_and_reap(process)?;
|
||||
}
|
||||
stop_network(network.take())?;
|
||||
stop_git_routing(git_routing.take())?;
|
||||
send_message(control, &SupervisorEvent::Stopped)?;
|
||||
return Ok(true);
|
||||
}
|
||||
CoordinatorMessage::HelperStagedAck(_)
|
||||
| CoordinatorMessage::MountStateAnchor
|
||||
| CoordinatorMessage::StateAnchorStagedAck(_)
|
||||
| CoordinatorMessage::StartGitRouting
|
||||
| CoordinatorMessage::GitRoutingStagedAck(_)
|
||||
| CoordinatorMessage::StartSidecar
|
||||
| CoordinatorMessage::NetworkAttestationAccepted(_)
|
||||
| CoordinatorMessage::NetworkReadinessCommitted
|
||||
| CoordinatorMessage::StagedAck(_)
|
||||
| CoordinatorMessage::SandboxCommitted => {
|
||||
return Err(io::Error::other("unexpected duplicate coordinator 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>,
|
||||
git_routing: Option<RuntimeGitRouting>,
|
||||
network: Option<PreparedNetwork>,
|
||||
error: io::Error,
|
||||
) -> io::Result<()> {
|
||||
if let Some(process) = process {
|
||||
fail_with_runtime_cleanup(process, git_routing, network, error)
|
||||
} else {
|
||||
fail_runtime_components(git_routing, network, error)
|
||||
}
|
||||
}
|
||||
|
||||
fn terminate_and_reap(process: RetainedProcess) -> io::Result<()> {
|
||||
let term_error = process.send_signal(Signal::TERM).err();
|
||||
let deadline = Instant::now() + TERMINATION_GRACE;
|
||||
while !process.has_exited()? && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
if !process.has_exited()? {
|
||||
process.send_signal(Signal::KILL)?;
|
||||
}
|
||||
child_broker().reap(process)?;
|
||||
if let Some(error) = term_error {
|
||||
Err(error)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn fail_with_runtime_cleanup(
|
||||
process: RetainedProcess,
|
||||
git_routing: Option<RuntimeGitRouting>,
|
||||
network: Option<PreparedNetwork>,
|
||||
error: io::Error,
|
||||
) -> io::Result<()> {
|
||||
let runsc_cleanup = terminate_and_reap(process);
|
||||
let network_cleanup = stop_network(network);
|
||||
let routing_cleanup = stop_git_routing(git_routing);
|
||||
match (runsc_cleanup, routing_cleanup, network_cleanup) {
|
||||
(Ok(()), Ok(()), Ok(())) => Err(error),
|
||||
(runsc, routing, network) => Err(io::Error::new(
|
||||
error.kind(),
|
||||
format!(
|
||||
"{error}; exact runtime cleanup failed: runsc={runsc:?}, Git-routing={routing:?}, network={network:?}"
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_stopped_runtime(
|
||||
process: RetainedProcess,
|
||||
git_routing: Option<RuntimeGitRouting>,
|
||||
network: Option<PreparedNetwork>,
|
||||
delete: &TargetCommand,
|
||||
terminate: bool,
|
||||
) -> io::Result<()> {
|
||||
let runsc_cleanup = if terminate {
|
||||
terminate_and_reap(process)
|
||||
} else {
|
||||
child_broker().reap(process)
|
||||
};
|
||||
let delete_cleanup = child_broker().run_transient(delete).map(drop);
|
||||
let network_cleanup = stop_network(network);
|
||||
let routing_cleanup = stop_git_routing(git_routing);
|
||||
let failures = [
|
||||
("attached runsc", runsc_cleanup),
|
||||
("runsc container", delete_cleanup),
|
||||
("network", network_cleanup),
|
||||
("Git routing", routing_cleanup),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(component, result)| result.err().map(|error| format!("{component}: {error}")))
|
||||
.collect::<Vec<_>>();
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::other(failures.join("; ")))
|
||||
}
|
||||
}
|
||||
|
||||
fn fail_runtime_components<T>(
|
||||
git_routing: Option<RuntimeGitRouting>,
|
||||
network: Option<PreparedNetwork>,
|
||||
error: io::Error,
|
||||
) -> io::Result<T> {
|
||||
match cleanup_runtime_components(git_routing, network) {
|
||||
Ok(()) => Err(error),
|
||||
Err(cleanup) => Err(io::Error::new(
|
||||
error.kind(),
|
||||
format!("{error}; exact component cleanup failed: {cleanup}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_runtime_components(
|
||||
git_routing: Option<RuntimeGitRouting>,
|
||||
network: Option<PreparedNetwork>,
|
||||
) -> io::Result<()> {
|
||||
let routing_cleanup = stop_git_routing(git_routing);
|
||||
let network_cleanup = stop_network(network);
|
||||
match (routing_cleanup, network_cleanup) {
|
||||
(Ok(()), Ok(())) => Ok(()),
|
||||
(routing, network) => Err(io::Error::other(format!(
|
||||
"Git-routing={routing:?}, network={network:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_git_routing(routing: Option<RuntimeGitRouting>) -> io::Result<()> {
|
||||
let Some(routing) = routing else {
|
||||
return Ok(());
|
||||
};
|
||||
let RuntimeGitRouting {
|
||||
process,
|
||||
shutdown,
|
||||
sentry,
|
||||
mountpoint,
|
||||
mount,
|
||||
} = routing;
|
||||
let sentry_cleanup = sentry.unmount();
|
||||
drop(shutdown);
|
||||
let process_cleanup = wait_and_reap(process);
|
||||
let mount_cleanup = inherited_directory_at(&mountpoint)
|
||||
.and_then(mount_identity)
|
||||
.and_then(|observed| {
|
||||
if observed == mount {
|
||||
Err(io::Error::other(
|
||||
"Git-routing mount remains after router shutdown",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
let failures = [
|
||||
("Git Sentry", sentry_cleanup),
|
||||
("router process", process_cleanup),
|
||||
("router mount", mount_cleanup),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(component, result)| result.err().map(|error| format!("{component}: {error}")))
|
||||
.collect::<Vec<_>>();
|
||||
if !failures.is_empty() {
|
||||
return Err(io::Error::other(failures.join("; ")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_and_reap(process: RetainedProcess) -> io::Result<()> {
|
||||
let deadline = Instant::now() + TERMINATION_GRACE;
|
||||
while !process.has_exited()? && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
if !process.has_exited()? {
|
||||
return terminate_and_reap(process).and_then(|()| {
|
||||
Err(io::Error::other(
|
||||
"Git router did not exit after its shutdown channel closed",
|
||||
))
|
||||
});
|
||||
}
|
||||
child_broker().reap(process)
|
||||
}
|
||||
|
||||
fn fail_network<T>(network: Option<PreparedNetwork>, error: io::Error) -> io::Result<T> {
|
||||
match stop_network(network) {
|
||||
Ok(()) => Err(error),
|
||||
Err(cleanup) => Err(io::Error::new(
|
||||
error.kind(),
|
||||
format!("{error}; exact connectivity-helper cleanup failed: {cleanup}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_network(network: Option<PreparedNetwork>) -> io::Result<()> {
|
||||
match network {
|
||||
Some(network) => network.stop(),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn require_lifetime_held() -> io::Result<()> {
|
||||
if lifetime_lost()? {
|
||||
Err(io::Error::other("Host Coordinator lifetime lease is lost"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn lifetime_lost() -> io::Result<bool> {
|
||||
let stderr = std::io::stderr();
|
||||
let mut descriptors = [PollFd::new(&stderr, PollFlags::IN)];
|
||||
poll(&mut descriptors, Some(&Timespec::default()))?;
|
||||
Ok(descriptors[0]
|
||||
.revents()
|
||||
.intersects(PollFlags::IN | PollFlags::HUP | PollFlags::ERR))
|
||||
}
|
||||
|
||||
fn send_message<T: Serialize>(descriptor: impl AsFd, message: &T) -> io::Result<()> {
|
||||
let encoded = encode(message).map_err(|error| io::Error::other(error.to_string()))?;
|
||||
if encoded.len() > MAX_CONTROL_MESSAGE {
|
||||
return Err(io::Error::other("runtime control message is oversized"));
|
||||
}
|
||||
let length = u32::try_from(encoded.len())
|
||||
.map_err(|_| io::Error::other("runtime control message is oversized"))?
|
||||
.to_be_bytes();
|
||||
send_all(descriptor.as_fd(), &length)?;
|
||||
send_all(descriptor.as_fd(), &encoded)
|
||||
}
|
||||
|
||||
fn receive_message<T>(descriptor: impl AsFd) -> io::Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned + Serialize,
|
||||
{
|
||||
let mut length = [0_u8; 4];
|
||||
receive_exact(descriptor.as_fd(), &mut length)?;
|
||||
let length = usize::try_from(u32::from_be_bytes(length))
|
||||
.map_err(|_| io::Error::other("runtime control length is invalid"))?;
|
||||
if length > MAX_CONTROL_MESSAGE {
|
||||
return Err(io::Error::other("runtime control message is oversized"));
|
||||
}
|
||||
let mut encoded = vec![0_u8; length];
|
||||
receive_exact(descriptor.as_fd(), &mut encoded)?;
|
||||
decode(&encoded).map_err(|error| io::Error::other(error.to_string()))
|
||||
}
|
||||
|
||||
fn send_all(descriptor: impl AsFd + Copy, mut bytes: &[u8]) -> io::Result<()> {
|
||||
while !bytes.is_empty() {
|
||||
let sent = write(descriptor, bytes)?;
|
||||
if sent == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::WriteZero,
|
||||
"runtime control channel closed",
|
||||
));
|
||||
}
|
||||
bytes = &bytes[sent..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn receive_exact(descriptor: impl AsFd + Copy, mut bytes: &mut [u8]) -> io::Result<()> {
|
||||
while !bytes.is_empty() {
|
||||
let received = read(descriptor, &mut *bytes)?;
|
||||
if received == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"runtime control channel closed",
|
||||
));
|
||||
}
|
||||
bytes = &mut bytes[received..];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
bytes.iter().fold(String::new(), |mut output, byte| {
|
||||
write!(output, "{byte:02x}").expect("writing to a String cannot fail");
|
||||
output
|
||||
})
|
||||
}
|
||||
653
crates/lazaret-runtime-supervisor/src/supervision/startup.rs
Normal file
653
crates/lazaret-runtime-supervisor/src/supervision/startup.rs
Normal file
|
|
@ -0,0 +1,653 @@
|
|||
fn run_supervisor(arguments: &RuntimeArguments) -> io::Result<()> {
|
||||
require_lifetime_held()?;
|
||||
let namespaces = enter_generation_filesystem_namespaces()?;
|
||||
if arguments.network.is_none() {
|
||||
enter_generation_network_namespace()?;
|
||||
}
|
||||
let control = std::io::stdout();
|
||||
let parent = current_process_identity()?;
|
||||
let mut state_anchor = prepare_state_anchor(arguments, parent, namespaces, &control)?;
|
||||
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(),
|
||||
format!("{error}; exact State Anchor cleanup failed: {cleanup}"),
|
||||
)),
|
||||
(Err(error), _) => Err(error),
|
||||
(Ok(()), Some(Err(error))) => Err(io::Error::other(error)),
|
||||
(Ok(()), _) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_state_anchor(
|
||||
arguments: &RuntimeArguments,
|
||||
parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||
namespaces: (
|
||||
lazaret_core::identity::NamespaceIdentity,
|
||||
lazaret_core::identity::NamespaceIdentity,
|
||||
),
|
||||
control: &std::io::Stdout,
|
||||
) -> io::Result<Option<RuntimeStateAnchorMount>> {
|
||||
let Some(state) = &arguments.state_anchor else {
|
||||
return Ok(None);
|
||||
};
|
||||
let command: CoordinatorMessage = receive_message(control)?;
|
||||
if command != CoordinatorMessage::MountStateAnchor {
|
||||
return Err(io::Error::other(
|
||||
"Runtime Supervisor received no State Anchor mount command",
|
||||
));
|
||||
}
|
||||
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,
|
||||
..arguments.ownership
|
||||
};
|
||||
let mount = authority
|
||||
.acquire(
|
||||
parent,
|
||||
ownership,
|
||||
|staged| send_message(control, &SupervisorEvent::StateAnchorStaged(staged)),
|
||||
|staged| match receive_message(control)? {
|
||||
CoordinatorMessage::StateAnchorStagedAck(acknowledgment)
|
||||
if acknowledgment.acknowledges(&staged) =>
|
||||
{
|
||||
Ok(acknowledgment)
|
||||
}
|
||||
_ => Err(io::Error::other(
|
||||
"Runtime Supervisor received a mismatched State Anchor acknowledgment",
|
||||
)),
|
||||
},
|
||||
|readiness| {
|
||||
send_message(
|
||||
control,
|
||||
&SupervisorEvent::StateAnchorReady {
|
||||
staged: readiness.staged(),
|
||||
user_namespace: namespaces.0,
|
||||
mount_namespace: namespaces.1,
|
||||
mount: readiness.mount(),
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
.map_err(io::Error::other)?;
|
||||
mount
|
||||
.require_persistent_destinations(&state.persistent_destinations)
|
||||
.map_err(io::Error::other)?;
|
||||
Ok(Some(mount))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
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<()> {
|
||||
let git_routing = match &arguments.git_routing {
|
||||
Some(routing) => Some(prepare_git_routing(arguments, routing, parent, control)?),
|
||||
None => None,
|
||||
};
|
||||
if arguments.state_anchor.is_some() {
|
||||
let message: CoordinatorMessage = receive_message(control)?;
|
||||
if message != CoordinatorMessage::StartSidecar {
|
||||
return Err(io::Error::other(
|
||||
"Runtime Supervisor received no sidecar start command",
|
||||
));
|
||||
}
|
||||
send_message(control, &SupervisorEvent::SidecarStarted)?;
|
||||
}
|
||||
let network = match &arguments.network {
|
||||
Some(network) => Some(prepare_runtime_network(
|
||||
arguments, network, parent, control,
|
||||
)?),
|
||||
None => None,
|
||||
};
|
||||
if let Some(routing) = &git_routing {
|
||||
let observed = inherited_directory_at(&routing.mountpoint).and_then(mount_identity);
|
||||
match observed {
|
||||
Ok(observed) if observed == routing.mount => {}
|
||||
Ok(_) => {
|
||||
return fail_runtime_components(
|
||||
git_routing,
|
||||
network,
|
||||
io::Error::other("Git-routing mount changed before runsc launch"),
|
||||
);
|
||||
}
|
||||
Err(error) => return fail_runtime_components(git_routing, network, error),
|
||||
}
|
||||
}
|
||||
let pid_file = File::from(memfd_create("lazaret-runsc-pid", MemfdFlags::empty())?);
|
||||
fcntl_setfd(pid_file.as_fd(), FdFlags::empty())?;
|
||||
let runsc = runsc_target(arguments, pid_file.as_raw_fd())?;
|
||||
let delete = runsc_delete_target(arguments)?;
|
||||
let staged = std::cell::Cell::new(None);
|
||||
let process = match child_broker().acquire_with_stdio(
|
||||
parent,
|
||||
arguments.ownership,
|
||||
&runsc,
|
||||
attached_runsc_standard_io(&runsc)?,
|
||||
|ownership| {
|
||||
staged.set(Some(ownership));
|
||||
send_message(control, &SupervisorEvent::Staged(ownership))
|
||||
},
|
||||
|ownership| match receive_message(control)? {
|
||||
CoordinatorMessage::StagedAck(acknowledgment)
|
||||
if acknowledgment.acknowledges(&ownership) =>
|
||||
{
|
||||
Ok(acknowledgment)
|
||||
}
|
||||
_ => Err(io::Error::other(
|
||||
"Runtime Supervisor received a mismatched staged acknowledgment",
|
||||
)),
|
||||
},
|
||||
) {
|
||||
Ok(process) => process,
|
||||
Err(error) => return fail_runtime_components(git_routing, network, error),
|
||||
};
|
||||
if let Err(error) = fcntl_setfd(pid_file.as_fd(), FdFlags::CLOEXEC) {
|
||||
return fail_with_runtime_cleanup(process, git_routing, network, error.into());
|
||||
}
|
||||
let Some(staged) = staged.get() else {
|
||||
return fail_with_runtime_cleanup(
|
||||
process,
|
||||
git_routing,
|
||||
network,
|
||||
io::Error::other("attached runsc ownership was not staged"),
|
||||
);
|
||||
};
|
||||
let sentry_pid_inventory = match wait_for_runsc_ready(&pid_file, &process) {
|
||||
Ok(pid) => pid,
|
||||
Err(error) => return fail_with_runtime_cleanup(process, git_routing, network, error),
|
||||
};
|
||||
if let Err(error) = capability::irreversibly_drop(CapabilitySet::NET_ADMIN) {
|
||||
return fail_with_runtime_cleanup(process, git_routing, network, error);
|
||||
}
|
||||
if let Err(error) = capability::require_regain_refused(CapabilitySet::NET_ADMIN) {
|
||||
return fail_with_runtime_cleanup(process, git_routing, network, error);
|
||||
}
|
||||
if let Err(error) = send_message(
|
||||
control,
|
||||
&SupervisorEvent::Ready {
|
||||
staged,
|
||||
sentry_pid_inventory,
|
||||
},
|
||||
) {
|
||||
return fail_with_runtime_cleanup(process, git_routing, network, error);
|
||||
}
|
||||
let committed: CoordinatorMessage = match receive_message(control) {
|
||||
Ok(committed) => committed,
|
||||
Err(error) => return fail_with_runtime_cleanup(process, git_routing, network, error),
|
||||
};
|
||||
if committed != CoordinatorMessage::SandboxCommitted {
|
||||
return fail_with_runtime_cleanup(
|
||||
process,
|
||||
git_routing,
|
||||
network,
|
||||
io::Error::other("sandbox commit acknowledgment was not received"),
|
||||
);
|
||||
}
|
||||
supervise(
|
||||
control,
|
||||
process,
|
||||
git_routing,
|
||||
network,
|
||||
namespaces,
|
||||
state_anchor,
|
||||
state_anchor_absence,
|
||||
&delete,
|
||||
)
|
||||
}
|
||||
|
||||
struct RuntimeGitRouting {
|
||||
process: RetainedProcess,
|
||||
shutdown: File,
|
||||
sentry: GitSentry,
|
||||
mountpoint: PathBuf,
|
||||
mount: MountIdentity,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn prepare_git_routing(
|
||||
arguments: &RuntimeArguments,
|
||||
routing: &RuntimeGitRoutingArguments,
|
||||
parent: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||
control: &std::io::Stdout,
|
||||
) -> io::Result<RuntimeGitRouting> {
|
||||
let message: CoordinatorMessage = receive_message(control)?;
|
||||
if message != CoordinatorMessage::StartGitRouting {
|
||||
return Err(io::Error::other(
|
||||
"Runtime Supervisor received no Git-routing start command",
|
||||
));
|
||||
}
|
||||
let workspace = inherited_directory(routing.workspace_fd)
|
||||
.map_err(|error| git_routing_error("opening workspace authority", &error))?;
|
||||
let isolated = inherited_directory(routing.isolated_fd)
|
||||
.map_err(|error| git_routing_error("opening isolated-state authority", &error))?;
|
||||
let mountpoint_authority = inherited_directory(routing.mountpoint_fd)
|
||||
.map_err(|error| git_routing_error("opening mountpoint authority", &error))?;
|
||||
let underlying_mount = mount_identity(&mountpoint_authority)
|
||||
.map_err(|error| git_routing_error("reading mountpoint identity", &error))?;
|
||||
for descriptor in [&workspace, &isolated] {
|
||||
fcntl_setfd(descriptor, FdFlags::empty()).map_err(|error| {
|
||||
git_routing_error(
|
||||
"making router authority inheritable",
|
||||
&io::Error::from(error),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let (shutdown_reader, shutdown_writer) = pipe_with(PipeFlags::CLOEXEC).map_err(|error| {
|
||||
git_routing_error("creating router shutdown channel", &io::Error::from(error))
|
||||
})?;
|
||||
let threads = std::thread::available_parallelism()
|
||||
.map_or(1, std::num::NonZero::get)
|
||||
.min(8);
|
||||
let target = TargetCommand::new("/proc/self/exe")
|
||||
.argument(PRIVATE_ROUTER_ROLE)
|
||||
.argument("--workspace-fd")
|
||||
.argument(workspace.as_raw_fd().to_string())
|
||||
.argument("--workspace-label")
|
||||
.argument(&routing.workspace_label)
|
||||
.argument("--isolated-fd")
|
||||
.argument(isolated.as_raw_fd().to_string())
|
||||
.argument("--mountpoint")
|
||||
.argument(&routing.mountpoint_label)
|
||||
.argument("--global-excludes")
|
||||
.argument(
|
||||
routing
|
||||
.global_excludes
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| std::path::Path::new("")),
|
||||
)
|
||||
.argument("--isolate-ignored")
|
||||
.argument(if routing.isolate_ignored {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
})
|
||||
.argument("--threads")
|
||||
.argument(threads.to_string())
|
||||
.with_target_stdin(File::from(shutdown_reader))
|
||||
.map_err(|error| git_routing_error("attaching router shutdown channel", &error))?;
|
||||
let ownership = ProcessOwnershipContext {
|
||||
slot: HostProcessOwnershipSlot::GitRoutingDaemon,
|
||||
..arguments.ownership
|
||||
};
|
||||
let acquired = child_broker()
|
||||
.acquire_captured(
|
||||
parent,
|
||||
ownership,
|
||||
&target,
|
||||
|staged| send_message(control, &SupervisorEvent::GitRoutingStaged(staged)),
|
||||
|staged| match receive_message(control)? {
|
||||
CoordinatorMessage::GitRoutingStagedAck(acknowledgment)
|
||||
if acknowledgment.acknowledges(&staged) =>
|
||||
{
|
||||
Ok(acknowledgment)
|
||||
}
|
||||
_ => Err(io::Error::other(
|
||||
"Runtime Supervisor received a mismatched Git-routing acknowledgment",
|
||||
)),
|
||||
},
|
||||
)
|
||||
.map_err(|error| git_routing_error("acquiring router process", &error));
|
||||
let mut restore_error = None;
|
||||
for descriptor in [&workspace, &isolated] {
|
||||
if let Err(error) = fcntl_setfd(descriptor, FdFlags::CLOEXEC)
|
||||
&& restore_error.is_none()
|
||||
{
|
||||
let error = io::Error::from(error);
|
||||
restore_error = Some(git_routing_error(
|
||||
"restoring router authority close-on-exec",
|
||||
&error,
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut captured = match (acquired, restore_error) {
|
||||
(Ok(captured), None) => captured,
|
||||
(Ok(captured), Some(error)) => {
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
error,
|
||||
);
|
||||
}
|
||||
(Err(error), None) => return Err(error),
|
||||
(Err(error), Some(restore)) => {
|
||||
return Err(io::Error::new(
|
||||
error.kind(),
|
||||
format!("{error}; restoring Git-routing descriptor flags failed: {restore}"),
|
||||
));
|
||||
}
|
||||
};
|
||||
if let Err(error) = wait_for_router_ready(&mut captured)
|
||||
.map_err(|error| git_routing_error("waiting for router readiness", &error))
|
||||
{
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
error,
|
||||
);
|
||||
}
|
||||
let mounted_workspace = match inherited_directory_at(&routing.mountpoint_label)
|
||||
.map_err(|error| git_routing_error("opening routed workspace", &error))
|
||||
{
|
||||
Ok(mounted_workspace) => mounted_workspace,
|
||||
Err(error) => {
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
let mount = match mount_identity(&mounted_workspace)
|
||||
.map_err(|error| git_routing_error("reading routed mount identity", &error))
|
||||
{
|
||||
Ok(mount) => mount,
|
||||
Err(error) => {
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
if mount == underlying_mount {
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
io::Error::other("Git router reported ready without its exact mount"),
|
||||
);
|
||||
}
|
||||
let git_common = match reopen_exact_directory(routing.git_common_fd, &routing.git_common_label)
|
||||
.map_err(|error| git_routing_error("opening Git common-directory authority", &error))
|
||||
{
|
||||
Ok(git_common) => git_common,
|
||||
Err(error) => {
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
let transient_hooks =
|
||||
match reopen_exact_directory(routing.transient_hooks_fd, &routing.transient_hooks_label)
|
||||
.map_err(|error| git_routing_error("opening transient hooks authority", &error))
|
||||
{
|
||||
Ok(transient_hooks) => transient_hooks,
|
||||
Err(error) => {
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
let transient_config = match reopen_exact_regular_file(
|
||||
routing.transient_config_fd,
|
||||
&routing.transient_config_label,
|
||||
)
|
||||
.map_err(|error| git_routing_error("opening transient config authority", &error))
|
||||
{
|
||||
Ok(transient_config) => transient_config,
|
||||
Err(error) => {
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
let sentry = match GitSentry::install_from_authorities(
|
||||
mounted_workspace,
|
||||
git_common,
|
||||
transient_hooks,
|
||||
routing.transient_hooks_label.clone(),
|
||||
transient_config,
|
||||
routing.transient_config_label.clone(),
|
||||
)
|
||||
.map_err(|error| git_routing_error("installing Git Sentry", &error))
|
||||
{
|
||||
Ok(sentry) => sentry,
|
||||
Err(error) => {
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
let staged = StagedProcessOwnership::v1(
|
||||
arguments.ownership.domain_id,
|
||||
arguments.ownership.generation_id,
|
||||
arguments.ownership.coordinator_id,
|
||||
HostProcessOwnershipSlot::GitRoutingDaemon,
|
||||
captured.process().identity(),
|
||||
);
|
||||
if let Err(error) = send_message(control, &SupervisorEvent::GitRoutingReady { staged, mount }) {
|
||||
let _ = sentry.unmount();
|
||||
return fail_router_before_commit(
|
||||
captured.into_process(),
|
||||
File::from(shutdown_writer),
|
||||
error,
|
||||
);
|
||||
}
|
||||
Ok(RuntimeGitRouting {
|
||||
process: captured.into_process(),
|
||||
shutdown: File::from(shutdown_writer),
|
||||
sentry,
|
||||
mountpoint: routing.mountpoint_label.clone(),
|
||||
mount,
|
||||
})
|
||||
}
|
||||
|
||||
fn git_routing_error(action: &str, error: &io::Error) -> io::Error {
|
||||
io::Error::new(error.kind(), format!("{action}: {error}"))
|
||||
}
|
||||
|
||||
fn inherited_directory(descriptor: i32) -> io::Result<File> {
|
||||
if descriptor < 0 {
|
||||
return Err(io::Error::other(
|
||||
"Runtime Supervisor inherited directory descriptor is invalid",
|
||||
));
|
||||
}
|
||||
inherited_directory_at(PathBuf::from(format!("/proc/self/fd/{descriptor}")))
|
||||
}
|
||||
|
||||
fn reopen_exact_directory(descriptor: i32, label: &std::path::Path) -> io::Result<File> {
|
||||
reopen_exact(descriptor, label, OFlags::RDONLY | OFlags::DIRECTORY, true)
|
||||
}
|
||||
|
||||
fn reopen_exact_regular_file(descriptor: i32, label: &std::path::Path) -> io::Result<File> {
|
||||
reopen_exact(descriptor, label, OFlags::RDWR, false)
|
||||
}
|
||||
|
||||
fn reopen_exact(
|
||||
descriptor: i32,
|
||||
label: &std::path::Path,
|
||||
flags: OFlags,
|
||||
directory: bool,
|
||||
) -> io::Result<File> {
|
||||
if descriptor < 0 {
|
||||
return Err(io::Error::other(
|
||||
"Runtime Supervisor inherited authority descriptor is invalid",
|
||||
));
|
||||
}
|
||||
let inherited = File::open(format!("/proc/self/fd/{descriptor}"))?;
|
||||
let expected = fstat(&inherited)?;
|
||||
let reopened = File::from(openat2(
|
||||
rustix::fs::CWD,
|
||||
label,
|
||||
flags | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||
)?);
|
||||
let observed = fstat(&reopened)?;
|
||||
let expected_type = FileType::from_raw_mode(expected.st_mode);
|
||||
let observed_type = FileType::from_raw_mode(observed.st_mode);
|
||||
if expected.st_dev != observed.st_dev
|
||||
|| expected.st_ino != observed.st_ino
|
||||
|| expected_type != observed_type
|
||||
|| observed_type.is_dir() != directory
|
||||
{
|
||||
return Err(io::Error::other(
|
||||
"namespace-local authority differs from its inherited identity",
|
||||
));
|
||||
}
|
||||
Ok(reopened)
|
||||
}
|
||||
|
||||
fn inherited_directory_at(path: impl AsRef<std::path::Path>) -> io::Result<File> {
|
||||
let file = File::open(path)?;
|
||||
if !FileType::from_raw_mode(fstat(&file)?.st_mode).is_dir() {
|
||||
return Err(io::Error::other(
|
||||
"Runtime Supervisor inherited authority is not a directory",
|
||||
));
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn wait_for_router_ready(process: &mut lazaret_linux::process::CapturedProcess) -> io::Result<()> {
|
||||
let deadline = Instant::now() + READINESS_TIMEOUT;
|
||||
let mut output = Vec::new();
|
||||
loop {
|
||||
let mut buffer = [0_u8; 64];
|
||||
match process.streams().0.read(&mut buffer) {
|
||||
Ok(0) if process.process().has_exited()? => {
|
||||
return Err(io::Error::other("Git router exited before readiness"));
|
||||
}
|
||||
Ok(0) => {}
|
||||
Ok(length) => {
|
||||
output.extend_from_slice(&buffer[..length]);
|
||||
if output == b"ready\n" {
|
||||
return Ok(());
|
||||
}
|
||||
if output.len() > 64 || !b"ready\n".starts_with(&output) {
|
||||
return Err(io::Error::other("Git router readiness is malformed"));
|
||||
}
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
if process.process().has_exited()? {
|
||||
return Err(io::Error::other("Git router exited before readiness"));
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return Err(io::Error::other("Git router readiness timed out"));
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
fn fail_router_before_commit<T>(
|
||||
process: RetainedProcess,
|
||||
shutdown: File,
|
||||
error: io::Error,
|
||||
) -> io::Result<T> {
|
||||
drop(shutdown);
|
||||
let _ = wait_and_reap(process);
|
||||
Err(error)
|
||||
}
|
||||
|
||||
fn prepare_runtime_network(
|
||||
arguments: &RuntimeArguments,
|
||||
network: &RuntimeNetworkArguments,
|
||||
runtime_supervisor: lazaret_core::identity::PidfsProcessIdentityV1,
|
||||
control: &std::io::Stdout,
|
||||
) -> io::Result<PreparedNetwork> {
|
||||
let policy_file = File::open(format!("/proc/self/fd/{}", network.policy_fd))?;
|
||||
let staged = std::cell::Cell::new(None);
|
||||
let prepared = prepare_network(
|
||||
&policy_file,
|
||||
network.policy_digest,
|
||||
NetworkAttestationContext {
|
||||
domain_id: arguments.ownership.domain_id,
|
||||
generation_id: arguments.ownership.generation_id,
|
||||
configuration_fingerprint: network.configuration_fingerprint,
|
||||
network_sidecar_id: network.network_sidecar_id,
|
||||
runtime_supervisor,
|
||||
coordinator_id: arguments.ownership.coordinator_id,
|
||||
},
|
||||
|ownership| {
|
||||
staged.set(Some(ownership));
|
||||
send_message(control, &SupervisorEvent::HelperStaged(ownership))
|
||||
},
|
||||
|ownership| match receive_message(control)? {
|
||||
CoordinatorMessage::HelperStagedAck(acknowledgment)
|
||||
if acknowledgment.acknowledges(&ownership) =>
|
||||
{
|
||||
Ok(acknowledgment)
|
||||
}
|
||||
_ => Err(io::Error::other(
|
||||
"Runtime Supervisor received a mismatched helper acknowledgment",
|
||||
)),
|
||||
},
|
||||
)?;
|
||||
let staged = staged
|
||||
.get()
|
||||
.ok_or_else(|| io::Error::other("connectivity helper ownership was not staged"))?;
|
||||
if let Err(error) = send_message(control, &SupervisorEvent::HelperReady(staged)) {
|
||||
return fail_network(Some(prepared), error);
|
||||
}
|
||||
if let Err(error) = send_message(
|
||||
control,
|
||||
&SupervisorEvent::NetworkAttestation(prepared.attestation().clone()),
|
||||
) {
|
||||
return fail_network(Some(prepared), error);
|
||||
}
|
||||
let message: CoordinatorMessage = match receive_message(control) {
|
||||
Ok(message) => message,
|
||||
Err(error) => return fail_network(Some(prepared), error),
|
||||
};
|
||||
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,
|
||||
Err(error) => return fail_network(Some(prepared), error),
|
||||
};
|
||||
if let Err(error) = send_message(control, &SupervisorEvent::NetworkProbe(probe)) {
|
||||
return fail_network(Some(prepared), error);
|
||||
}
|
||||
let committed: CoordinatorMessage = match receive_message(control) {
|
||||
Ok(message) => message,
|
||||
Err(error) => return fail_network(Some(prepared), error),
|
||||
};
|
||||
if committed != CoordinatorMessage::NetworkReadinessCommitted {
|
||||
return fail_network(
|
||||
Some(prepared),
|
||||
io::Error::other("durable network readiness was not acknowledged"),
|
||||
);
|
||||
}
|
||||
Ok(prepared)
|
||||
}
|
||||
|
||||
261
crates/lazaret-runtime-supervisor/src/supervision/tests.rs
Normal file
261
crates/lazaret-runtime-supervisor/src/supervision/tests.rs
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ffi::OsString;
|
||||
|
||||
use lazaret_core::identity::{CoordinatorId, DomainId, GenerationId};
|
||||
use lazaret_core::records::HostProcessOwnershipSlot;
|
||||
use lazaret_linux::ipc::private_seqpacket_pair;
|
||||
use lazaret_linux::process::ChildStandardIoPolicy;
|
||||
use rustix::pipe::{PipeFlags, pipe_with};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn private_stream_framing_round_trips_typed_events() {
|
||||
let (coordinator, supervisor) = private_seqpacket_pair().unwrap();
|
||||
let expected = SupervisorEvent::Refused("refused".into());
|
||||
send_message(&supervisor, &expected).unwrap();
|
||||
assert_eq!(
|
||||
CoordinatorChannel::new(coordinator).receive().unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coordinator_channel_preserves_phased_resource_commands() {
|
||||
let (coordinator, supervisor) = private_seqpacket_pair().unwrap();
|
||||
let channel = CoordinatorChannel::new(coordinator);
|
||||
channel.mount_state_anchor().unwrap();
|
||||
assert_eq!(
|
||||
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||
CoordinatorMessage::MountStateAnchor
|
||||
);
|
||||
channel.start_git_routing().unwrap();
|
||||
assert_eq!(
|
||||
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||
CoordinatorMessage::StartGitRouting
|
||||
);
|
||||
channel.start_sidecar().unwrap();
|
||||
assert_eq!(
|
||||
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||
CoordinatorMessage::StartSidecar
|
||||
);
|
||||
channel.stop_sandbox().unwrap();
|
||||
assert_eq!(
|
||||
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||
CoordinatorMessage::StopSandbox
|
||||
);
|
||||
channel.stop_git_routing().unwrap();
|
||||
assert_eq!(
|
||||
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||
CoordinatorMessage::StopGitRouting
|
||||
);
|
||||
channel.stop_sidecar().unwrap();
|
||||
assert_eq!(
|
||||
receive_message::<CoordinatorMessage>(&supervisor).unwrap(),
|
||||
CoordinatorMessage::StopSidecar
|
||||
);
|
||||
channel.unmount_state_anchor().unwrap();
|
||||
assert_eq!(
|
||||
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(),
|
||||
CoordinatorMessage::Exit
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_supervisor_and_runsc_keep_distinct_edge_and_stdio_policies() {
|
||||
let ownership = ProcessOwnershipContext {
|
||||
domain_id: DomainId::new([1; 32]),
|
||||
generation_id: GenerationId::new(7),
|
||||
coordinator_id: CoordinatorId::new([2; 32]),
|
||||
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||
};
|
||||
let launch = RuntimeSupervisorLaunch {
|
||||
target: TargetCommand::new("/proc/self/exe").with_lifetime_pipe_parent_edge(),
|
||||
ownership,
|
||||
policy_file: None,
|
||||
policy_digest: None,
|
||||
configuration_fingerprint: None,
|
||||
runsc_command: RunscCommand::new(
|
||||
"runsc",
|
||||
[OsString::from("run"), OsString::from("container")],
|
||||
[0; 32],
|
||||
),
|
||||
network_sidecar_id: None,
|
||||
state_anchor: None,
|
||||
git_routing: None,
|
||||
};
|
||||
let (_coordinator_control, supervisor_control) = private_seqpacket_pair().unwrap();
|
||||
let (supervisor_lifetime, _coordinator_lifetime) = pipe_with(PipeFlags::CLOEXEC).unwrap();
|
||||
assert_eq!(launch.target().parent_edge(), ParentEdge::LifetimePipe);
|
||||
assert_eq!(
|
||||
launch
|
||||
.child_standard_io(supervisor_control, supervisor_lifetime)
|
||||
.unwrap()
|
||||
.policy(),
|
||||
ChildStandardIoPolicy::PrivateControlWithLifetimePipe
|
||||
);
|
||||
|
||||
let runsc_arguments = RuntimeArguments {
|
||||
ownership: ProcessOwnershipContext {
|
||||
slot: HostProcessOwnershipSlot::SandboxAttachedRunsc,
|
||||
..ownership
|
||||
},
|
||||
state_anchor: None,
|
||||
git_routing: None,
|
||||
network: None,
|
||||
runsc_program: "runsc".into(),
|
||||
runsc_arguments: ["-root=/runtime", "run", "container"]
|
||||
.map(OsString::from)
|
||||
.into(),
|
||||
};
|
||||
let runsc = runsc_target(&runsc_arguments, 9).unwrap();
|
||||
assert_eq!(runsc.parent_edge(), ParentEdge::ParentDeathKill);
|
||||
assert_eq!(
|
||||
attached_runsc_standard_io(&runsc).unwrap().policy(),
|
||||
ChildStandardIoPolicy::Silenced
|
||||
);
|
||||
let delete = runsc_delete_target(&runsc_arguments).unwrap();
|
||||
assert_eq!(delete.program(), "runsc");
|
||||
assert_eq!(
|
||||
delete.arguments(),
|
||||
["-root=/runtime", "delete", "--force", "container"]
|
||||
.map(OsString::from)
|
||||
.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_role_parser_preserves_exact_ownership_and_runsc_arguments() {
|
||||
let domain = [1_u8; 32];
|
||||
let coordinator = [2_u8; 32];
|
||||
let arguments = [
|
||||
"--domain".into(),
|
||||
hex(&domain).into(),
|
||||
"--generation".into(),
|
||||
"7".into(),
|
||||
"--coordinator".into(),
|
||||
hex(&coordinator).into(),
|
||||
"--runsc".into(),
|
||||
"runsc".into(),
|
||||
"-root=/runtime".into(),
|
||||
"run".into(),
|
||||
"container".into(),
|
||||
];
|
||||
let parsed = parse_runtime_arguments(&arguments).unwrap();
|
||||
assert_eq!(parsed.ownership.domain_id, DomainId::new(domain));
|
||||
assert_eq!(parsed.ownership.generation_id, GenerationId::new(7));
|
||||
assert_eq!(
|
||||
parsed.ownership.coordinator_id,
|
||||
CoordinatorId::new(coordinator)
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.ownership.slot,
|
||||
HostProcessOwnershipSlot::SandboxAttachedRunsc
|
||||
);
|
||||
assert_eq!(parsed.runsc_program, OsString::from("runsc"));
|
||||
assert_eq!(
|
||||
parsed.runsc_arguments,
|
||||
["-root=/runtime", "run", "container"].map(OsString::from)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_role_parser_refuses_non_runsc_target() {
|
||||
let arguments = [
|
||||
"--domain".into(),
|
||||
hex(&[1_u8; 32]).into(),
|
||||
"--generation".into(),
|
||||
"7".into(),
|
||||
"--coordinator".into(),
|
||||
hex(&[2_u8; 32]).into(),
|
||||
"--runsc".into(),
|
||||
"foreign-runtime".into(),
|
||||
];
|
||||
assert!(parse_runtime_arguments(&arguments).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_role_parser_preserves_git_routing_authorities() {
|
||||
let arguments = [
|
||||
"--domain".into(),
|
||||
hex(&[1_u8; 32]).into(),
|
||||
"--generation".into(),
|
||||
"7".into(),
|
||||
"--coordinator".into(),
|
||||
hex(&[2_u8; 32]).into(),
|
||||
"--git-workspace-fd".into(),
|
||||
"3".into(),
|
||||
"--git-workspace-label".into(),
|
||||
"/workspace".into(),
|
||||
"--git-isolated-fd".into(),
|
||||
"4".into(),
|
||||
"--git-mountpoint-fd".into(),
|
||||
"5".into(),
|
||||
"--git-mountpoint-label".into(),
|
||||
"/runtime/workspace".into(),
|
||||
"--git-common-fd".into(),
|
||||
"6".into(),
|
||||
"--git-common-label".into(),
|
||||
"/workspace/.git".into(),
|
||||
"--git-hooks-fd".into(),
|
||||
"7".into(),
|
||||
"--git-hooks-label".into(),
|
||||
"/runtime/hooks".into(),
|
||||
"--git-config-fd".into(),
|
||||
"8".into(),
|
||||
"--git-config-label".into(),
|
||||
"/runtime/config".into(),
|
||||
"--git-global-excludes".into(),
|
||||
"/home/developer/.config/git/ignore".into(),
|
||||
"--git-isolate-ignored".into(),
|
||||
"true".into(),
|
||||
"--runsc".into(),
|
||||
"runsc".into(),
|
||||
"run".into(),
|
||||
"container".into(),
|
||||
];
|
||||
let parsed = parse_runtime_arguments(&arguments).unwrap();
|
||||
let routing = parsed.git_routing.unwrap();
|
||||
assert_eq!(routing.workspace_fd, 3);
|
||||
assert_eq!(routing.isolated_fd, 4);
|
||||
assert_eq!(routing.mountpoint_fd, 5);
|
||||
assert_eq!(routing.git_common_fd, 6);
|
||||
assert_eq!(routing.transient_hooks_fd, 7);
|
||||
assert_eq!(routing.transient_config_fd, 8);
|
||||
assert!(routing.isolate_ignored);
|
||||
|
||||
let mut negative = arguments;
|
||||
negative[7] = "-1".into();
|
||||
assert!(parse_runtime_arguments(&negative).is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue