Skip to content

Commit 7a0104c

Browse files
committed
Harden Linux process/OS metadata reporting
Follow-ups from the Linux backend soundness audit: - Decode the waitpid(2)-style status word from /proc/<pid>/stat into a real exit code in Process::state(): normal exits report the exit(3) code, signal deaths report the negated signal number. Previously the raw status word leaked through (exit(3) surfaced as Dead(768)). - Derive OsInfo.arch and ProcessInfo::{sys_arch,proc_arch} from the compile target (x86_64/x86/aarch64) instead of hardcoding x86-64, and make the process module list callback emit the same arch field that module_by_address keys on. - Replace OS kernel-module handles (indices into a name-sorted snapshot that shift on module load/unload) with a stable hash of the module name (std DefaultHasher, fixed-seeded), resolved against the live snapshot. - Report the real process state in process_info_by_pid via the shared process_state() helper instead of hardcoding Alive.
1 parent cbe66e3 commit 7a0104c

2 files changed

Lines changed: 121 additions & 18 deletions

File tree

src/linux/mod.rs

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,61 @@ use procfs::KernelModule;
88

99
use itertools::Itertools;
1010

11+
use std::collections::hash_map::DefaultHasher;
12+
use std::hash::{Hash, Hasher};
13+
1114
pub mod mem;
1215
use mem::ProcessVirtualMemory;
1316

1417
pub mod process;
18+
use process::process_state;
1519
pub use process::LinuxProcess;
1620

