diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml
new file mode 100644
index 0000000..e432ed8
--- /dev/null
+++ b/.github/workflows/audit.yml
@@ -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 }}
diff --git a/Cargo.toml b/Cargo.toml
index 7955976..19d0198 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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" }
diff --git a/src/bin/shadow-rs.rs b/src/bin/shadow-rs.rs
index 9ddd65e..13ba322 100644
--- a/src/bin/shadow-rs.rs
+++ b/src/bin/shadow-rs.rs
@@ -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);
diff --git a/src/shadow-core/src/process.rs b/src/shadow-core/src/process.rs
index 95dc54b..9ef24a3 100644
--- a/src/shadow-core/src/process.rs
+++ b/src/shadow-core/src/process.rs
@@ -265,3 +265,42 @@ pub fn getpwuid(uid: u32) -> io::Result