M5.5: this-flow indirect-dispatch resolution via vptr-write inference
Closes the dominant case M5 could not resolve — `lwz vt, off(this);
lwz fn, slot(vt); mtctr; bcctrl` (real C++ dispatch). Implements
class-membership inference using constructor-side vptr writes as an
oracle for which vtables can land at each offset.
## Algorithm
Phase 1 — vptr-write scan: walk every function with the existing
lis+addi register tracker. When `stw rA, off(rB)` writes a known M3
vtable address into off(rB), record `(vtable_addr, vptr_offset,
writer_pc, writer_function)` as a constructor-side vptr write.
Phase 2 — invert by offset: `vtables_by_offset[off] = {V : V written
at off in any ctor}`.
Phase 3 — dispatch detection: from each `bcctrl LK=1`, walk back
≤16 instructions looking for the canonical chain. Bail on register
clobber, branch, or label (basic-block) boundary.
Phase 4 — edge emission: for `(dispatch_pc, vptr_off, slot)`, emit one
`xrefs.kind='ind_call'` row per vtable V where:
- `vtables_by_offset[vptr_off]` contains V, AND
- `V.length > slot` (V actually has a method at that slot)
Multi-candidate sites (the common case at offset 0) are an
over-approximation; downstream queries filter to single-candidate sites
for high confidence:
`WHERE candidate_count=1` in `indirect_dispatch_sites`.
## Schema
NEW TABLES:
- `vptr_writes(writer_pc, vtable_address, vptr_offset, writer_function)`
- `indirect_dispatch_sites(dispatch_pc PK, vptr_offset, slot, candidate_count)`
- `indirect_dispatch_candidates(dispatch_pc, vtable_address, method_address)`
NEW INDICES on vtable_address / vptr_offset / method_address /
(vptr_offset, slot) for fast joins.
## Sylpheed yield
- 567 vptr writes / 214 vtables / 29 offsets (offset 0 = 88%).
- 6,842 dispatch sites resolved: 97 single-candidate (high-confidence) +
6,745 multi-candidate.
- 687,963 ind_call xref rows.
- 2,746 newly-reachable functions via v_indirect_reachability_from_entry
(compared to 0 with M5 alone).
- Audit-009 cluster: functions including 0x823BC9E0, 0x823BC290,
0x823BC5A0, 0x823BB158 newly reachable — actionable for the
renderer-plateau hunt.
Tests 640→649 (+4 ind_dispatch_typed unit tests + 5 from tighter golden
expansion). Schema golden + write_analysis_results signature updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -307,7 +307,7 @@ impl DbWriter {
|
||||
/// `vtables` is the M3 result; pass an empty slice when the caller has
|
||||
/// not run the vtable scan (the tables are still created, just empty).
|
||||
/// `strings` is the M7 result; same convention. `funcptr_arrays` is the
|
||||
/// M8/M11 result.
|
||||
/// M8/M11 result. `typed_ind` is the M5.5 result.
|
||||
#[tracing::instrument(skip_all, name = "db.write_analysis_results")]
|
||||
pub fn write_analysis_results(
|
||||
&mut self,
|
||||
@@ -319,6 +319,7 @@ impl DbWriter {
|
||||
vtables: &[crate::vtables::Vtable],
|
||||
strings: &[crate::strings::DetectedString],
|
||||
funcptr_arrays: &[crate::funcptr_arrays::FuncPtrArray],
|
||||
typed_ind: Option<&crate::ind_dispatch_typed::TypedIndirectResult>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.conn.execute_batch("
|
||||
CREATE TABLE functions (
|
||||
@@ -407,6 +408,39 @@ impl DbWriter {
|
||||
PRIMARY KEY (array_address, slot)
|
||||
);
|
||||
|
||||
-- M5.5 — typed indirect-dispatch resolutions. Each row is one
|
||||
-- bcctrl site that matched the canonical lwz vt, off(this);
|
||||
-- lwz fn, slot(vt); mtctr; bcctrl pattern. candidate_count > 1
|
||||
-- means the analysis could not pick a single class; downstream
|
||||
-- queries should treat such rows as reachability-only.
|
||||
CREATE TABLE indirect_dispatch_sites (
|
||||
dispatch_pc BIGINT PRIMARY KEY,
|
||||
vptr_offset BIGINT NOT NULL,
|
||||
slot BIGINT NOT NULL,
|
||||
candidate_count BIGINT NOT NULL
|
||||
);
|
||||
|
||||
-- M5.5 — one row per (dispatch site × candidate vtable). The
|
||||
-- ind_call xref edges in the `xrefs` table are derived from
|
||||
-- this; this view lets you join back to vtable / method info.
|
||||
CREATE TABLE indirect_dispatch_candidates (
|
||||
dispatch_pc BIGINT NOT NULL,
|
||||
vtable_address BIGINT NOT NULL,
|
||||
method_address BIGINT NOT NULL,
|
||||
PRIMARY KEY (dispatch_pc, vtable_address)
|
||||
);
|
||||
|
||||
-- M5.5 — every detected `stw rVtable, vptr_off(rThis)` writer
|
||||
-- found in any function. Useful for diagnosing why a class
|
||||
-- has (or does not have) coverage in the dispatch resolver.
|
||||
CREATE TABLE vptr_writes (
|
||||
writer_pc BIGINT NOT NULL,
|
||||
vtable_address BIGINT NOT NULL,
|
||||
vptr_offset BIGINT NOT NULL,
|
||||
writer_function BIGINT NOT NULL,
|
||||
PRIMARY KEY (writer_pc, vtable_address, vptr_offset)
|
||||
);
|
||||
|
||||
CREATE TABLE demangled_names (
|
||||
address BIGINT, -- VA the mangled name is associated with; NULL when from a non-address source (e.g. RTTI-only string)
|
||||
mangled VARCHAR NOT NULL, -- original mangled symbol (e.g. ?Foo@Bar@@QEAAXXZ)
|
||||
@@ -437,6 +471,9 @@ impl DbWriter {
|
||||
insert_methods_and_classes(&self.conn, vtables, labels)?;
|
||||
insert_strings(&self.conn, strings)?;
|
||||
insert_funcptr_arrays(&self.conn, funcptr_arrays)?;
|
||||
if let Some(t) = typed_ind {
|
||||
insert_typed_ind_dispatch(&self.conn, t)?;
|
||||
}
|
||||
insert_xrefs_streaming(&self.conn, xrefs, pe, info.image_base, func_analysis, labels)?;
|
||||
|
||||
let indices = [
|
||||
@@ -454,6 +491,11 @@ impl DbWriter {
|
||||
("idx_xrefs_addr_mode", "CREATE INDEX idx_xrefs_addr_mode ON xrefs(addr_mode)"),
|
||||
("idx_fparrays_kind", "CREATE INDEX idx_fparrays_kind ON function_pointer_arrays(kind)"),
|
||||
("idx_fpentries_function", "CREATE INDEX idx_fpentries_function ON function_pointer_array_entries(function_address)"),
|
||||
("idx_indcand_method", "CREATE INDEX idx_indcand_method ON indirect_dispatch_candidates(method_address)"),
|
||||
("idx_indcand_vtable", "CREATE INDEX idx_indcand_vtable ON indirect_dispatch_candidates(vtable_address)"),
|
||||
("idx_indsites_offset_slot", "CREATE INDEX idx_indsites_offset_slot ON indirect_dispatch_sites(vptr_offset, slot)"),
|
||||
("idx_vptrw_vtable", "CREATE INDEX idx_vptrw_vtable ON vptr_writes(vtable_address)"),
|
||||
("idx_vptrw_offset", "CREATE INDEX idx_vptrw_offset ON vptr_writes(vptr_offset)"),
|
||||
("idx_xrefs_target", "CREATE INDEX idx_xrefs_target ON xrefs(target)"),
|
||||
("idx_xrefs_source", "CREATE INDEX idx_xrefs_source ON xrefs(source)"),
|
||||
("idx_xrefs_source_func", "CREATE INDEX idx_xrefs_source_func ON xrefs(source_func)"),
|
||||
@@ -482,7 +524,7 @@ impl DbWriter {
|
||||
xrefs: &XrefMap,
|
||||
) -> anyhow::Result<()> {
|
||||
self.ingest_instructions(pe, info, func_analysis, labels)?;
|
||||
self.write_analysis_results(pe, info, func_analysis, labels, xrefs, &[], &[], &[])?;
|
||||
self.write_analysis_results(pe, info, func_analysis, labels, xrefs, &[], &[], &[], None)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -951,6 +993,64 @@ fn insert_strings(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_typed_ind_dispatch(
|
||||
conn: &Connection,
|
||||
t: &crate::ind_dispatch_typed::TypedIndirectResult,
|
||||
) -> anyhow::Result<()> {
|
||||
if !t.dispatches.is_empty() {
|
||||
let mut stmt_site = conn.prepare(
|
||||
"INSERT INTO indirect_dispatch_sites
|
||||
(dispatch_pc, vptr_offset, slot, candidate_count)
|
||||
VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING"
|
||||
)?;
|
||||
let mut stmt_cand = conn.prepare(
|
||||
"INSERT INTO indirect_dispatch_candidates
|
||||
(dispatch_pc, vtable_address, method_address)
|
||||
VALUES (?, ?, ?) ON CONFLICT DO NOTHING"
|
||||
)?;
|
||||
let mut n_sites = 0u64;
|
||||
let mut n_cand = 0u64;
|
||||
for d in &t.dispatches {
|
||||
stmt_site.execute(params![
|
||||
d.dispatch_pc as i64,
|
||||
d.vptr_offset as i64,
|
||||
d.slot as i64,
|
||||
d.candidate_vtables.len() as i64,
|
||||
])?;
|
||||
n_sites += 1;
|
||||
for (vt, m) in d.candidate_vtables.iter().zip(d.method_pcs.iter()) {
|
||||
stmt_cand.execute(params![
|
||||
d.dispatch_pc as i64, *vt as i64, *m as i64,
|
||||
])?;
|
||||
n_cand += 1;
|
||||
}
|
||||
}
|
||||
metrics::counter!("db.rows", "table" => "indirect_dispatch_sites").increment(n_sites);
|
||||
metrics::counter!("db.rows", "table" => "indirect_dispatch_candidates").increment(n_cand);
|
||||
tracing::info!(sites = n_sites, candidates = n_cand, "typed indirect-dispatch insert complete");
|
||||
}
|
||||
if !t.vptr_writes.is_empty() {
|
||||
let mut stmt = conn.prepare(
|
||||
"INSERT INTO vptr_writes
|
||||
(writer_pc, vtable_address, vptr_offset, writer_function)
|
||||
VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING"
|
||||
)?;
|
||||
let mut n = 0u64;
|
||||
for w in &t.vptr_writes {
|
||||
stmt.execute(params![
|
||||
w.writer_pc as i64,
|
||||
w.vtable_addr as i64,
|
||||
w.vptr_offset as i64,
|
||||
w.writer_function as i64,
|
||||
])?;
|
||||
n += 1;
|
||||
}
|
||||
metrics::counter!("db.rows", "table" => "vptr_writes").increment(n);
|
||||
tracing::info!(rows = n, "vptr_writes insert complete");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_funcptr_arrays(
|
||||
conn: &Connection,
|
||||
arrays: &[crate::funcptr_arrays::FuncPtrArray],
|
||||
|
||||
Reference in New Issue
Block a user