21+
/// Architecture of the host the backend is running on.
22+
///
23+
/// memflow-native works through native syscalls, so the inspected processes always run
24+
/// under the same kernel/ISA as this build. We therefore report the compile target's
25+
/// architecture rather than assuming x86-64. 32-bit processes running under a 64-bit
26+
/// kernel are still reported as 64-bit here; distinguishing them would require sniffing
27+
/// the ELF class of `/proc/<pid>/exe`.
28+
fn host_arch() -> ArchitectureIdent {
29+
#[cfg(target_arch = "x86_64")]
30+
{
31+
ArchitectureIdent::X86(64, false)
32+
}
33+
#[cfg(target_arch = "x86")]
34+
{
35+
ArchitectureIdent::X86(32, false)
36+
}
37+
#[cfg(target_arch = "aarch64")]
38+
{
39+
// Page size is read at runtime; only 4k is currently supported by memflow.
40+
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
41+
ArchitectureIdent::AArch64(if page_size > 0 {
42+
page_size as usize
43+
} else {
44+
0x1000
45+
})
46+
}
47+
#[cfg(not(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64")))]
48+
{
49+
ArchitectureIdent::Unknown(0)
50+
}
51+
}
52+
53+
/// Stable, ordering-independent handle for a kernel module, derived from its name.
54+
/// `procfs::KernelModule` exposes no kernel address we could reuse, so hashing the name
55+
/// keeps the handle valid across module load/unload churn (unlike a list index).
56+
///
57+
/// `DefaultHasher` is fixed-seeded (unlike the randomized `RandomState` behind
58+
/// `HashMap`), so the handle is consistent across the two lookups within a process run,
59+
/// which is all this handle needs.
60+
fn module_handle(name: &str) -> Address {
61+
let mut hasher = DefaultHasher::new();
62+
name.hash(&mut hasher);
63+
Address::from(hasher.finish())
64+
}
65+
1766
pub struct LinuxOs {
1867
info: OsInfo,
1968
}
@@ -46,7 +95,7 @@ impl Default for LinuxOs {
4695
let info = OsInfo {
4796
base: Address::NULL,
4897
size: 0,
49-
arch: ArchitectureIdent::X86(64, false),
98+
arch: host_arch(),
5099
};
51100

52101
Self { info }
@@ -108,15 +157,17 @@ impl Os for LinuxOs {
108157

109158
let path = path.into();
110159

160+
let arch = host_arch();
161+
111162
Ok(ProcessInfo {
112163
address: (proc.pid() as umem).into(),
113164
pid,
114165
command_line,
115166
path,
116167
name,
117-
sys_arch: ArchitectureIdent::X86(64, false),
118-
proc_arch: ArchitectureIdent::X86(64, false),
119-
state: ProcessState::Alive,
168+
sys_arch: arch,
169+
proc_arch: arch,
170+
state: process_state(pid as pid_t),
120171
// dtb is not known/used here
121172
dtb1: Address::invalid(),
122173
dtb2: Address::invalid(),
@@ -145,8 +196,9 @@ impl Os for LinuxOs {
145196
fn module_address_list_callback(&mut self, mut callback: AddressCallback) -> Result<()> {
146197
let modules = self.kernel_modules_sorted()?;
147198

148-
(0..modules.len())
149-
.map(Address::from)
199+
modules
200+
.iter()
201+
.map(|km| module_handle(&km.name))
150202
.take_while(|a| callback.call(*a))
151203
.for_each(|_| {});
152204

@@ -161,7 +213,8 @@ impl Os for LinuxOs {
161213
let modules = self.kernel_modules_sorted()?;
162214

163215
modules
164-
.get(address.to_umem() as usize)
216+
.iter()
217+
.find(|km| module_handle(&km.name) == address)
165218
.map(|km| ModuleInfo {
166219
address,
167220
size: km.size as umem,

src/linux/process.rs

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,41 @@ impl LinuxProcess {
147147
}
148148
}
149149

150+
/// Decodes a waitpid(2)-style status word (as exposed in `/proc/<pid>/stat` field 52)
151+
/// into a memflow [`ExitCode`].
152+
///
153+
/// A normal exit reports its `exit(3)` code; a process killed by a signal reports the
154+
/// negated signal number so the two cases stay distinguishable.
155+
fn decode_exit_code(status: i32) -> i32 {
156+
if status & 0x7f == 0 {
157+
// WIFEXITED: low 7 bits clear -> exit code is in bits 8..16.
158+
(status >> 8) & 0xff
159+
} else {
160+
// WIFSIGNALED: low 7 bits carry the terminating signal.
161+
-(status & 0x7f)
162+
}
163+
}
164+
165+
/// Resolves the [`ProcessState`] of `pid` from `/proc/<pid>/stat`.
166+
///
167+
/// Zombie/dead states map to [`ProcessState::Dead`] (carrying the decoded exit code
168+
/// when the kernel exposes it); a vanished PID is treated as reaped (`Dead(0)`), and a
169+
/// stat that fails for any other reason (e.g. permissions) stays [`ProcessState::Unknown`].
170+
pub(crate) fn process_state(pid: pid_t) -> ProcessState {
171+
match procfs::process::Process::new(pid).and_then(|p| p.stat()) {
172+
Ok(stat) => match stat.state {
173+
// Z = zombie (exited, unreaped), X/x = dead
174+
'Z' | 'X' | 'x' => {
175+
ProcessState::Dead(stat.exit_code.map(decode_exit_code).unwrap_or(0))
176+
}
177+
_ => ProcessState::Alive,
178+
},
179+
// /proc/<pid> vanished -> the process was reaped; exit code is no longer recoverable
180+
Err(procfs::ProcError::NotFound(_)) => ProcessState::Dead(0),
181+
Err(_) => ProcessState::Unknown,
182+
}
183+
}
184+
150185
cglue_impl_group!(LinuxProcess, ProcessInstance, {});
151186
cglue_impl_group!(LinuxProcess, IntoProcessInstance, {});
152187

@@ -171,7 +206,9 @@ impl Process for LinuxProcess {
171206
.take_while(|map| {
172207
callback.call(ModuleAddressInfo {
173208
address: Address::from(map.address.0),
174-
arch: self.info.proc_arch,
209+
// Match `module_by_address`, which keys on `sys_arch`; the maps are
210+
// also filtered above by `sys_arch`.
211+
arch: self.info.sys_arch,
175212
})
176213
})
177214
.for_each(|_| {});
@@ -264,16 +301,7 @@ impl Process for LinuxProcess {
264301

265302
/// Retrieves the state of the process
266303
fn state(&mut self) -> ProcessState {
267-
match procfs::process::Process::new(self.pid).and_then(|p| p.stat()) {
268-
Ok(stat) => match stat.state {
269-
// Z = zombie (exited, unreaped), X/x = dead
270-
'Z' | 'X' | 'x' => ProcessState::Dead(stat.exit_code.unwrap_or(0)),
271-
_ => ProcessState::Alive,
272-
},
273-
// /proc/<pid> vanished -> the process was reaped; exit code is no longer recoverable
274-
Err(procfs::ProcError::NotFound(_)) => ProcessState::Dead(0),
275-
Err(_) => ProcessState::Unknown,
276-
}
304+
process_state(self.pid)
277305
}
278306

279307
/// Changes the dtb this process uses for memory translations.
@@ -393,3 +421,25 @@ impl MemoryView for LinuxProcess {
393421
self.virt_mem.metadata()
394422
}
395423
}
424+
425+
#[cfg(test)]
426+
mod tests {
427+
use super::decode_exit_code;
428+
429+
#[test]
430+
fn normal_exit_decodes_to_exit_code() {
431+
// exit(0) and exit(3) as reported by waitpid: code in bits 8..16.
432+
assert_eq!(decode_exit_code(0x0000), 0);
433+
assert_eq!(decode_exit_code(3 << 8), 3);
434+
assert_eq!(decode_exit_code(255 << 8), 255);
435+
}
436+
437+
#[test]
438+
fn signalled_exit_decodes_to_negative_signal() {
439+
// Killed by SIGKILL (9) / SIGSEGV (11): low 7 bits carry the signal.
440+
assert_eq!(decode_exit_code(9), -9);
441+
assert_eq!(decode_exit_code(11), -11);
442+
// Core-dump flag (0x80) must not bleed into the signal number.
443+
assert_eq!(decode_exit_code(11 | 0x80), -11);
444+
}
445+
}

0 commit comments

Comments
 (0)