Add process-scoped scanning algorithms: multi-pattern, code-xref, pointer-map, dissection, snapshot diff

All are OS-agnostic handlers keyed by vmie_mem* + cr3, built on the windowed
sweep / region walk / matcher; none names a Windows concept and each compiles
against include/ alone.

Scanning: a compiled multi-pattern automaton (Aho-Corasick over each pattern's
longest literal anchor, then a masked verify) finds N signatures in one sweep
pass (sigscan.h sigset; scan.h gva_sig_scan_multi). gva_code_xref decodes
rel32 call/jmp and RIP-relative lea/mov to find every instruction targeting a
given VA.

Pointer graph (pmap.h): one sweep indexes every qword whose value lands in a
mapped region into reverse + forward edges. pmap_referrers is the keystone -
it answers who-points-here, class-instance enumeration (referrers of a vtable
VA), and string xref (referrers of a string VA) from the same index;
pmap_paths is the indexed counterpart to scan_pointer's one-shot DFS;
struct_dissect classifies the qwords of an instance (pointer/vtable/float/
int/string) into a field map.

Temporal (snapdiff.h): snap_take captures a window's bytes, snap_diff reports
the changed runs against a later read.
This commit is contained in:
2026-06-16 17:38:10 +03:00
parent 25b8ed8ca9
commit c36ffe295d
9 changed files with 1160 additions and 1 deletions
+5 -1
View File
@@ -18,7 +18,11 @@ add_library(vmie STATIC
src/engine/win32/profile.c src/engine/win32/profile.c
src/engine/win32/text.c src/engine/win32/text.c
src/handlers/scan.c src/handlers/scan.c
src/handlers/sigscan.c) src/handlers/sigscan.c
src/handlers/sigset.c
src/handlers/codescan.c
src/handlers/pmap.c
src/handlers/snapdiff.c)
target_include_directories(vmie target_include_directories(vmie
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # public API: include/*.h PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # public API: include/*.h
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/core/include # private: core.h PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/core/include # private: core.h
+82
View File
@@ -0,0 +1,82 @@
/* pmap.h - pointer-graph index and structure analysis (OS-agnostic handler).
*
* Layered above the memory-model contract (memmodel.h) and the scanning surface
* (scan.h, for `range` and `scan_ptr_path`). A `pmap` is a one-pass reverse +
* forward index of every intra-address-space pointer under a `cr3`: for each
* 8-byte-aligned qword whose VALUE lands inside a mapped region, it records the
* edge `referrer_va -> target_va`. Two sorted views answer the keystone queries
* in O(log n): who-points-here (referrers) and what-does-this-point-to
* (targets). Everything is keyed by `vmie_mem* + cr3`; it names no Windows
* object.
*
* Ownership: pmap_build / pmap_free (create/destroy). All queries are read-only
* and re-entrant against a built pmap; pmap_free is safe on NULL.
*/
#ifndef VMIE_PMAP_H
#define VMIE_PMAP_H
#include <stdint.h>
#include <stddef.h>
#include "memmodel.h" /* vmie_mem, vregion, range */
#include "scan.h" /* scan_ptr_path, SCAN_PTR_MAXDEPTH */
typedef struct pmap pmap; /* reverse + forward index (opaque) */
/* One gva_sweep over [lo,hi] (prot filter): for every 8-byte-aligned qword whose
* VALUE lands inside a mapped region (membership tested against a gva_regions
* set), record the edge referrer_va -> target_va. Stores two sorted views (by
* target, by referrer) for O(log n) queries. Returns NULL on OOM or bad input. */
pmap* pmap_build(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi, uint32_t prot_any);
/* Release a pmap from pmap_build. Safe on NULL. */
void pmap_free(pmap* pm);
/* All VAs holding a pointer whose value == target_va. THE keystone query:
* who-points-here; vtable-instance enumeration (target = a vtable VA, since an
* object's first qword is its vtable); string-xref target. Writes up to `max`
* referrer VAs to `out` (NULL to count only) and returns the TOTAL. */
int pmap_referrers(const pmap* pm, uint64_t target_va, uint64_t* out, int max);
/* Forward edges: pointer VALUES stored at/around referrer_va (what this region
* points to). For path walking / dissection. Writes up to `max` target VAs to
* `out` (NULL to count only) and returns the TOTAL. */
int pmap_targets(const pmap* pm, uint64_t referrer_va, uint64_t* out, int max);
/* Map-accelerated pointer paths to `target`, anchored on module ranges `mods`.
* Same result type as scan_pointer. Cost profile differs deliberately:
* scan_pointer is a ONE-SHOT live DFS that builds no index (low memory for a
* single query); pmap_paths runs over the ALREADY-BUILT index (one sweep amort-
* ized across many cheap queries). Not a duplicate path - a different trade-off.
* Writes up to `max` paths to `out` and returns the TOTAL, or -1 on bad input. */
int pmap_paths(const pmap* pm, uint64_t target, const range* mods, int nmods,
int max_depth, uint32_t max_off, scan_ptr_path* out, int max);
/* string-xref: find the needle bytes anywhere in the AS (matcher over the
* sweep), then pmap_referrers for each occurrence's VA. The caller pre-encodes
* the byte image (e.g. a UTF-16 string), so `needle`/`nlen` are matched as raw
* bytes. Writes up to `max` referrer VAs to `out` (NULL to count only) and
* returns the TOTAL number of referrers, or -1 on bad input. */
int xref_string(vmie_mem* m, uintptr_t cr3, const pmap* pm,
const void* needle, size_t nlen, uint64_t* out, int max);
/* ---- structure dissection ------------------------------------------------ *
* Classify each 8-byte slot in [va, va+nbytes). */
typedef enum {
FK_UNKNOWN, FK_PTR, FK_VTABLE, FK_F32, FK_F64, FK_I32, FK_I64, FK_ASCII, FK_UTF16
} field_kind;
typedef struct {
uint32_t off; /* byte offset from va */
field_kind kind;
uint64_t raw; /* the raw 8 bytes at off */
uint64_t target; /* pointee VA for FK_PTR/FK_VTABLE, else 0 */
} field_desc;
/* Classify each slot: PTR = value lands in a mapped region. VTABLE = value
* points into a non-writable region whose first qwords are themselves pointers
* into X-regions. F32/F64 = finite, sane magnitude. ASCII/UTF16 = printable run
* >= 4. Else I32/I64/UNKNOWN. Reuses gva_read + gva_regions (+ pm if given, may
* be NULL). Writes slots to `out` and returns the number written (<= max). */
int struct_dissect(vmie_mem* m, uintptr_t cr3, uint64_t va, size_t nbytes,
const pmap* pm, field_desc* out, int max);
#endif /* VMIE_PMAP_H */
+28
View File
@@ -53,6 +53,34 @@ int scan_pointer(vmie_mem* m, uintptr_t cr3, const range* mods, int nmods,
uint64_t target, int max_depth, uint32_t max_off, uint64_t target, int max_depth, uint32_t max_off,
scan_ptr_path* out, int max); scan_ptr_path* out, int max);
/* ---- multi-pattern + code-xref bridges (over sigscan.h / gva_sweep) ------ *
* Same windowed-seam discipline as gva_sig_scan, but for a compiled sigset and
* a heuristic rel32 decoder. Both stream guest memory through gva_sweep and
* report VAs in the guest's own coordinate space. */
/* One attributed multi-pattern hit: which compiled pattern, and where. */
typedef struct { int pattern; uint64_t va; } sig_multi_hit;
/* Windowed multi-pattern scan over [lo,hi]: drives sig_set_each on each window,
* seam-deduped like gva_sig_scan. The sweep overlap is (longest pattern len - 1)
* = sigset_maxlen(s) - 1, so no full pattern is split at a window boundary.
* Writes up to `max` hits to `out` (NULL to count only) and returns the TOTAL
* number of hits, or -1 on a NULL/empty sigset. */
int gva_sig_scan_multi(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
uint32_t prot_any, const sigset* s,
sig_multi_hit* out, int max);
/* code-xref: every instruction in the X-regions of [lo,hi] whose rel32 operand
* targets `target_va`. Heuristic decoder (NOT a full disassembler): recognizes
* E8 call / E9 jmp (next_rip + disp32) and the RIP-relative ModRM forms
* (mod=00, rm=101) of lea/mov (REX.W 8D / 8B) where target = next_rip +
* (int32)disp. Records each matching instruction-start VA. The sweep forces
* VR_X and carries a >=15-byte overlap (max x86 instruction length) so no
* instruction is cut at a window seam. Writes up to `max` VAs to `out` (NULL to
* count only) and returns the TOTAL number of matches, or -1 on bad input. */
int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
uint64_t target_va, uint64_t* out, int max);
/* gva bridges to the signature matcher: build mem_view from guest memory and feed sigscan.h */ /* gva bridges to the signature matcher: build mem_view from guest memory and feed sigscan.h */
int gva_sig_scan (vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi, int gva_sig_scan (vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
uint32_t prot_any, const sig_pattern_t* p, uint64_t* out, int max); uint32_t prot_any, const sig_pattern_t* p, uint64_t* out, int max);
+30
View File
@@ -79,4 +79,34 @@ uint64_t sig_rip(mem_view_t v, uint64_t hit_va, size_t disp_off, size_t instr_le
* is actually available. Useful for narrowing a scan to a [start,end] window. */ * is actually available. Useful for narrowing a scan to a [start,end] window. */
mem_view_t mem_sub(mem_view_t v, uint64_t start_va, size_t size); mem_view_t mem_sub(mem_view_t v, uint64_t start_va, size_t size);
/* ---- compiled multi-pattern matcher (Aho-Corasick anchors) --------------- *
* A sigset compiles N patterns into one automaton scanned in a single pass. It
* is still PURE (only mem_view_t, no vmie_mem). Each pattern contributes its
* longest contiguous non-wildcard run as a literal anchor; an Aho-Corasick goto
* over those anchors finds candidate sites, and on an anchor hit the FULL masked
* pattern is verified (mem_sub + mask compare) before the match is reported.
* This is the building block under gva_sig_scan_multi (see scan.h). */
typedef struct sigset sigset; /* compiled automaton (opaque) */
/* Compile `n` patterns into a sigset. The patterns are borrowed for the call
* only (their bytes are copied into the automaton). Returns NULL on OOM, on
* n <= 0, or if any pattern is empty / all-wildcard (no literal anchor). Release
* with sigset_free(). */
sigset* sigset_compile(const sig_pattern_t* pats, int n);
/* Release a sigset produced by sigset_compile. Safe on NULL. */
void sigset_free(sigset* s);
/* Invoke cb(user, pat_index, match_va) for every full-pattern match of any
* compiled pattern in `v`, anchor-driven (not necessarily in ascending order
* across patterns). `cb` returns nonzero to stop early. The longest-anchor
* length is what a windowed caller uses as overlap to de-dup across seams. */
void sig_set_each(const sigset* s, mem_view_t v,
int (*cb)(void* user, int pat, uint64_t va), void* user);
/* Longest compiled pattern length, in bytes. A windowed sweep carries
* (this - 1) leading-overlap bytes so no full pattern is split at a seam (the
* gva_sig_scan_multi overlap contract). 0 on NULL. */
size_t sigset_maxlen(const sigset* s);
#endif /* VMIE_SIGSCAN_H */ #endif /* VMIE_SIGSCAN_H */
+34
View File
@@ -0,0 +1,34 @@
/* snapdiff.h - per-process temporal snapshot + diff (OS-agnostic handler).
*
* A `snapshot` captures the bytes of every mapped run in a VA window under a
* `cr3` at time T0. snap_diff re-reads the same window now and emits the runs
* whose bytes changed (coalesced VA-contiguous diffs), including runs that
* appeared or disappeared since T0. Keyed by `vmie_mem* + cr3`; it names no
* Windows object.
*
* Ownership: snap_take / snap_free (create/destroy). snap_free is safe on NULL.
*/
#ifndef VMIE_SNAPDIFF_H
#define VMIE_SNAPDIFF_H
#include <stdint.h>
#include <stddef.h>
#include "memmodel.h" /* vmie_mem, vregion */
typedef struct snapshot snapshot;
/* Capture the bytes of every mapped run in [lo,hi] (prot filter) under `cr3` at
* T0. Returns a heap-owned snapshot, or NULL on OOM / bad input. */
snapshot* snap_take(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi, uint32_t prot_any);
/* Release a snapshot from snap_take. Safe on NULL. */
void snap_free(snapshot* s);
/* Re-read the window now, compare to the snapshot, and emit changed runs as
* vregion {va, len, prot = current} - coalescing VA-contiguous changed bytes
* into one run. Runs that appeared or disappeared since T0 count as changed.
* Writes up to `max` runs to `changed` (NULL to count only) and returns the
* TOTAL number of changed runs, or -1 on bad input. */
int snap_diff(const snapshot* s, vmie_mem* m, uintptr_t cr3,
vregion* changed, int max);
#endif /* VMIE_SNAPDIFF_H */
+130
View File
@@ -0,0 +1,130 @@
/* codescan.c - windowed multi-pattern scan + heuristic rel32 code-xref.
*
* Both bridges stream guest memory through gva_sweep and report guest VAs:
* gva_sig_scan_multi - drives a compiled sigset over each window, seam-deduped
* (overlap = longest pattern len - 1).
* gva_code_xref - heuristic decode of the rel32 instruction forms in
* X-regions; records instruction starts whose computed
* target equals target_va. Overlap >= 15 (max x86 insn
* length) keeps an instruction whole across a seam.
*
* Handler boundary: only memmodel.h / scan.h / sigscan.h.
*/
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "memmodel.h"
#include "sigscan.h"
#include "scan.h"
/* x86-64 maximum instruction length; the code-xref sweep overlap. A decoded
* instruction may be up to this long, so a window must carry this many leading
* bytes to re-present an instruction split at the previous seam. */
#define X86_MAX_INSN 15
/* ---- multi-pattern scan -------------------------------------------------- */
struct multi_cb {
const sigset* s;
sig_multi_hit* out; int max, n;
uint64_t win_base; size_t win_len, win_ov; int win_last;
};
__attribute__((hot))
static int multi_hit(void* u, int pat, uint64_t va) {
struct multi_cb* c = u;
const size_t off = (size_t)(va - c->win_base);
if (!c->win_last && c->win_len > c->win_ov && off >= c->win_len - c->win_ov) {
return 0; /* trailing overlap: next window owns it */
}
if (c->out && c->n < c->max) { c->out[c->n].pattern = pat; c->out[c->n].va = va; }
c->n++;
return 0;
}
__attribute__((hot))
static int multi_sweep_cb(void* u, const uint8_t* data, size_t len,
uint64_t base, size_t ov, int last) {
struct multi_cb* c = u;
c->win_base = base; c->win_len = len; c->win_ov = ov; c->win_last = last;
const mem_view_t v = { data, len, base };
sig_set_each(c->s, v, multi_hit, c);
return 0;
}
int gva_sig_scan_multi(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
uint32_t prot_any, const sigset* s,
sig_multi_hit* out, int max) {
const size_t maxlen = sigset_maxlen(s);
if (maxlen == 0) { return -1; }
struct multi_cb c; memset(&c, 0, sizeof c);
c.s = s; c.out = out; c.max = max;
if (gva_sweep(m, cr3, lo, hi, prot_any, maxlen - 1, multi_sweep_cb, &c) < 0) {
return -1;
}
return c.n;
}
/* ---- heuristic rel32 code-xref ------------------------------------------- *
* Decode just enough to recover a rel32 target. Two recognized shapes:
* E8/E9 disp32 (call/jmp) : start+5 + disp
* REX.W 8D|8B modrm(00,*,101) disp32 (lea/mov rip) : start+7 + disp
* The lea/mov form REQUIRES the REX.W prefix (0x48..0x4F with W set), per the
* 64-bit operand RIP-relative encoding; a bare 8D/8B is not accepted (it would
* also let the decoder re-recognize the same instruction one byte past its REX
* prefix). Returns the encoded length (>=5) and writes the target via *target,
* or 0 if `p[0..avail)` is not one of the forms. */
__attribute__((hot))
static size_t decode_rel32(const uint8_t* p, size_t avail,
uint64_t start_va, uint64_t* target) {
if (avail >= 5 && (p[0] == 0xE8 || p[0] == 0xE9)) {
int32_t disp; memcpy(&disp, p + 1, 4);
*target = start_va + 5 + (int64_t)disp;
return 5;
}
/* REX.W prefix (0x48..0x4F: bit 3 = W), then 8D/8B with RIP-rel ModRM */
if (avail >= 7 && (p[0] & 0xF8) == 0x48 && (p[1] == 0x8D || p[1] == 0x8B)) {
const uint8_t modrm = p[2];
if ((modrm & 0xC0) == 0x00 && (modrm & 0x07) == 0x05) { /* mod=00 rm=101 */
int32_t disp; memcpy(&disp, p + 3, 4);
*target = start_va + 7 + (int64_t)disp; /* rex op modrm disp32 */
return 7;
}
}
return 0;
}
struct xref_cb {
uint64_t target;
uint64_t* out; int max, n;
};
__attribute__((hot))
static int xref_sweep_cb(void* u, const uint8_t* data, size_t len,
uint64_t base, size_t ov, int last) {
struct xref_cb* c = u;
/* Decode at every byte offset (heuristic, overlapping). A match that STARTS
* in the trailing overlap of a non-last window is dropped: the next window
* re-presents that instruction whole in its leading overlap. */
const size_t limit = last ? len : (len > ov ? len - ov : 0);
for (size_t off = 0; off < len; off++) {
if (!last && off >= limit) { break; }
uint64_t tgt = 0;
const size_t ilen = decode_rel32(data + off, len - off, base + off, &tgt);
if (ilen && tgt == c->target) {
if (c->out && c->n < c->max) { c->out[c->n] = base + off; }
c->n++;
}
}
return 0;
}
int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
uint64_t target_va, uint64_t* out, int max) {
struct xref_cb c; memset(&c, 0, sizeof c);
c.target = target_va; c.out = out; c.max = max;
if (gva_sweep(m, cr3, lo, hi, VR_X, X86_MAX_INSN, xref_sweep_cb, &c) < 0) {
return -1;
}
return c.n;
}
+411
View File
@@ -0,0 +1,411 @@
/* pmap.c - pointer-graph index + structure dissection (OS-agnostic handler).
*
* pmap_build runs one gva_sweep, collecting every 8-aligned qword whose value
* lands in a mapped region as an edge referrer_va -> target_va, then sorts two
* index views (by target, by referrer) for O(log n) queries. pmap_referrers /
* pmap_targets are binary-search range scans over those views. pmap_paths walks
* the reverse index DFS (indexed analogue of scan_pointer's one-shot live DFS).
* xref_string finds a needle then resolves its referrers. struct_dissect
* classifies fixed-size slots by re-reading memory and testing membership.
*
* Handler boundary: only memmodel.h / scan.h / sigscan.h.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "memmodel.h"
#include "sigscan.h"
#include "scan.h"
#include "pmap.h"
#define PM_REG_CAP (1 << 16)
/* A directed pointer edge: a qword at `ref` holding the value `tgt`. */
typedef struct { uint64_t tgt, ref; } pedge;
struct pmap {
pedge* edges; /* all edges (insertion order) */
size_t nedge;
uint32_t* by_tgt; /* edge indices sorted by (tgt, ref) */
uint32_t* by_ref; /* edge indices sorted by (ref, tgt) */
vregion* regs; /* mapped region set used for membership tests */
int nregs;
};
/* ---- mapped-region membership (sorted regs, binary search) --------------- */
static int reg_cmp(const void* a, const void* b) {
const uint64_t x = ((const vregion*)a)->va, y = ((const vregion*)b)->va;
return (x > y) - (x < y);
}
__attribute__((hot))
static int in_mapped(const vregion* regs, int n, uint64_t v) {
int lo = 0, hi = n; /* find last region with va <= v */
while (lo < hi) {
const int mid = (lo + hi) / 2;
if (regs[mid].va <= v) { lo = mid + 1; } else { hi = mid; }
}
if (lo == 0) { return 0; }
const vregion* r = &regs[lo - 1];
return v >= r->va && v < r->va + r->len;
}
/* ---- index sort (data-oriented: sort packed keys, emit edge indices) ----- *
* qsort takes no user pointer, so rather than a context-keyed comparator the
* keys are packed into a self-contained array (primary, secondary, edge index)
* sorted by value; the sorted index column is then peeled off. */
typedef struct { uint64_t a, b; uint32_t idx; } skey;
static int skey_cmp(const void* x, const void* y) {
const skey* p = x; const skey* q = y;
if (p->a != q->a) { return p->a < q->a ? -1 : 1; }
if (p->b != q->b) { return p->b < q->b ? -1 : 1; }
return 0;
}
/* Build a sorted index `out` (len nedge) from edges, keyed (primary,secondary).
* primary_is_tgt selects (tgt,ref) vs (ref,tgt). Returns 0 / -1 on OOM. */
__attribute__((cold))
static int build_index(const pedge* edges, size_t nedge, int primary_is_tgt,
uint32_t** out) {
if (nedge == 0) { *out = NULL; return 0; }
skey* k = malloc(nedge * sizeof *k);
uint32_t* idx = malloc(nedge * sizeof *idx);
if (!k || !idx) { free(k); free(idx); return -1; }
for (size_t i = 0; i < nedge; i++) {
k[i].a = primary_is_tgt ? edges[i].tgt : edges[i].ref;
k[i].b = primary_is_tgt ? edges[i].ref : edges[i].tgt;
k[i].idx = (uint32_t)i;
}
qsort(k, nedge, sizeof *k, skey_cmp);
for (size_t i = 0; i < nedge; i++) { idx[i] = k[i].idx; }
free(k);
*out = idx;
return 0;
}
/* ---- sweep collector ----------------------------------------------------- */
struct pm_cb {
pedge* edges; size_t nedge, capedge;
const vregion* regs; int nregs;
int oom;
uint64_t win_base; size_t win_len, win_ov; int win_last;
};
static int edge_push(struct pm_cb* c, uint64_t ref, uint64_t tgt) {
if (c->nedge == c->capedge) {
const size_t nc = c->capedge ? c->capedge * 2 : 65536;
pedge* ne = realloc(c->edges, nc * sizeof *ne);
if (!ne) { c->oom = 1; return -1; }
c->edges = ne; c->capedge = nc;
}
c->edges[c->nedge].ref = ref;
c->edges[c->nedge].tgt = tgt;
c->nedge++;
return 0;
}
__attribute__((hot))
static int pm_sweep_cb(void* u, const uint8_t* data, size_t len,
uint64_t base, size_t ov, int last) {
struct pm_cb* c = u;
/* qword-aligned scan: start at the first 8-aligned VA in this window. Seam-
* dedup: an 8-aligned qword whose VA is in the trailing overlap of a
* non-last window is dropped (the next window re-presents it 8-aligned). */
size_t off = 0;
const size_t m = (size_t)(base & 7u);
if (m) { off = 8 - m; }
const size_t limit = last ? len : (len > ov ? len - ov : 0);
for (; off + 8 <= len; off += 8) {
if (!last && off >= limit) { break; }
uint64_t v; memcpy(&v, data + off, 8);
if (in_mapped(c->regs, c->nregs, v)) {
if (edge_push(c, base + off, v)) { return 1; }
}
}
return 0;
}
/* ---- build / free -------------------------------------------------------- */
__attribute__((cold))
pmap* pmap_build(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi, uint32_t prot_any) {
pmap* pm = calloc(1, sizeof *pm);
if (!pm) { return NULL; }
pm->regs = malloc((size_t)PM_REG_CAP * sizeof *pm->regs);
if (!pm->regs) { free(pm); return NULL; }
pm->nregs = gva_regions(m, cr3, lo, hi, prot_any, pm->regs, PM_REG_CAP);
if (pm->nregs < 0) { pm->nregs = 0; }
if (pm->nregs > PM_REG_CAP) { pm->nregs = PM_REG_CAP; }
qsort(pm->regs, (size_t)pm->nregs, sizeof *pm->regs, reg_cmp);
struct pm_cb c; memset(&c, 0, sizeof c);
c.regs = pm->regs; c.nregs = pm->nregs;
const int sw = gva_sweep(m, cr3, lo, hi, prot_any, 8, pm_sweep_cb, &c);
if (sw < 0 || c.oom) { free(c.edges); pmap_free(pm); return NULL; }
pm->edges = c.edges; pm->nedge = c.nedge;
if (build_index(pm->edges, pm->nedge, 1, &pm->by_tgt) ||
build_index(pm->edges, pm->nedge, 0, &pm->by_ref)) {
pmap_free(pm);
return NULL;
}
return pm;
}
__attribute__((cold))
void pmap_free(pmap* pm) {
if (!pm) { return; }
free(pm->edges);
free(pm->by_tgt);
free(pm->by_ref);
free(pm->regs);
free(pm);
}
/* ---- queries ------------------------------------------------------------- */
/* Lower bound of `key` in a sorted index `idx` over edges, keying on `field`
* (offsetof tgt or ref via a selector). Returns the first index position with
* key value >= key. */
__attribute__((hot))
static size_t idx_lb(const pedge* edges, const uint32_t* idx, size_t n,
int by_tgt, uint64_t key) {
size_t lo = 0, hi = n;
while (lo < hi) {
const size_t mid = (lo + hi) / 2;
const uint64_t kv = by_tgt ? edges[idx[mid]].tgt : edges[idx[mid]].ref;
if (kv < key) { lo = mid + 1; } else { hi = mid; }
}
return lo;
}
int pmap_referrers(const pmap* pm, uint64_t target_va, uint64_t* out, int max) {
if (!pm) { return -1; }
int total = 0;
for (size_t i = idx_lb(pm->edges, pm->by_tgt, pm->nedge, 1, target_va);
i < pm->nedge && pm->edges[pm->by_tgt[i]].tgt == target_va; i++) {
if (out && total < max) { out[total] = pm->edges[pm->by_tgt[i]].ref; }
total++;
}
return total;
}
int pmap_targets(const pmap* pm, uint64_t referrer_va, uint64_t* out, int max) {
if (!pm) { return -1; }
int total = 0;
for (size_t i = idx_lb(pm->edges, pm->by_ref, pm->nedge, 0, referrer_va);
i < pm->nedge && pm->edges[pm->by_ref[i]].ref == referrer_va; i++) {
if (out && total < max) { out[total] = pm->edges[pm->by_ref[i]].tgt; }
total++;
}
return total;
}
/* ---- indexed pointer paths (analogue of scan_pointer's one-shot DFS) ------ *
* scan_pointer builds no index and walks live memory for a single query; this
* walks the already-built reverse index, so a built pmap amortizes one sweep
* across many path queries. */
struct ppaths {
const pmap* pm;
const range* mods; int nmods;
uint32_t max_off; int max_depth;
scan_ptr_path* out; int max, n;
int32_t disc[SCAN_PTR_MAXDEPTH];
};
__attribute__((hot))
static int pp_in_module(const struct ppaths* P, uint64_t a) {
for (int i = 0; i < P->nmods; i++) {
if (a >= P->mods[i].base && a < P->mods[i].base + P->mods[i].size) { return 1; }
}
return 0;
}
static void pp_dfs(struct ppaths* P, uint64_t need, int hops) {
if (hops > 0 && pp_in_module(P, need) && P->n < P->max) {
scan_ptr_path* o = &P->out[P->n++];
o->base = need; o->depth = hops;
for (int k = 0; k < hops; k++) { o->off[k] = P->disc[hops - 1 - k]; }
}
if (hops >= P->max_depth || P->n >= P->max) { return; }
/* edges with tgt in [need - max_off, need]: each is a referrer location
* whose stored pointer lands `off` bytes below `need`. */
const uint64_t loV = need > P->max_off ? need - P->max_off : 0;
const pmap* pm = P->pm;
for (size_t i = idx_lb(pm->edges, pm->by_tgt, pm->nedge, 1, loV);
i < pm->nedge && pm->edges[pm->by_tgt[i]].tgt <= need; i++) {
P->disc[hops] = (int32_t)(need - pm->edges[pm->by_tgt[i]].tgt);
pp_dfs(P, pm->edges[pm->by_tgt[i]].ref, hops + 1);
if (P->n >= P->max) { return; }
}
}
int pmap_paths(const pmap* pm, uint64_t target, const range* mods, int nmods,
int max_depth, uint32_t max_off, scan_ptr_path* out, int max) {
if (!pm || max_depth < 1 || max < 1) { return -1; }
if (max_depth > SCAN_PTR_MAXDEPTH) { max_depth = SCAN_PTR_MAXDEPTH; }
if (nmods < 0) { nmods = 0; }
struct ppaths P; memset(&P, 0, sizeof P);
P.pm = pm; P.mods = mods; P.nmods = nmods;
P.max_off = max_off; P.max_depth = max_depth; P.out = out; P.max = max;
pp_dfs(&P, target, 0);
return P.n;
}
/* ---- string-xref: locate needle bytes, then resolve referrers ------------ */
struct strx {
const pmap* pm;
uint64_t* out; int max, n;
};
/* For one needle occurrence at `va`, append its referrers. Writes into the
* remaining output room (index-backed batch query) and always advances the
* running total so truncation past `max` is observable to the caller. */
static void strx_occurrence(struct strx* sx, uint64_t va) {
uint64_t* dst = (sx->out && sx->n < sx->max) ? sx->out + sx->n : NULL;
const int room = (sx->out && sx->n < sx->max) ? sx->max - sx->n : 0;
const int total = pmap_referrers(sx->pm, va, dst, room);
sx->n += total; /* total >= written: count is exact */
}
struct strx_sweep {
struct strx* sx;
const sig_pattern_t* p;
uint64_t win_base; size_t win_len, win_ov; int win_last;
};
static int strx_hit(void* u, uint64_t va) {
struct strx_sweep* w = u;
const size_t off = (size_t)(va - w->win_base);
if (!w->win_last && w->win_len > w->win_ov && off >= w->win_len - w->win_ov) {
return 0; /* trailing overlap: next window owns it */
}
strx_occurrence(w->sx, va);
return 0;
}
static int strx_sweep_cb(void* u, const uint8_t* data, size_t len,
uint64_t base, size_t ov, int last) {
struct strx_sweep* w = u;
w->win_base = base; w->win_len = len; w->win_ov = ov; w->win_last = last;
const mem_view_t v = { data, len, base };
sig_each(v, w->p, strx_hit, w);
return 0;
}
int xref_string(vmie_mem* m, uintptr_t cr3, const pmap* pm,
const void* needle, size_t nlen, uint64_t* out, int max) {
if (!pm || !needle || nlen == 0) { return -1; }
sig_pattern_t p;
if (!sig_from_bytes((const uint8_t*)needle, nlen, &p)) { return -1; }
struct strx sx; memset(&sx, 0, sizeof sx);
sx.pm = pm; sx.out = out; sx.max = max;
struct strx_sweep w; memset(&w, 0, sizeof w);
w.sx = &sx; w.p = &p;
/* the AS spans two canonical halves; gva_sweep requires a window within one,
* so sweep each half (a needle cannot straddle the non-canonical gap). */
int sw = gva_sweep(m, cr3, USER_MIN, USER_MAX, 0, nlen - 1, strx_sweep_cb, &w);
if (sw >= 0) {
sw = gva_sweep(m, cr3, KERN_MIN, ~0ull, 0, nlen - 1, strx_sweep_cb, &w);
}
sig_free(&p);
if (sw < 0) { return -1; }
return sx.n;
}
/* ---- structure dissection ------------------------------------------------ */
/* Membership test: prefer the pmap's region set; else query gva_regions live. */
__attribute__((hot))
static int sd_mapped(vmie_mem* m, uintptr_t cr3, const pmap* pm, uint64_t v,
uint32_t* prot_out) {
if (pm) {
if (!in_mapped(pm->regs, pm->nregs, v)) { return 0; }
/* recover prot from the region set */
int lo = 0, hi = pm->nregs;
while (lo < hi) { const int md = (lo + hi) / 2;
if (pm->regs[md].va <= v) { lo = md + 1; } else { hi = md; } }
if (prot_out && lo > 0) { *prot_out = pm->regs[lo - 1].prot; }
return 1;
}
vregion rg[8];
const int n = gva_regions(m, cr3, v, v, 0, rg, 8);
if (n <= 0) { return 0; }
if (prot_out) { *prot_out = rg[0].prot; }
return 1;
}
/* A VTABLE points into a non-writable region whose first qwords are themselves
* pointers into X-regions. */
__attribute__((hot))
static int sd_is_vtable(vmie_mem* m, uintptr_t cr3, const pmap* pm,
uint64_t v, uint32_t vprot) {
if (vprot & VR_W) { return 0; } /* vtables live in read-only memory */
uint64_t slot0 = 0;
if (gva_read(m, cr3, v, &slot0, 8)) { return 0; }
uint32_t sprot = 0;
if (!sd_mapped(m, cr3, pm, slot0, &sprot)) { return 0; }
return (sprot & VR_X) != 0; /* first entry points at code */
}
/* Classify a slot as text on a printable RUN of >= 4 from the start: a leading
* ASCII run (NUL/end terminates), or a leading UTF-16 run (printable low byte,
* zero high byte). ASCII is preferred when both qualify. */
#define SD_TEXT_MIN 4
__attribute__((hot))
static field_kind sd_text(const uint8_t* b, size_t len) {
size_t arun = 0; /* leading printable-ASCII run */
while (arun < len && b[arun] >= 0x20 && b[arun] < 0x7f) { arun++; }
if (arun >= SD_TEXT_MIN) { return FK_ASCII; }
size_t urun = 0; /* leading printable-UTF16 run (chars) */
while ((urun * 2 + 1) < len &&
b[urun * 2] >= 0x20 && b[urun * 2] < 0x7f && b[urun * 2 + 1] == 0) {
urun++;
}
return urun >= SD_TEXT_MIN ? FK_UTF16 : FK_UNKNOWN;
}
int struct_dissect(vmie_mem* m, uintptr_t cr3, uint64_t va, size_t nbytes,
const pmap* pm, field_desc* out, int max) {
if (!out || max <= 0) { return 0; }
int w = 0;
for (size_t off = 0; off + 8 <= nbytes && w < max; off += 8) {
uint8_t raw8[8];
if (gva_read(m, cr3, va + off, raw8, 8)) { continue; }
uint64_t raw; memcpy(&raw, raw8, 8);
field_desc* d = &out[w++];
d->off = (uint32_t)off;
d->raw = raw;
d->target = 0;
d->kind = FK_UNKNOWN;
uint32_t vprot = 0;
if (sd_mapped(m, cr3, pm, raw, &vprot)) {
d->target = raw;
d->kind = sd_is_vtable(m, cr3, pm, raw, vprot) ? FK_VTABLE : FK_PTR;
continue;
}
/* text run over the raw bytes */
const field_kind t = sd_text(raw8, 8);
if (t != FK_UNKNOWN) { d->kind = t; continue; }
/* float sanity: finite and sane magnitude */
float f32; memcpy(&f32, raw8, 4);
double f64; memcpy(&f64, raw8, 8);
const double af = f32 < 0 ? -(double)f32 : (double)f32;
const double ad = f64 < 0 ? -f64 : f64;
if (f32 == f32 && af >= 1e-6 && af <= 1e9) { d->kind = FK_F32; continue; }
if (f64 == f64 && ad >= 1e-6 && ad <= 1e12) { d->kind = FK_F64; continue; }
/* integer fallback: nonzero high half => I64, else I32 */
d->kind = (raw >> 32) ? FK_I64 : FK_I32;
}
return w;
}
+253
View File
@@ -0,0 +1,253 @@
/* sigset.c - compiled multi-pattern matcher (Aho-Corasick over literal anchors).
*
* Each pattern's longest contiguous non-wildcard run is its literal ANCHOR. An
* Aho-Corasick automaton (goto + failure links) is built over all anchors and
* driven once over the view; on reaching an accepting state the matcher walks
* the output list, aligns each owning pattern to its start (anchor_va -
* anchor_off), and verifies the FULL masked pattern before reporting. Patterns
* may share anchors and overlap; verification disambiguates.
*
* PURE: only mem_view_t (memmodel.h via sigscan.h), no vmie_mem, no I/O.
*/
#include "sigscan.h"
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
/* One compiled pattern: its bytes/mask (owned copies) and the anchor span. */
typedef struct {
uint8_t* bytes; /* owned copy of pattern bytes (len) */
uint8_t* mask; /* owned copy of pattern mask (len) */
size_t len; /* pattern length */
size_t anchor_off; /* anchor start offset within the pattern */
size_t anchor_len; /* anchor length (the literal run fed to AC) */
} spat;
/* AC trie node: 256-way goto via a flat row, plus failure link and an output
* list head. Output entries chain pattern indices that END at this node. */
#define AC_NIL (-1)
typedef struct {
int next[256]; /* goto transitions (AC_NIL = none in goto graph) */
int fail; /* failure link */
int out; /* head of output list (index into out_pat/out_nxt) */
} acnode;
struct sigset {
spat* pats; /* n compiled patterns */
int n;
size_t maxlen; /* longest full pattern length */
acnode* node; /* AC nodes (node[0] == root) */
int nnode, capnode;
int* out_pat; /* output list: owning pattern index */
int* out_nxt; /* output list: next link (AC_NIL = end) */
int nout, capout;
};
/* ---- pattern anchor selection (cold setup) ------------------------------- */
/* Longest contiguous run of set mask bits; writes start/len, returns len. */
__attribute__((cold))
static size_t longest_anchor(const uint8_t* mask, size_t len,
size_t* out_off) {
size_t best_off = 0, best_len = 0, i = 0;
while (i < len) {
if (!mask[i]) { i++; continue; }
const size_t run_off = i;
while (i < len && mask[i]) { i++; }
const size_t run_len = i - run_off;
if (run_len > best_len) { best_len = run_len; best_off = run_off; }
}
*out_off = best_off;
return best_len;
}
/* ---- AC trie construction (cold setup) ----------------------------------- */
__attribute__((cold))
static int node_new(sigset* s) {
if (s->nnode == s->capnode) {
const int nc = s->capnode ? s->capnode * 2 : 64;
acnode* nn = realloc(s->node, (size_t)nc * sizeof *nn);
if (!nn) { return AC_NIL; }
s->node = nn; s->capnode = nc;
}
const int id = s->nnode++;
acnode* nd = &s->node[id];
for (int c = 0; c < 256; c++) { nd->next[c] = AC_NIL; }
nd->fail = 0;
nd->out = AC_NIL;
return id;
}
/* Push pattern `pat` onto node `nd`'s output list. */
__attribute__((cold))
static int out_push(sigset* s, int nd, int pat) {
if (s->nout == s->capout) {
const int nc = s->capout ? s->capout * 2 : 64;
int* np = realloc(s->out_pat, (size_t)nc * sizeof *np);
if (!np) { return -1; }
s->out_pat = np;
int* nx = realloc(s->out_nxt, (size_t)nc * sizeof *nx);
if (!nx) { return -1; }
s->out_nxt = nx; s->capout = nc;
}
const int e = s->nout++;
s->out_pat[e] = pat;
s->out_nxt[e] = s->node[nd].out;
s->node[nd].out = e;
return 0;
}
/* Insert one anchor (pattern `pat`'s literal run) into the goto trie. */
__attribute__((cold))
static int trie_insert(sigset* s, int pat) {
const spat* p = &s->pats[pat];
int cur = 0;
for (size_t i = 0; i < p->anchor_len; i++) {
const uint8_t c = p->bytes[p->anchor_off + i];
if (s->node[cur].next[c] == AC_NIL) {
const int id = node_new(s);
if (id == AC_NIL) { return -1; }
s->node[cur].next[c] = id; /* node_new may realloc; re-fetch ok */
}
cur = s->node[cur].next[c];
}
return out_push(s, cur, pat);
}
/* Build failure links + merge output lists (BFS over the goto graph). */
__attribute__((cold))
static int build_failure(sigset* s) {
int* queue = malloc((size_t)s->nnode * sizeof *queue);
if (!queue) { return -1; }
int head = 0, tail = 0;
for (int c = 0; c < 256; c++) { /* depth-1 nodes fail to root */
const int v = s->node[0].next[c];
if (v != AC_NIL) { s->node[v].fail = 0; queue[tail++] = v; }
else { s->node[0].next[c] = 0; } /* root self-loop goto */
}
while (head < tail) {
const int u = queue[head++];
for (int c = 0; c < 256; c++) {
const int v = s->node[u].next[c];
if (v == AC_NIL) {
s->node[u].next[c] = s->node[s->node[u].fail].next[c];
continue;
}
s->node[v].fail = s->node[s->node[u].fail].next[c];
queue[tail++] = v;
/* merge fail node's outputs into v's list (chain the lists) */
const int fout = s->node[s->node[v].fail].out;
if (fout != AC_NIL && s->node[v].out == AC_NIL) {
s->node[v].out = fout; /* share tail: read-only traversal */
} else if (fout != AC_NIL) {
int e = s->node[v].out;
while (s->out_nxt[e] != AC_NIL) { e = s->out_nxt[e]; }
s->out_nxt[e] = fout; /* append fail-chain to own list */
}
}
}
free(queue);
return 0;
}
/* ---- public surface ------------------------------------------------------ */
__attribute__((cold))
sigset* sigset_compile(const sig_pattern_t* pats, int n) {
if (!pats || n <= 0) { return NULL; }
sigset* s = calloc(1, sizeof *s);
if (!s) { return NULL; }
s->pats = calloc((size_t)n, sizeof *s->pats);
if (!s->pats) { free(s); return NULL; }
for (int i = 0; i < n; i++) {
const sig_pattern_t* src = &pats[i];
if (!src->bytes || !src->mask || src->len == 0) { goto fail; }
size_t aoff = 0;
const size_t alen = longest_anchor(src->mask, src->len, &aoff);
if (alen == 0) { goto fail; } /* all-wildcard: no literal anchor */
spat* p = &s->pats[i];
p->bytes = malloc(src->len);
p->mask = malloc(src->len);
if (!p->bytes || !p->mask) { goto fail; }
memcpy(p->bytes, src->bytes, src->len);
memcpy(p->mask, src->mask, src->len);
p->len = src->len; p->anchor_off = aoff; p->anchor_len = alen;
if (src->len > s->maxlen) { s->maxlen = src->len; }
s->n++;
}
if (node_new(s) == AC_NIL) { goto fail; } /* root */
for (int i = 0; i < s->n; i++) {
if (trie_insert(s, i)) { goto fail; }
}
if (build_failure(s)) { goto fail; }
return s;
fail:
sigset_free(s);
return NULL;
}
__attribute__((cold))
void sigset_free(sigset* s) {
if (!s) { return; }
if (s->pats) {
for (int i = 0; i < s->n; i++) { free(s->pats[i].bytes); free(s->pats[i].mask); }
free(s->pats);
}
free(s->node);
free(s->out_pat);
free(s->out_nxt);
free(s);
}
size_t sigset_maxlen(const sigset* s) { return s ? s->maxlen : 0; }
/* Verify the full masked pattern `pat` at start VA `start` against `v`. */
__attribute__((hot))
static int verify(const sigset* s, mem_view_t v, int pat, uint64_t start) {
const spat* p = &s->pats[pat];
if (start < v.base_va) { return 0; }
const size_t off = (size_t)(start - v.base_va);
if (off + p->len > v.size) { return 0; }
const uint8_t* d = v.data + off;
for (size_t i = 0; i < p->len; i++) {
if (p->mask[i] && d[i] != p->bytes[i]) { return 0; }
}
return 1;
}
__attribute__((hot))
void sig_set_each(const sigset* s, mem_view_t v,
int (*cb)(void* user, int pat, uint64_t va), void* user) {
if (!s || !s->node || !v.data || v.size == 0 || s->n == 0) { return; }
int cur = 0;
for (size_t i = 0; i < v.size; i++) {
cur = s->node[cur].next[v.data[i]]; /* goto fully closed in build */
if (s->node[cur].out == AC_NIL) { continue; }
/* i is the END index of one or more anchors. Walk output list. */
for (int e = s->node[cur].out; e != AC_NIL; e = s->out_nxt[e]) {
const int pat = s->out_pat[e];
const spat* p = &s->pats[pat];
/* anchor ends at i => anchor start = i - (anchor_len-1); pattern
* start = anchor_start - anchor_off. */
if (i + 1 < p->anchor_len) { continue; }
const uint64_t astart = v.base_va + (i + 1 - p->anchor_len);
if (astart < v.base_va + p->anchor_off) { continue; }
const uint64_t pstart = astart - p->anchor_off;
if (verify(s, v, pat, pstart)) {
if (cb(user, pat, pstart)) { return; }
}
}
}
}
+187
View File
@@ -0,0 +1,187 @@
/* snapdiff.c - per-process temporal snapshot + diff (OS-agnostic handler).
*
* snap_take copies the bytes of every mapped run in a window at T0. snap_diff
* re-reads each run now and emits VA-contiguous changed ranges: byte-level
* differences, plus runs that appeared (mapped now, not at T0) or disappeared
* (mapped at T0, not now). Changed sub-ranges that are VA-contiguous are
* coalesced into a single emitted run.
*
* Handler boundary: only memmodel.h / snapdiff.h + stdlib/string.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "memmodel.h"
#include "snapdiff.h"
#define SNAP_REG_CAP (1 << 16)
/* One captured run: its VA, length, and an owned byte copy at T0. */
typedef struct {
uint64_t va, len;
uint8_t* bytes; /* owned copy of `len` bytes (NULL if capture failed) */
} snaprun;
struct snapshot {
vmie_mem* m;
uintptr_t cr3;
uint64_t lo, hi;
uint32_t prot;
snaprun* runs;
int nruns;
};
/* ---- capture (cold) ------------------------------------------------------ */
__attribute__((cold))
snapshot* snap_take(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi, uint32_t prot_any) {
snapshot* s = calloc(1, sizeof *s);
if (!s) { return NULL; }
s->m = m; s->cr3 = cr3; s->lo = lo; s->hi = hi; s->prot = prot_any;
vregion* rg = malloc((size_t)SNAP_REG_CAP * sizeof *rg);
if (!rg) { free(s); return NULL; }
int nr = gva_regions(m, cr3, lo, hi, prot_any, rg, SNAP_REG_CAP);
if (nr < 0) { nr = 0; }
if (nr > SNAP_REG_CAP) { nr = SNAP_REG_CAP; }
s->runs = calloc((size_t)nr, sizeof *s->runs);
if (!s->runs && nr) { free(rg); free(s); return NULL; }
s->nruns = nr;
for (int i = 0; i < nr; i++) {
s->runs[i].va = rg[i].va;
s->runs[i].len = rg[i].len;
uint8_t* b = malloc((size_t)rg[i].len);
if (!b) { continue; } /* leave bytes NULL: capture gap */
if (gva_read(m, cr3, rg[i].va, b, (size_t)rg[i].len)) { free(b); b = NULL; }
s->runs[i].bytes = b;
}
free(rg);
return s;
}
__attribute__((cold))
void snap_free(snapshot* s) {
if (!s) { return; }
for (int i = 0; i < s->nruns; i++) { free(s->runs[i].bytes); }
free(s->runs);
free(s);
}
/* ---- diff (coalescing emitter) ------------------------------------------- */
struct demit {
vregion* out; int max, n;
uint32_t cur_prot; /* prot stamped on emitted runs */
int have; /* a pending run is open */
uint64_t pva, pend; /* pending [pva, pend) */
};
/* Flush the pending run to the output (counting even past `max`). */
__attribute__((hot))
static void emit_flush(struct demit* e) {
if (!e->have) { return; }
if (e->out && e->n < e->max) {
e->out[e->n].va = e->pva;
e->out[e->n].len = e->pend - e->pva;
e->out[e->n].prot = e->cur_prot;
}
e->n++;
e->have = 0;
}
/* Mark [va, va+len) changed, coalescing with the pending run if VA-contiguous. */
__attribute__((hot))
static void emit_changed(struct demit* e, uint64_t va, uint64_t len, uint32_t prot) {
if (len == 0) { return; }
if (e->have && va == e->pend) { /* contiguous: extend */
e->pend = va + len;
return;
}
emit_flush(e);
e->have = 1; e->pva = va; e->pend = va + len; e->cur_prot = prot;
}
/* Current prot at `va` (for stamping emitted runs); 0 if unmapped now. */
__attribute__((hot))
static uint32_t cur_prot_at(vmie_mem* m, uintptr_t cr3, uint64_t va) {
vregion rg[4];
const int n = gva_regions(m, cr3, va, va, 0, rg, 4);
return n > 0 ? rg[0].prot : 0u;
}
int snap_diff(const snapshot* s, vmie_mem* m, uintptr_t cr3,
vregion* changed, int max) {
if (!s) { return -1; }
struct demit e; memset(&e, 0, sizeof e);
e.out = changed; e.max = max;
/* enumerate current runs once for the "appeared" pass and prot lookup */
vregion* now = malloc((size_t)SNAP_REG_CAP * sizeof *now);
if (!now) { return -1; }
int nnow = gva_regions(m, cr3, s->lo, s->hi, s->prot, now, SNAP_REG_CAP);
if (nnow < 0) { nnow = 0; }
if (nnow > SNAP_REG_CAP) { nnow = SNAP_REG_CAP; }
/* Pass 1: walk T0 runs; compare bytes that are still mapped, mark the rest
* (disappeared / read-failure) as changed. Byte-level diffs are coalesced
* across page boundaries within a run via the contiguous emitter. */
uint8_t* live = NULL; size_t livecap = 0;
for (int i = 0; i < s->nruns; i++) {
const snaprun* r = &s->runs[i];
const size_t len = (size_t)r->len;
if (len > livecap) {
uint8_t* nl = realloc(live, len);
if (!nl) { free(live); free(now); return -1; }
live = nl; livecap = len;
}
if (!r->bytes || gva_read(m, cr3, r->va, live, len)) {
/* disappeared or unreadable now (or T0 capture gap): whole run changed */
emit_changed(&e, r->va, r->len, cur_prot_at(m, cr3, r->va));
continue;
}
/* byte compare; coalesce contiguous differing bytes */
const uint32_t prot = cur_prot_at(m, cr3, r->va);
size_t j = 0;
while (j < len) {
if (live[j] != r->bytes[j]) {
const size_t start = j;
while (j < len && live[j] != r->bytes[j]) { j++; }
emit_changed(&e, r->va + start, j - start, prot);
} else {
j++;
}
}
}
free(live);
/* Pass 2: current runs that did NOT exist at T0 (appeared) are changed. A
* current byte is "new" if its VA is not covered by any T0 run. */
for (int i = 0; i < nnow; i++) {
uint64_t va = now[i].va;
const uint64_t end = now[i].va + now[i].len;
while (va < end) {
/* find a T0 run covering `va`; if none, this byte is new */
uint64_t cover_end = end;
int covered = 0;
for (int k = 0; k < s->nruns; k++) {
const snaprun* r = &s->runs[k];
if (va >= r->va && va < r->va + r->len) {
covered = 1; cover_end = r->va + r->len; break;
}
if (r->va > va && r->va < cover_end) { cover_end = r->va; }
}
const uint64_t seg_end = cover_end < end ? cover_end : end;
if (!covered) {
emit_changed(&e, va, seg_end - va, now[i].prot);
}
va = seg_end;
}
}
free(now);
emit_flush(&e);
return e.n;
}