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.
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.
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 VMmake 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.
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.
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.
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.
| 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. |