mirror of
https://github.com/uutils/shadow.git
synced 2026-10-16 02:48:14 -07:00
* docs: the upstream suite is not GNU, and is mostly BSD-licensed The README called the original shadow-utils a GNU project and GPL-2.0+, and CONTRIBUTING grounded the clean-room policy on that. Neither holds, as #320 points out: shadow-maint/shadow is not a GNU project, and its tree is mostly BSD-3-Clause with GPL-2.0-or-later files among it -- su.c, vipw.c, contributed scripts, some translated manuals. Checked against Debian's copyright file for the package and Fedora's licence tag, BSD-3-Clause AND GPL-2.0-or-later, not against the source. The policy stands, on the accurate ground: this is an independent MIT implementation that copies from neither part. Copying from the BSD part would carry attribution terms this project does not take on; from the GPL part, a derivative work; sorting files by licence would mean reading them. So the rule is unchanged: do not read that tree. Every "GNU" that meant upstream is now "upstream" or "the distribution's package": README, CONTRIBUTING, Makefile, the man pages' section heading, the e2e image and suite, test comments. make test-gnu-compat is make test-upstream-compat and its script is renamed with it. The 2026-05-04 audit record gets an erratum at the top and is otherwise left as it was run. * docs: the last upstream-as-GNU mentions in the compat suite and the tool table * tests: say upstream, not GNU, in the test comments and skip messages too * tests: one more * docs: keep the README's clean-room line as it was, reworded around it
219 lines
10 KiB
Makefile
219 lines
10 KiB
Makefile
PREFIX ?= /usr/local
|
|
BINDIR ?= $(PREFIX)/bin
|
|
SBINDIR ?= $(PREFIX)/sbin
|
|
|
|
# Tools that need setuid-root to allow non-root callers (change own password,
|
|
# GECOS, shell, effective group, or administer a group as a group admin).
|
|
SETUID_TOOLS = passwd chfn chsh newgrp gpasswd sg newuidmap newgidmap
|
|
|
|
# Root-only tools (no setuid; fail at getuid() check for non-root callers).
|
|
ROOT_TOOLS = useradd userdel usermod chpasswd chgpasswd newusers \
|
|
groupadd groupdel groupmod pwck grpck vipw vigr \
|
|
pwconv pwunconv grpconv grpunconv
|
|
|
|
# Tools an ordinary user runs, and which therefore go in bin rather than sbin:
|
|
# sbin is not on a normal user's PATH, so `passwd` there is `command not
|
|
# found`. This is where upstream shadow puts them -- verified against the Debian
|
|
# package, which ships passwd, chage, chfn, chsh and newgrp in /usr/bin and
|
|
# everything else in /usr/sbin.
|
|
# login lives in bin, and is root-only without being setuid: getty runs it as
|
|
# root, and it refuses to run as anyone else.
|
|
USER_BIN_TOOLS = login
|
|
# Tools a user runs on their own account that have to read /etc/shadow for
|
|
# it: expiry reads the caller's line to judge it, chage -l reads it to print
|
|
# it. Neither writes anything for a caller who is not root -- both check the
|
|
# real uid -- so the upstream suite ships them setgid shadow, enough to read the
|
|
# file and no more.
|
|
SETGID_SHADOW_TOOLS = expiry chage
|
|
|
|
# The group allowed to read /etc/shadow. Debian and Ubuntu have one, `shadow`,
|
|
# and that is what the tools above are made setgid to. Fedora and Arch have no
|
|
# such group: /etc/shadow there is readable by root alone, and the upstream tool
|
|
# that reads it on a user's behalf, chage, is setuid root instead. Where the
|
|
# group does not exist at install time -- those systems, or a packager's
|
|
# chroot -- the tools above follow that convention and are installed setuid
|
|
# root; they check the caller's real uid, so the extra privilege buys a user
|
|
# nothing beyond reading their own line. Packagers whose target has the group
|
|
# under another name set SHADOW_GROUP.
|
|
SHADOW_GROUP ?= shadow
|
|
ifneq ($(shell getent group '$(SHADOW_GROUP)' >/dev/null 2>&1 && echo yes),)
|
|
SETGID_INSTALL = install -Dm2755 -g '$(SHADOW_GROUP)'
|
|
SETGID_LABEL = setgid $(SHADOW_GROUP) (2755)
|
|
else
|
|
SETGID_INSTALL = install -Dm4755
|
|
SETGID_LABEL = setuid (4755; no group '$(SHADOW_GROUP)' on this system)
|
|
endif
|
|
USER_TOOLS = $(SETUID_TOOLS) $(USER_BIN_TOOLS) $(SETGID_SHADOW_TOOLS)
|
|
|
|
ALL_TOOLS = $(SETUID_TOOLS) $(ROOT_TOOLS) $(USER_BIN_TOOLS) $(SETGID_SHADOW_TOOLS)
|
|
|
|
.PHONY: all build build-multicall build-arm64 dist-musl check test test-upstream-compat test-unprivileged test-arm64 verify-release install install-multicall uninstall clean
|
|
|
|
all: build
|
|
|
|
# `pam` is not a default cargo feature, so a plain build produces a `passwd`
|
|
# that refuses the interactive change path ("PAM support is not compiled in").
|
|
# Installed binaries must have it; the PAM headers are already listed as a
|
|
# build requirement in the README.
|
|
build:
|
|
cargo build --release --workspace --bins --exclude uu_shadow \
|
|
--features uu_passwd/pam,uu_chfn/pam,uu_chsh/pam,uu_login/pam,uu_expiry/pam
|
|
|
|
build-multicall:
|
|
cargo build --release --bin shadow-rs --features pam
|
|
|
|
# Static musl archive, published as a release asset next to the glibc one
|
|
# (dist-workspace.toml runs this target; see issue #224). Built without `pam`:
|
|
# Linux-PAM dlopen()s its modules, which a static binary cannot do, and
|
|
# shadow-core refuses the combination at compile time. The archive name carries
|
|
# the "-static" label and docs/PLATFORM-SUPPORT.md, shipped inside, spells out
|
|
# what the build does and does not do.
|
|
MUSL_TARGET = x86_64-unknown-linux-musl
|
|
MUSL_ARCHIVE = uu_shadow-$(MUSL_TARGET)-static
|
|
MUSL_DIST_DIR = target/dist-musl
|
|
|
|
dist-musl:
|
|
rustup target add $(MUSL_TARGET)
|
|
cargo build --release --locked --bin shadow-rs --target $(MUSL_TARGET)
|
|
@# A DT_NEEDED entry would mean a shared object slipped in and the archive
|
|
@# is not the self-contained binary its name promises.
|
|
@if readelf -d target/$(MUSL_TARGET)/release/shadow-rs | grep -q NEEDED; then \
|
|
echo "error: shadow-rs is not statically linked" >&2; exit 1; \
|
|
fi
|
|
rm -rf $(MUSL_DIST_DIR)
|
|
mkdir -p $(MUSL_DIST_DIR)/$(MUSL_ARCHIVE)
|
|
cp target/$(MUSL_TARGET)/release/shadow-rs LICENSE README.md CHANGELOG.md \
|
|
docs/PLATFORM-SUPPORT.md $(MUSL_DIST_DIR)/$(MUSL_ARCHIVE)/
|
|
tar -C $(MUSL_DIST_DIR) --owner=0 --group=0 --numeric-owner \
|
|
-czf $(MUSL_DIST_DIR)/$(MUSL_ARCHIVE).tar.gz $(MUSL_ARCHIVE)
|
|
cd $(MUSL_DIST_DIR) && sha256sum $(MUSL_ARCHIVE).tar.gz > $(MUSL_ARCHIVE).tar.gz.sha256
|
|
@echo "Built $(MUSL_DIST_DIR)/$(MUSL_ARCHIVE).tar.gz"
|
|
|
|
# Everything CI gates on, in one place, so the README, CONTRIBUTING, the git
|
|
# hooks and ci.yml stop each carrying their own copy of the command list.
|
|
# Run it inside a container: docker compose run --rm debian make check
|
|
check:
|
|
cargo fmt --all --check
|
|
cargo clippy --workspace --all-targets -- -D warnings
|
|
cargo clippy --workspace --all-targets --features pam -- -D warnings
|
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
|
$(MAKE) test
|
|
$(MAKE) test-unprivileged
|
|
|
|
# `install` ships binaries built with pam, so the tests must cover that build
|
|
# as well as the default one: the feature changes which code paths exist.
|
|
test:
|
|
cargo test --workspace
|
|
cargo test --workspace --features pam
|
|
|
|
ARM64_TARGET = aarch64-unknown-linux-gnu
|
|
ARM64_BIN = target/$(ARM64_TARGET)/release/shadow-rs
|
|
|
|
# Cross-build for arm64 and check the result actually runs.
|
|
#
|
|
# The release publishes an arm64 archive; nothing used to execute one, so a
|
|
# broken arm64 binary would reach a user before anyone ran it. The toolchain is
|
|
# installed here rather than baked into the image: it is 32 packages, and every
|
|
# other job would carry them for a check that is run occasionally.
|
|
build-arm64:
|
|
dpkg --add-architecture arm64
|
|
apt-get update
|
|
apt-get install -y --no-install-recommends \
|
|
gcc-aarch64-linux-gnu libpam0g-dev:arm64 libcrypt-dev:arm64 qemu-user-static
|
|
rustup target add $(ARM64_TARGET)
|
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
|
cargo build --release --locked --target $(ARM64_TARGET) --bin shadow-rs --features pam
|
|
|
|
# Runs under qemu user-mode emulation, which is enough to catch what actually
|
|
# differs across architectures: integer widths, struct layouts, and the crypt
|
|
# and PAM FFI boundaries.
|
|
test-arm64: build-arm64
|
|
ARM64_BIN=$(ARM64_BIN) bash tests/arm64-smoke.sh
|
|
|
|
# Run the archives of a published release the way a user will, before the
|
|
# release is announced: make verify-release TAG=0.5.0. Runs on the host, since
|
|
# it drives docker (debian:12 for the glibc archive, alpine for the static
|
|
# one, the debian image with qemu for arm64); needs `gh` logged in.
|
|
verify-release:
|
|
@test -n "$(TAG)" || { echo "usage: make verify-release TAG=<version>" >&2; exit 2; }
|
|
bash tests/verify-release.sh $(TAG)
|
|
|
|
# Run the suite as an unprivileged user.
|
|
#
|
|
# Every container in docker-compose.yml runs as root, so a test that silently
|
|
# assumes root passes here and fails in CI, where one job runs as an ordinary
|
|
# user -- and a test asserting a failure exit code passes for the wrong reason,
|
|
# because "permission denied" is also a failure. This target is that job,
|
|
# locally. The binaries are already built, so it only re-runs them.
|
|
#
|
|
# SHADOW_TEST_REQUIRE_ROOT is cleared deliberately: it exists to turn a skip
|
|
# into a failure when the suite *is* running as root, which is the opposite of
|
|
# what this target does.
|
|
UNPRIV_USER = shadowtest
|
|
|
|
test-unprivileged:
|
|
cargo test --workspace --no-run 2>&1 \
|
|
| sed -n 's/.*(\(target\/debug\/deps\/[^)]*\))$$/\1/p' > /tmp/test-binaries
|
|
@id -u $(UNPRIV_USER) >/dev/null 2>&1 || useradd -m $(UNPRIV_USER)
|
|
@chmod -R a+rX target
|
|
@fail=0; while read -r bin; do \
|
|
printf '== %s\n' "$$bin"; \
|
|
su $(UNPRIV_USER) -s /bin/sh -c "SHADOW_TEST_REQUIRE_ROOT= $(CURDIR)/$$bin" \
|
|
|| fail=1; \
|
|
done < /tmp/test-binaries; \
|
|
exit $$fail
|
|
|
|
# Compare our output and exit codes against the upstream tools installed
|
|
# alongside. Needs root and the distribution's shadow package, so it belongs
|
|
# in a container.
|
|
test-upstream-compat:
|
|
bash tests/upstream-compat.sh
|
|
|
|
# Default install: 28 standalone per-tool binaries, with the setuid layout and
|
|
# the bin/sbin split the upstream package uses. Only $(SETUID_TOOLS) are setuid.
|
|
install: build
|
|
@for tool in $(SETUID_TOOLS); do \
|
|
install -Dm4755 target/release/$$tool $(DESTDIR)$(BINDIR)/$$tool || exit 1; \
|
|
done
|
|
@for tool in $(USER_BIN_TOOLS); do \
|
|
install -Dm0755 target/release/$$tool $(DESTDIR)$(BINDIR)/$$tool || exit 1; \
|
|
done
|
|
@for tool in $(SETGID_SHADOW_TOOLS); do \
|
|
$(SETGID_INSTALL) target/release/$$tool $(DESTDIR)$(BINDIR)/$$tool || exit 1; \
|
|
done
|
|
@for tool in $(ROOT_TOOLS); do \
|
|
install -Dm0755 target/release/$$tool $(DESTDIR)$(SBINDIR)/$$tool || exit 1; \
|
|
done
|
|
@echo "Installed $(words $(ALL_TOOLS)) standalone binaries"
|
|
@echo " $(DESTDIR)$(BINDIR)/ setuid (4755): $(SETUID_TOOLS)"
|
|
@echo " $(DESTDIR)$(BINDIR)/ user (0755): $(USER_BIN_TOOLS)"
|
|
@echo " $(DESTDIR)$(BINDIR)/ $(SETGID_LABEL): $(SETGID_SHADOW_TOOLS)"
|
|
@echo " $(DESTDIR)$(SBINDIR)/ root (0755): $(ROOT_TOOLS)"
|
|
|
|
# Opt-in install: single multicall binary with symlinks. Smaller footprint.
|
|
# The binary is installed setuid-root for $(SETUID_TOOLS); the other applets
|
|
# drop back to the caller's uid before running, so the privilege model matches
|
|
# the per-tool layout. Intended for container/embedded use.
|
|
install-multicall: build-multicall
|
|
install -Dm4755 target/release/shadow-rs $(DESTDIR)$(SBINDIR)/shadow-rs
|
|
@install -d $(DESTDIR)$(BINDIR)
|
|
@for tool in $(USER_TOOLS); do \
|
|
ln -sf $(SBINDIR)/shadow-rs $(DESTDIR)$(BINDIR)/$$tool || exit 1; \
|
|
done
|
|
@for tool in $(ROOT_TOOLS); do \
|
|
ln -sf shadow-rs $(DESTDIR)$(SBINDIR)/$$tool || exit 1; \
|
|
done
|
|
@echo "Installed multicall shadow-rs to $(DESTDIR)$(SBINDIR)/ with"
|
|
@echo " $(words $(USER_TOOLS)) symlinks in $(DESTDIR)$(BINDIR)/: $(USER_TOOLS)"
|
|
@echo " $(words $(ROOT_TOOLS)) symlinks in $(DESTDIR)$(SBINDIR)/: $(ROOT_TOOLS)"
|
|
|
|
uninstall:
|
|
@for tool in $(ALL_TOOLS); do \
|
|
rm -f $(DESTDIR)$(BINDIR)/$$tool $(DESTDIR)$(SBINDIR)/$$tool; \
|
|
done
|
|
rm -f $(DESTDIR)$(BINDIR)/shadow-rs $(DESTDIR)$(SBINDIR)/shadow-rs
|
|
@echo "Uninstalled shadow-rs from $(DESTDIR)$(BINDIR)/ and $(DESTDIR)$(SBINDIR)/"
|
|
|
|
clean:
|
|
cargo clean
|