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]
|
[dependencies]
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
clap_complete = { workspace = true, optional = true }
|
clap_complete = { workspace = true, optional = true }
|
||||||
|
shadow-core = { path = "src/shadow-core" }
|
||||||
|
rustix = { workspace = true }
|
||||||
|
|
||||||
# Tool crates (optional, enabled by features)
|
# Tool crates (optional, enabled by features)
|
||||||
passwd = { optional = true, version = "0.2.0", package = "uu_passwd", path = "src/uu/passwd" }
|
passwd = { optional = true, version = "0.2.0", package = "uu_passwd", path = "src/uu/passwd" }
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ fn main() -> ExitCode {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.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")
|
// Direct invocation via symlink (e.g., argv[0] = "passwd")
|
||||||
if let Some(code) = dispatch(&binary_name, &args) {
|
if let Some(code) = dispatch(&binary_name, &args) {
|
||||||
return to_exit_code(code);
|
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>> {
|
pub fn lookup_username(uid: u32) -> io::Result<Option<String>> {
|
||||||
Ok(getpwuid(uid)?.map(|e| e.name))
|
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