use crate::error::Error;
pub fn harden_process() {
#[cfg(target_os = "linux")]
unsafe {
libc::prctl(libc::PR_SET_DUMPABLE, 0);
let rlimit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
libc::setrlimit(libc::RLIMIT_CORE, &rlimit);
libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if let Some(pid_str) = line.strip_prefix("TracerPid:\t") {
if pid_str != "0" {
eprintln!("\n🚨 FATAL: Security compromised. Active debugger (ptrace) detected on envseal process. Aborting to protect vault.");
std::process::exit(101);
}
}
}
}
}
#[cfg(target_os = "macos")]
{
if macos_traced() {
eprintln!("\n🚨 FATAL: Security compromised. Active debugger (ptrace/sysdiagnose) detected on envseal process. Aborting to protect vault.");
std::process::exit(101);
}
if let Err(detail) = check_macos_hardened_runtime() {
let dev_override = std::env::var("ENVSEAL_ALLOW_UNHARDENED_MACOS")
.is_ok_and(|v| !v.is_empty() && v != "0" && v != "false");
if dev_override {
eprintln!(
"envseal: WARNING — hardened-runtime check failed ({detail}); \
ENVSEAL_ALLOW_UNHARDENED_MACOS is set, allowing this run. \
Production builds MUST be signed with `codesign -o runtime`."
);
} else if cfg!(debug_assertions) {
eprintln!("envseal: hardened-runtime check skipped (debug build): {detail}");
} else {
eprintln!(
"\n🚨 FATAL: macOS hardened runtime is not engaged ({detail}). \
Without it, any same-user process can read envseal's memory \
via task_for_pid and exfiltrate the unlocked master key. \
Re-sign with `codesign -o runtime` and re-notarize, or set \
ENVSEAL_ALLOW_UNHARDENED_MACOS=1 if this is an intentional \
development run."
);
std::process::exit(102);
}
}
}
#[cfg(windows)]
{
let _ = harden_process_dacl_windows();
if windows_debugger_present() {
eprintln!("\n🚨 FATAL: Security compromised. Active debugger detected on envseal process. Aborting to protect vault.");
std::process::exit(101);
}
}
}
#[cfg(target_os = "macos")]
fn macos_traced() -> bool {
const P_TRACED: i32 = 0x0000_0800;
const KERN_PROC_PID: libc::c_int = 1;
let mut mib: [libc::c_int; 4] = [
libc::CTL_KERN,
libc::KERN_PROC,
KERN_PROC_PID,
unsafe { libc::getpid() },
];
let mut info: libc::kinfo_proc = unsafe { std::mem::zeroed() };
let mut size = std::mem::size_of::<libc::kinfo_proc>();
let rc = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
4,
std::ptr::addr_of_mut!(info).cast::<libc::c_void>(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
return false;
}
(info.kp_proc.p_flag & P_TRACED) != 0
}
#[cfg(windows)]
fn windows_debugger_present() -> bool {
use windows_sys::Win32::System::Diagnostics::Debug::{
CheckRemoteDebuggerPresent, IsDebuggerPresent,
};
use windows_sys::Win32::System::Threading::GetCurrentProcess;
if unsafe { IsDebuggerPresent() } != 0 {
return true;
}
let mut remote: i32 = 0;
let _ = unsafe { CheckRemoteDebuggerPresent(GetCurrentProcess(), &mut remote) };
remote != 0
}
#[cfg(windows)]
fn harden_process_dacl_windows() -> Result<(), Error> {
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Security::Authorization::{
ConvertStringSecurityDescriptorToSecurityDescriptorW, SetSecurityInfo, SE_KERNEL_OBJECT,
};
use windows_sys::Win32::Security::{
GetSecurityDescriptorDacl, ACL, DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR,
};
use windows_sys::Win32::System::Threading::GetCurrentProcess;
const SDDL_REVISION_1: u32 = 1;
let sddl: Vec<u16> = "D:(A;;0x1F0FFF;;;SY)(A;;0x00121401;;;OW)\0"
.encode_utf16()
.collect();
let mut psd: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
let ok = unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.as_ptr(),
SDDL_REVISION_1,
&mut psd,
std::ptr::null_mut(),
)
};
if ok == 0 || psd.is_null() {
return Err(Error::CryptoFailure(
"harden_process_dacl: SDDL parse failed".to_string(),
));
}
let mut dacl_present: i32 = 0;
let mut dacl_ptr: *mut ACL = std::ptr::null_mut();
let mut dacl_defaulted: i32 = 0;
let got = unsafe {
GetSecurityDescriptorDacl(psd, &mut dacl_present, &mut dacl_ptr, &mut dacl_defaulted)
};
if got == 0 || dacl_present == 0 || dacl_ptr.is_null() {
unsafe {
LocalFree(psd.cast());
}
return Err(Error::CryptoFailure(
"harden_process_dacl: GetSecurityDescriptorDacl failed".to_string(),
));
}
let rc = unsafe {
SetSecurityInfo(
GetCurrentProcess().cast(),
SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
dacl_ptr,
std::ptr::null_mut(),
)
};
unsafe {
LocalFree(psd.cast());
}
if rc != 0 {
return Err(Error::CryptoFailure(format!(
"harden_process_dacl: SetSecurityInfo failed (Win32 error {rc})"
)));
}
Ok(())
}
#[cfg(windows)]
#[must_use]
pub fn vm_read_access_blocked() -> bool {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
GetCurrentProcessId, OpenProcess, PROCESS_VM_READ,
};
unsafe {
let pid = GetCurrentProcessId();
let h = OpenProcess(PROCESS_VM_READ, 0, pid);
if h.is_null() {
true
} else {
CloseHandle(h);
false
}
}
}
#[cfg(not(windows))]
#[must_use]
pub fn vm_read_access_blocked() -> bool {
false
}
#[cfg(target_os = "macos")]
pub fn check_macos_hardened_runtime() -> Result<(), String> {
const CS_VALID: u32 = 0x0000_0001;
const CS_HARD: u32 = 0x0000_0100;
const CSOPS_GET_FLAGS: i32 = 0;
extern "C" {
fn csops(pid: libc::pid_t, ops: u32, useraddr: *mut u32, usersize: libc::size_t) -> i32;
}
let mut flags: u32 = 0;
let rc = unsafe {
csops(
0, CSOPS_GET_FLAGS as u32,
&mut flags as *mut u32,
std::mem::size_of::<u32>(),
)
};
if rc != 0 {
return Err(format!(
"csops(GET_FLAGS) returned {} — running binary is unsigned, \
so hardened runtime cannot be engaged. task_for_pid is \
permitted from any same-user process and an attacker can \
read envseal's address space. Sign the release artifact \
with `codesign -o runtime` and notarize.",
rc
));
}
if flags & CS_VALID == 0 {
return Err(
"code signature is not valid — task_for_pid is unrestricted. \
Sign the binary and notarize."
.to_string(),
);
}
if flags & CS_HARD == 0 {
return Err(format!(
"code signature is valid but the CS_HARD (hardened runtime) \
flag is NOT set (flags=0x{:08x}). task_for_pid still works \
from same-user processes — sign with `codesign -o runtime` \
and re-notarize.",
flags
));
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
#[allow(clippy::missing_errors_doc)]
pub fn check_macos_hardened_runtime() -> Result<(), String> {
Ok(())
}
#[cfg(target_os = "linux")]
pub fn mark_dontdump(ptr: *const u8, len: usize) {
unsafe {
libc::madvise(ptr as *mut libc::c_void, len, libc::MADV_DONTDUMP);
}
}
#[must_use]
pub fn test_memfd_secret() -> bool {
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
{
const SYS_MEMFD_SECRET: libc::c_long = 447;
let fd = unsafe { libc::syscall(SYS_MEMFD_SECRET, 0_u32) };
if fd >= 0 {
#[allow(clippy::cast_possible_truncation)]
unsafe {
libc::close(fd as i32);
}
return true;
}
false
}
#[cfg(not(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
)))]
{
false
}
}
#[must_use]
pub fn assess_ptrace_signals(_ctx: &super::DetectorContext) -> Vec<super::Signal> {
#[cfg(target_os = "linux")]
{
if let Some(detail) = check_ptrace_scope() {
return vec![super::Signal::new(
super::SignalId::new("process.ptrace.permissive"),
super::Category::EnvironmentInjection,
super::Severity::Warn,
"ptrace_scope is permissive",
detail,
"set kernel.yama.ptrace_scope to at least 1 (sudo sysctl …)",
)];
}
}
Vec::new()
}
#[must_use]
pub fn assess_preload_signals(_ctx: &super::DetectorContext) -> Vec<super::Signal> {
#[cfg(target_os = "linux")]
{
if let Err(e) = check_self_preload() {
return vec![super::Signal::new(
super::SignalId::new("process.preload.detected"),
super::Category::EnvironmentInjection,
super::Severity::Critical,
"process started with library injection",
e.to_string(),
"restart envseal in a clean environment without LD_PRELOAD / LD_AUDIT",
)];
}
}
Vec::new()
}
pub fn check_self_preload() -> Result<(), Error> {
#[cfg(target_os = "linux")]
{
if let Ok(environ) = std::fs::read("/proc/self/environ") {
let entries: Vec<&[u8]> = environ.split(|&b| b == 0).collect();
for entry in entries {
let entry_str = String::from_utf8_lossy(entry);
if entry_str.starts_with("LD_PRELOAD=")
|| entry_str.starts_with("LD_AUDIT=")
|| entry_str.starts_with("LD_PROFILE=")
|| entry_str.starts_with("GLIBC_TUNABLES=")
{
return Err(Error::EnvironmentCompromised(format!(
"this process was started with {entry_str}. \
a malicious library may be loaded in this process's memory. \
refusing to handle secrets. restart envseal without LD_PRELOAD."
)));
}
}
}
}
#[cfg(target_os = "macos")]
{
for (key, _value) in std::env::vars() {
if key == "DYLD_INSERT_LIBRARIES" || key == "DYLD_LIBRARY_PATH" {
return Err(Error::EnvironmentCompromised(format!(
"this process was started with {key}. \
a malicious library may be loaded in this process's memory. \
refusing to handle secrets. restart envseal without DYLD_INSERT_LIBRARIES."
)));
}
}
}
Ok(())
}
#[must_use]
pub fn check_ptrace_scope() -> Option<String> {
#[cfg(target_os = "linux")]
{
match std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope") {
Ok(scope) => {
let scope = scope.trim();
if scope == "0" {
return Some(
"ptrace_scope is 0 (permissive): any same-UID process can attach to \
envseal and read secrets from memory. set ptrace_scope to 1 or higher: \
sudo sysctl kernel.yama.ptrace_scope=1"
.to_string(),
);
}
None
}
Err(_) => None,
}
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
#[cfg(test)]
mod check_self_preload_tests {
use super::check_self_preload;
#[test]
#[cfg(target_os = "linux")]
fn detects_ld_profile() {
std::env::set_var("LD_PROFILE", "/tmp/fake.so");
let result = check_self_preload();
std::env::remove_var("LD_PROFILE");
assert!(
matches!(result, Err(Error::EnvironmentCompromised(_))),
"LD_PROFILE should trigger EnvironmentCompromised: {:?}",
result
);
}
#[test]
#[cfg(target_os = "linux")]
fn detects_glibc_tunables() {
std::env::set_var("GLIBC_TUNABLES", "glibc.rtld.nns=1");
let result = check_self_preload();
std::env::remove_var("GLIBC_TUNABLES");
assert!(
matches!(result, Err(Error::EnvironmentCompromised(_))),
"GLIBC_TUNABLES should trigger EnvironmentCompromised: {:?}",
result
);
}
#[test]
#[cfg(target_os = "macos")]
fn detects_dyld_insert_libraries() {
std::env::set_var("DYLD_INSERT_LIBRARIES", "/tmp/evil.dylib");
let result = check_self_preload();
std::env::remove_var("DYLD_INSERT_LIBRARIES");
assert!(
matches!(result, Err(Error::EnvironmentCompromised(_))),
"DYLD_INSERT_LIBRARIES should trigger EnvironmentCompromised: {:?}",
result
);
}
#[test]
fn clean_env_passes() {
let result = check_self_preload();
assert!(
result.is_ok(),
"check_self_preload should succeed in a clean test environment: {result:?}",
);
}
}