Every phase before this one read a process — its memory maps, its perf events, its stacks. Instrumentation writes: it stops the target, borrows its registers, executes syscalls in its address space, and rewrites the first bytes of its functions to jump into injected code. It is the part of Orbit that most looks like a debugger, and the part where a bug corrupts a live process rather than mis-drawing a flame graph. Phase 6 ports it bottom-up, each layer verified before the next stands on it.
6a — the substrate, verified by a tracee's own eyes
orbit-ptrace ports attach/detach, tracee-memory read/write through /proc/<pid>/mem, and the executable-region scan. The verification sets the tone for the whole phase: instead of the string-matching C++ test (whose assertions grep for OrbitBase's error text — exactly the re-implemented error types the blog argues against), a live round trip forks a child that spins on a byte, attaches, writes 0x42 over it, and the resumed child exits with that code. The tracee itself confirms the write landed. A region-scan differential holds identical against the C++, sabotage-checked on the [vsyscall] skip.
6b — executing a syscall you were not invited to
How do you call mmap inside another process? You borrow eight bytes of its executable memory, write the two-byte syscall instruction 0f 05, point its instruction pointer at those bytes with the arguments in the SysV syscall registers, single-step one instruction, read the result out of rax, and put everything back. orbit-ptrace ports that — register backup/restore over PTRACE_GETREGSET (the extended state kept as an opaque blob, its size read back from the kernel rather than computed by cpuid), the injector itself, and MemoryInTracee on top (mmap/munmap/mprotect).
This is the first ported piece that writes to a live process's registers and runs code in it, so safety is a property, not a hope: a Rust Drop guard restores the registers and the borrowed bytes on every exit path, the way the C++ leans on shared_ptr deleters. The differential runs the whole lifecycle — mmap, write+read a marker, flip to executable and back, munmap, confirm the page is gone — with the C++ and the Rust each instrumenting an identical child, and requires every observable outcome to agree. Addresses are ASLR-random, so behavior is what's compared; it was IDENTICAL across five rounds, and mis-mapping one syscall argument register diverged it.
6c — where does a trampoline go?
A trampoline has to sit within a 32-bit relative jump — ±2GB — of the function it hooks, so the jump there and back fits in a four-byte offset. orbit-trampoline ports the search: build the taken-range map of the process (the [0, mmap_min_addr) guard plus every mapping, touching ones joined), then probe for a free page-aligned slot first just below the code, then just above. Pure logic over a maps parse — so the differential is byte-exact: the live scan compared range-for-range, and a battery of synthetic layouts fed to both implementations for identical chosen ranges.
One sabotage is worth recording for what it didn't do: flipping the page rounding from down to up changed nothing, because real map addresses are page-aligned and the two roundings coincide. The differential correctly reported no difference — a reminder that a sabotage check proves the harness bites only when the sabotage is actually reachable; a mis-computed placement address (the reachable kind) diverged immediately.
6d — the hardest single file
When you overwrite a function's first bytes with a jump, you have to move the instructions you displaced somewhere else and run them there — but any instruction encoded relative to the instruction pointer breaks when it moves. RelocateInstruction is the 250-line heart of Trampoline.cpp that fixes them: RIP-relative memory operands get their displacement adjusted; relative jumps become a jump through an absolute address; conditional jumps get their condition inverted and hop over an absolute jump; calls and loops are refused by design; everything else is copied.
The C++ decodes with capstone. The Rust decodes with iced-x86 — a pure-Rust x86 decoder, one transitive dependency — but emits the same bytes, mirroring the C++'s hand-written sequences rather than using iced's own re-encoder, so the two are byte-comparable. And they were compared, at scale: the differential disassembles the binary's own executable pages and relocates every instruction both ways at matched addresses.
| Outcome | Count | Meaning |
|---|---|---|
| byte-for-byte identical | 888,154 | relocated code and absolute-address position matched exactly |
| rejected by both | 74,451 | calls and loops, refused on both sides |
| mismatches | 0 | — |
Nearly a million real instructions, zero divergences. Inverting the conditional-jump condition with the wrong bit diverged 44,026 of them — the harness bites. This is the largest corpus in the port after the ELF symbol differential, and unlike synthetic tests it is whatever the compiler actually emitted.
6e & 6f — assembling the trampoline
The last two milestones compose the pieces into the buffer that actually gets written into the target. 6e ports the fixed code sequences — the register backup (nine general-purpose registers, then all sixteen vector registers, the width chosen by AVX support, over a 32-byte-aligned stack), the payload call, the restore, and the whole return trampoline around the exit payload. These are constant byte streams, locked two ways: golden-byte unit tests transcribed from the C++ source, and a live differential comparing every stage against the C++ emitters — byte-identical.
6f assembles them. build_trampoline lays down the backup/call/restore blocks, relocates the function's prologue instruction by instruction (6d) until five bytes have moved — keeping a map so a forward jump into a later prologue instruction is repointed at its moved copy — and writes the jump back. Its differential disassembles the binary's own .text and builds a whole trampoline both ways at every real function start: 93,696 whole trampolines byte-for-byte identical, 11,025 correctly rejected (harmful jumps, undisassemblable prologues), zero mismatches. The mechanism is done and verified against real code end to end of the byte production.
What Phase 6 has left
Every algorithmic and code-generation part of instrumentation is now ported and byte-verified: the ptrace substrate, syscall injection, placement, the 888k-instruction relocator, the code sequences, and the whole-trampoline builder. What remains is InstrumentProcess — the live installation that allocates the trampolines in the tracee, writes them, overwrites the function prologues with jumps, and keeps the instrumented-function registry. That glue is driven by the service's instrumentation request, so it belongs with the service port, and its verification is end-to-end — instrument a real function and watch the payload run. That is the seam where Phase 6 hands off to Phase 7.