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)
Copilot findings (PR #36):
- parsers: use trim_start() for blank/comment detection, parse
untrimmed line to preserve GECOS whitespace
- pam: newline after password prompt is best-effort (let _ =)
New tests (#37):
- test_concurrent_lock_operations: 4 threads racing lock/unlock
- test_gnu_compat_status_output: compare -S output with GNU passwd
- test_gnu_compat_lock_unlock: verify lock/unlock matches GNU behavior
147 tests, zero clippy warnings.