#![allow(unsafe_code)]
use super::{ProcessProtection, ProtectionError};
pub struct LinuxProcessProtection;
fn last_errno() -> i32 {
unsafe { *libc::__errno_location() }
}
impl ProcessProtection for LinuxProcessProtection {
fn lock_memory(&self) -> Result<(), ProtectionError> {
let rc = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
if rc == 0 {
Ok(())
} else {
Err(ProtectionError::SyscallFailed {
op: "mlockall",
code: last_errno(),
})
}
}
fn disable_core_dump(&self) -> Result<(), ProtectionError> {
let rc = unsafe {
libc::prctl(
libc::PR_SET_DUMPABLE,
0 as libc::c_ulong,
0 as libc::c_ulong,
0 as libc::c_ulong,
0 as libc::c_ulong,
)
};
if rc == 0 {
Ok(())
} else {
Err(ProtectionError::SyscallFailed {
op: "prctl(PR_SET_DUMPABLE)",
code: last_errno(),
})
}
}
fn disable_ptrace(&self) -> Result<(), ProtectionError> {
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if let Some(rest) = line.strip_prefix("TracerPid:") {
if rest.trim() != "0" {
return Err(ProtectionError::DebuggerAttached(
"TracerPid != 0 in /proc/self/status",
));
}
break;
}
}
}
let rc = unsafe {
libc::prctl(
libc::PR_SET_PTRACER,
0 as libc::c_ulong,
0 as libc::c_ulong,
0 as libc::c_ulong,
0 as libc::c_ulong,
)
};
if rc != 0 {
return Err(ProtectionError::SyscallFailed {
op: "prctl(PR_SET_PTRACER)",
code: last_errno(),
});
}
match std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope") {
Ok(content) => {
let scope = content.trim();
if scope != "2" {
eprintln!(
"arkhe-forge-platform: yama.ptrace_scope={scope} (want 2); \
per-process ptrace protection cannot cover this gap."
);
}
}
Err(_) => {
eprintln!(
"arkhe-forge-platform: /proc/sys/kernel/yama/ptrace_scope unreadable; \
yama LSM likely disabled."
);
}
}
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn apply_all_returns_ok_or_specific_error() {
let proto = LinuxProcessProtection;
match proto.apply_all() {
Ok(()) => {}
Err(ProtectionError::SyscallFailed { op, code }) => {
eprintln!("apply_all reported SyscallFailed op={op} code={code}");
}
Err(ProtectionError::DebuggerAttached(reason)) => {
eprintln!("apply_all reported DebuggerAttached: {reason}");
}
Err(other) => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn disable_core_dump_either_succeeds_or_reports_errno() {
let proto = LinuxProcessProtection;
match proto.disable_core_dump() {
Ok(()) => {}
Err(ProtectionError::SyscallFailed { op, code }) => {
assert_eq!(op, "prctl(PR_SET_DUMPABLE)");
assert!(code != 0, "errno should be non-zero on failure");
}
Err(other) => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn disable_ptrace_signals_attach_explicitly() {
let proto = LinuxProcessProtection;
match proto.disable_ptrace() {
Ok(()) => {}
Err(ProtectionError::DebuggerAttached(_)) => {}
Err(ProtectionError::SyscallFailed { op, .. }) => {
assert_eq!(op, "prctl(PR_SET_PTRACER)");
}
Err(other) => panic!("unexpected error variant: {other:?}"),
}
}
}