Skip to content

make install: default to standalone per-tool binaries (#138)#139

Merged
pierre-warnier merged 3 commits into
mainfrom
feat/138-standalone-binaries-install
Apr 16, 2026
Merged

make install: default to standalone per-tool binaries (#138)#139
pierre-warnier merged 3 commits into
mainfrom
feat/138-standalone-binaries-install

Conversation

@pierre-warnier

Copy link
Copy Markdown
Collaborator

Summary

Switch the default make install to build and install 14 standalone per-tool binaries with a least-privilege setuid layout matching GNU shadow-utils. Raised by @oech3 in uutils/coreutils#11828.

Fixes #138.

The problem

The old layout installed one multicall shadow-rs binary with 14 symlinks, then chmod 4755 on the passwd/chfn/chsh/newgrp symlinks. Because chmod on a symlink follows through to the ELF target, the underlying binary ended up setuid root — so useradd, usermod, userdel, etc. all ran with `euid=0` when invoked via symlink. Each tool's internal `getuid() == 0` check was defense-in-depth, not OS-level least privilege.

The fix

`make install` now builds each tool as its own binary (each crate already has a `[[bin]]` entry) and installs them individually:

  • `4755` setuid: `passwd`, `chfn`, `chsh`, `newgrp` (4 tools)
  • `0755` root-only: `useradd`, `userdel`, `usermod`, `chpasswd`, `chage`, `groupadd`, `groupdel`, `groupmod`, `pwck`, `grpck` (10 tools)

This matches GNU shadow-utils' install layout exactly.

The previous multicall install is preserved as `make install-multicall` for container/embedded use cases where the ~14× disk savings matter and the enlarged setuid attack surface is acceptable.

Test plan

Verified in Docker (debian image):

  • `make build` produces all 14 binaries in `target/release/`
  • `make install DESTDIR=/tmp/test` installs 14 files with correct modes: `4755` on passwd/chfn/chsh/newgrp, `0755` on the other 10
  • `make install-multicall DESTDIR=/tmp/test` installs 1 binary + 14 symlinks with shadow-rs binary at `4755`
  • `make uninstall` removes all files in both layouts (14 installed → 0 after uninstall)
  • `cargo fmt --check` and `cargo clippy -- -D warnings` still clean (no Rust changes)
  • Pre-push hook: full test suite on debian/alpine/fedora all pass

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.
Copilot AI review requested due to automatic review settings April 15, 2026 12:24

Copilot AI left a comment

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.

Pull request overview

Updates the project’s installation workflow to default to a least-privilege layout by installing 14 standalone tool binaries, avoiding the “setuid-on-multicall-ELF” problem caused by chmod-following symlinks. Keeps the previous multicall + symlinks install as an opt-in target for footprint-sensitive environments.

Changes:

  • Change make install to build/install 14 per-tool binaries and apply setuid only to passwd, chfn, chsh, newgrp.
  • Add make install-multicall (and build-multicall) to preserve the legacy multicall + symlink installation behavior.
  • Document the new default/alternative install flows in README.md and note the change in CHANGELOG.md.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
README.md Documents default standalone install vs. opt-in multicall install and the security rationale.
Makefile Implements standalone per-tool build/install by default; adds multicall build/install targets.
CHANGELOG.md Records the install default change and mentions the multicall alternative target.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Makefile
Comment thread Makefile Outdated
Address review comments on #139:

- build: replace 14-call loop with a single
  `cargo build --workspace --bins --exclude shadow-rs` invocation,
  eliminating per-tool Cargo startup/metadata overhead. Matches the
  uutils/coreutils GNUmakefile pattern.

- install: use `install -Dm4755` / `install -Dm0755` to set mode atomically
  during install instead of post-install chmod. Errors no longer swallowed:
  if install can't set the setuid bit, `make install` now fails loudly.

- install-multicall: same — `install -Dm4755` sets the shadow-rs binary
  setuid in one step.

Copilot AI left a comment

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.

Pull request overview

This PR changes the default make install behavior to install standalone per-tool binaries with least-privilege permissions, reducing the setuid-root footprint compared to the previous multicall+symlink layout.

Changes:

  • Make default install deploy 14 standalone binaries with 4755 only for passwd/chfn/chsh/newgrp and 0755 for the remaining tools.
  • Add opt-in install-multicall (setuid multicall binary + symlinks) and build-multicall targets.
  • Document the new install modes in README.md and record the behavior change in CHANGELOG.md.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
README.md Documents default standalone install and alternative multicall install.
Makefile Implements standalone install by default, adds multicall build/install targets, updates uninstall to remove all tools.
CHANGELOG.md Notes the new default make install layout and the install-multicall fallback.
Comments suppressed due to low confidence (1)

Makefile:57

  • uninstall now removes all standalone tool binaries/symlinks (and shadow-rs), but the status line only says "Uninstalled shadow-rs". This is misleading for the new default standalone layout; consider updating the message to reflect what was actually removed (e.g., tools + multicall binary) or at least make it layout-agnostic.
uninstall:
	@for tool in $(ALL_TOOLS); do \
		rm -f $(DESTDIR)$(BINDIR)/$$tool; \
	done
	rm -f $(DESTDIR)$(BINDIR)/shadow-rs
	@echo "Uninstalled shadow-rs from $(DESTDIR)$(BINDIR)/"

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Makefile
Comment thread Makefile Outdated
Comment thread README.md Outdated
Address Copilot review round 2:

- Remove PROFILE variable and hardcode --release / target/release/.
  Cargo maps "dev" profile to target/debug/ (not target/dev/), so
  PROFILE=dev would silently break install. Nobody installs debug
  builds of setuid-root tools.

- Fix stale comments in Makefile and README that referenced the old
  "chmod on symlink follows through" approach. The current code uses
  install -Dm4755 directly.
@pierre-warnier pierre-warnier requested a review from Copilot April 16, 2026 08:48
@pierre-warnier pierre-warnier merged commit 916d34e into main Apr 16, 2026
8 checks passed
@pierre-warnier pierre-warnier deleted the feat/138-standalone-binaries-install branch April 16, 2026 08:50

Copilot AI left a comment

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.

Pull request overview

Updates the install workflow to default to per-tool standalone binaries so setuid is limited to the small subset of tools that require it, reducing the privilege footprint compared to the multicall + symlink approach.

Changes:

  • Change make install to build/install 14 standalone tool binaries with setuid only on passwd/chfn/chsh/newgrp.
  • Add build-multicall / install-multicall targets to preserve the multicall + symlink installation as an opt-in.
  • Document the new default and multicall alternative in README and CHANGELOG.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
README.md Documents default standalone install vs opt-in multicall install.
Makefile Implements standalone install as default; adds multicall build/install targets.
CHANGELOG.md Notes the change in default install behavior and availability of multicall install.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread README.md
Comment thread Makefile
Comment thread Makefile
Comment thread CHANGELOG.md
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.

make install: default to standalone per-tool binaries (least-privilege setuid layout)

3 participants