use std::{path::Path, process::Command};
#[cfg(all(
unix,
any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "dragonfly",
),
))]
use color_eyre::eyre::{Context, eyre};
use tracing::info;
#[cfg(all(
unix,
any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "dragonfly",
),
))]
use tracing::info_span;
use crate::{error::BootstrapResult, observability::LOG_TARGET};
macro_rules! cfg_privilege_drop {
($($item:item)*) => {
$(
#[cfg(all(
unix,
any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "dragonfly",
),
))]
$item
)*
};
}
cfg_privilege_drop! {
use nix::unistd::{Gid, Uid, User, chown};
use std::os::unix::process::CommandExt;
use std::sync::atomic::{AtomicUsize, Ordering};
}
pub(crate) fn apply(payload_path: &Path, command: &mut Command) -> BootstrapResult<()> {
apply_impl(payload_path, command)
}
cfg_privilege_drop! {
fn apply_impl(payload_path: &Path, command: &mut Command) -> BootstrapResult<()> {
apply_unix(payload_path, command)
}
static SKIP_PRIVILEGE_DROP: AtomicUsize = AtomicUsize::new(0);
fn apply_unix(payload_path: &Path, command: &mut Command) -> BootstrapResult<()> {
let span = info_span!(
target: LOG_TARGET,
"privilege_drop",
payload = %payload_path.display()
);
let _entered = span.enter();
if skip_privilege_drop(payload_path) {
return Ok(());
}
apply_privilege_drop(payload_path, command)
}
fn apply_privilege_drop(payload_path: &Path, command: &mut Command) -> BootstrapResult<()> {
let (uid, gid) = resolve_nobody_ids()?;
chown_payload(payload_path, uid, gid)?;
configure_pre_exec(command, uid, gid);
info!(
target: LOG_TARGET,
payload = %payload_path.display(),
uid,
gid,
"configured worker command to drop privileges"
);
Ok(())
}
fn skip_privilege_drop(payload_path: &Path) -> bool {
let should_skip = skip_privilege_drop_for_tests();
if should_skip {
info!(
target: LOG_TARGET,
payload = %payload_path.display(),
"skipping privilege drop for tests"
);
}
should_skip
}
fn resolve_nobody_ids() -> BootstrapResult<(u32, u32)> {
let user = User::from_name("nobody")
.context("failed to resolve user 'nobody'")?
.ok_or_else(|| eyre!("user 'nobody' not found"))?;
Ok((user.uid.as_raw(), user.gid.as_raw()))
}
fn chown_payload(payload_path: &Path, uid: u32, gid: u32) -> BootstrapResult<()> {
chown(
payload_path,
Some(Uid::from_raw(uid)),
Some(Gid::from_raw(gid)),
)
.context("failed to chown worker payload to nobody")?;
Ok(())
}
fn configure_pre_exec(command: &mut Command, uid: u32, gid: u32) {
unsafe {
command.pre_exec(move || {
if libc::setgroups(0, std::ptr::null()) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::setgid(gid) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::setuid(uid) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
}
#[cfg(not(all(
unix,
any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "dragonfly",
),
)))]
fn apply_noop(_payload_path: &Path, _command: &mut Command) -> BootstrapResult<()> {
info!(
target: LOG_TARGET,
"privilege drop unsupported on this platform; worker command left unchanged"
);
Ok(())
}
#[cfg(not(all(
unix,
any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "dragonfly",
),
)))]
fn apply_impl(payload_path: &Path, command: &mut Command) -> BootstrapResult<()> {
apply_noop(payload_path, command)
}
cfg_privilege_drop! {
#[cfg(any(test, doc, feature = "privileged-tests"))]
#[derive(Debug)]
pub(crate) struct PrivilegeDropGuard;
#[cfg(any(test, doc, feature = "privileged-tests"))]
impl Drop for PrivilegeDropGuard {
fn drop(&mut self) {
decrement_skip_privilege_drop();
}
}
#[cfg(any(test, doc, feature = "privileged-tests"))]
#[must_use]
pub(crate) fn disable_privilege_drop_for_tests() -> PrivilegeDropGuard {
SKIP_PRIVILEGE_DROP.fetch_add(1, Ordering::SeqCst);
PrivilegeDropGuard
}
fn skip_privilege_drop_for_tests() -> bool {
SKIP_PRIVILEGE_DROP.load(Ordering::SeqCst) > 0
}
#[cfg(any(test, doc, feature = "privileged-tests"))]
fn decrement_skip_privilege_drop() {
let _update_result = SKIP_PRIVILEGE_DROP.fetch_update(
Ordering::SeqCst,
Ordering::SeqCst,
|value| {
debug_assert!(
value > 0,
"PrivilegeDropGuard dropped with zero privilege-drop counter"
);
Some(value.saturating_sub(1))
},
);
}
}
#[cfg(all(
test,
unix,
any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "dragonfly",
),
))]
mod tests {
use std::process::Command;
use color_eyre::eyre::ensure;
use serial_test::serial;
use tempfile::NamedTempFile;
use super::*;
use crate::test_support::capture_info_logs;
#[test]
#[serial(privilege_drop_toggle)]
fn skip_guard_logs_observability() -> color_eyre::Result<()> {
let payload = NamedTempFile::new()?;
let mut command = Command::new("true");
let guard = disable_privilege_drop_for_tests();
let (logs, result) = capture_info_logs(|| apply(payload.path(), &mut command));
drop(guard);
ensure!(result.is_ok(), "privilege drop skip should succeed");
ensure!(
logs.iter().any(|line| {
line.contains("skipping privilege drop for tests")
&& line.contains(&format!("{}", payload.path().display()))
}),
"expected skip log entry, got {logs:?}"
);
Ok(())
}
#[test]
#[serial(privilege_drop_toggle)]
fn skip_toggle_tracks_nested_guards() {
assert!(!skip_privilege_drop_for_tests(), "toggle starts disabled");
let outer = disable_privilege_drop_for_tests();
assert!(skip_privilege_drop_for_tests(), "outer guard enables skip");
let inner = disable_privilege_drop_for_tests();
assert!(skip_privilege_drop_for_tests(), "inner guard keeps skip");
drop(inner);
assert!(
skip_privilege_drop_for_tests(),
"skip remains while outer guard is held"
);
drop(outer);
assert!(
!skip_privilege_drop_for_tests(),
"skip clears once all guards drop"
);
}
#[test]
fn resolve_nobody_ids_returns_unprivileged_account() {
let (uid, gid) = resolve_nobody_ids().expect("resolve nobody account");
assert!(uid > 0, "nobody uid must not be root");
assert!(gid > 0, "nobody gid must not be root");
}
#[test]
fn configure_pre_exec_demotion_hook_fails_spawn_when_unprivileged() {
if nix::unistd::geteuid().is_root() {
return;
}
let mut command = Command::new("true");
configure_pre_exec(&mut command, 0, 0);
assert!(
command.spawn().is_err(),
"pre-exec demotion to uid 0 must fail for an unprivileged process"
);
}
}