Skip to main content

qcode/
assumption.rs

1//! Heuristic *assumptions* and proven *knowledge* shared by analysis passes.
2//!
3//! A [`Proposition`] is a positive statement about the program ("function `f`
4//! returns to its caller"). During analysis each proposition can be in one of
5//! four states, tracked by a [`Truth`] in a single map on the
6//! [`Context`](crate::context::Context):
7//!
8//! - **assumed true / assumed false** — a pass guessed, recorded via
9//!   [`Context::assume_true`](crate::context::Context::assume_true) /
10//!   [`assume_false`](crate::context::Context::assume_false). Assuming fails
11//!   (returns `false`) if the opposite polarity is already assumed or known.
12//! - **known true / known false** — proven by a verification pass via
13//!   [`Context::set_known`](crate::context::Context::set_known). Proving the
14//!   opposite of an existing assumption records a [`Violation`].
15//!
16//! Every entry carries the name of the pass that recorded it, picked up
17//! automatically from the [`pass_scope`](crate::pass_scope) thread-local set by
18//! the pipeline driver.
19//!
20//! Because analysis passes mutate the [`Context`](crate::context::Context)
21//! arena in place, a *violated* assumption leaves behind IR that is now
22//! incorrect. The invalidation strategy is **checkpoint + replay**: a
23//! freshly-lifted baseline `Context` is cloned before any speculative
24//! analysis; after a round, if any violation was recorded (or a novel fact
25//! proven), the working copy is discarded, its known facts are seeded into a
26//! fresh clone, and the round replays. Knowledge only ever grows, so replay
27//! terminates.
28
29use crate::value::VarnodeId;
30use crate::value::function::FunctionId;
31
32/// The register-space effect the opt-in [`Proposition::AssumeCallingConvention`]
33/// hypothesis assigns to an indirect / unresolved call, precomputed once from the
34/// module's calling convention by the `assume_calling_convention` pass and cached
35/// on the [`Shared`](crate::context::Shared) context so the mem2reg / alias
36/// register classifier can consult it without an ABI in hand.
37///
38/// `reads` is *all* convention argument registers (integer + SSE) — reads-all-args
39/// keeps pre-call argument setup live, since a variadic-arity callee may consume
40/// any of them — and `writes` is the convention's caller-saved (volatile) set.
41/// Neither the stack- nor the frame-pointer varnode appears in either list: both
42/// are callee-saved, so no ABI argument list or caller-saved set contains them.
43#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
44pub struct AssumedCallEffect {
45    /// Argument registers the call is assumed to read.
46    pub reads: Vec<VarnodeId>,
47    /// Caller-saved registers the call is assumed to write (clobber).
48    pub writes: Vec<VarnodeId>,
49}
50
51/// A positive statement about the program whose truth a pass may assume or
52/// prove. Used as the key of the truth map on the
53/// [`Context`](crate::context::Context).
54///
55/// `#[non_exhaustive]` so adding future propositions does not break exhaustive
56/// matches in downstream crates.
57#[non_exhaustive]
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
59pub enum Proposition {
60    /// Function returns normally to the fall-through after a call to it
61    /// (false = noreturn: `exit`/`abort`, infinite loops, no `Return` in body).
62    FunctionReturns(FunctionId),
63    /// Function performs an unresolved/dynamic stack read, or forwards a stack
64    /// pointer into one, so callers must keep their frame in memory.
65    UnboundedStackReader(FunctionId),
66    /// Function hands a pointer into its own frame to an unbounded-reading
67    /// callee, so its own frame must stay in memory.
68    FrameEscapingCaller(FunctionId),
69    /// Every incoming pointer parameter of this function (and any address offset
70    /// from one) is disjoint from the function's own *caller-frame* region — the
71    /// `@SP + k` (`k ≥ 0`) slots holding the return address and incoming stack
72    /// arguments. This lets the frame-freshness alias rule forward a load of a
73    /// caller-frame slot across a store through an incoming pointer (the
74    /// spilled-pointer reload idiom). Unlike own-frame freshness it is **not**
75    /// statically sound on its own — a caller could pass the address of one of its
76    /// outgoing-argument slots — so a pass records it `Assumed` and a verifier
77    /// (`verify_args_disjoint_caller_frame`) keeps it standing by default,
78    /// refuting it (→ replay) only when a direct caller *provably* passes a pointer
79    /// argument whose access interval overlaps the callee's argument slots.
80    ArgsDisjointFromCallerFrame(FunctionId),
81    /// Within this function, a pointer **loaded from a slot** does not alias that
82    /// slot: a store through `load(X) + …` cannot clobber `X` itself — the buffer a
83    /// pointer addresses does not overlap the storage of the pointer. This lets the
84    /// alias rule (see [`AliasResult::provably_disjoint`]) forward a spilled buffer
85    /// pointer's in-loop reload across the very store that writes *through* it,
86    /// which `argpromote` needs to region-promote a dynamic-index buffer loop.
87    /// Like [`ArgsDisjointFromCallerFrame`](crate::assumption::Proposition::ArgsDisjointFromCallerFrame) it is **not** statically sound on its
88    /// own — it fails only for a self-referential pointer (`*pp == &pp`), which real
89    /// code does not build — so a pass records it `Assumed`; v1 has no verifier
90    /// (nothing currently proves the negation), the checkpoint+replay net catching
91    /// any future refutation.
92    ///
93    /// [`AliasResult::provably_disjoint`]: crate
94    LoadedPointerDisjointFromSlot(FunctionId),
95    /// The source and destination buffers of a recognized copy/transform loop in
96    /// this function do not overlap — the C `strcpy`/`memcpy` contract, where
97    /// overlapping buffers are undefined behaviour (that is `memmove`'s job).
98    ///
99    /// A copy-until-terminator (`while (*src) *dst++ = *src++;`) loop pipelines
100    /// its loaded byte through a loop-carried register, and re-deriving that byte
101    /// as `load(src - step)` — the move that removes the carry and exposes the
102    /// `map(body, take_while(src))` shape — re-reads memory that the body also
103    /// writes through `dst`. That re-read is value-preserving only when the two
104    /// buffers are disjoint. Like [`ArgsDisjointFromCallerFrame`](crate::assumption::Proposition::ArgsDisjointFromCallerFrame) it is **not**
105    /// statically sound on its own — a caller may pass overlapping pointers — so a
106    /// pass records it `Assumed`; v1 has no verifier (the checkpoint+replay net
107    /// catches any future refutation).
108    CopyBuffersDisjoint(FunctionId),
109    /// The `size` bytes at virtual address `addr` (a jump-table entry the
110    /// jump-table resolver read out of read-only data) are assumed never
111    /// written at runtime; a write would invalidate the resolved jump target.
112    ImmutableMemory { addr: u64, size: u8 },
113    /// The mapped region `[start, end)` is executable code. The lifter assumes
114    /// this for any readable byte (default r/x) until the optional
115    /// `memory_protections` pass establishes the binary's real protections; a
116    /// target in a region the pass marks non-executable contradicts it. Keyed by
117    /// the whole containing segment (not per byte) so the truth map stays small.
118    ExecutableMemory { start: u64, end: u64 },
119    /// The binary is a Windows target of the given `bitness` (32 or 64), so the
120    /// segment-base register (`FS` on x86, `GS` on x64) points at the Thread
121    /// Information Block. Recorded `Assumed` by the TEB-seeding pass to justify
122    /// retyping that base as `PtrTo<TEB>`; it is an analyst aid and override
123    /// hook, with no verifier in v1 (nothing currently proves the negation).
124    WindowsTeb { bitness: u8 },
125    /// Whole-program, opt-in hypothesis: every indirect (`CallInd`) and
126    /// unresolved-direct call obeys the module's calling convention, so instead
127    /// of clobbering the entire register file it reads only the convention's
128    /// argument registers and writes only its caller-saved set (the effect is
129    /// cached as an [`AssumedCallEffect`] on the context). A deliberate,
130    /// *controllable unsoundness* — an indirect callee may violate the ABI — off
131    /// by default, recorded by the `assume_calling_convention` pass when the user
132    /// opts in, and surfaced in the assumptions panel.
133    ///
134    /// It is a **downstream refinement only**: it sharpens how the mem2reg / alias
135    /// register classifier (`classify_call_reg_effect`) clobbers around such
136    /// calls, and does *not* feed back into the argpromote effect-summary fixpoint
137    /// (which keeps modelling `CallInd` as `Some(empty)`). No verifier in v1 (it is
138    /// never proven, so it is not discharged and the checkpoint+replay net never
139    /// acts on it).
140    AssumeCallingConvention,
141    /// The external function (second field), reached from the caller (first
142    /// field), has a prototype-derived argmem footprint that it honours. Two
143    /// halves, both assumed:
144    ///
145    /// (a) it writes and reads **only within the objects addressed by its
146    /// pointer arguments** — e.g. `memset(dst, c, n)` touches only
147    /// `[dst, dst+n)`, never spilling past the object we hand it; and
148    ///
149    /// (b) the caller's subsequent same-base reads of such an object observe the
150    /// external's writes — a read of `local` after `memset(&local, …)` sees the
151    /// bytes memset wrote, i.e. the read does not exceed the written extent (it
152    /// never reads past `[dst, dst+n)` into stale frame bytes the external left
153    /// untouched).
154    ///
155    /// `argpromote`'s RAM effect lattice mints whole-object entries for such
156    /// externals and rebases them onto the caller's bases (`Frame`/`Param`/
157    /// `Global`); the `memset(&local)` fold — a write-only external landing on an
158    /// own-frame local, contained and dead at return — rests on (a), and the
159    /// summary scan's read-after-external licensing (a `load(local)` in a block
160    /// dominated by the `memset` call reading as a captured own write rather than
161    /// refuting frame freshness) rests on (b).
162    ///
163    /// Like [`LoadedPointerDisjointFromSlot`] it is **not** statically sound on
164    /// its own: the prototype could be lying (an `OutPtr` that secretly reads, a
165    /// callback hidden behind a plain pointer) or the length could run out of
166    /// bounds (`memset(dst, c, huge_n)`), either of which would touch memory
167    /// outside the addressed object. A pass records it `Assumed` at the point a
168    /// promotion is committed that relied on the containment (the promoted
169    /// function's summary flag-set names every external it transitively folded
170    /// through); v1 has **no verifier**, the checkpoint+replay net catching any
171    /// future refutation. A cheap future refuter is available where the caller
172    /// passes a *constant* length whose value exceeds the frame-layout distance
173    /// from the landing slot to the next live slot — a provable overflow.
174    ///
175    /// The first field is the invalidation target (the promoted caller whose IR
176    /// must be discarded on refutation); the second is the trust anchor (the
177    /// external whose prototype is being believed).
178    ///
179    /// [`LoadedPointerDisjointFromSlot`]: Proposition::LoadedPointerDisjointFromSlot
180    ExternalArgmemConfinement(FunctionId, FunctionId),
181}
182
183/// How certain we are about a proposition's recorded value.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
185pub enum Certainty {
186    /// A pass's heuristic guess; can be contradicted by [`set_known`]
187    /// (recording a [`Violation`]).
188    ///
189    /// [`set_known`]: crate::context::Context::set_known
190    Assumed,
191    /// Proven by a verification pass; survives checkpoint+replay rounds.
192    Known,
193}
194
195/// The recorded truth state of one [`Proposition`].
196#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
197pub struct Truth {
198    /// The polarity recorded for the proposition.
199    pub value: bool,
200    /// Guess or proven fact.
201    pub certainty: Certainty,
202    /// The pass that recorded this entry (from [`crate::pass_scope`]).
203    pub pass: PassName,
204}
205
206/// A proven fact contradicting an earlier assumption — the signal that the
207/// checkpoint+replay driver must discard the working copy and replay.
208#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
209pub struct Violation {
210    /// The contradicted proposition.
211    pub prop: Proposition,
212    /// The polarity that was assumed (the proven value is its negation).
213    pub assumed: bool,
214    /// The pass that made the wrong assumption.
215    pub assuming_pass: PassName,
216    /// The verification pass that proved the opposite.
217    pub asserting_pass: PassName,
218}
219
220/// A proven fact that contradicted an existing *known* fact (as opposed to a
221/// mere assumption). Unlike a [`Violation`], this is not a replay signal — it
222/// means two verification results, or a user-forced override and a verification
223/// result, disagree irreconcilably. The checkpoint+replay driver surfaces it as
224/// a hard error rather than looping.
225#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
226pub struct KnownContradiction {
227    /// The proposition proven two different ways.
228    pub prop: Proposition,
229    /// The value already recorded as known (e.g. a user override).
230    pub known: bool,
231    /// The value a later pass proved (the negation of `known`).
232    pub proven: bool,
233    /// The pass that recorded the original known fact.
234    pub known_pass: PassName,
235    /// The pass that proved the contradicting value.
236    pub proven_pass: PassName,
237}
238
239/// A pass name, as recorded on truth-map entries. A transparent `&'static str`
240/// wrapper: serde's derive would otherwise tie the deserializer lifetime to
241/// `'static`, so it gets manual impls — serialized as a string, deserialized by
242/// leaking. Pass names form a small finite set, so the leak is bounded.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
244pub struct PassName(pub &'static str);
245
246impl std::ops::Deref for PassName {
247    type Target = str;
248    fn deref(&self) -> &str {
249        self.0
250    }
251}
252
253impl std::fmt::Display for PassName {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        f.write_str(self.0)
256    }
257}
258
259impl PartialEq<&str> for PassName {
260    fn eq(&self, other: &&str) -> bool {
261        self.0 == *other
262    }
263}
264
265impl serde::Serialize for PassName {
266    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
267        s.serialize_str(self.0)
268    }
269}
270
271impl<'de> serde::Deserialize<'de> for PassName {
272    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
273        let s = String::deserialize(d)?;
274        Ok(PassName(Box::leak(s.into_boxed_str())))
275    }
276}