mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
#154: Reject setuid multicall invocations where argv[0] doesn't match AT_EXECFN from the ELF auxiliary vector. Prevents an attacker from spoofing argv[0] to route the multicall binary to a different tool in setuid context. Only enforced when euid != uid. #155: Add daily cargo-audit workflow matching uutils/coreutils pattern. Also triggers on Cargo.toml/Cargo.lock changes. Fixes #154. Fixes #155.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
name: Security audit
|
||||
|
||||
# spell-checker:ignore (misc) rustsec
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
push:
|
||||
paths:
|
||||
- "**/Cargo.toml"
|
||||
- "**/Cargo.lock"
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rustsec/audit-check@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -74,6 +74,8 @@ tempfile = "3"
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
clap_complete = { workspace = true, optional = true }
|
||||
shadow-core = { path = "src/shadow-core" }
|
||||
rustix = { workspace = true }
|
||||
|
||||
# Tool crates (optional, enabled by features)
|
||||
passwd = { optional = true, version = "0.2.0", package = "uu_passwd", path = "src/uu/passwd" }
|
||||
|
||||
@@ -30,6 +30,18 @@ fn main() -> ExitCode {
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// In setuid context, reject spoofed argv[0] that doesn't match AT_EXECFN.
|
||||
// Only enforced when euid != uid (setuid active) — non-setuid invocations
|
||||
// are harmless since the caller already has full privileges.
|
||||
let is_setuid = rustix::process::getuid() != rustix::process::geteuid();
|
||||
if is_setuid && !shadow_core::process::verify_argv0_matches_execfn(&binary_name) {
|
||||
let _ = writeln!(
|
||||
std::io::stderr().lock(),
|
||||
"shadow-rs: argv[0] does not match executed binary, aborting"
|
||||
);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
|
||||
// Direct invocation via symlink (e.g., argv[0] = "passwd")
|
||||
if let Some(code) = dispatch(&binary_name, &args) {
|
||||
return to_exit_code(code);
|
||||
|
||||
@@ -265,3 +265,42 @@ pub fn getpwuid(uid: u32) -> io::Result<Option<PwEntry>> {
|
||||
pub fn lookup_username(uid: u32) -> io::Result<Option<String>> {
|
||||
Ok(getpwuid(uid)?.map(|e| e.name))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AT_EXECFN validation (multicall setuid hardening)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Verify that `argv[0]` matches `AT_EXECFN` (the kernel-recorded executable path).
|
||||
///
|
||||
/// In setuid context, an attacker can spoof `argv[0]` to route a multicall
|
||||
/// binary to a different tool than the one the symlink points to. `AT_EXECFN`
|
||||
/// from the ELF auxiliary vector records the real path the kernel executed,
|
||||
/// which cannot be spoofed from userspace.
|
||||
///
|
||||
/// Returns `true` if the basenames match (or if `AT_EXECFN` is unavailable).
|
||||
/// Returns `false` if they differ — the caller should abort.
|
||||
pub fn verify_argv0_matches_execfn(argv0: &str) -> bool {
|
||||
// SAFETY: getauxval is a standard glibc/musl function. AT_EXECFN returns
|
||||
// a pointer to a null-terminated string in the auxiliary vector, valid for
|
||||
// the lifetime of the process.
|
||||
let execfn_ptr = unsafe { libc::getauxval(libc::AT_EXECFN) } as *const libc::c_char;
|
||||
if execfn_ptr.is_null() {
|
||||
// AT_EXECFN not available (old kernel or non-Linux) — allow.
|
||||
return true;
|
||||
}
|
||||
let execfn = unsafe { CStr::from_ptr(execfn_ptr) };
|
||||
let execfn = execfn.to_string_lossy();
|
||||
|
||||
// Compare basenames: argv[0] might be "passwd" while AT_EXECFN is
|
||||
// "/usr/sbin/passwd" or "/usr/sbin/shadow-rs".
|
||||
let argv0_base = std::path::Path::new(argv0)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
let execfn_base = std::path::Path::new(execfn.as_ref())
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
|
||||
argv0_base == execfn_base
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user