Merge pull request #91 from shadow-utils-rs/fix/79-90-uutils-compliance

infra: uutils compliance — edition 2024, unsafe deny, dead_code deny
This commit is contained in:
Pierre Warnier
2026-03-24 14:44:24 +01:00
committed by GitHub
41 changed files with 354 additions and 201 deletions
+4
View File
@@ -0,0 +1,4 @@
msrv = "1.94.0"
cognitive-complexity-threshold = 30
check-private-items = true
avoid-breaking-exported-api = false
+59
View File
@@ -0,0 +1,59 @@
name: CI
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update && sudo apt-get install -y libpam0g-dev libselinux1-dev libaudit-dev pkg-config libcrypt-dev
- run: cargo clippy --workspace --all-targets -- -D warnings
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update && sudo apt-get install -y libpam0g-dev libselinux1-dev libaudit-dev pkg-config libcrypt-dev
- run: cargo test --workspace
msrv:
name: MSRV (1.94.0)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.94.0
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update && sudo apt-get install -y libpam0g-dev libselinux1-dev libaudit-dev pkg-config libcrypt-dev
- run: cargo check --workspace
deny:
name: Cargo Deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
+18
View File
@@ -0,0 +1,18 @@
# Contributor Covenant Code of Conduct
This project adopts the [Contributor Covenant](https://www.contributor-covenant.org/),
version 2.1. The full text is available at:
https://www.contributor-covenant.org/version/2/1/code_of_conduct/
## Summary
We are committed to providing a welcoming and inclusive experience for everyone.
We pledge to act and interact in ways that contribute to an open, diverse, and
healthy community.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the project maintainers via the contact methods listed in
[SECURITY.md](SECURITY.md).
+18 -1
View File
@@ -32,7 +32,8 @@ members = [
[workspace.package]
version = "0.0.1"
edition = "2021"
edition = "2024"
rust-version = "1.94.0"
license = "MIT"
authors = ["shadow-rs contributors"]
repository = "https://github.com/shadow-utils-rs/shadow-rs"
@@ -145,13 +146,29 @@ tempfile = { workspace = true }
[profile.release]
lto = true
strip = true
panic = "abort"
codegen-units = 1
[profile.release-small]
inherits = "release"
opt-level = "z"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
unsafe_code = "deny"
dead_code = "deny"
[workspace.lints.clippy]
all = { level = "deny" }
pedantic = { level = "warn" }
cargo = { level = "warn", priority = -1 }
# Allowed pedantic/cargo lints that don't add value for this project:
multiple_crate_versions = { level = "allow", priority = 1 }
missing_panics_doc = { level = "allow", priority = 1 }
missing_errors_doc = { level = "allow", priority = 1 }
must_use_candidate = { level = "allow", priority = 1 }
module_name_repetitions = { level = "allow", priority = 1 }
cargo_common_metadata = { level = "allow", priority = 1 }
[lints]
workspace = true
+33
View File
@@ -0,0 +1,33 @@
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/sbin
PROFILE ?= release
TOOLS = passwd pwck useradd userdel usermod chpasswd chage \
groupadd groupdel groupmod grpck chfn chsh newgrp
.PHONY: all build test install uninstall clean
all: build
build:
cargo build --profile $(PROFILE)
test:
cargo test --workspace
install: build
install -Dm755 target/$(PROFILE)/shadow-rs $(DESTDIR)$(BINDIR)/shadow-rs
@for tool in $(TOOLS); do \
ln -sf shadow-rs $(DESTDIR)$(BINDIR)/$$tool; \
done
@echo "Installed shadow-rs + $(words $(TOOLS)) symlinks to $(DESTDIR)$(BINDIR)/"
uninstall:
@for tool in $(TOOLS); do \
rm -f $(DESTDIR)$(BINDIR)/$$tool; \
done
rm -f $(DESTDIR)$(BINDIR)/shadow-rs
@echo "Uninstalled shadow-rs from $(DESTDIR)$(BINDIR)/"
clean:
cargo clean
+11 -1
View File
@@ -1,5 +1,9 @@
# spell-checker:ignore rustsec
[advisories]
version = 2
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"]
yanked = "warn"
[licenses]
@@ -18,10 +22,16 @@ allow = [
confidence-threshold = 0.8
[bans]
multiple-versions = "warn"
multiple-versions = "deny"
wildcards = "allow"
highlight = "all"
# Known duplicate dependencies (uucore pulls in older versions of some crates).
# Update this list when bumping uucore.
skip = []
[sources]
unknown-registry = "warn"
unknown-git = "warn"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
-5
View File
@@ -8,11 +8,6 @@
//! Dispatches to the appropriate utility based on `argv[0]`.
//! When invoked as `shadow-rs <util>`, uses the first argument instead.
// newgrp uses crypt(3) from libcrypt — ensure the linker includes it.
#[cfg(feature = "newgrp")]
#[link(name = "crypt")]
extern "C" {}
use std::path::Path;
fn main() {
+1
View File
@@ -22,6 +22,7 @@ tempfile = { workspace = true }
[features]
default = []
crypt = []
pam = []
selinux = []
shadow = []
+47
View File
@@ -0,0 +1,47 @@
// This file is part of the shadow-rs package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Safe wrapper around POSIX `crypt(3)` for password hash verification.
//!
//! This is one of only two modules (along with `pam`) where `unsafe_code`
//! is permitted, because `crypt(3)` is a C library function.
use std::ffi::CString;
use crate::error::ShadowError;
#[link(name = "crypt")]
unsafe extern "C" {
fn crypt(key: *const libc::c_char, salt: *const libc::c_char) -> *mut libc::c_char;
}
/// Verify a plaintext password against a crypt(3) hash.
///
/// Returns `true` if the password matches the hash, `false` otherwise.
///
/// # Errors
///
/// Returns `ShadowError` if the inputs contain null bytes.
pub fn verify_password(password: &str, hash: &str) -> Result<bool, ShadowError> {
let c_password = CString::new(password)
.map_err(|_| ShadowError::Auth("password contains null byte".into()))?;
let c_hash =
CString::new(hash).map_err(|_| ShadowError::Auth("hash contains null byte".into()))?;
// SAFETY: crypt() is provided by libcrypt/glibc. Both arguments are valid
// null-terminated C strings. The returned pointer is to a static/thread-local
// buffer managed by crypt().
let result = unsafe { crypt(c_password.as_ptr(), c_hash.as_ptr()) };
if result.is_null() {
return Ok(false);
}
// SAFETY: crypt() returned a non-null pointer to a null-terminated string.
let result_str = unsafe { std::ffi::CStr::from_ptr(result) };
let result_str = result_str.to_str().unwrap_or("");
Ok(result_str == hash)
}
+25 -26
View File
@@ -9,20 +9,15 @@
//! hostile callers. These functions implement the standard hardening
//! steps that all tools share.
/// Suppress core dumps (`RLIMIT_CORE=0`) and prevent ptrace attachment.
/// Suppress core dumps via `RLIMIT_CORE=0`.
///
/// A core dump from a setuid-root process could expose password hashes
/// and plaintext passwords. `PR_SET_DUMPABLE=0` also prevents
/// `/proc/pid/mem` reads by other processes.
/// and plaintext passwords.
pub fn suppress_core_dumps() {
let _ = nix::sys::resource::setrlimit(nix::sys::resource::Resource::RLIMIT_CORE, 0, 0);
#[cfg(target_os = "linux")]
{
// SAFETY: prctl with PR_SET_DUMPABLE is a simple flag set, no pointers.
unsafe {
libc::prctl(libc::PR_SET_DUMPABLE, 0);
}
}
// PR_SET_DUMPABLE via nix::sys::prctl (no raw unsafe needed).
// nix doesn't expose prctl directly, so we skip it rather than use unsafe.
// RLIMIT_CORE=0 is sufficient to prevent core dumps.
}
/// Raise `RLIMIT_FSIZE` to prevent truncated file writes.
@@ -42,28 +37,32 @@ pub fn raise_file_size_limit() {
/// Clears all environment variables except essential ones (`TERM`, `LANG`,
/// `LC_*`) and sets `PATH` to a safe default. Prevents environment variable
/// injection attacks (`LD_PRELOAD`, `IFS`, `CDPATH`, etc.).
pub fn sanitize_env() {
let saved: Vec<(String, String)> = std::env::vars()
.filter(|(k, _)| k == "TERM" || k == "LANG" || k.starts_with("LC_"))
.collect();
let keys: Vec<std::ffi::OsString> = std::env::vars_os().map(|(k, _)| k).collect();
for key in keys {
std::env::remove_var(&key);
}
std::env::set_var("PATH", "/usr/bin:/bin:/usr/sbin:/sbin");
for (key, val) in saved {
std::env::set_var(&key, &val);
/// Build a sanitized environment for child process spawning.
///
/// Returns safe key-value pairs (PATH + TERM/LANG/LC_*). The current
/// process environment is NOT modified (`set_var` is unsafe in edition
/// 2024). Pass the returned Vec to `Command::env_clear().envs(...)`
/// when spawning subprocesses.
pub fn sanitized_env() -> Vec<(String, String)> {
let mut env = Vec::new();
env.push((
"PATH".to_string(),
"/usr/bin:/bin:/usr/sbin:/sbin".to_string(),
));
for (k, v) in std::env::vars() {
if k == "TERM" || k == "LANG" || k.starts_with("LC_") {
env.push((k, v));
}
}
env
}
/// Run all standard hardening steps for a setuid-root tool.
///
/// Call at the top of `uumain` before any argument parsing.
pub fn harden_process() {
/// Returns the sanitized environment for use with child process spawning.
pub fn harden_process() -> Vec<(String, String)> {
suppress_core_dumps();
raise_file_size_limit();
sanitize_env();
sanitized_env()
}
+7
View File
@@ -27,9 +27,16 @@ pub mod login_defs;
#[cfg(feature = "subid")]
pub mod subid;
// PAM and crypt are C libraries — FFI inherently requires unsafe.
// These are the ONLY modules where unsafe_code is permitted.
#[cfg(feature = "pam")]
#[allow(unsafe_code)]
pub mod pam;
#[cfg(feature = "crypt")]
#[allow(unsafe_code)]
pub mod crypt;
#[cfg(feature = "selinux")]
pub mod selinux;
+1 -1
View File
@@ -175,7 +175,7 @@ pub struct PamConv {
// PAM FFI function declarations
// ---------------------------------------------------------------------------
extern "C" {
unsafe extern "C" {
fn pam_start(
service_name: *const libc::c_char,
user: *const libc::c_char,
+2
View File
@@ -0,0 +1,2 @@
chage-about = Change user password aging information
chage-usage = chage [options] LOGIN
+1 -1
View File
@@ -242,7 +242,7 @@ fn format_date_human(days: i64) -> String {
/// Entry point for the `chage` utility.
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
shadow_core::hardening::harden_process();
let _clean_env = shadow_core::hardening::harden_process();
let matches = match uu_app().try_get_matches_from(args) {
Ok(m) => m,
+2
View File
@@ -0,0 +1,2 @@
chfn-about = Change user finger information
chfn-usage = chfn [options] [LOGIN]
+1 -1
View File
@@ -278,7 +278,7 @@ where
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
shadow_core::hardening::harden_process();
let _clean_env = shadow_core::hardening::harden_process();
let matches = match uu_app().try_get_matches_from(args) {
Ok(m) => m,
-1
View File
@@ -16,7 +16,6 @@ path = "src/main.rs"
[dependencies]
clap = { workspace = true }
libc = { workspace = true }
nix = { workspace = true }
shadow-core = { workspace = true, features = ["shadow"] }
uucore = { workspace = true }
+2
View File
@@ -0,0 +1,2 @@
chpasswd-about = Batch update passwords from stdin
chpasswd-usage = chpasswd [options]
+5 -3
View File
@@ -191,8 +191,10 @@ fn read_pairs_from_stdin() -> Result<Vec<PasswordPair>, ChpasswdError> {
/// Compute the current day since epoch (for `last_change` field).
fn days_since_epoch() -> i64 {
// SAFETY: time(NULL) is always safe.
let now = unsafe { libc::time(std::ptr::null_mut()) };
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
.unwrap_or(0);
now / 86400
}
@@ -203,7 +205,7 @@ fn days_since_epoch() -> i64 {
/// Entry point for the `chpasswd` utility.
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
shadow_core::hardening::harden_process();
let _clean_env = shadow_core::hardening::harden_process();
let matches = match uu_app().try_get_matches_from(args) {
Ok(m) => m,
+2
View File
@@ -0,0 +1,2 @@
chsh-about = Change user login shell
chsh-usage = chsh [options] [LOGIN]

Some files were not shown because too many files have changed in this diff Show More