From 698eaf7cf3dcaf40281015dd6be9f4787606b9b8 Mon Sep 17 00:00:00 2001 From: Pierre Warnier Date: Wed, 15 Apr 2026 14:19:21 +0200 Subject: [PATCH] make install: default to standalone per-tool binaries (#138) Switch the default install to build and install 14 standalone per-tool binaries with a least-privilege setuid layout matching GNU shadow-utils. Only passwd/chfn/chsh/newgrp are setuid-root; the other 10 tools are 0755. The previous multicall install is preserved as `make install-multicall` for container/embedded use cases where the ~14x disk savings matter and the enlarged setuid attack surface is acceptable. The old layout chmod'd symlinks pointing to the multicall binary, which (because chmod follows symlinks) marked the underlying ELF setuid-root. That meant all 14 tools ran with euid=0 when invoked via symlink. Each tool's internal getuid() check was defense-in-depth, not OS-level least privilege. The standalone layout gives the latter. Raised by @oech3 in uutils/coreutils#11828. --- CHANGELOG.md | 4 ++++ Makefile | 52 +++++++++++++++++++++++++++++++++++++++------------- README.md | 19 +++++++++++++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbe1f2..0437948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Cargo.toml metadata aligned with uutils ecosystem conventions - Tool crate descriptions normalized to `"tool ~ (shadow-rs) verb phrase"` format - Edition 2024 consistently applied across root and workspace packages +- `make install` now defaults to 14 standalone per-tool binaries with + least-privilege setuid layout matching GNU shadow-utils (#138). Only + `passwd`/`chfn`/`chsh`/`newgrp` are setuid-root; the other 10 are `0755`. + The previous multicall install is available as `make install-multicall`. ### Fixed diff --git a/Makefile b/Makefile index ce854d1..0db1075 100644 --- a/Makefile +++ b/Makefile @@ -2,34 +2,60 @@ PREFIX ?= /usr/local BINDIR ?= $(PREFIX)/sbin PROFILE ?= release -TOOLS = passwd pwck useradd userdel usermod chpasswd chage \ - groupadd groupdel groupmod grpck chfn chsh newgrp +# Tools that need setuid-root to allow non-root callers (change own password, +# GECOS, shell, effective group). +SETUID_TOOLS = passwd chfn chsh newgrp -.PHONY: all build test install uninstall clean +# Root-only tools (no setuid; fail at getuid() check for non-root callers). +ROOT_TOOLS = useradd userdel usermod chpasswd chage \ + groupadd groupdel groupmod pwck grpck + +ALL_TOOLS = $(SETUID_TOOLS) $(ROOT_TOOLS) + +.PHONY: all build build-multicall test install install-multicall uninstall clean all: build build: - cargo build --profile $(PROFILE) + @for tool in $(ALL_TOOLS); do \ + cargo build --profile $(PROFILE) -p uu_$$tool --bin $$tool || exit 1; \ + done + +build-multicall: + cargo build --profile $(PROFILE) --bin shadow-rs test: cargo test --workspace +# Default install: 14 standalone per-tool binaries with least-privilege setuid +# layout matching GNU shadow-utils. Only passwd/chfn/chsh/newgrp are setuid. install: build - install -Dm755 target/$(PROFILE)/shadow-rs $(DESTDIR)$(BINDIR)/shadow-rs - @for tool in $(TOOLS); do \ - ln -sf shadow-rs $(DESTDIR)$(BINDIR)/$$tool; \ + @for tool in $(ALL_TOOLS); do \ + install -Dm755 target/$(PROFILE)/$$tool $(DESTDIR)$(BINDIR)/$$tool; \ done - @# Setuid-root for tools that need it. chmod on symlinks affects the - @# underlying multicall binary — this is intentional for drop-in - @# replacement since all traditional shadow-utils tools are setuid-root. - @for tool in passwd chfn chsh newgrp; do \ + @for tool in $(SETUID_TOOLS); do \ chmod 4755 $(DESTDIR)$(BINDIR)/$$tool 2>/dev/null || true; \ done - @echo "Installed shadow-rs + $(words $(TOOLS)) symlinks to $(DESTDIR)$(BINDIR)/" + @echo "Installed $(words $(ALL_TOOLS)) standalone binaries to $(DESTDIR)$(BINDIR)/" + @echo " setuid (4755): $(SETUID_TOOLS)" + @echo " root-only (0755): $(ROOT_TOOLS)" + +# Opt-in install: single multicall binary with symlinks. Smaller footprint but +# larger setuid attack surface — chmod on any setuid symlink follows through to +# the ELF, so all tools end up running with euid=root. Intended for +# container/embedded use where disk savings matter and attack surface does not. +install-multicall: build-multicall + install -Dm755 target/$(PROFILE)/shadow-rs $(DESTDIR)$(BINDIR)/shadow-rs + @for tool in $(ALL_TOOLS); do \ + ln -sf shadow-rs $(DESTDIR)$(BINDIR)/$$tool; \ + done + @for tool in $(SETUID_TOOLS); do \ + chmod 4755 $(DESTDIR)$(BINDIR)/$$tool 2>/dev/null || true; \ + done + @echo "Installed multicall shadow-rs + $(words $(ALL_TOOLS)) symlinks to $(DESTDIR)$(BINDIR)/" uninstall: - @for tool in $(TOOLS); do \ + @for tool in $(ALL_TOOLS); do \ rm -f $(DESTDIR)$(BINDIR)/$$tool; \ done rm -f $(DESTDIR)$(BINDIR)/shadow-rs diff --git a/README.md b/README.md index 379d661..c16d8d2 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,25 @@ docker compose build debian docker compose run --rm debian cargo build --release ``` +### Install + +Default install: 14 standalone per-tool binaries with least-privilege setuid +layout matching GNU shadow-utils. Only `passwd`, `chfn`, `chsh`, `newgrp` are +installed setuid-root; the other 10 are plain `0755`. + +```shell +sudo make install PREFIX=/usr/local +``` + +Alternative: single multicall binary with symlinks. Smaller footprint (~14× +disk savings) but larger setuid attack surface, since `chmod` on a setuid +symlink follows through to the underlying ELF — all 14 tools end up running +with `euid=root` when invoked. Intended for container/embedded use cases. + +```shell +sudo make install-multicall PREFIX=/usr/local +``` + ### Test All builds and tests run inside Docker containers to isolate from the host