mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
security: core dump suppression, RLIMIT_FSIZE, zero-length guard
From OpenBSD pw_init() pattern: Fixes #43 — suppress_core_dumps(): RLIMIT_CORE=0 + PR_SET_DUMPABLE=0. Prevents core dumps that could expose password hashes, and prevents ptrace attachment to the setuid process. Fixes #44 — raise_file_size_limit(): RLIMIT_FSIZE=RLIM_INFINITY. Prevents malicious caller from truncating /etc/shadow via ulimit -f. Fixes #45 — Zero-length output guard in atomic_write. Refuses to replace original file with empty output. A zero-length /etc/shadow locks out all users. 3 new tests: test_core_dump_suppression, test_raise_file_size_limit, test_zero_length_write_rejected. 151 tests, zero clippy warnings.
This commit is contained in:
+1
-1
@@ -38,7 +38,7 @@ uucore = "0.7"
|
||||
thiserror = "2"
|
||||
|
||||
# Unix/Linux
|
||||
nix = { version = "0.29", features = ["user", "fs", "process", "signal", "term"] }
|
||||
nix = { version = "0.29", features = ["user", "fs", "process", "signal", "term", "resource"] }
|
||||
libc = "0.2"
|
||||
|
||||
# Security
|
||||
|
||||
@@ -85,6 +85,15 @@ where
|
||||
|
||||
f(&mut tmp_file)?;
|
||||
|
||||
// Zero-length output guard: a zero-length shadow file locks out all users.
|
||||
// OpenBSD checks this in pw_mkdb before replacing the original.
|
||||
let written = tmp_file.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
if written == 0 {
|
||||
return Err(ShadowError::Other(
|
||||
"refusing to write zero-length file".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Flush and fsync.
|
||||
tmp_file
|
||||
.flush()
|
||||
|
||||
@@ -108,6 +108,37 @@ impl UError for PasswdError {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Security hardening — process hardening
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Suppress core dumps and prevent ptrace attachment.
|
||||
///
|
||||
/// OpenBSD's `pw_init()` sets `RLIMIT_CORE=0`. A core dump from a setuid
|
||||
/// passwd process could expose password hashes and plaintext passwords.
|
||||
fn suppress_core_dumps() {
|
||||
// RLIMIT_CORE = 0: no core dumps.
|
||||
let _ = nix::sys::resource::setrlimit(nix::sys::resource::Resource::RLIMIT_CORE, 0, 0);
|
||||
// PR_SET_DUMPABLE = 0: prevent ptrace attachment and /proc/pid/mem reads.
|
||||
// SAFETY: prctl with PR_SET_DUMPABLE is a simple flag set, no pointers.
|
||||
unsafe {
|
||||
libc::prctl(libc::PR_SET_DUMPABLE, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Raise `RLIMIT_FSIZE` to prevent truncated file writes.
|
||||
///
|
||||
/// OpenBSD raises `RLIMIT_FSIZE` to infinity before file operations. A
|
||||
/// malicious caller could `ulimit -f 1` before invoking setuid passwd,
|
||||
/// causing /etc/shadow to be truncated mid-write.
|
||||
fn raise_file_size_limit() {
|
||||
let _ = nix::sys::resource::setrlimit(
|
||||
nix::sys::resource::Resource::RLIMIT_FSIZE,
|
||||
nix::sys::resource::RLIM_INFINITY,
|
||||
nix::sys::resource::RLIM_INFINITY,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Security hardening — environment sanitization
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -171,6 +202,8 @@ fn apply_landlock_inner(_root: &SysRoot) {
|
||||
#[uucore::main]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
suppress_core_dumps();
|
||||
raise_file_size_limit();
|
||||
sanitize_env();
|
||||
|
||||
let matches = match uu_app().try_get_matches_from(args) {
|
||||
@@ -1477,6 +1510,50 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// OpenBSD hardening tests
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_core_dump_suppression() {
|
||||
// After calling suppress_core_dumps(), RLIMIT_CORE should be 0.
|
||||
suppress_core_dumps();
|
||||
let (soft, _hard) =
|
||||
nix::sys::resource::getrlimit(nix::sys::resource::Resource::RLIMIT_CORE).unwrap();
|
||||
assert_eq!(soft, 0, "RLIMIT_CORE should be 0 after suppression");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raise_file_size_limit() {
|
||||
// After calling raise_file_size_limit(), `RLIMIT_FSIZE` should be RLIM_INFINITY.
|
||||
raise_file_size_limit();
|
||||
let (soft, _hard) =
|
||||
nix::sys::resource::getrlimit(nix::sys::resource::Resource::RLIMIT_FSIZE).unwrap();
|
||||
assert_eq!(
|
||||
soft,
|
||||
nix::sys::resource::RLIM_INFINITY,
|
||||
"`RLIMIT_FSIZE` should be RLIM_INFINITY"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_length_write_rejected() {
|
||||
// atomic_write should refuse to replace a file with zero-length output.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let target = dir.path().join("shadow");
|
||||
std::fs::write(&target, "original content\n").unwrap();
|
||||
|
||||
let result = shadow_core::atomic::atomic_write(&target, |_file| {
|
||||
// Write nothing — zero-length output.
|
||||
Ok(())
|
||||
});
|
||||
|
||||
assert!(result.is_err(), "zero-length write should be rejected");
|
||||
// Original file should be untouched.
|
||||
let content = std::fs::read_to_string(&target).unwrap();
|
||||
assert_eq!(content, "original content\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mutation_with_aging_combined() {
|
||||
if skip_unless_root() {
|
||||
|
||||
Reference in New Issue
Block a user