Files
shadow/docker/Dockerfile.fedora
T
Pierre Warnier 4ac1bdfe32 login: begin a session on the system (#297)
* shadow-core: take /etc/.pwd.lock before the per-file lock

FileLock took the per-file `.lock` first and `/etc/.pwd.lock` second. Two
tools working on different files at once could then meet in the classic
shape: one holding passwd.lock and waiting for .pwd.lock, the other holding
shadow.lock and waiting for passwd.lock's holder to let go of .pwd.lock.
Neither can move; both sit out the fifteen-second timeout; both fail -- on a
system that was merely busy.

Sampling the lock files every 250 ms while the whole suite ran showed it
plainly: a chpasswd held shadow.lock for 28 s and a useradd held passwd,
group and gshadow for 14 s, each waiting on the other, and seven tests
failed with "timed out" on one lock or the other. Three runs after the
change: no failures, and the suite back from 35-50 s to 17 s.

.pwd.lock now comes first, the order lckpwdf(3) and the GNU tools use and
the only one that cannot deadlock: whoever holds it is the only process
asking for per-file locks, so it never waits for anyone who is waiting for
it. If the per-file lock then cannot be taken, the .pwd.lock reference is
given back, so nothing is held on failure.

* useradd: lock the shadow file with the others, and write it first

useradd wrote /etc/passwd and the group files in one transaction, then
opened /etc/shadow on its own afterwards -- under a comment claiming the
first locks were still held, which they were not: commit_all releases them.
A shadow lock that timed out at that point left the account half made: a
passwd line with no shadow line, which a second useradd then reported as
already existing. That is exactly what turned up when the login tests put
the suite under load.

The shadow file is now locked with passwd, group and gshadow before
anything is written, and written first, for the reason pwconv writes shadow
first: a failure between the writes must never leave a passwd line whose
hash is nowhere. The old write_shadow_entry helper stays for its unit test
only.

A regression test puts a directory where the shadow file should be and
checks that useradd fails with /etc/passwd untouched. Before this change the
passwd line was written and the failure came after.

* login: begin a session on the system

login is the twenty-fifth tool and the one uutils/login was archived
towards. It is what getty runs on a terminal: it prompts for a name and,
through the `login` PAM service, a password; refuses after LOGIN_RETRIES
failures or LOGIN_TIMEOUT seconds; opens the PAM session and takes its
environment; records the login in utmp and wtmp; hands the terminal to the
user; runs the login shell as the user; and when the shell exits, closes the
session and records the logout.

Which login to match was a question. Debian 13 and Ubuntu 25.10 take login
from util-linux, but every release in service -- Ubuntu 20.04, 22.04 and
24.04, Debian 12 -- ships shadow's; the two were run side by side on real
pseudo-terminals and agree on everything that matters. This implements the
option set they share (-p, -f, -h, name) plus util-linux's -H and --help,
which conflict with nothing. There is no -h for help: -h has been the
remote host since telnetd. -r, the rlogin protocol, is refused. util-linux's
vhangup is not reproduced; shadow's login, which the installed base runs,
does not do it either.

A wrong password and an unknown name get the same words, after the same
delay. -f skips authentication and with it the auth stack, pam_nologin
included, as both tools do. A missing home falls back to / with a notice
when DEFAULT_HOME allows. The shell runs as a child so the session can be
closed afterwards; it gets a clean signal mask, login keeps its own, and
groups, gid and uid are dropped in that order in one pre_exec.

shadow-core grew what login needed and nothing had: pam setcred,
open_session, close_session, user and environment; utmp/wtmp recording;
getpwnam through NSS; a getty-style spawn_with_controlling_tty; spawn_as_user;
and a watchdog. Without the pam feature the applet says PAM is not compiled
in and exits: a login that cannot authenticate is not a login, and the
static musl archive is documented accordingly.

The tests drive the real binary on a pseudo-terminal the way getty would,
answering each prompt once it appears: autologin, the environment with and
without -p, a wrong then a right password, an unknown user, the retry
limit, the missing home, and the terminal handover. Writing them under the
full parallel suite is what exposed the two fixes committed before this
one.

Verified: make check clean with the unprivileged run; 892 tests on alpine
and 882 on fedora; 368 e2e assertions against a real install, including the
password path on a pty; 52 GNU comparisons; 32 arm64 assertions under
emulation.

* docker: give the Fedora image the login PAM service

Fedora ships /etc/pam.d/login in util-linux, which the test image did not
install. With no service file PAM falls back to `other`, which on Fedora
denies everything: `login -f` failed its account check and the password
path was refused before any prompt, so every login test failed there and
only there.

util-linux is now installed, which is what any real Fedora has. The login
tests then run through Fedora's own stack -- pam_selinux, pam_loginuid,
pam_namespace -- and pass, which makes it the second PAM configuration the
session code is exercised against rather than a gap.
2026-09-07 15:45:29 +02:00

58 lines
2.7 KiB
Docker

# shadow-rs development image — Fedora 45
#
# PAM: pam-devel (Linux-PAM)
# SELinux: libselinux-devel — SELinux is ENFORCING by default
# Default shell: /bin/bash
# Shadow-utils package: shadow-utils
# Note: Fedora has different PAM stack config and login.defs defaults than Debian.
# SELinux is enforcing, which affects file context on /etc/passwd, /etc/shadow.
# Pinned: see the note in Dockerfile.debian.
FROM fedora:46
RUN dnf install -y \
gcc \
pam-devel \
# util-linux carries /etc/pam.d/login on Fedora; without it PAM falls back
# to `other`, which denies everything, and login cannot be tested at all.
util-linux \
libselinux-devel \
audit-libs-devel \
pkgconf-pkg-config \
shadow-utils \
&& dnf clean all
# Install Rust via rustup
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
--default-toolchain stable \
--component clippy,rustfmt
ENV PATH="/root/.cargo/bin:${PATH}"
# Two throwaway PAM services so the PAM wrapper can be tested against a stack
# whose answer is known. Without them the only coverage is the end-to-end image
# authenticating a real account, which cannot exercise a *refusal* without
# depending on a wrong password being wrong.
RUN printf 'auth required pam_permit.so\naccount required pam_permit.so\npassword required pam_permit.so\nsession required pam_permit.so\n' > /etc/pam.d/shadow-rs-test-permit \
&& printf 'auth required pam_deny.so\naccount required pam_deny.so\npassword required pam_deny.so\nsession required pam_deny.so\n' > /etc/pam.d/shadow-rs-test-deny
# cargo-deny: pinned, checksum-verified prebuilt binary. The static musl build
# runs on glibc too, so every image shares one asset. Installing the binary
# (instead of `cargo install cargo-deny`) skips a multi-minute source compile.
ARG CARGO_DENY_VERSION=0.19.8
ARG CARGO_DENY_SHA256=70e769ae3872e34d45132b17040859175e11401dc12dddb0303e0b8c7d088f3f
RUN curl -LsSf "https://github.com/EmbarkStudios/cargo-deny/releases/download/${CARGO_DENY_VERSION}/cargo-deny-${CARGO_DENY_VERSION}-x86_64-unknown-linux-musl.tar.gz" -o /tmp/cargo-deny.tar.gz \
&& echo "${CARGO_DENY_SHA256} /tmp/cargo-deny.tar.gz" | sha256sum -c - \
&& tar zxf /tmp/cargo-deny.tar.gz -C /tmp \
&& install -m 0755 "/tmp/cargo-deny-${CARGO_DENY_VERSION}-x86_64-unknown-linux-musl/cargo-deny" /root/.cargo/bin/cargo-deny \
&& rm -rf /tmp/cargo-deny.tar.gz "/tmp/cargo-deny-${CARGO_DENY_VERSION}-x86_64-unknown-linux-musl" \
&& cargo-deny --version
RUN useradd -m -s /bin/bash testuser \
&& useradd -m -s /bin/sh testuser2 \
&& groupadd testgroup \
&& usermod -aG testgroup testuser
WORKDIR /workspace
CMD ["/bin/bash"]