A quick honest scope, before the fun part: this works best on simple, focused targets — a CLI tool, a small daemon, a single-threaded worker. Point it at something with real weight instead — a GPU-accelerated desktop app, a browser, anything juggling live sockets and dozens of threads — and it hits real walls fast: GPU memory, kernel-injected pages, live file descriptors, none of which a snapshot can carry across. Both ends of that range are demonstrated below, including exactly where and why the harder end stops.
What "cloning a process" actually means
Every static loader mwemu already had — ELF, PE, Mach-O, .ko — starts
from a file on disk and does the linker's job itself: place sections, apply relocations,
build an argv/auxv, hand control to an entry point. A
live process has already paid that cost. ld.so already ran, every
shared library already sits at its final address, every relocation is already patched
in. So cloning a running process is a different, in some ways easier problem: don't
build any of that — just copy what's already there, byte for byte, register for
register, and resume from the exact instruction it was on.
ptrace, not gdb
The mechanism is deliberately small and lives entirely outside mwemu's core, in a
standalone tool (mwemu-snapshot) so the "unsafe, talks to the kernel
directly" code never touches the emulator itself:
PTRACE_ATTACH+PTRACE_GETREGS— every general-purpose register, includingfs_base/gs_base(the real TLS base on Linux, not a Windows-style segment selector — mwemu already treats it that way on the Linux code path)./proc/<pid>/maps, parsed by hand — the format hasn't changed in decades, and a 20-line parser turned out more robust than a general-purpose crate (see below).process_vm_readvto bulk-copy every readable region in one syscall per mapping, instead ofPTRACE_PEEKDATAa word at a time.
That triple — registers, memory, done — builds a real SerializableEmu
directly, the same internal type mwemu's Windows minidump loader already produces. No
new file format, no round-trip: .into() turns it into a live
Emu on the spot.
Why not minidump
The obvious first design was: ptrace via
minidump-writer,
producing a real .dmp, read by the MinidumpReader mwemu already
had for Windows crash dumps. It's a dead end as shipped — its
MemoryInfoList section parses /proc/pid/maps with a crate that
chokes on this kernel, and that failure is hard-coded fatal to the *entire* dump, even
though it's metadata mwemu's own reader doesn't strictly need. Forking a dependency to
route around a section nobody reads wasn't worth it — talking to
/proc/pid/maps and ptrace directly, with
nix, sidesteps the whole file format.
First proof: sleep 300, and a clock with no wristwatch
Spawning a plain sleep 300, snapshotting it mid-startup, and resuming the
snapshot under mwemu replayed the real ld.so/glibc boot sequence faithfully
— real mmaps of the real libc.so.6, real
arch_prctl(ARCH_SET_FS), real getrandom — all the way into
sleep's own main(). Which then printed a real, honest error:
sleep: invalid time interval '300'
Try 'sleep --help' for more information.
Not a bug in sleep — a gap in the clone. Only general-purpose registers
get captured (PTRACE_GETREGS), not FPU/SSE/AVX state. GNU sleep
parses fractional seconds with xstrtod, which uses SSE — and an emulator
that starts with a zeroed FPU instead of the process's real one gets that comparison
wrong. A clean, fully-explained failure is exactly what a healthy tool should produce.
Second proof, and the real wall: a live Telegram Desktop
The obvious next target: something with actual weight. mwemu-snapshot
against a real, running Telegram Desktop — over a thousand mapped regions, 60 threads —
attached, copied, and resumed the same way. It replayed real dynamic-linker startup all
over again, until it hit this:
/!\ error dereferencing dword on 0x7f4a4b74c000
unhandled exception... type = DWordDereferencing — stopping emulation
That address is [vvar] — a page the kernel injects into every process to
serve clock_gettime() without a real syscall. It, and a hundred-plus
anon_inode:i915.gem regions alongside it (GPU buffer objects — Telegram
decodes a lot of video: stickers, GIFs, calls), share the same root problem:
process_vm_readv can't read them at all. They aren't ordinary memory; they
have no backing page in the normal sense, only a kernel fault handler that fills them
in on the fly.
The tempting fix — since [vvar]'s content is identical for every
process on the same kernel, just substitute our own copy at the target's captured
address — turned out to be a genuine dead end, and an instructive one. Reading it via
/proc/self/mem fails for exactly the same underlying reason
(get_user_pages can't follow this kind of mapping, ours or anyone else's).
A raw pointer load bypasses that — and crashed the tool outright with SIGBUS,
despite /proc/pid/maps reporting the page as perfectly readable
(r--p). The permission bit lied, or rather, described something a plain CPU
load doesn't honor for this class of page. Twice. On my own machine.
The fix was to stop trying. A live crash on the analyst's own box is a strictly worse
outcome than a page that's simply absent — mwemu already turns "absent" into a clean,
catchable, well-described stop, the same one sleep hit. GPU memory can't be
substituted this way at all regardless; it's live rendering state with no equivalent
anywhere else. If [vvar] support is worth having later, the right place for
it is the *replay* side, inside mwemu's own sandboxed context — synthesizing a page when
it recognizes the pattern — not a live read of dangerous kernel memory from the tool
doing the capturing.
Third proof: a process stuck reliving the same second
To get a clean answer with none of Telegram's noise, I wrote the smallest possible real workload: single-threaded, no GPU, no sockets — just a counter written to a file, read back, printed, and a two-second sleep, forever:
int main(void) {
const char *path = "/tmp/dummy_counter.txt";
long counter = 0;
for (;;) {
char buf[64];
int n = snprintf(buf, sizeof(buf), "counter=%ld pid=%d\n", counter, getpid());
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { write(fd, buf, n); fsync(fd); close(fd); }
fd = open(path, O_RDONLY); // read it back too
if (fd >= 0) {
char readback[64] = {0};
read(fd, readback, sizeof(readback) - 1);
close(fd);
printf("wrote and read back: %s", readback);
}
counter++;
sleep(2);
}
}
Launched, then cloned mid-flight from a completely unrelated shell — a real
ptrace(PTRACE_ATTACH) on someone else's PID, not a spawned child, so Yama's
default ptrace_scope=1 demands root:
sudo mwemu-snapshot attach $(pgrep -x dummy) -o /tmp/dummy_snapshot.mwemu
Then handed straight to the CLI to resume:
mwemu -f dummy -d /tmp/dummy_snapshot.mwemu -v
22 regions captured, only the same three unreadable kernel pages
skipped, no crash. And it kept running, syscall after real syscall, exactly as
written:
Thirty million instructions in, and the counter never moved past 101 — the exact
value it held at the instant of capture. The loop is real: it opens, writes, fsyncs,
closes, reopens, reads, prints, sleeps — every syscall fires, in order, forever. What
doesn't happen is the one plain counter++ between iterations. A process
cloned mid-flight keeps its body but not, it seems, its sense of time passing — it can
act out the same beat indefinitely without ever quite finishing the step that would move
it forward. Whether that's a scheduling/memory-commit quirk specific to how the loop
re-enters after a snapshot boundary, or something narrower, is open — logged as exactly
that: a real, reproducible, currently unexplained finding, not a guess dressed up as an
answer.
What this is actually good for
Not, it turns out, replacing a debugger. Cloning the whole live state of a serious process — GPU-accelerated, many threads, real sockets — runs into walls that aren't about this specific tool: GPU buffers, live kernel objects, and now a clock that won't advance are all things a mid-flight snapshot fundamentally can't carry across. Tools built to do this for real either record a process from birth, intercepting every non-deterministic input as they go, or work a full layer down, snapshotting an entire virtual machine. Reaching into an already-running native process and expecting to move it wholesale into a different execution engine was always the ambitious version of the idea.
The narrower version holds up well, and matches how the rest of this project's kernel driver work has gone: a debugger is the right tool for finding where something interesting happens live; mwemu is the right tool for repeatable, instrumented, safe analysis of a narrow slice of it — one function, one buffer, one struct pulled out at a breakpoint — fuzzed with a real memory-safety ledger watching, without risking or even needing the process it came from to still be there.
References
- mwemu — the emulator
- A kernel without a kernel — the toy-driver chapter
- A beacon, a scan, and one byte of stack — the real-driver chapter
- nix — the ptrace/process_vm_readv bindings used here
Every target here was either spawned for the purpose (the
sleep and counter-loop examples) or the author's own running process
(Telegram Desktop, on the author's own machine). Nothing here reads, stores, or shares
anything from a process that wasn't the author's own.