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:
ms2k 2026-07-19 19:51:47 +09:00
commit 73e3e9fdee

View file

@ -4,15 +4,18 @@ use super::recovery::{
read_start_time, require_default_sigchld, require_host_pid_namespace, require_reapable_sigchld,
};
use super::{
AcknowledgedChildAcquisition, AsFd, BarrierRelease, BootstrapChild, CapturedProcess,
ChildBroker, ChildStandardIo, ChildStandardIoPolicy, File, HostPidLocator, OFlags, OsString,
OwnedFd, PRIVATE_PROCESS_ROLE, ParentEdge, Pid, PidfdFlags, PidfsProcessIdentityV1, PollFd,
PollFlags, ProcessExit, ProcessOwnershipCommitAck, ProcessOwnershipContext,
ProcessStartTimeTicks, RetainedProcess, SIGCHLD_PROBE_ROLE, Signal, StagedProcessOwnership,
Stdio, TargetCommand, Timespec, WRAPPER_ROLE, WaitId, WaitIdOptions, fcntl_getfl, fcntl_setfl,
io, launch_barrier, poll, recover_process, require_captured_parent_edge,
require_parent_edge_standard_io,
AcknowledgedChildAcquisition, AsFd, BARRIER_RELEASE, BarrierRelease, BootstrapChild,
CapturedProcess, ChildBroker, ChildStandardIo, ChildStandardIoPolicy, File, HostPidLocator,
MAPPED_WRAPPER_ROLE, OFlags, OsString, OwnedFd, PRIVATE_PROCESS_ROLE, ParentEdge, Pid,
PidfdFlags, PidfsProcessIdentityV1, PollFd, PollFlags, ProcessExit, ProcessOwnershipCommitAck,
ProcessOwnershipContext, ProcessStartTimeTicks, RetainedProcess, SIGCHLD_PROBE_ROLE, Signal,
StagedProcessOwnership, Stdio, TargetCommand, Timespec, WRAPPER_ROLE, WaitId, WaitIdOptions,
fcntl_getfl, fcntl_setfl, io, launch_barrier, poll, recover_process,
require_captured_parent_edge, require_parent_edge_standard_io,
};
use crate::ipc::private_stream_pair;
use crate::user_namespace::UserNamespaceMap;
use std::io::{Read as _, Write as _};
impl ChildBroker {
/// Spawns the private Host Coordinator bootstrap with exact pipe ownership and pidfd reaping.
@ -132,6 +135,64 @@ impl ChildBroker {
}
}
/// Runs a non-durable direct child as root in the supplied caller-configured user namespace.
///
/// # Errors
/// Refuses invalid mapping, causal launch, helper execution, map attestation, or child exit.
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
pub fn run_transient_in_user_namespace(
&self,
target: &TargetCommand,
map: &UserNamespaceMap,
) -> io::Result<ProcessExit> {
let _guard = self
.exclusion
.lock()
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
require_parent_edge_standard_io(target.parent_edge(), ChildStandardIoPolicy::Inherit)?;
require_default_sigchld()?;
require_reapable_sigchld()?;
map.validate()?;
let parent = current_process_identity()?;
let (handshake, child_handshake) = private_stream_pair()?;
let mut command = mapped_wrapper_command();
append_parent_identity(&mut command, parent);
target.append_to(&mut command);
command
.stdin(Stdio::from(child_handshake))
.stdout(Stdio::null())
.stderr(Stdio::inherit());
let mut child = command.spawn()?;
let process = match acquire_spawned_child(child.id()) {
Ok(process) => process,
Err(error) => {
drop(handshake);
let _ = child.wait();
return Err(error);
}
};
if let Err(error) = configure_user_namespace(&process, File::from(handshake), map) {
return fail_running_mapped(process, child, error);
}
let status =
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?
.ok_or_else(|| io::Error::other("mapped transient child exited without status"))?;
let exit = ProcessExit {
exit_code: status.exit_status(),
terminating_signal: status.terminating_signal(),
};
drop(child);
drop(process);
if exit.exit_code == Some(0) && exit.terminating_signal.is_none() {
Ok(exit)
} else {
Err(io::Error::other(format!(
"mapped transient child failed with exit {:?} and signal {:?}",
exit.exit_code, exit.terminating_signal
)))
}
}
/// Starts a non-durable direct child with split captured output and exact retained authority.
///
/// # Errors
@ -223,6 +284,89 @@ impl ChildBroker {
)
}
/// Acquires an exact child as root in a helper-configured subordinate user namespace.
///
/// # Errors
/// Refuses staging, acknowledgment, namespace helper, live-map attestation, or release
/// failures and reaps the child on every failure.
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
pub fn acquire_with_stdio_in_user_namespace<Stage, Acknowledge>(
&self,
acquisition: AcknowledgedChildAcquisition<'_>,
map: &UserNamespaceMap,
stage: Stage,
acknowledge: Acknowledge,
) -> io::Result<RetainedProcess>
where
Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>,
Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result<ProcessOwnershipCommitAck>,
{
let _guard = self
.exclusion
.lock()
.map_err(|_| io::Error::other("child broker lock poisoned"))?;
require_parent_edge_standard_io(
acquisition.target.parent_edge(),
acquisition.standard_io.policy(),
)?;
require_default_sigchld()?;
require_reapable_sigchld()?;
map.validate()?;
let (handshake, child_handshake) = private_stream_pair()?;
let mut command = mapped_wrapper_command();
append_parent_identity(&mut command, acquisition.expected_parent);
acquisition.target.append_to(&mut command);
command.stdin(Stdio::from(child_handshake));
let mut standard_io = acquisition.standard_io;
if let Some(stdout) = standard_io.stdout.take() {
command.stdout(stdout);
}
if let Some(stderr) = standard_io.stderr.take() {
command.stderr(stderr);
}
let mut child = command.spawn()?;
let process = match acquire_spawned_child(child.id()) {
Ok(process) => process,
Err(error) => {
drop(handshake);
let _ = child.wait();
return Err(error);
}
};
let staged = StagedProcessOwnership::v1(
acquisition.ownership.domain_id,
acquisition.ownership.generation_id,
acquisition.ownership.coordinator_id,
acquisition.ownership.slot,
process.identity,
);
if let Err(error) = stage(staged) {
drop(handshake);
return fail_after_pidfd_closed(process, error);
}
let acknowledgment = match acknowledge(staged) {
Ok(acknowledgment) => acknowledgment,
Err(error) => {
drop(handshake);
return fail_after_pidfd_closed(process, error);
}
};
if !acknowledgment.acknowledges(&staged) {
drop(handshake);
return fail_after_pidfd_closed(
process,
io::Error::other(
"process ownership acknowledgment does not match staged ownership",
),
);
}
if let Err(error) = configure_user_namespace(&process, File::from(handshake), map) {
return fail_running_mapped(process, child, error);
}
drop(child);
Ok(process)
}
/// Acquires an exact child while retaining its blocked wrapper across one acknowledged
/// parent transition.
///
@ -439,6 +583,84 @@ fn child_pipe_file(fd: OwnedFd) -> io::Result<File> {
Ok(File::from(fd))
}
#[allow(clippy::disallowed_types)]
fn mapped_wrapper_command() -> std::process::Command {
private_role_command(MAPPED_WRAPPER_ROLE)
}
fn configure_user_namespace(
process: &RetainedProcess,
mut handshake: File,
map: &UserNamespaceMap,
) -> io::Result<()> {
handshake.write_all(BARRIER_RELEASE)?;
handshake.flush()?;
let mut marker = [0_u8; 1];
handshake.read_exact(&mut marker)?;
if marker != *b"u" {
return Err(io::Error::other(
"mapped child sent an invalid user-namespace marker",
));
}
let pid = process.identity.host_pid().get();
run_mapping_helper("/usr/bin/newuidmap", &map.uid_helper_arguments(pid))?;
run_mapping_helper("/usr/bin/newgidmap", &map.gid_helper_arguments(pid))?;
map.verify_process(pid)?;
handshake.write_all(b"m")?;
handshake.flush()
}
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
fn run_mapping_helper(program: &str, arguments: &[String]) -> io::Result<()> {
let mut command = std::process::Command::new(program);
command
.args(arguments)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
let mut child = command.spawn()?;
let process = match acquire_spawned_child(child.id()) {
Ok(process) => process,
Err(error) => {
let _ = child.kill();
let _ = child.wait();
return Err(error);
}
};
let status =
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?
.ok_or_else(|| io::Error::other("ID-map helper exited without status"))?;
let mut detail = String::new();
if let Some(stderr) = child.stderr.take() {
stderr.take(4096).read_to_string(&mut detail)?;
}
drop(child);
drop(process);
if status.exit_status() == Some(0) && status.terminating_signal().is_none() {
Ok(())
} else {
Err(io::Error::other(format!(
"{program} failed with exit {:?} and signal {:?}: {}",
status.exit_status(),
status.terminating_signal(),
detail.trim()
)))
}
}
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
fn fail_running_mapped<T>(
process: RetainedProcess,
child: std::process::Child,
error: io::Error,
) -> io::Result<T> {
let _ = process.send_signal(Signal::KILL);
rustix::process::waitid(WaitId::PidFd(process.pidfd.as_fd()), WaitIdOptions::EXITED)?;
drop(child);
drop(process);
Err(error)
}
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
fn fail_running_captured<T>(
process: RetainedProcess,
@ -476,12 +698,57 @@ pub fn dispatch_private_process_role(arguments: &[OsString]) -> Option<io::Resul
Some(BarrierWait::from_stdin().and_then(BarrierWait::wait))
}
Some(WRAPPER_ROLE) => Some(run_wrapper_role(arguments[2..].to_vec())),
Some(MAPPED_WRAPPER_ROLE) => Some(run_mapped_wrapper_role(arguments[2..].to_vec())),
_ => Some(Err(io::Error::other(
"unknown or malformed private process role",
))),
}
}
#[allow(deprecated)]
fn run_mapped_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
let (expected_parent, target) = parse_wrapper_arguments(arguments)?;
let mut release = [0_u8; BARRIER_RELEASE.len()];
std::io::stdin().read_exact(&mut release)?;
if release != *BARRIER_RELEASE {
return Err(io::Error::other(
"mapped wrapper launch release is malformed",
));
}
match target.parent_edge {
ParentEdge::ParentDeathKill => {
arm_parent_death_and_recheck(Signal::KILL, expected_parent)?;
}
ParentEdge::LifetimePipe => {
require_lifetime_pipe_and_recheck(expected_parent)?;
}
}
rustix::thread::unshare(rustix::thread::UnshareFlags::NEWUSER)?;
let stdin = std::io::stdin();
rustix::io::write(&stdin, b"u")?;
let mut marker = [0_u8; 1];
if rustix::io::read(&stdin, &mut marker)? != 1 || marker != *b"m" {
return Err(io::Error::other(
"mapped wrapper did not receive a valid map release",
));
}
rustix::io::fcntl_setfd(&stdin, rustix::io::FdFlags::CLOEXEC)?;
rustix::thread::set_thread_res_gid(
rustix::process::Gid::ROOT,
rustix::process::Gid::ROOT,
rustix::process::Gid::ROOT,
)?;
rustix::thread::set_thread_res_uid(
rustix::process::Uid::ROOT,
rustix::process::Uid::ROOT,
rustix::process::Uid::ROOT,
)?;
if let Some(stdin) = target.stdin.as_ref() {
rustix::stdio::dup2_stdin(stdin)?;
}
Err(exec_target(target))
}
fn run_wrapper_role(arguments: Vec<OsString>) -> io::Result<()> {
let (expected_parent, target) = parse_wrapper_arguments(arguments)?;
BarrierWait::from_stdin()?.wait()?;