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}