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(windows)]
{
let _ = harden_process_dacl_windows();
}
}
#[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=") {
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."
)));
}
}
}
}
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
}
}