Skip to content

shadow-core: read AT_EXECFN through rustix, drop the getauxval unsafe - #402

Closed
pierre-warnier wants to merge 2 commits into
mainfrom
shadow-core-safe-execfn
Closed

pierre-warnier wants to merge 2 commits into
mainfrom
shadow-core-safe-execfn

Conversation

@pierre-warnier

Copy link
Copy Markdown
Collaborator

The argv[0] check in the multicall binary read AT_EXECFN with a direct unsafe call to libc::getauxval. That call went in for #302: the static musl archive refused every setuid invocation because rustix found getauxval by name at run time, and nothing in a static link pulls that symbol in.

The by-name lookup was never rustix's default. It comes from rustix's use-libc-auxv feature, which uucore 0.11 turned on; since uucore 0.12 nothing in the graph enables it. Without it, rustix's linux_raw backend reads the auxiliary vector itself (PR_GET_AUXV, falling back to /proc/self/auxv). See bytecodealliance/rustix#1734. Thanks to @oech3 for suggesting this.

This PR:

  • uses rustix::param::linux_execfn() and removes the unsafe block. An empty value still fails closed. If rustix cannot read the auxiliary vector at all (no PR_GET_AUXV and no /proc), it panics, which is an abort in release builds, so that case fails closed too.
  • bans rustix's use-libc-auxv and use-libc in deny.toml. use-libc switches rustix to its libc backend, which does the same weak lookup. If a dependency ever turns either one back on, cargo deny check bans fails instead of the regression shipping silently.

shadow-core::process keeps #[allow(unsafe_code)]. It still wraps setuid, getpwuid_r, sigprocmask and others.

Evidence:

  • rustix 1.1.6 is locked, and cargo tree -e features shows use-libc-auxv off everywhere.
  • I built a small static musl probe. It returned the correct AT_EXECFN when installed setuid root and run by an unprivileged user, both normally and with PR_GET_AUXV blocked by seccomp (EINVAL, which forces the /proc fallback). With use-libc-auxv on, the same probe returned an empty string, which is the shadow-core: read AT_EXECFN through getauxval, not a run-time lookup #302 failure.
  • Real archive: I built it with make dist-musl and ran it in Alpine, installed 4755 root, reached through /usr/local/bin/passwd -> /usr/local/sbin/shadow-rs, as an unprivileged user. I ran it under the default seccomp profile and again with PR_GET_AUXV blocked. Both times passwd -S printed probe L 2026-11-08 0 99999 7 -1 and exited 0, and exec -a chsh /usr/local/bin/passwd -S was refused with argv[0] does not match executed binary.
  • Ban check: I enabled each feature on rustix in turn. cargo-deny 0.19.8 (our images) and 0.20.2 (the CI action) both report feature-banned and bans FAILED.
  • Debian: cargo fmt --all --check, clippy -D warnings (with and without pam), cargo test -p uu_shadow_core and cargo deny check all pass.

Regression coverage: the existing Static musl archive CI job already installs the archive 4755 root behind a passwd symlink and runs passwd -S as nobody. That is exactly the case #302 broke, so I did not add a second copy of it. tests/verify-release.sh also checks that a spoofed argv[0] is refused.

Reviews welcome. Without objection, this will be merged after 7 days.

Either feature makes rustix look getauxval(3) up by name at run time. In
the static musl binary nothing links that symbol in, so AT_EXECFN reads
as empty and the argv[0] check refuses every setuid invocation (#302,
which uucore 0.11 brought in by enabling use-libc-auxv). Banning both
makes cargo-deny fail if any crate in the graph turns them back on.
With use-libc-auxv off, rustix's linux_raw backend reads the auxiliary
vector itself (PR_GET_AUXV, falling back to /proc/self/auxv), so
rustix::param::linux_execfn is correct in the static musl binary too
and the unsafe getauxval call is no longer needed. An empty value still
fails closed, and rustix panics, an abort in release builds, when it can
read the auxiliary vector neither way.

Suggested by oech3.
@oech3

oech3 commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

You cannot use raw-backend because linux_execfn panics if /proc is not mounted on the legacy kernel prctl is not supported.

> unshare -rm sh -c "mount -t tmpfs tmpfs /proc && strace -o /dev/null -e inject=prctl:error=ENOSYS /tmp/dec/coreutils-0.8.0-x86_64-unknown-linux-musl/coreutils"

thread 'main' panicked at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rustix-1.1.4/src/backend/linux_raw/param/auxv.rs:269:22:
called `Result::unwrap()` on an `Err` value: ()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

@pierre-warnier

Copy link
Copy Markdown
Collaborator Author

@oech3 since this follows your suggestion on #302, a review from you would be welcome.

@oech3

oech3 commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Yes. But we cannot drop it yet. I opened bytecodealliance/rustix#1735 for the future.

I'm not sure if people want to use shadow-utils on the system /proc is not mounted, but uutils/coreutils has use-libc-auxv since it can be used inside of container.

std::ffi::CStr::from_ptr(ptr)
};
let execfn = rustix::param::linux_execfn();
if execfn.is_empty() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if .is_empty() is possible. Maybe, debug_assert!?

if execfn.is_empty() {
return false;
}
let execfn = execfn.to_string_lossy();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not coming from this PR. But can we avoid lossy conversion e.g. as_encoded_bytes?
We need last utf-8 part only (since all util names are utf-8).

@pierre-warnier

Copy link
Copy Markdown
Collaborator Author

@oech3 you're right, thanks for checking. Without /proc and without PR_GET_AUXV, rustix's own reader panics, which is worse for a setuid tool than the getauxval call it replaces. I'm closing this and keeping the direct call for now.

Once bytecodealliance/rustix#1735 is released, we can switch to linux_execfn() with use-libc-auxv and drop the unsafe then. I've opened an issue to track that. The lossy conversion you spotted will get its own PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants