Files
Pierre Warnier 92783375aa review: fix all findings from comprehensive code review
Security fixes:
- newgrp: add initgroups() before execv to reset supplementary groups (H-3)
- pam: use Zeroizing<String> for password buffers on all exit paths (M-5)
- atomic: retry-once on stale tmp file from crashed run (M-3)
- atomic: UmaskGuard opts out of Send/Sync via PhantomData (L-2)
- lock: flush + fsync in write_pid_file before hard_link (L-7)

Deduplication (~300 lines removed):
- Centralize caller_is_root(), current_username(), SignalBlocker in
  shadow_core::hardening — remove 10+5+5 private copies across tools
- Add SignalBlocker to all 6 remaining mutating tools (M-4)
- Remove dead CantSort variant from grpck, fix pwck allow (M-8)
- Remove dead read_passwd from test_chfn (M-8)

Binary cleanup:
- shadow-rs multicall: use ExitCode instead of process::exit (M-6)
- completions: use ExitCode + Result pattern, remove unwrap/exit (H-3)

Documentation:
- crypt: thread-safety warning on public API (H-1)
- shadow: clarify unlock() behavior for * password (H-2)
- validate: document $ rejection deviation from GNU (L-4)
- selinux/audit: note shell-out implementation status (L-6)
- chpasswd/chage tests: TODO for --prefix integration (M-8)
- userdel: document inline skip_unless_root reason (L-9)

Testing:
- Add fuzz targets for group and gshadow parsers (M-7)
- skel: use create_dir for tighter error detection (L-8)
- crypt: compile-time modulo bias assertion (L-1)
2026-04-03 19:27:25 +02:00

166 lines
5.7 KiB
Rust

// 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.
// spell-checker:ignore chpasswd
//! Integration tests for the `chpasswd` utility.
//!
//! Tests that require root are guarded by `common::skip_unless_root()` and run inside
//! Docker CI containers. Non-root tests exercise clap parsing and error paths
//! that do not need privilege.
use std::ffi::OsString;
#[path = "../common/mod.rs"]
mod common;
/// Run `uumain` with the given args, returning the exit code.
fn run(args: &[&str]) -> i32 {
let os_args: Vec<OsString> = args.iter().map(|s| (*s).into()).collect();
chpasswd::uumain(os_args.into_iter())
}
/// Helper to create a temp dir with an `etc/shadow` file.
fn setup_shadow(shadow_content: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("failed to create temp dir");
let etc = dir.path().join("etc");
std::fs::create_dir_all(&etc).expect("failed to create etc dir");
std::fs::write(etc.join("shadow"), shadow_content).expect("failed to write shadow file");
dir
}
/// Read the shadow file content back from a prefix dir.
fn read_shadow(dir: &tempfile::TempDir) -> String {
std::fs::read_to_string(dir.path().join("etc/shadow")).expect("failed to read shadow file")
}
// ---------------------------------------------------------------------------
// Non-root tests — exercise clap parsing and error paths
// ---------------------------------------------------------------------------
#[test]
fn test_help_exits_zero() {
let code = run(&["chpasswd", "--help"]);
assert_eq!(code, 0, "--help should exit 0");
}
#[test]
fn test_invalid_crypt_method_exits_error() {
let code = run(&["chpasswd", "-c", "BOGUS"]);
// clap error for invalid value
assert_ne!(code, 0, "invalid crypt method should exit non-zero");
}
// ---------------------------------------------------------------------------
// Root-only tests — exercise real operations via shadow-core
// ---------------------------------------------------------------------------
//
// TODO(#integration): These tests directly manipulate shadow-core data
// structures instead of calling chpasswd::uumain(). Full end-to-end
// integration via uumain() is not yet feasible because chpasswd only supports
// --root (which performs a real chroot(2) and requires root), not --prefix
// (path-prefix without chroot). Once chpasswd gains a --prefix flag, replace
// these tests with uumain() calls using run(&["chpasswd", "--prefix", ...,
// "-e", ...]) with synthetic files.
#[test]
fn test_batch_password_update() {
if common::skip_unless_root() {
return;
}
let dir =
setup_shadow("alice:$6$oldhash:19500:0:99999:7:::\nbob:$6$oldhash:19500:0:99999:7:::\n");
let shadow_path = dir.path().join("etc/shadow");
// Simulate what chpasswd -e does: update password hashes.
let mut entries =
shadow_core::shadow::read_shadow_file(&shadow_path).expect("failed to read shadow");
for entry in &mut entries {
if entry.name == "alice" {
entry.passwd = "$6$newhash_alice".to_string();
} else if entry.name == "bob" {
entry.passwd = "$6$newhash_bob".to_string();
}
}
let file = std::fs::File::create(&shadow_path).expect("failed to create shadow file");
shadow_core::shadow::write_shadow(&entries, file).expect("failed to write shadow");
let content = read_shadow(&dir);
assert!(
content.contains("alice:$6$newhash_alice:"),
"alice's hash should be updated, got: {content}"
);
assert!(
content.contains("bob:$6$newhash_bob:"),
"bob's hash should be updated, got: {content}"
);
}
#[test]
fn test_preserves_other_fields() {
if common::skip_unless_root() {
return;
}
let dir = setup_shadow("testuser:$6$oldhash:19500:5:180:14:30:20000:\n");
let shadow_path = dir.path().join("etc/shadow");
let mut entries =
shadow_core::shadow::read_shadow_file(&shadow_path).expect("failed to read shadow");
let entry = entries
.iter_mut()
.find(|e| e.name == "testuser")
.expect("testuser not found");
entry.passwd = "$6$newhash".to_string();
let file = std::fs::File::create(&shadow_path).expect("failed to create shadow file");
shadow_core::shadow::write_shadow(&entries, file).expect("failed to write shadow");
let content = read_shadow(&dir);
// Verify aging fields are preserved.
assert!(
content.contains(":5:180:14:30:20000:"),
"aging fields should be preserved, got: {content}"
);
}
#[test]
fn test_multiple_users_atomic() {
if common::skip_unless_root() {
return;
}
let dir = setup_shadow(
"user1:$6$old1:19500:0:99999:7:::\nuser2:$6$old2:19500:0:99999:7:::\nuser3:$6$old3:19500:0:99999:7:::\n",
);
let shadow_path = dir.path().join("etc/shadow");
let mut entries =
shadow_core::shadow::read_shadow_file(&shadow_path).expect("failed to read shadow");
// Update only user1 and user3, leave user2 alone.
for entry in &mut entries {
match entry.name.as_str() {
"user1" => entry.passwd = "$6$new1".to_string(),
"user3" => entry.passwd = "$6$new3".to_string(),
_ => {}
}
}
let file = std::fs::File::create(&shadow_path).expect("failed to create shadow file");
shadow_core::shadow::write_shadow(&entries, file).expect("failed to write shadow");
let content = read_shadow(&dir);
assert!(content.contains("user1:$6$new1:"));
assert!(
content.contains("user2:$6$old2:"),
"user2 should be unchanged"
);
assert!(content.contains("user3:$6$new3:"));
}