Verify irreversible capability removal

This commit is contained in:
ms2k 2026-07-15 19:08:52 +09:00
commit 3171c41399
2 changed files with 44 additions and 1 deletions

View file

@ -58,6 +58,33 @@ pub fn irreversibly_drop(capability: CapabilitySet) -> io::Result<()> {
Ok(()) Ok(())
} }
/// # Errors
/// Refuses if an absent capability can be restored or any verification fails.
pub fn require_regain_refused(capability: CapabilitySet) -> io::Result<()> {
let before = inspect(capability)?;
if before.sets.effective.intersects(capability)
|| before.sets.permitted.intersects(capability)
|| before.sets.inheritable.intersects(capability)
|| before.ambient
|| before.bounding
{
return Err(io::Error::other(
"capability remains available before regain verification",
));
}
let mut attempted = before.sets;
attempted.effective.insert(capability);
attempted.permitted.insert(capability);
attempted.inheritable.insert(capability);
if set_capabilities(None, attempted).is_ok()
|| configure_capability_in_ambient_set(capability, true).is_ok()
|| inspect(capability)? != before
{
return Err(io::Error::other("capability regain was not refused"));
}
Ok(())
}
fn require_single_threaded_process() -> io::Result<()> { fn require_single_threaded_process() -> io::Result<()> {
let mut tasks = std::fs::read_dir("/proc/self/task")?; let mut tasks = std::fs::read_dir("/proc/self/task")?;
if tasks.next().transpose()?.is_some() && tasks.next().transpose()?.is_none() { if tasks.next().transpose()?.is_some() && tasks.next().transpose()?.is_none() {
@ -73,7 +100,7 @@ fn require_single_threaded_process() -> io::Result<()> {
mod tests { mod tests {
use rustix::thread::CapabilitySet; use rustix::thread::CapabilitySet;
use super::{inspect, irreversibly_drop}; use super::{inspect, irreversibly_drop, require_regain_refused};
#[test] #[test]
fn inspects_current_capability_sets() { fn inspects_current_capability_sets() {
@ -95,4 +122,17 @@ mod tests {
waiting.wait(); waiting.wait();
child.join().unwrap(); child.join().unwrap();
} }
#[test]
fn absent_capability_cannot_be_regained() {
let state = inspect(CapabilitySet::NET_ADMIN).unwrap();
if !state.sets.effective.intersects(CapabilitySet::NET_ADMIN)
&& !state.sets.permitted.intersects(CapabilitySet::NET_ADMIN)
&& !state.sets.inheritable.intersects(CapabilitySet::NET_ADMIN)
&& !state.ambient
&& !state.bounding
{
require_regain_refused(CapabilitySet::NET_ADMIN).unwrap();
}
}
} }

View file

@ -56,6 +56,9 @@ fn main() -> ExitCode {
if lazaret_linux::capability::irreversibly_drop(capability).is_err() { if lazaret_linux::capability::irreversibly_drop(capability).is_err() {
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
if lazaret_linux::capability::require_regain_refused(capability).is_err() {
return ExitCode::FAILURE;
}
let Ok(observed) = lazaret_linux::capability::inspect(capability) else { let Ok(observed) = lazaret_linux::capability::inspect(capability) else {
return ExitCode::FAILURE; return ExitCode::FAILURE;
}; };