Skip to content

shadow-core: read AT_EXECFN through getauxval, not a run-time lookup - #302

Merged
pierre-warnier merged 1 commit into
mainfrom
fix/static-musl-execfn
Sep 7, 2026
Merged

pierre-warnier merged 1 commit into
mainfrom
fix/static-musl-execfn

Conversation

@pierre-warnier

Copy link
Copy Markdown
Collaborator

The static musl archive aborted every setuid invocation with argv[0] does not match executed binary. Deployed as make install-multicall deploys it, setuid root and reached through symlinks, no user could run passwd, chfn, chsh, newgrp, sg or gpasswd; root, for whom the check does not run, saw nothing wrong. Every static archive published so far, 0.2.2 through 0.4.0, has the defect. The glibc archives never did.

Cause. The check compared the basename of argv[0] with AT_EXECFN obtained from rustix's param module. rustix reaches getauxval(3) by name at run time, through dlsym(3), so that it can tolerate a glibc older than 2.16. In a statically linked binary a symbol is present only if something links it in, and a lookup by name links nothing in: the archive carried no getauxval at all, rustix returned an empty string, and the tool's name never matched it. Confirmed by instrumenting the abort (argv0="passwd" execfn="") and by a probe binary that calls libc::getauxval directly, which made rustix's own lookup succeed too because the symbol was then linked.

Fix. verify_argv0_matches_execfn calls libc::getauxval(AT_EXECFN) directly. That links the symbol and needs no lookup. glibc 2.34, the floor of the glibc archives, and musl both provide it. A null result still fails closed.

Verified on the rebuilt static archive in an Alpine 3.23 container, installed 4755 root with symlinks, as an unprivileged user:

passwd -S                    -> fresh P 2026-09-07 0 99999 7 -1   (exit 0)
exec -a chsh passwd -S       -> argv[0] does not match executed binary (exit 1, still refused)
expiry -c, fresh account     -> silent, exit 0
expiry -c, expired account   -> refused, exit 1
expiry -f, expired password  -> PAM notice pointing at passwd(1), exit 1
sg fresh -c id               -> uid=1000(fresh) gid=1000(fresh) (exit 0)

CI. The Static musl archive job now installs the archive setuid root, links passwd to it and runs passwd -S as nobody. Nothing exercised the static binary in setuid context before; the whole suite runs as root, where the check is skipped.

Also silences, with the reason, the libc crate's musl deprecation warnings on the utmp calls: stubs are the documented behaviour of the static build.

The static musl archive aborted every setuid invocation with "argv[0]
does not match executed binary". Deployed as `make install-multicall`
deploys it, setuid root and reached through symlinks, no user could run
passwd, chfn, chsh, newgrp, sg or gpasswd; root, for whom the check does
not run, saw nothing wrong. Every static archive published so far, 0.2.2
through 0.4.0, has the defect. The glibc archives never did.

The check compared the basename of argv[0] with AT_EXECFN obtained from
rustix's `param` module. rustix reaches getauxval(3) by name at run
time, through dlsym(3), so that it can tolerate a glibc older than 2.16.
In a statically linked binary a symbol is present only if something
links it in, and a lookup by name links nothing in: the archive carried
no getauxval at all, rustix returned an empty string, and the tool's
name never matched it. Instrumenting the abort showed argv0="passwd"
against execfn=""; a probe that calls libc::getauxval directly made
rustix's own lookup succeed too, the symbol being linked by then.

verify_argv0_matches_execfn now calls libc::getauxval(AT_EXECFN)
itself, which links the symbol and needs no lookup. glibc 2.34, the
floor of the glibc archives, and musl both provide it. A null result
still fails closed.

Verified on the rebuilt archive in an Alpine container, installed 4755
root with symlinks, as an unprivileged user: passwd -S answers, a
spoofed argv[0] (exec -a chsh passwd) is still refused, expiry -c is
silent on a fresh account and refuses an expired one, expiry -f points
at passwd(1), sg runs its command.

The Static musl archive CI job now installs the archive setuid root,
links passwd to it and runs passwd -S as nobody. Nothing exercised the
static binary in setuid context before: the whole suite runs as root,
where the check is skipped.

Also silences, with the reason, the libc crate's musl deprecation
warnings on the utmp calls: stubs are the documented behaviour of the
static build.
@pierre-warnier
pierre-warnier merged commit b94a012 into main Sep 7, 2026
13 checks passed
@pierre-warnier pierre-warnier mentioned this pull request Sep 7, 2026
@pierre-warnier
pierre-warnier deleted the fix/static-musl-execfn branch September 7, 2026 16:10
@oech3

oech3 commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

@oech3

oech3 commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Actually, rustix has MSRV 1.65 which is enought to drop weak!.

@pierre-warnier

Copy link
Copy Markdown
Collaborator Author

Good point, that's better than special-casing musl. Since Rust 1.64 the Linux targets require glibc 2.17, and getauxval is in glibc since 2.16. So with rustix's MSRV at 1.65, weak! can go and getauxval can be linked directly on every target here. That also fixes the static musl case, where the run-time lookup finds nothing.

We don't depend on it any more, since this PR calls libc::getauxval(AT_EXECFN) directly. Thanks for taking it upstream.

@oech3

oech3 commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

It would be still better to drop unsafe from our code. We could send a PR to rustix, but recient rustix's update cycle is very mild.

@pierre-warnier

Copy link
Copy Markdown
Collaborator Author

Agreed, and it turns out we don't have to wait for rustix: #402 drops the unsafe block.

The empty value came from use-libc-auxv, which uucore 0.11 used to turn on. Since uucore 0.12, nothing in our tree enables it, so rustix reads the auxv itself, and linux_execfn() is correct on the static musl archive too. I checked that installed setuid root, run by an unprivileged user, including with PR_GET_AUXV blocked. #402 also bans that feature in deny.toml, so it can't come back silently.

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