mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
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:
@@ -0,0 +1,4 @@
|
||||
msrv = "1.94.0"
|
||||
cognitive-complexity-threshold = 30
|
||||
check-private-items = true
|
||||
avoid-breaking-exported-api = false
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -22,6 +22,7 @@ tempfile = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
crypt = []
|
||||
pam = []
|
||||
selinux = []
|
||||
shadow = []
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
chage-about = Change user password aging information
|
||||
chage-usage = chage [options] LOGIN
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
chfn-about = Change user finger information
|
||||
chfn-usage = chfn [options] [LOGIN]
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
chpasswd-about = Batch update passwords from stdin
|
||||
chpasswd-usage = chpasswd [options]
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user