use std::path::PathBuf;
#[derive(Debug, Default, Clone)]
pub struct HardeningReport {
pub no_core_dumps: bool,
pub no_ptrace: bool,
pub mlocked: bool,
pub coredump_filter_safe: bool,
pub failures: Vec<String>,
}
pub fn apply_default_protections() -> HardeningReport {
let mut report = HardeningReport::default();
#[cfg(target_os = "linux")]
{
let rc = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
if rc == 0 {
report.no_core_dumps = true;
report.no_ptrace = true;
} else {
let err = std::io::Error::last_os_error();
report
.failures
.push(format!("prctl(PR_SET_DUMPABLE): {err}"));
}
}
#[cfg(target_os = "macos")]
{
const PT_DENY_ATTACH: libc::c_int = 31;
let rc = unsafe { libc::ptrace(PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0) };
if rc == 0 {
report.no_ptrace = true;
report.no_core_dumps = true;
} else {
let err = std::io::Error::last_os_error();
report
.failures
.push(format!("ptrace(PT_DENY_ATTACH): {err}"));
}
}
#[cfg(target_os = "windows")]
{
report.no_core_dumps = true;
report.no_ptrace = true;
}
report
}
pub fn apply_lockdown_protections() -> HardeningReport {
let mut report = apply_default_protections();
#[cfg(target_os = "linux")]
{
let rc = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
if rc == 0 {
report.mlocked = true;
} else {
let err = std::io::Error::last_os_error();
report.failures.push(format!("mlockall: {err}"));
}
let filter = std::fs::read_to_string("/proc/self/coredump_filter")
.ok()
.and_then(|s| u32::from_str_radix(s.trim(), 16).ok());
match filter {
Some(0) => report.coredump_filter_safe = true,
Some(other) => report.failures.push(format!(
"/proc/self/coredump_filter = 0x{other:x} — anonymous pages would be dumped; \
set RLIMIT_CORE=0 or write 0 to /proc/self/coredump_filter before exec"
)),
None => {
report
.failures
.push("could not read /proc/self/coredump_filter".into());
}
}
}
#[cfg(not(target_os = "linux"))]
{
report.mlocked = false;
}
report
}
#[must_use]
pub fn lockdown_disk_cache_violations() -> Vec<PathBuf> {
let mut hits = Vec::new();
if let Some(cache_root) = dirs::cache_dir() {
let keyhog_root = cache_root.join("keyhog");
if keyhog_root.exists() {
hits.push(keyhog_root);
}
}
hits
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_protections_are_idempotent() {
let first = apply_default_protections();
let second = apply_default_protections();
assert_eq!(first.no_core_dumps, second.no_core_dumps);
assert_eq!(first.no_ptrace, second.no_ptrace);
}
#[test]
fn report_starts_empty() {
let r = HardeningReport::default();
assert!(!r.no_core_dumps);
assert!(!r.no_ptrace);
assert!(!r.mlocked);
assert!(r.failures.is_empty());
}
}