XRef 0.3.0: from a running process back to the instruction that wrote a field
XRef 0.3.0 adds live inspection of Linux x86-64 processes and ties it to the same static analysis: find a value in memory, translate pointers to static addresses, count calls, and catch the store that changes a field. A recorded walkthrough on a stripped fixture, and the limits.
A static tool answers what a stripped binary could do. Some questions are about what one
running copy of it is doing: where a value lives right now, how often a function is called,
which instruction just changed a field. Until this release XRef could only answer the first
kind. XRef 0.3.0 adds xref live, which
looks at a running Linux x86-64 process from outside and, for addresses inside the selected
executable, hands them back as static addresses to the same analysis the rest of the tool uses.
The release is tag v0.3.0 (commit 6388c5e). It also renames the product from XRef Code to
XRef; the command is still xref, the crate still xref-code.
v0.3.0
Four levels
The live guide builds the feature in four stages:
- L1, read only.
live mapsreads/proc/<pid>/maps, checks that the file given with-bis the executable that is running, and computes the PIE slide between runtime and static addresses.live resolveturns runtime addresses into static ones.live memscansearches private writable memory for 64-bit values and classifies the words around each hit. - L2, probes.
live probeplaces uprobes through perf events on chosen functions of that one process and reports hit counts, the calling site from the return address, and sampled registers. - L3, watchpoints.
live watcharms an x86 hardware write watchpoint on an address and reports the instruction that most likely wrote it. - L4, MCP.
xref mcpstays a read-only static server. A live tool is exposed only when the server is started with--allow-live-pidfor one verified process.
One field, end to end
The repository ships a recorded walkthrough on a small
stripped program from its own test fixtures, a daemon that keeps a “vault” structure on the heap
and changes it on commands such as DEPOSIT. The walkthrough starts from one known fact, a
16-byte signature at the start of the structure, and ends at the instruction that writes one of
its fields.
live maps first confirms that the file is the running program
(match verified_build_id) and gives the slide for that run. live memscan finds the
signature on the heap and, next to it, a code pointer and a string pointer. live resolve
translates both into static addresses, and from there the ordinary static commands take over:
refs finds the code that stores the string, c prints the deposit handler as pseudo-C.
Then the process is observed while it runs. With a probe on the deposit handler armed, the
recording sends two deposits and a key rotation; the probe reports two hits on the handler and
one on the function stored in the structure, and names the caller of each from its return
address. With a watchpoint on the field at offset 0x28, two more deposits produce this
(abridged):
watch runtime 0x55819217ddc8 static - len 8B access write region [heap]+0xdc8 (rw-p) init 0xfedcba99ddb00a59 latest 0xfedcba99db270a59 hits 2
writer 0x1fa7d (trap 0x5581807e9a81 / static 0x1fa81) fn 0x1f9f0 (-) insn "mov [r15+0x28], rax" conf candidate_linear_predecessor_store hits 2
The CPU raises the trap after the store, at static 0x1fa81. XRef names the store before it,
mov [r15+0x28], rax at 0x1fa7d in function 0x1f9f0, the same deposit handler that the
static pass decompiled. The output says how sure it is: the writer is a
candidate_linear_predecessor_store, and the value is read from the source register at the
trap, not proven to be what the store wrote. The value read back from memory after the window
matched the last sampled one in this run, and the walkthrough says that this alone would not
prove it.
A fixed address stops being useful when the data moves. For a growable buffer, watch --follow ptr_slot=<ADDR> watches the pointer slot and the element it points to, and re-arms on the new
allocation after a reallocation; the last step of the walkthrough does exactly that. There the trap
address is a branch target, so XRef names no single writer or value (ambiguous_branch_target,
unknown), and the hit counts are marked as a lower bound: a write to the new buffer made before
its pointer is published happens before the watch can move.
What the recording is
Every command and output above comes from the walkthrough at tag v0.3.0, which records its own
provenance: an xref binary built from the main branch at d2085bb that still identified itself
as 0.2.0, on a Linux 5.15 development host, on 2026-11-03. It is a pre-release build of the live
code, not the published 0.3.0 binary, and the fixture is a test program written for the purpose,
not a real application. Nothing here was rerun for this post. PIDs and addresses belong to that
run; ASLR changes them on every start.
Limits
- Linux x86-64 only, one target executable per session. The macOS builds report live commands as unsupported.
- Privileges.
mapsandresolveread process metadata from/proc(stat,maps,smaps,exe) together with the image file;memscanalso opens/proc/<pid>/mem, and so doeswatchfor its memory snapshots. The recording ran them as the same user against a fixture that allows tracing.probeandwatchopen perf events on another process and need root orCAP_PERFMON; in the recording only those two ran undersudo, only against the fixture. - They touch the process. Probes and watchpoints add traps to the observed program, which
keeps running; the probes do not stop it or attach to it through
ptrace. - Not every address has a static one. A heap address, like the watched field, has none
(
static -in the output), and callers and writers inside shared libraries are reported only as outside the target, without a static function or value. - Evidence, not proof. A return address can come from a tail call, register values do not prove source types, and a watchpoint’s writer is a candidate. The tags in the output say which of these applies.
The rule the feature follows is the one the static side already had: wherever a runtime observation points into the selected executable, it comes back as a static address, so the next question can be asked of the binary itself.
Cite this article
Alexander Panasenko (2026-11-03). XRef 0.3.0: from a running process back to the instruction that wrote a field. https://prod.codes/blog/xref-0-3-0-from-a-running-process-to-the-store/