passwd: fix 7 critical bugs (issues #1-#7)

Fixes #1 — PAM feature compiles: ConvMode::Tty, correct method
signatures (authenticate(0), chauthtok(0/PAM_CHANGE_EXPIRED_AUTHTOK),
acct_mgmt(0), set_item_str), removed non-existent set_repository.

Fixes #2 — Permission check on -S: non-root can only view own status,
-Sa requires root.

Fixes #3 — --prefix no longer bypasses root check. Tests run as root
in Docker so they pass without the bypass.

Fixes #4 — Mutation + aging flags work together: passwd -l -n 7 -x 90
now applies both lock AND aging in a single mutate_shadow call.

Fixes #5 — pam_acct_mgmt called between authenticate and chauthtok.

Fixes #6 — PAM errors return exit code 10 (was 3). Added PAM_ERROR
constant.

Fixes #7 — Password strings zeroed on drop via zeroize crate. Added
zeroize as workspace dependency, used in PAM conversation function.

3 new tests: test_pam_exit_code_defined, test_mutation_with_aging_combined,
test_status_permission_denied_code_path.

95 tests passing on Debian/Alpine/Fedora, zero clippy warnings.
This commit is contained in:
Pierre Warnier
2026-03-23 13:42:44 +01:00
parent 9229c376ca
commit 5c32839a72
4 changed files with 232 additions and 95 deletions
+3
View File
@@ -38,6 +38,9 @@ thiserror = "2"
nix = { version = "0.29", features = ["user", "fs", "process", "signal", "term"] }
libc = "0.2"
# Security
zeroize = "1"
# Testing
proptest = "1"
tempfile = "3"
+1
View File
@@ -14,6 +14,7 @@ path = "src/lib.rs"
libc = { workspace = true }
nix = { workspace = true }
thiserror = { workspace = true }
zeroize = { workspace = true }
[dev-dependencies]
proptest = { workspace = true }
+9 -1
View File
@@ -28,6 +28,8 @@ use std::io::{self, BufRead, Write};
use std::os::unix::io::AsRawFd;
use std::ptr;
use zeroize::Zeroize;
use crate::error::ShadowError;
// ---------------------------------------------------------------------------
@@ -351,7 +353,13 @@ fn prompt_for_input(
};
match input {
Ok(line) => alloc_c_response(&line),
Ok(mut line) => {
let result = alloc_c_response(&line);
// Zeroize the Rust string so password data does not linger in
// process memory after being copied to the C-allocated response.
line.zeroize();
result
}
Err(_) => Err(()),
}
}
File diff suppressed because it is too large Load Diff