Close hostile recovery and concurrency gaps

This commit is contained in:
ms2k 2026-07-18 05:47:15 +09:00
commit 3f9b1b3543
2 changed files with 306 additions and 113 deletions

View file

@ -1070,12 +1070,12 @@ where
return Ok(PidfsRecoveryMatch::BootMismatch);
}
let host_pid = identity.host_pid().get();
let pidfd = open(host_pid)?;
let pidfd = normalize_process_absence(open(host_pid))?;
let observed = inspect(pidfd.as_fd(), current_boot)?;
if observed != identity.authority() {
return Err(io::Error::other("pidfs process authority mismatch"));
}
if start(host_pid)? != identity.start_time_ticks().get() {
if normalize_process_absence(start(host_pid))? != identity.start_time_ticks().get() {
return Err(io::Error::other("process start time mismatch"));
}
Ok(PidfsRecoveryMatch::Matched(RetainedProcess {
@ -1084,6 +1084,16 @@ where
}))
}
fn normalize_process_absence<T>(result: io::Result<T>) -> io::Result<T> {
result.map_err(|error| {
if error.raw_os_error() == Some(rustix::io::Errno::NOENT.raw_os_error()) {
io::Error::from_raw_os_error(rustix::io::Errno::SRCH.raw_os_error())
} else {
error
}
})
}
/// # Errors
/// Refuses if arming fails or the exact parent changes during the recheck.
pub fn arm_parent_death_and_recheck(
@ -1745,6 +1755,50 @@ mod tests {
}
}
#[test]
fn recovery_normalizes_vanished_proc_evidence_to_esrch() {
let authority = PidfsProcessAuthorityV1 {
boot_id: BootId::new([1; 16]),
pidfs_st_dev: PidfsDeviceId::new(7),
pidfs_st_ino: PidfsInodeId::new(9),
};
let identity = PidfsProcessIdentityV1::new(
authority,
HostPidLocator::new(41),
ProcessStartTimeTicks::new(43),
);
for missing in ["open", "start"] {
let result = match_recovery_process_with(
identity,
|| Ok(authority.boot_id),
|_| {
if missing == "open" {
Err(io::Error::from_raw_os_error(
rustix::io::Errno::NOENT.raw_os_error(),
))
} else {
Ok(File::open("/dev/null").unwrap().into())
}
},
|_, _| Ok(authority),
|_| {
if missing == "start" {
Err(io::Error::from_raw_os_error(
rustix::io::Errno::NOENT.raw_os_error(),
))
} else {
Ok(43)
}
},
)
.unwrap_err();
assert_eq!(
result.raw_os_error(),
Some(rustix::io::Errno::SRCH.raw_os_error())
);
}
}
#[test]
fn recovery_action_reconciles_boot_without_process_action() {
let mut old_boot = *read_boot_id().unwrap().as_bytes();

View file

@ -8,42 +8,55 @@ use rustix::thread::{
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
fn main() -> ExitCode {
if std::env::var_os("LAZARET_CAPABILITY_DROP_ISOLATED").is_none() {
let before = lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN)
.expect("inspect unprivileged capability state");
if lazaret_linux::capability::irreversibly_drop(CapabilitySet::NET_ADMIN).is_err()
|| lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN).ok() != Some(before)
{
return ExitCode::FAILURE;
}
return match std::process::Command::new("unshare")
let isolated = match std::process::Command::new("unshare")
.args(["--user", "--map-root-user"])
.arg(std::env::current_exe().expect("test executable path"))
.env("LAZARET_CAPABILITY_DROP_ISOLATED", "1")
.output()
{
Ok(output) if output.status.success() => ExitCode::SUCCESS,
Ok(output) if output.status.success() => true,
Ok(output)
if String::from_utf8_lossy(&output.stderr).contains("Operation not permitted") =>
{
eprintln!("SKIP isolated capability-positive branch: user namespaces unavailable");
ExitCode::SUCCESS
true
}
Ok(output) => {
eprintln!(
"isolated capability-positive branch failed: {}",
String::from_utf8_lossy(&output.stderr)
);
ExitCode::FAILURE
false
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
eprintln!("SKIP isolated capability-positive branch: unshare is unavailable");
ExitCode::SUCCESS
true
}
Err(error) => {
eprintln!("failed to run isolated capability-positive branch: {error}");
ExitCode::FAILURE
false
}
};
if !isolated {
return ExitCode::FAILURE;
}
let before = lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN)
.expect("inspect unprivileged capability state");
return match lazaret_linux::capability::irreversibly_drop(CapabilitySet::NET_ADMIN) {
Ok(())
if lazaret_linux::capability::require_regain_refused(CapabilitySet::NET_ADMIN)
.is_ok() =>
{
ExitCode::SUCCESS
}
Err(_)
if lazaret_linux::capability::inspect(CapabilitySet::NET_ADMIN).ok()
== Some(before) =>
{
ExitCode::SUCCESS
}
Ok(()) | Err(_) => ExitCode::FAILURE,
};
}
let capability = CapabilitySet::NET_ADMIN;