use std::env;
#[derive(Debug, Clone)]
pub struct MitigationOutcome {
pub name: &'static str,
pub applied: bool,
pub note: Option<String>,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum HardenRefusal {
#[error("refusing to run setuid (effective uid {euid} != real uid {uid})")]
Setuid { euid: u32, uid: u32 },
#[error("refusing to run with injection-style environment variable set: {0}")]
InjectionEnv(&'static str),
}
const REFUSAL_ENV_VARS: &[&str] = &[
"LD_PRELOAD",
"LD_AUDIT",
"DYLD_INSERT_LIBRARIES",
"DYLD_LIBRARY_PATH",
"DYLD_FRAMEWORK_PATH",
"DYLD_FALLBACK_LIBRARY_PATH",
"DYLD_FALLBACK_FRAMEWORK_PATH",
];
pub fn check_refusal_conditions() -> Result<(), HardenRefusal> {
#[cfg(unix)]
{
let euid = unsafe { libc::geteuid() } as u32;
let uid = unsafe { libc::getuid() } as u32;
if euid != uid {
return Err(HardenRefusal::Setuid { euid, uid });
}
}
for name in REFUSAL_ENV_VARS {
if env::var_os(name).is_some() {
return Err(HardenRefusal::InjectionEnv(name));
}
}
Ok(())
}
pub fn apply_mitigations() -> Vec<MitigationOutcome> {
let mut out = Vec::new();
#[cfg(target_os = "linux")]
{
out.push(linux::set_dumpable_zero());
out.push(linux::set_core_rlimit_zero());
}
#[cfg(target_os = "macos")]
{
out.push(macos::set_core_rlimit_zero());
}
#[cfg(windows)]
{
out.push(win::set_error_mode());
out.push(win::set_default_dll_directories());
out.push(win::set_dynamic_code_policy());
out.push(win::set_extension_point_policy());
out.push(win::wer_add_excluded_application());
}
out
}
pub fn harden_process() -> Result<Vec<MitigationOutcome>, HardenRefusal> {
check_refusal_conditions()?;
Ok(apply_mitigations())
}
#[derive(Debug, Clone, Copy)]
pub struct HardeningToken(());
pub fn install() -> Result<HardeningToken, HardenRefusal> {
check_refusal_conditions()?;
apply_mitigations();
Ok(HardeningToken(()))
}
#[cfg(feature = "memory-lock")]
pub fn lock_secret_pages(bytes: &[u8]) -> Vec<MitigationOutcome> {
#[cfg(target_os = "linux")]
return linux::lock_pages(bytes);
#[cfg(target_os = "macos")]
return macos::lock_pages(bytes);
#[cfg(windows)]
return win::lock_pages(bytes);
#[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))]
return generic_unix::lock_pages(bytes);
#[cfg(not(any(unix, windows)))]
vec![MitigationOutcome {
name: "memory-lock:unsupported-platform",
applied: false,
note: Some("memory-lock is not implemented for this platform".into()),
}]
}
#[cfg(target_os = "linux")]
mod linux {
use super::MitigationOutcome;
pub fn set_dumpable_zero() -> MitigationOutcome {
const PR_SET_DUMPABLE: libc::c_int = 4;
let rc = unsafe { libc::prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) };
MitigationOutcome {
name: "linux:prctl(PR_SET_DUMPABLE,0)",
applied: rc == 0,
note: if rc == 0 {
None
} else {
Some(format!("errno={}", std::io::Error::last_os_error()))
},
}
}
pub fn set_core_rlimit_zero() -> MitigationOutcome {
let rl = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let rc = unsafe { libc::setrlimit(libc::RLIMIT_CORE, &rl) };
MitigationOutcome {
name: "linux:setrlimit(RLIMIT_CORE,0)",
applied: rc == 0,
note: if rc == 0 {
None
} else {
Some(format!("errno={}", std::io::Error::last_os_error()))
},
}
}
#[cfg(feature = "memory-lock")]
pub fn lock_pages(bytes: &[u8]) -> Vec<super::MitigationOutcome> {
let mut out = Vec::new();
if bytes.is_empty() {
return out;
}
let (base, len) = page_range(bytes);
let rc = unsafe { libc::mlock(base as *const libc::c_void, len) };
out.push(super::MitigationOutcome {
name: "linux:mlock",
applied: rc == 0,
note: if rc == 0 {
None
} else {
Some(format!("errno={}", std::io::Error::last_os_error()))
},
});
let rc2 = unsafe { libc::madvise(base as *mut libc::c_void, len, libc::MADV_DONTDUMP) };
out.push(super::MitigationOutcome {
name: "linux:madvise(MADV_DONTDUMP)",
applied: rc2 == 0,
note: if rc2 == 0 {
None
} else {
Some(format!("errno={}", std::io::Error::last_os_error()))
},
});
let rc3 = unsafe { libc::madvise(base as *mut libc::c_void, len, libc::MADV_WIPEONFORK) };
out.push(super::MitigationOutcome {
name: "linux:madvise(MADV_WIPEONFORK)",
applied: rc3 == 0,
note: if rc3 == 0 {
None
} else {
Some(format!("errno={}", std::io::Error::last_os_error()))
},
});
out
}
#[cfg(feature = "memory-lock")]
fn page_range(bytes: &[u8]) -> (usize, usize) {
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
let page_size = if page_size == 0 { 4096 } else { page_size };
let addr = bytes.as_ptr() as usize;
let base = addr & !(page_size - 1);
let end = (addr + bytes.len() + page_size - 1) & !(page_size - 1);
(base, end - base)
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::MitigationOutcome;
pub fn set_core_rlimit_zero() -> MitigationOutcome {
let rl = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let rc = unsafe { libc::setrlimit(libc::RLIMIT_CORE, &rl) };
MitigationOutcome {
name: "macos:setrlimit(RLIMIT_CORE,0)",
applied: rc == 0,
note: if rc == 0 {
None
} else {
Some(format!("errno={}", std::io::Error::last_os_error()))
},
}
}
#[cfg(feature = "memory-lock")]
pub fn lock_pages(bytes: &[u8]) -> Vec<super::MitigationOutcome> {
if bytes.is_empty() {
return vec![];
}
let (base, len) = page_range(bytes);
let rc = unsafe { libc::mlock(base as *const libc::c_void, len) };
vec![super::MitigationOutcome {
name: "macos:mlock",
applied: rc == 0,
note: if rc == 0 {
None
} else {
Some(format!("errno={}", std::io::Error::last_os_error()))
},
}]
}
#[cfg(feature = "memory-lock")]
fn page_range(bytes: &[u8]) -> (usize, usize) {
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
let page_size = if page_size == 0 { 16384 } else { page_size }; let addr = bytes.as_ptr() as usize;
let base = addr & !(page_size - 1);
let end = (addr + bytes.len() + page_size - 1) & !(page_size - 1);
(base, end - base)
}
}
#[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))]
mod generic_unix {
#[cfg(feature = "memory-lock")]
pub fn lock_pages(bytes: &[u8]) -> Vec<super::MitigationOutcome> {
if bytes.is_empty() {
return vec![];
}
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
let page_size = if page_size == 0 { 4096 } else { page_size };
let addr = bytes.as_ptr() as usize;
let base = addr & !(page_size - 1);
let end = (addr + bytes.len() + page_size - 1) & !(page_size - 1);
let len = end - base;
let rc = unsafe { libc::mlock(base as *const libc::c_void, len) };
vec![super::MitigationOutcome {
name: "unix:mlock",
applied: rc == 0,
note: if rc == 0 {
None
} else {
Some(format!("errno={}", std::io::Error::last_os_error()))
},
}]
}
}
#[cfg(windows)]
mod win {
use super::MitigationOutcome;
#[cfg(feature = "memory-lock")]
pub fn lock_pages(bytes: &[u8]) -> Vec<MitigationOutcome> {
use windows_sys::Win32::System::Memory::VirtualLock;
if bytes.is_empty() {
return vec![];
}
let ok = unsafe { VirtualLock(bytes.as_ptr().cast(), bytes.len()) };
vec![MitigationOutcome {
name: "windows:VirtualLock",
applied: ok != 0,
note: if ok != 0 {
None
} else {
Some(format!("GetLastError={}", std::io::Error::last_os_error()))
},
}]
}
use windows_sys::Win32::System::Diagnostics::Debug::{
SetErrorMode, SEM_FAILCRITICALERRORS, SEM_NOGPFAULTERRORBOX, SEM_NOOPENFILEERRORBOX,
};
use windows_sys::Win32::System::ErrorReporting::WerAddExcludedApplication;
use windows_sys::Win32::System::LibraryLoader::{
SetDefaultDllDirectories, LOAD_LIBRARY_SEARCH_SYSTEM32,
};
use windows_sys::Win32::System::Threading::{
ProcessDynamicCodePolicy, ProcessExtensionPointDisablePolicy, SetProcessMitigationPolicy,
PROCESS_MITIGATION_DYNAMIC_CODE_POLICY, PROCESS_MITIGATION_EXTENSION_POINT_DISABLE_POLICY,
};
pub fn set_error_mode() -> MitigationOutcome {
let prev = unsafe { SetErrorMode(0) };
let mask = SEM_NOGPFAULTERRORBOX | SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX;
unsafe { SetErrorMode(prev | mask) };
MitigationOutcome {
name: "windows:SetErrorMode(NOGPFAULTERRORBOX|FAILCRITICALERRORS|NOOPENFILEERRORBOX)",
applied: true,
note: None,
}
}
pub fn set_default_dll_directories() -> MitigationOutcome {
let ok = unsafe { SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32) };
MitigationOutcome {
name: "windows:SetDefaultDllDirectories(SEARCH_SYSTEM32)",
applied: ok != 0,
note: if ok != 0 {
None
} else {
Some(format!("GetLastError={}", std::io::Error::last_os_error()))
},
}
}
pub fn set_dynamic_code_policy() -> MitigationOutcome {
let policy = PROCESS_MITIGATION_DYNAMIC_CODE_POLICY {
Anonymous:
windows_sys::Win32::System::Threading::PROCESS_MITIGATION_DYNAMIC_CODE_POLICY_0 {
Flags: 1, },
};
let ok = unsafe {
SetProcessMitigationPolicy(
ProcessDynamicCodePolicy,
&policy as *const _ as *const _,
std::mem::size_of::<PROCESS_MITIGATION_DYNAMIC_CODE_POLICY>(),
)
};
MitigationOutcome {
name: "windows:SetProcessMitigationPolicy(ProcessDynamicCodePolicy)",
applied: ok != 0,
note: if ok != 0 {
None
} else {
Some(format!("GetLastError={}", std::io::Error::last_os_error()))
},
}
}
pub fn set_extension_point_policy() -> MitigationOutcome {
let policy = PROCESS_MITIGATION_EXTENSION_POINT_DISABLE_POLICY {
Anonymous: windows_sys::Win32::System::Threading::PROCESS_MITIGATION_EXTENSION_POINT_DISABLE_POLICY_0 {
Flags: 1, },
};
let ok = unsafe {
SetProcessMitigationPolicy(
ProcessExtensionPointDisablePolicy,
&policy as *const _ as *const _,
std::mem::size_of::<PROCESS_MITIGATION_EXTENSION_POINT_DISABLE_POLICY>(),
)
};
MitigationOutcome {
name: "windows:SetProcessMitigationPolicy(ProcessExtensionPointDisablePolicy)",
applied: ok != 0,
note: if ok != 0 {
None
} else {
Some(format!("GetLastError={}", std::io::Error::last_os_error()))
},
}
}
pub fn wer_add_excluded_application() -> MitigationOutcome {
let exe = match std::env::current_exe() {
Ok(p) => p,
Err(e) => {
return MitigationOutcome {
name: "windows:WerAddExcludedApplication",
applied: false,
note: Some(format!("current_exe failed: {e}")),
}
}
};
let name = match exe.file_name().and_then(|s| s.to_str()) {
Some(s) => s.to_owned(),
None => {
return MitigationOutcome {
name: "windows:WerAddExcludedApplication",
applied: false,
note: Some("could not derive exe basename".into()),
}
}
};
let mut wide: Vec<u16> = name.encode_utf16().collect();
wide.push(0);
let hr = unsafe { WerAddExcludedApplication(wide.as_ptr(), 0) };
MitigationOutcome {
name: "windows:WerAddExcludedApplication",
applied: hr >= 0,
note: if hr >= 0 {
None
} else {
Some(format!("HRESULT={hr:#010x}"))
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_mitigations_returns_at_least_one_outcome_per_supported_platform() {
let outcomes = apply_mitigations();
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
assert!(!outcomes.is_empty());
let _ = outcomes;
}
use crate::test_utils::ENV_LOCK;
#[test]
fn check_refusal_rejects_ld_preload() {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { std::env::set_var("LD_PRELOAD", "/tmp/nonexistent.so") };
let result = check_refusal_conditions();
unsafe { std::env::remove_var("LD_PRELOAD") };
match result {
Err(HardenRefusal::InjectionEnv("LD_PRELOAD")) => {}
other => panic!("expected LD_PRELOAD refusal, got {other:?}"),
}
}
#[test]
fn check_refusal_rejects_dyld_insert_libraries() {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { std::env::set_var("DYLD_INSERT_LIBRARIES", "/tmp/x.dylib") };
let result = check_refusal_conditions();
unsafe { std::env::remove_var("DYLD_INSERT_LIBRARIES") };
match result {
Err(HardenRefusal::InjectionEnv("DYLD_INSERT_LIBRARIES")) => {}
other => panic!("expected DYLD_INSERT_LIBRARIES refusal, got {other:?}"),
}
}
}