ship one record per function: merge the release set, gen reads it, descriptions as doc comments, gates for what was only claimed; v3.0
This commit is contained in:
parent
71ce34edd2
commit
3410a79b6a
28 changed files with 30596 additions and 955 deletions
103
src/pulse.rs
103
src/pulse.rs
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue