Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 96 additions & 5 deletions src/linux/mem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,18 @@ impl ProcessVirtualMemory {
let mut remaining_written =
if libcret == -1 { 0 } else { libcret as usize };

for (liof, (_, meta)) in iov_local
.iter()
.take(cnt)
.zip(iov_remote.iter().zip(self.temp_meta.iter()))
{
// The syscall above operated on the window [win, win + cnt),
// so result dispatch and byte accounting must start at `win`
// too. `offset` is advanced inside the loop, so snapshot it
// before iterating.
let win = offset;

for (liof, (_, meta)) in iov_local.iter().skip(win).take(cnt).zip(
iov_remote
.iter()
.skip(win)
.zip(self.temp_meta.iter().skip(win)),
) {
offset += 1;
let to_write = remaining_written;

Expand Down Expand Up @@ -250,3 +257,87 @@ impl MemoryView for ProcessVirtualMemory {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use memflow::cglue::CTup2;

fn vmem_for_pid(pid: pid_t) -> ProcessVirtualMemory {
const IOV_MAX: usize = 1024;
ProcessVirtualMemory {
pid,
temp_iov: vec![
IoSendVec(iovec {
iov_base: std::ptr::null_mut::<c_void>(),
iov_len: 0,
});
IOV_MAX * 2
]
.into_boxed_slice(),
temp_meta: vec![Address::INVALID; IOV_MAX].into_boxed_slice(),
}
}

// Regression test for the partial-transfer retry window in `process_rw`.
//
// A batched read of [valid, unmapped, valid] forces `process_vm_readv` to transfer
// the first region, fault on the middle one, and require a retry for the third.
// Before the `.skip(win)` fix the retry dispatched from index 0, re-reporting the
// first region and silently dropping the third. Reading from our own PID lets us
// exercise this without spawning a child.
#[test]
fn partial_read_across_hole_reports_each_region_once() {
let src_a = [0xAAu8; 8];
let src_c = [0xCCu8; 8];

let addr_a = Address::from(src_a.as_ptr() as u64);
let addr_c = Address::from(src_c.as_ptr() as u64);
// Below the default mmap_min_addr, so reliably unmapped (EFAULT on read).
let addr_bad = Address::from(0x1000u64);

let mut dst_a = [0u8; 8];
let mut dst_b = [0u8; 8];
let mut dst_c = [0u8; 8];

let mut ok: Vec<(Address, Vec<u8>)> = Vec::new();
let mut fail: Vec<Address> = Vec::new();

{
let inp = vec![
CTup2(addr_a, (&mut dst_a[..]).into()),
CTup2(addr_bad, (&mut dst_b[..]).into()),
CTup2(addr_c, (&mut dst_c[..]).into()),
];

let mut ok_cb = |CTup2(a, d): ReadData| {
ok.push((a, d.to_vec()));
true
};
let mut fail_cb = |CTup2(a, _): ReadData| {
fail.push(a);
true
};
let mut ok_oc: ReadCallback = (&mut ok_cb).into();
let mut fail_oc: ReadCallback = (&mut fail_cb).into();

let mut mem = vmem_for_pid(unsafe { libc::getpid() });
mem.read_iter(inp.into_iter(), Some(&mut ok_oc), Some(&mut fail_oc))
.unwrap();
}

ok.sort_by_key(|(a, _)| a.to_umem());
let mut expected = vec![(addr_a, vec![0xAAu8; 8]), (addr_c, vec![0xCCu8; 8])];
expected.sort_by_key(|(a, _)| a.to_umem());

assert_eq!(
ok, expected,
"each readable region must be reported exactly once with correct data"
);
assert_eq!(
fail,
vec![addr_bad],
"the unmapped region must be the only failure"
);
}
}
67 changes: 60 additions & 7 deletions src/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,61 @@ use procfs::KernelModule;

use itertools::Itertools;

use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

pub mod mem;
use mem::ProcessVirtualMemory;

pub mod process;
use process::process_state;
pub use process::LinuxProcess;

/// Architecture of the host the backend is running on.
///
/// memflow-native works through native syscalls, so the inspected processes always run
/// under the same kernel/ISA as this build. We therefore report the compile target's
/// architecture rather than assuming x86-64. 32-bit processes running under a 64-bit
/// kernel are still reported as 64-bit here; distinguishing them would require sniffing
/// the ELF class of `/proc/<pid>/exe`.
fn host_arch() -> ArchitectureIdent {
#[cfg(target_arch = "x86_64")]
{
ArchitectureIdent::X86(64, false)
}
#[cfg(target_arch = "x86")]
{
ArchitectureIdent::X86(32, false)
}
#[cfg(target_arch = "aarch64")]
{
// Page size is read at runtime; only 4k is currently supported by memflow.
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
ArchitectureIdent::AArch64(if page_size > 0 {
page_size as usize
} else {
0x1000
})
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64")))]
{
ArchitectureIdent::Unknown(0)
}
}

/// Stable, ordering-independent handle for a kernel module, derived from its name.
/// `procfs::KernelModule` exposes no kernel address we could reuse, so hashing the name
/// keeps the handle valid across module load/unload churn (unlike a list index).
///
/// `DefaultHasher` is fixed-seeded (unlike the randomized `RandomState` behind
/// `HashMap`), so the handle is consistent across the two lookups within a process run,
/// which is all this handle needs.
fn module_handle(name: &str) -> Address {
let mut hasher = DefaultHasher::new();
name.hash(&mut hasher);
Address::from(hasher.finish())
}

pub struct LinuxOs {
info: OsInfo,
}
Expand Down Expand Up @@ -46,7 +95,7 @@ impl Default for LinuxOs {
let info = OsInfo {
base: Address::NULL,
size: 0,
arch: ArchitectureIdent::X86(64, false),
arch: host_arch(),
};

Self { info }
Expand Down Expand Up @@ -108,15 +157,17 @@ impl Os for LinuxOs {

let path = path.into();

let arch = host_arch();

Ok(ProcessInfo {
address: (proc.pid() as umem).into(),
pid,
command_line,
path,
name,
sys_arch: ArchitectureIdent::X86(64, false),
proc_arch: ArchitectureIdent::X86(64, false),
state: ProcessState::Alive,
sys_arch: arch,
proc_arch: arch,
state: process_state(pid as pid_t),
// dtb is not known/used here
dtb1: Address::invalid(),
dtb2: Address::invalid(),
Expand Down Expand Up @@ -145,8 +196,9 @@ impl Os for LinuxOs {
fn module_address_list_callback(&mut self, mut callback: AddressCallback) -> Result<()> {
let modules = self.kernel_modules_sorted()?;

(0..modules.len())
.map(Address::from)
modules
.iter()
.map(|km| module_handle(&km.name))
.take_while(|a| callback.call(*a))
.for_each(|_| {});

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

modules
.get(address.to_umem() as usize)
.iter()
.find(|km| module_handle(&km.name) == address)
.map(|km| ModuleInfo {
address,
size: km.size as umem,
Expand Down
Loading
Loading