release · · 4 min

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 maps reads /proc/<pid>/maps, checks that the file given with -b is the executable that is running, and computes the PIE slide between runtime and static addresses. live resolve turns runtime addresses into static ones. live memscan searches private writable memory for 64-bit values and classifies the words around each hit.
  • L2, probes. live probe places 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 watch arms an x86 hardware write watchpoint on an address and reports the instruction that most likely wrote it.
  • L4, MCP. xref mcp stays a read-only static server. A live tool is exposed only when the server is started with --allow-live-pid for 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. maps and resolve read process metadata from /proc (stat, maps, smaps, exe) together with the image file; memscan also opens /proc/<pid>/mem, and so does watch for its memory snapshots. The recording ran them as the same user against a fixture that allows tracing. probe and watch open perf events on another process and need root or CAP_PERFMON; in the recording only those two ran under sudo, 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
Citation
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/