ship one record per function: merge the release set, gen reads it, descriptions as doc comments, gates for what was only claimed; v3.0
Some checks failed
CI / fuzz (push) Successful in 2m2s
CI / lint (push) Successful in 15s
CI / test (push) Failing after 18s

This commit is contained in:
Kamal Tufekcic 2026-08-02 22:01:36 +03:00
commit 3410a79b6a
28 changed files with 30596 additions and 955 deletions

View file

@ -26,7 +26,7 @@
use crate::elf::CodeImage;
use iced_x86::{Decoder, DecoderOptions, FlowControl, Instruction, Mnemonic, OpKind, Register};
use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, HashMap, HashSet};
/// Highest `PulseValueType_t` enumerator (`PVAL_COUNT`) plus headroom for a build that adds a few. The
/// enum is schema-registered, so the DERIVED values are what a caller should validate against — this is
@ -101,23 +101,22 @@ fn full(r: Register) -> Register {
if r.is_gpr() { r.full_register() } else { r }
}
/// Constant-propagate through the accessor, recording every fixed-address store, every call's argument
/// registers, and the vector the fast path returns.
/// Every instruction address reachable from `entry` inside `[entry, entry+code.len())`, in ADDRESS order.
///
/// Deliberately a single ADDRESS-ORDER pass rather than a CFG walk: the guard-protected initializer is
/// straight-line, and a pass that only ever believes values it computed itself cannot invent one. Every
/// instruction it does not model invalidates what it writes.
fn trace(img: &CodeImage, entry: u64, seed_rdi: Option<u64>) -> Option<Trace> {
let code = img.code_at(entry)?;
let cap = code.len().min(MAX_SPAN);
let in_span = |t: u64| t >= entry && ((t - entry) as usize) < cap;
// Reachable instruction addresses, then walked in address order.
let mut seen: HashMap<u64, usize> = HashMap::new();
/// One walk, two callers: the descriptor trace and the shim's liveness read need exactly the same thing —
/// flow-reachable addresses rather than a linear sweep, so a jump table or an interleaved neighbour cannot
/// contribute instructions the function never executes. They differ only in how far they are willing to
/// walk, which is the `cap`.
///
/// `cap` bounds the SET, not the span: a crafted image can present a small span with pathological branch
/// density, and this is on the fuzz surface.
fn reachable(code: &[u8], entry: u64, cap: usize) -> Vec<u64> {
let end = entry.saturating_add(code.len() as u64);
let mut seen: HashSet<u64> = HashSet::new();
let mut work = vec![entry];
let mut insn = Instruction::default();
while let Some(at) = work.pop() {
if seen.contains_key(&at) || !in_span(at) || seen.len() > 4000 {
if at < entry || at >= end || seen.contains(&at) || seen.len() > cap {
continue;
}
let mut dec =
@ -129,7 +128,7 @@ fn trace(img: &CodeImage, entry: u64, seed_rdi: Option<u64>) -> Option<Trace> {
if insn.is_invalid() || insn.len() == 0 {
continue;
}
seen.insert(at, insn.len());
seen.insert(at);
match insn.flow_control() {
FlowControl::Return
| FlowControl::IndirectBranch
@ -143,9 +142,22 @@ fn trace(img: &CodeImage, entry: u64, seed_rdi: Option<u64>) -> Option<Trace> {
_ => work.push(at + insn.len() as u64),
}
}
let mut addrs: Vec<u64> = seen.keys().copied().collect();
let mut addrs: Vec<u64> = seen.into_iter().collect();
addrs.sort_unstable();
addrs
}
/// Constant-propagate through the accessor, recording every fixed-address store, every call's argument
/// registers, and the vector the fast path returns.
///
/// Deliberately a single ADDRESS-ORDER pass rather than a CFG walk: the guard-protected initializer is
/// straight-line, and a pass that only ever believes values it computed itself cannot invent one. Every
/// instruction it does not model invalidates what it writes.
fn trace(img: &CodeImage, entry: u64, seed_rdi: Option<u64>) -> Option<Trace> {
let all = img.code_at(entry)?;
let code = &all[..all.len().min(MAX_SPAN)];
let mut insn = Instruction::default();
let addrs = reachable(code, entry, 4000);
let mut out = Trace::default();
let mut regs: HashMap<Register, u64> = HashMap::new();
@ -606,6 +618,22 @@ impl ShimReads {
}
}
/// Where an accessor's descriptor region LIVES, and how many elements it holds: `(base, count)`.
///
/// The signature reader reconstructs the region's CONTENTS by constant-propagating the initialiser,
/// because on disk the elements are zeroes — they are written at runtime. That reconstruction is the
/// only offline route, and it is also unverified: the multi-library duplicate check reports that CS2
/// disagrees with itself on 331 of 419 repeat registrations, and nothing offline can say which account
/// is right.
///
/// A running server can. The region is a plain static, so at `slide + base` a live process holds the
/// POPULATED elements, and reading them settles the question against the same build rather than against
/// a dump of a different one. This accessor exists for that oracle; the derivation itself never needs it.
pub fn record_region(img: &CodeImage, accessor: u64) -> Option<(u64, u64)> {
let r = record(img, accessor)?;
(r.base != 0).then_some((r.base, r.count))
}
/// Measure which of a shim's seven arguments it reads.
///
/// Reachable instructions in ADDRESS order, which needs two guards that cost real time to find:
@ -618,46 +646,9 @@ impl ShimReads {
/// argument live that the shim never consumes; `xor edi, edi` alone accounted for 143 false positives.
pub fn shim_reads(img: &CodeImage, entry: u64) -> Option<ShimReads> {
let all = img.code_at(entry)?;
let extent = (all.len() as u64).min(SHIM_SPAN);
let code = &all[..extent as usize];
// Saturating: `extent` derives from the section length, so on a crafted image `entry + extent` can
// wrap and turn the span test inside out — and the fuzz harness builds with overflow checks, where a
// plain add aborts. The same shape `fuzz_concmd` was written for.
let end = entry.saturating_add(extent);
let in_span = |t: u64| t >= entry && t < end;
let mut seen: HashMap<u64, ()> = HashMap::new();
let mut work = vec![entry];
let code = &all[..(all.len() as u64).min(SHIM_SPAN) as usize];
let mut insn = Instruction::default();
while let Some(at) = work.pop() {
if seen.contains_key(&at) || !in_span(at) || seen.len() > 20000 {
continue;
}
let mut dec =
Decoder::with_ip(64, &code[(at - entry) as usize..], at, DecoderOptions::NONE);
if !dec.can_decode() {
continue;
}
dec.decode_out(&mut insn);
if insn.is_invalid() || insn.len() == 0 {
continue;
}
seen.insert(at, ());
match insn.flow_control() {
FlowControl::Return
| FlowControl::IndirectBranch
| FlowControl::Exception
| FlowControl::Interrupt => {}
FlowControl::UnconditionalBranch => work.push(insn.near_branch_target()),
FlowControl::ConditionalBranch => {
work.push(at + insn.len() as u64);
work.push(insn.near_branch_target());
}
_ => work.push(at + insn.len() as u64),
}
}
let mut addrs: Vec<u64> = seen.keys().copied().collect();
addrs.sort_unstable();
let addrs = reachable(code, entry, 20000);
let mut live: BTreeMap<Register, bool> = BTreeMap::new();
let mut sink = false;