Fix rootless launches with subordinate ID maps
Resolve and verify canonical caller subordinate ranges, launch preparation and runtime workers through mapped causal acquisition, and keep runsc in the existing user namespace. Repair descriptor-relative OCI cleanup and absent-mount recovery so failed launches can progress to the corrected path.
This commit is contained in:
parent
b9dbd78087
commit
73e3e9fdee
8 changed files with 1424 additions and 172 deletions
|
|
@ -228,12 +228,11 @@ where
|
|||
/// Returns an error when namespace creation, ID mapping, or mount isolation fails.
|
||||
pub fn enter_generation_filesystem_namespaces() -> io::Result<(NamespaceIdentity, NamespaceIdentity)>
|
||||
{
|
||||
let uid = getuid().as_raw();
|
||||
let gid = getgid().as_raw();
|
||||
rustix::thread::unshare(UnshareFlags::NEWUSER)?;
|
||||
std::fs::write("/proc/self/setgroups", "deny")?;
|
||||
std::fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))?;
|
||||
std::fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))?;
|
||||
if getuid().as_raw() != 0 || getgid().as_raw() != 0 {
|
||||
return Err(io::Error::other(
|
||||
"Runtime Supervisor did not enter the caller-configured user namespace as root",
|
||||
));
|
||||
}
|
||||
rustix::thread::unshare(UnshareFlags::NEWNS)?;
|
||||
mount_change(
|
||||
"/",
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ pub use lazaret_sandbox_contract::{
|
|||
TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION,
|
||||
};
|
||||
use oci_spec::runtime::{
|
||||
Capabilities, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxIdMappingBuilder,
|
||||
LinuxNamespaceBuilder, LinuxNamespaceType, Mount, MountBuilder, ProcessBuilder, RootBuilder,
|
||||
Spec, SpecBuilder, UserBuilder,
|
||||
Capabilities, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxNamespaceBuilder,
|
||||
LinuxNamespaceType, Mount, MountBuilder, ProcessBuilder, RootBuilder, Spec, SpecBuilder,
|
||||
UserBuilder,
|
||||
};
|
||||
use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, StatxFlags, openat2, statx};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use super::{
|
||||
AtFlags, BTreeMap, Capabilities, ContainerId, DEFAULT_PATH, File, FileIdentity, FileType,
|
||||
HandoffPathType, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxIdMappingBuilder,
|
||||
LinuxNamespaceBuilder, LinuxNamespaceType, Mode, Mount, NETWORK_NAMESPACE_PATH, OFlags,
|
||||
OsString, OwnedFd, Path, PathBuf, ProcessBuilder, RUNTIME_FILE_DESTINATIONS, Read,
|
||||
ResolveFlags, ResolvedDomainConfiguration, RootProcess, RunscCommand, RuntimeHandoffSource,
|
||||
HandoffPathType, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxNamespaceBuilder,
|
||||
LinuxNamespaceType, Mode, Mount, NETWORK_NAMESPACE_PATH, OFlags, OsString, OwnedFd, Path,
|
||||
PathBuf, ProcessBuilder, RUNTIME_FILE_DESTINATIONS, Read, ResolveFlags,
|
||||
ResolvedDomainConfiguration, RootProcess, RunscCommand, RuntimeHandoffSource,
|
||||
SandboxBuildError, SandboxBuildErrorKind, StagedHostTerminal, StatxFlags,
|
||||
TERMINFO_ROOT_DESTINATION, UserBuilder, VolumeAccess, WORKSPACE_DESTINATION, bind_mount,
|
||||
control_mounts, hex_lower, host_path, oci_error, openat2, staging_error, standard_mounts,
|
||||
|
|
@ -253,7 +253,6 @@ pub(super) fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildErro
|
|||
LinuxNamespaceType::Ipc,
|
||||
LinuxNamespaceType::Uts,
|
||||
LinuxNamespaceType::Mount,
|
||||
LinuxNamespaceType::User,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|typ| {
|
||||
|
|
@ -270,22 +269,8 @@ pub(super) fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildErro
|
|||
.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)
|
||||
|
|
@ -305,7 +290,6 @@ pub(super) fn build_runsc_command(
|
|||
.to_str()
|
||||
.expect("validated runtime path remains UTF-8")
|
||||
)),
|
||||
OsString::from("-rootless"),
|
||||
OsString::from("-network=host"),
|
||||
OsString::from("-ignore-cgroups"),
|
||||
OsString::from(format!(
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ use lazaret_linux::process::{
|
|||
ChildStandardIo, ParentEdge, ProcessOwnershipContext, RetainedProcess, TargetCommand,
|
||||
child_broker,
|
||||
};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_state_anchor::{
|
||||
RuntimeStateAnchorAbsenceAuthority, RuntimeStateAnchorMount, StateAnchorLaunchAuthority,
|
||||
};
|
||||
|
|
@ -328,6 +329,7 @@ pub struct RuntimeSupervisorLaunch {
|
|||
runsc_command: RunscCommand,
|
||||
state_anchor: Option<StateAnchorLaunchAuthority>,
|
||||
git_routing: Option<RuntimeGitRoutingLaunch>,
|
||||
user_namespace_map: Option<UserNamespaceMap>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
|
@ -443,6 +445,7 @@ impl RuntimeSupervisorLaunch {
|
|||
runsc_command: command.clone(),
|
||||
state_anchor: None,
|
||||
git_routing: None,
|
||||
user_namespace_map: None,
|
||||
};
|
||||
launch.refresh_target()?;
|
||||
Ok(launch)
|
||||
|
|
@ -485,6 +488,16 @@ impl RuntimeSupervisorLaunch {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
/// Adds the canonical caller-configured user namespace required by the Runtime Supervisor.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a non-canonical rootless map.
|
||||
pub fn with_user_namespace_map(mut self, map: UserNamespaceMap) -> io::Result<Self> {
|
||||
map.validate()?;
|
||||
self.user_namespace_map = Some(map);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn refresh_target(&mut self) -> io::Result<()> {
|
||||
let encoded = build_runtime_handoff(self)?;
|
||||
let handoff = sealed_memfd("lazaret-runtime-handoff", &encoded)?;
|
||||
|
|
@ -522,6 +535,11 @@ impl RuntimeSupervisorLaunch {
|
|||
&self.runsc_command
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn user_namespace_map(&self) -> Option<&UserNamespaceMap> {
|
||||
self.user_namespace_map.as_ref()
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses a Runtime Supervisor target without the lifetime-pipe parent edge.
|
||||
pub fn child_standard_io(
|
||||
|
|
|
|||
|
|
@ -626,6 +626,7 @@ mod handoff_tests {
|
|||
),
|
||||
state_anchor: None,
|
||||
git_routing: None,
|
||||
user_namespace_map: None,
|
||||
};
|
||||
assert!(build_runtime_handoff(&launch).is_err());
|
||||
assert!(parse_runtime_handoff(&[]).is_err());
|
||||
|
|
|
|||
|
|
@ -427,6 +427,7 @@ fn runtime_supervisor_and_runsc_keep_distinct_edge_and_stdio_policies() {
|
|||
network_sidecar_id: None,
|
||||
state_anchor: None,
|
||||
git_routing: None,
|
||||
user_namespace_map: None,
|
||||
};
|
||||
let (_coordinator_control, supervisor_control) = private_seqpacket_pair().unwrap();
|
||||
let (supervisor_lifetime, _coordinator_lifetime) = pipe_with(PipeFlags::CLOEXEC).unwrap();
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ use lazaret_core::records::{
|
|||
};
|
||||
use lazaret_linux::filesystem::{atomic_replace, require_write_sealed, sealed_memfd};
|
||||
use lazaret_linux::process::{
|
||||
ProcessOwnershipContext, TargetCommand, child_broker, current_process_identity, recover_process,
|
||||
AcknowledgedChildAcquisition, ChildStandardIo, ProcessOwnershipContext, TargetCommand,
|
||||
child_broker, current_process_identity, recover_process,
|
||||
};
|
||||
use lazaret_linux::user_namespace::UserNamespaceMap;
|
||||
use lazaret_runtime_supervisor::network::{
|
||||
DnsProbeTarget, NetworkAttestationContext, NetworkProbeRequest, TcpProbeTarget,
|
||||
enter_generation_filesystem_namespaces, prepare_network, probe_current_network,
|
||||
|
|
@ -77,15 +79,20 @@ fn run_parent() -> std::io::Result<()> {
|
|||
.argument(policy_file.as_raw_fd().to_string())
|
||||
.argument(hex(policy.digest().as_bytes()));
|
||||
let parent = current_process_identity()?;
|
||||
let runtime = child_broker().acquire(
|
||||
parent,
|
||||
ProcessOwnershipContext {
|
||||
domain_id: DomainId::new([1; 32]),
|
||||
generation_id: GenerationId::new(7),
|
||||
coordinator_id: CoordinatorId::new([2; 32]),
|
||||
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||
let map = UserNamespaceMap::resolve_current()?;
|
||||
let runtime = child_broker().acquire_with_stdio_in_user_namespace(
|
||||
AcknowledgedChildAcquisition {
|
||||
expected_parent: parent,
|
||||
ownership: ProcessOwnershipContext {
|
||||
domain_id: DomainId::new([1; 32]),
|
||||
generation_id: GenerationId::new(7),
|
||||
coordinator_id: CoordinatorId::new([2; 32]),
|
||||
slot: HostProcessOwnershipSlot::SandboxRuntimeSupervisor,
|
||||
},
|
||||
target: &target,
|
||||
standard_io: ChildStandardIo::inherit(),
|
||||
},
|
||||
&target,
|
||||
&map,
|
||||
|_| Ok(()),
|
||||
|staged| Ok(ProcessOwnershipCommitAck::v1(staged)),
|
||||
)?;
|
||||
|
|
|
|||
|
|
@ -295,24 +295,14 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
|
|||
.iter()
|
||||
.map(|namespace| namespace["type"].as_str().unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
["pid", "ipc", "uts", "mount", "user", "network"]
|
||||
["pid", "ipc", "uts", "mount", "network"]
|
||||
);
|
||||
for namespace in &namespaces[..5] {
|
||||
for namespace in &namespaces[..4] {
|
||||
assert!(namespace.get("path").is_none());
|
||||
}
|
||||
assert_eq!(namespaces[5]["path"], NETWORK_NAMESPACE_PATH);
|
||||
assert_eq!(
|
||||
specification["linux"]["uidMappings"],
|
||||
serde_json::json!([{
|
||||
"hostID": 0, "containerID": 0, "size": 1
|
||||
}])
|
||||
);
|
||||
assert_eq!(
|
||||
specification["linux"]["gidMappings"],
|
||||
serde_json::json!([{
|
||||
"hostID": 0, "containerID": 0, "size": 1
|
||||
}])
|
||||
);
|
||||
assert_eq!(namespaces[4]["path"], NETWORK_NAMESPACE_PATH);
|
||||
assert!(specification["linux"].get("uidMappings").is_none());
|
||||
assert!(specification["linux"].get("gidMappings").is_none());
|
||||
|
||||
assert_runtime_command(&first, &fixture);
|
||||
assert_eq!(
|
||||
|
|
@ -360,7 +350,6 @@ fn assert_runtime_command(bundle: &SandboxBundle, fixture: &Fixture) {
|
|||
.collect::<Vec<_>>(),
|
||||
[
|
||||
format!("-root={runtime_domain}/runsc"),
|
||||
"-rootless".to_owned(),
|
||||
"-network=host".to_owned(),
|
||||
"-ignore-cgroups".to_owned(),
|
||||
format!(
|
||||
|
|
@ -411,12 +400,8 @@ fn product_init_role_has_exact_terminal_neutral_rootless_bundle_shape() {
|
|||
);
|
||||
assert_eq!(specification["process"]["user"]["uid"], 0);
|
||||
assert_eq!(specification["process"]["user"]["gid"], 0);
|
||||
assert_eq!(
|
||||
specification["linux"]["uidMappings"],
|
||||
serde_json::json!([{
|
||||
"hostID": 0, "containerID": 0, "size": 1
|
||||
}])
|
||||
);
|
||||
assert!(specification["linux"].get("uidMappings").is_none());
|
||||
assert!(specification["linux"].get("gidMappings").is_none());
|
||||
assert_eq!(
|
||||
mounts(&specification)[SANDBOX_INIT_DESTINATION]["source"],
|
||||
host_path(
|
||||
|
|
|
|||
Loading…
Reference in a new issue