Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

krw

Read another process's memory as plain loads instead of process_vm_readv copies. A Linux kernel module that maps a target's physical pages into your debugger, plus a memflow backend that uses it.

Pointer-chasing reads go from 134 ns to 8 ns.

Warning

Out-of-tree kernel module, x86-64 only, and deliberately unsafe in ways a production tool would not be — see Trade-offs. Develop and test it in the provided VM, not on your workstation.

How it works

process_vm_readv never context-switches to the target — it already runs in the debugger's context. Its cost is a syscall per call, plus a page-table walk and a pin → copy → unpin per iovec. For scattered reads that per-fragment cost dominates, so krw removes the copy rather than the syscall.

mmap-ing /dev/krw with the file offset set to a target virtual address creates a window mirroring that region. The first touch of each page faults into the module, which walks the target's page tables and installs the physical page; every access after that is an ordinary load with no kernel involvement.

Because the mapping offset is the target VA, the module's address_space is indexed by target address — so an mmu_notifier can zap exactly the affected shadow PTEs with one unmap_mapping_range() when the target remaps something. Nothing is pinned, so the target stays swappable and migratable.

Quick start

Needs a Linux 6.12-ish x86-64 host with kernel headers, cargo, qemu, KVM.

git clone https://github.com/lihe07/krw && cd krw

# One-off: the rootfs used by the test suite.
curl -Lo vm/debian13.qcow2 \
  https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2

make          # module, crate, tests, benchmarks
make vm       # run the suite in a throwaway VM

make vm boots the host's own kernel with the Debian image as rootfs, so a module built against host headers loads without ABI mismatch. Each run uses a fresh overlay, so a wedged VM costs nothing.

Using it

If you already go through memflow's Os abstraction, wrap the process you get back. One added line:

use memflow::prelude::v1::*;
use memflow_krw::KrwProcess;

let mut os = memflow_native::create_os(&Default::default(), Default::default())?;
let proc = os.process_by_name("target")?;
let mut proc = KrwProcess::new(proc)?;      // <-- memory now comes from krw

let value: u64 = proc.read(Address::from(0x7fff_0000u64))?;

KrwProcess implements Process + MemoryView, which is what memflow's ProcessInstance group requires, so it goes anywhere a ProcessInstance goes. Process identity and module enumeration stay with the wrapped OS layer — memflow-native reads those from /proc/<pid>/maps, involving no memory access at all — while everything that touches target memory goes through krw. Import, export and section parsing are re-pointed at krw too, since walking ELF structures is exactly the scattered-small-read pattern it's fastest at.

If you know the pid and don't need the OS layer, use the memory view directly:

use memflow_krw::KrwMemory;
let mut mem = KrwMemory::open(target_pid)?;

The device interface underneath is two calls: ioctl(KRW_IOC_ATTACH, &{pid}), then mmap(fd, offset = target_va). See include/krw.h.

Results

In-VM, 4 vCPU / 4 GiB, 256 MiB region, kernel 6.12.96. Absolutes move run to run under KVM; the ratios are stable.

process_vm_readv krw
Random 8 B (pointer chase) 133.8 ns 8.1 ns 17×
Hot re-read 4 KiB 224.6 ns 28.7 ns 7.8×
Random 4 KiB 311.6 ns 175.1 ns 1.8×
Sequential scan 8.77 GB/s 13.17 GB/s 1.5×
Single read::<u64> (memflow) 278.7 ns 79.1 ns 3.5×
Batched 512 × 8 B (memflow) 131.1 ns 55.8 ns 2.3×

Linking a page costs ~479 ns, paid once.

The win concentrates where a debugger lives: small scattered reads and re-reads of hot regions. Bulk scans gain least, since process_vm_readv is already near copy speed there. Batching doesn't help krw — there's no per-call cost left to amortize — but it helps process_vm_readv a lot, which is why the batched ratio is smaller. The gap between 8 ns raw and ~56 ns through memflow is memflow's own API overhead, which is backend-independent.

memflow-native already batches into one syscall per IOV_MAX iovecs, so a batched-ioctl design would have gained nothing. That was measured and abandoned before any kernel code was written.

Trade-offs

krw is a debugger backend. The debugger owns address validity, and the module does not pay to defend against it. Two consequences, both deliberate:

Stale shadow PTEs. The fault path is not serialized against the invalidate callback, so a page freed between resolution and insertion leaves a mapping no later invalidation clears.

Writes don't break COW. A read fault installs a read-only shadow; a later write is promoted by the kernel's write-protect path without re-entering our handler, so it never resolves with FOLL_WRITE. The write lands on the shared page — poking 0xCC into a shared library would be visible to every process mapping it. tests/test_cow.c demonstrates this and fails by design; it does not gate the suite.

Also worth knowing: attaching holds the target's mm alive for the fd's lifetime, huge pages are linked as 4 KiB pfns (512 faults per 2 MiB), windows are never unmapped, and the SIGBUS guard installs process-wide signal handlers.

Layout

Path What
kernel/ The module: char device, fault handler, MMU notifier.
include/krw.h UAPI header, shared by the module and the C tests.
memflow-krw/ The memflow MemoryView backend.
tests/, bench/ C correctness tests and benchmarks.
vm/ Throwaway-VM harness.

About

Efficient virtual memory inspection with memflow

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages