Skip to main content

r2smt_pcode/
machine.rs

1//! P-code → `IrStmt` lifter (strict, sound subset).
2//!
3//! Soundness boundary: only the **Z** flag is mapped to the canonical
4//! `ZF` the branch-condition composer reads (P-code `ZR` ≡ zero, no
5//! polarity ambiguity). P-code `NG`/`CY`/`OV` are *not* mapped to the
6//! canonical `SF`/`CF`/`OF`, because ARM `NZCV` polarity differs from
7//! the per-mnemonic `AArch64` model and a name-level merge would be
8//! unsound. They are lifted into distinct `pc_*` vars instead, so a
9//! branch that needs C/V/N simply leaves the canonical flag a free
10//! input downstream → the solver returns `BothPossible` (sound, never
11//! a fabricated verdict). Branches that read only Z (`eq`/`ne`,
12//! `cbz`/`cbnz`, `test;jz`, …) get a precise decompiler-grade slice.
13//! Any opcode outside the modelled subset returns [`PcodeError`] so
14//! the caller falls back to the ESIL / per-mnemonic lifter.
15
16use std::collections::BTreeMap;
17
18use r2smt_common::Arch;
19use r2smt_ir::expr::{Expr, Var};
20use r2smt_ir::stmt::IrStmt;
21
22use crate::parse::{ParseError, PcodeOp, Varnode, parse_pcode};
23
24/// Reasons the P-code lifter declines (caller falls back to ESIL).
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum PcodeError {
27    /// The `pdgsd` text could not be structurally parsed.
28    Parse(String),
29    /// An opcode outside the modelled sound subset.
30    UnsupportedOpcode(String),
31    /// A varnode shape the lifter does not model (e.g. nested `ram`).
32    BadVarnode(String),
33    /// An op had the wrong operand arity for its opcode.
34    Arity(String),
35}
36
37impl From<ParseError> for PcodeError {
38    fn from(e: ParseError) -> Self {
39        Self::Parse(e.0)
40    }
41}
42
43/// Lifter output — mirrors `r2smt_esil::EsilLift` so callers can
44/// splice it into their statement list directly.
45#[derive(Debug, Clone)]
46pub struct PcodeLift {
47    /// Statements produced, in execution order.
48    pub statements: Vec<IrStmt>,
49}
50
51/// Lift a `pdgsd` dump under `arch` into IR statements.
52///
53/// # Errors
54///
55/// Returns [`PcodeError`] on a parse failure or any construct outside
56/// the sound subset; the caller treats that as "use another IR".
57pub fn lift_pcode(text: &str, arch: Arch) -> Result<PcodeLift, PcodeError> {
58    let insns = parse_pcode(text)?;
59    let mut m = Machine::new(arch);
60    for insn in &insns {
61        for op in &insn.ops {
62            m.step(op)?;
63        }
64    }
65    Ok(PcodeLift {
66        statements: m.statements,
67    })
68}
69
70struct Machine {
71    arch: Arch,
72    /// Defined width (in bits) per varnode key, so a later read uses
73    /// the width its defining op assigned.
74    widths: BTreeMap<String, u16>,
75    statements: Vec<IrStmt>,
76}
77
78impl Machine {
79    fn new(arch: Arch) -> Self {
80        Self {
81            arch,
82            widths: BTreeMap::new(),
83            statements: Vec::new(),
84        }
85    }
86
87    fn step(&mut self, op: &PcodeOp) -> Result<(), PcodeError> {
88        match op.opcode.as_str() {
89            "STORE" => self.lift_store(op),
90            // Control flow carries no data-flow definition. The branch
91            // predicate is computed by the preceding flag ops and read
92            // by the existing slicer; emit nothing.
93            "BRANCH" | "CBRANCH" | "BRANCHIND" | "CALL" | "CALLIND" | "CALLOTHER" | "RETURN" => {
94                Ok(())
95            }
96            _ => self.lift_defining(op),
97        }
98    }
99
100    fn lift_store(&mut self, op: &PcodeOp) -> Result<(), PcodeError> {
101        let [addr, value] = op.inputs.as_slice() else {
102            return Err(PcodeError::Arity(format!("STORE inputs: {:?}", op.inputs)));
103        };
104        let addr_expr = self.read_mem_addr(addr)?;
105        let val_expr = self.read(value)?;
106        let bits = self.var_width(value);
107        self.statements.push(IrStmt::StoreMem {
108            address: addr_expr,
109            value: val_expr,
110            bits,
111        });
112        Ok(())
113    }
114
115    fn lift_defining(&mut self, op: &PcodeOp) -> Result<(), PcodeError> {
116        let Some(out) = &op.out else {
117            return Err(PcodeError::Arity(format!(
118                "{} expects an output",
119                op.opcode
120            )));
121        };
122
123        if op.opcode == "LOAD" {
124            let [src] = op.inputs.as_slice() else {
125                return Err(PcodeError::Arity(format!("LOAD inputs: {:?}", op.inputs)));
126            };
127            let addr = self.read_mem_addr(src)?;
128            let (dst, bits) = self.define(out, varnode_bits(out, self.arch));
129            self.statements.push(IrStmt::LoadMem {
130                dst,
131                address: addr,
132                bits,
133            });
134            return Ok(());
135        }
136
137        let out_bits = varnode_bits(out, self.arch);
138        let expr = self.eval(&op.opcode, &op.inputs, out_bits)?;
139        let (dst, _) = self.define(out, expr_bits(&expr, out_bits));
140        self.statements.push(IrStmt::Assign { dst, src: expr });
141        Ok(())
142    }
143
144    /// Build the value expression for a defining opcode.
145    fn eval(
146        &mut self,
147        opcode: &str,
148        inputs: &[Varnode],
149        out_bits: u16,
150    ) -> Result<Expr, PcodeError> {
151        let bin = |m: &mut Self| -> Result<(Expr, Expr), PcodeError> {
152            let [a, b] = inputs else {
153                return Err(PcodeError::Arity(format!("{opcode} needs 2 inputs")));
154            };
155            // A size-less constant takes its co-operand's width, not the
156            // pointer width. An `INT_SRIGHT` amount or an `INT_SLESS`
157            // immediate must share the register operand's architectural
158            // width, or the encoder zero-extends the *value* past its sign
159            // bit — a wrong arithmetic shift or signed compare. The output
160            // width is the wrong hint here: a compare's output is one bit.
161            // Falls back to the pointer width only when neither operand
162            // fixes it.
163            let pb = m.ptr_bits();
164            let (wa, wb) = (m.operand_bits(a), m.operand_bits(b));
165            Ok((
166                m.read_operand(a, wb.unwrap_or(pb))?,
167                m.read_operand(b, wa.unwrap_or(pb))?,
168            ))
169        };
170        let un = |m: &mut Self| -> Result<Expr, PcodeError> {
171            let [a] = inputs else {
172                return Err(PcodeError::Arity(format!("{opcode} needs 1 input")));
173            };
174            m.read(a)
175        };
176        let all_ones: u128 = if out_bits >= 128 {
177            u128::MAX
178        } else {
179            (1u128 << out_bits) - 1
180        };
181
182        match opcode {
183            "COPY" => un(self),
184            "INT_ADD" => bin(self).map(|(a, b)| Expr::add(a, b)),
185            "INT_SUB" => bin(self).map(|(a, b)| Expr::sub(a, b)),
186            "INT_MULT" => bin(self).map(|(a, b)| Expr::mul(a, b)),
187            "INT_AND" => bin(self).map(|(a, b)| Expr::bv_and(a, b)),
188            "INT_OR" => bin(self).map(|(a, b)| Expr::bv_or(a, b)),
189            "INT_XOR" => bin(self).map(|(a, b)| Expr::bv_xor(a, b)),
190            "INT_LEFT" => bin(self).map(|(a, b)| Expr::shl(a, b)),
191            "INT_RIGHT" => bin(self).map(|(a, b)| Expr::lshr(a, b)),
192            "INT_SRIGHT" => bin(self).map(|(a, b)| Expr::ashr(a, b)),
193            "INT_NEGATE" => un(self).map(|a| Expr::bv_xor(a, Expr::konst(all_ones, out_bits))),
194            "INT_2COMP" => un(self).map(|a| Expr::sub(Expr::konst(0, out_bits), a)),
195            "INT_ZEXT" => un(self).map(|a| Expr::zero_ext(a, out_bits)),
196            "INT_SEXT" => un(self).map(|a| Expr::sign_ext(a, out_bits)),
197            "INT_EQUAL" => bin(self).map(|(a, b)| Expr::eq(a, b)),
198            "INT_NOTEQUAL" => bin(self).map(|(a, b)| Expr::ne(a, b)),
199            "INT_LESS" => bin(self).map(|(a, b)| Expr::ult(a, b)),
200            "INT_LESSEQUAL" => bin(self).map(|(a, b)| Expr::ule(a, b)),
201            "INT_SLESS" => bin(self).map(|(a, b)| Expr::slt(a, b)),
202            "INT_SLESSEQUAL" => bin(self).map(|(a, b)| Expr::sle(a, b)),
203            "BOOL_NEGATE" => un(self).map(|a| Expr::Ite {
204                cond: Box::new(Expr::eq(a, Expr::konst(0, 1))),
205                then_expr: Box::new(Expr::konst(1, 1)),
206                else_expr: Box::new(Expr::konst(0, 1)),
207            }),
208            "BOOL_AND" => bin(self).map(|(a, b)| Expr::bv_and(a, b)),
209            "BOOL_OR" => bin(self).map(|(a, b)| Expr::bv_or(a, b)),
210            "BOOL_XOR" => bin(self).map(|(a, b)| Expr::bv_xor(a, b)),
211            "SUBPIECE" => {
212                let [a, off] = inputs else {
213                    return Err(PcodeError::Arity("SUBPIECE needs 2 inputs".into()));
214                };
215                let Varnode::Const {
216                    value: byte_off, ..
217                } = off
218                else {
219                    return Err(PcodeError::BadVarnode(format!(
220                        "SUBPIECE offset must be const: {off:?}"
221                    )));
222                };
223                let src = self.read(a)?;
224                let lo = u16::try_from(byte_off.saturating_mul(8))
225                    .map_err(|_| PcodeError::BadVarnode("SUBPIECE offset too large".into()))?;
226                let hi = lo.saturating_add(out_bits.saturating_sub(1));
227                Ok(Expr::extract(src, hi, lo))
228            }
229            other => Err(PcodeError::UnsupportedOpcode(other.to_string())),
230        }
231    }
232
233    /// The operand's bit width when it is fixed by the varnode itself:
234    /// `None` for a size-less constant (whose width a binary op must take
235    /// from its co-operand), `Some` otherwise.
236    fn operand_bits(&self, vn: &Varnode) -> Option<u16> {
237        match vn {
238            Varnode::Const { size: None, .. } => None,
239            _ => Some(varnode_bits(vn, self.arch)),
240        }
241    }
242
243    /// Read a varnode, sizing a size-less constant to `sizeless_const_bits`
244    /// (its co-operand's width) instead of the pointer width.
245    fn read_operand(&mut self, vn: &Varnode, sizeless_const_bits: u16) -> Result<Expr, PcodeError> {
246        if let Varnode::Const { value, size: None } = vn {
247            return Ok(Expr::konst(u128::from(*value), sizeless_const_bits));
248        }
249        self.read(vn)
250    }
251
252    /// Resolve a varnode read to an [`Expr`].
253    fn read(&mut self, vn: &Varnode) -> Result<Expr, PcodeError> {
254        match vn {
255            Varnode::Const { value, size } => {
256                let bits = size.map_or(self.ptr_bits(), |s| u16::from(s).saturating_mul(8));
257                Ok(Expr::konst(u128::from(*value), bits))
258            }
259            Varnode::Register(_) | Varnode::Unique { .. } => {
260                let name = Self::var_name(vn);
261                let bits = self.var_width(vn);
262                Ok(Expr::Var(Var::new(name, bits)))
263            }
264            Varnode::Ram(_) | Varnode::CodeAddr(_) => Err(PcodeError::BadVarnode(format!(
265                "value position cannot be {vn:?}"
266            ))),
267        }
268    }
269
270    fn read_mem_addr(&mut self, vn: &Varnode) -> Result<Expr, PcodeError> {
271        match vn {
272            Varnode::Ram(inner) => self.read(inner),
273            Varnode::Register(_) | Varnode::Unique { .. } => self.read(vn),
274            _ => Err(PcodeError::BadVarnode(format!("bad mem address: {vn:?}"))),
275        }
276    }
277
278    /// Register the defined width for `out` and return its IR `Var`.
279    fn define(&mut self, out: &Varnode, bits: u16) -> (Var, u16) {
280        let name = Self::var_name(out);
281        self.widths.insert(name.clone(), bits);
282        (Var::new(name, bits), bits)
283    }
284
285    fn var_width(&self, vn: &Varnode) -> u16 {
286        let name = Self::var_name(vn);
287        if let Some(w) = self.widths.get(&name) {
288            return *w;
289        }
290        varnode_bits(vn, self.arch)
291    }
292
293    /// Canonical IR variable name for a varnode. Only the Z flag is
294    /// mapped onto the canonical `ZF`; N/C/V get distinct `pc_*` names
295    /// so they never collide with the per-mnemonic flag model.
296    fn var_name(vn: &Varnode) -> String {
297        match vn {
298            Varnode::Unique { offset, size } => format!("u_{offset:x}_{size}"),
299            Varnode::Register(r) => map_register(r),
300            other => format!("?{other:?}"),
301        }
302    }
303
304    fn ptr_bits(&self) -> u16 {
305        match self.arch {
306            Arch::X86 | Arch::Arm => 32,
307            _ => 64,
308        }
309    }
310}
311
312/// Map a P-code register/flag name to a canonical IR variable name.
313fn map_register(r: &str) -> String {
314    match r {
315        // Z flag is polarity-free: P-code `ZR` ≡ canonical `ZF`.
316        "ZR" | "tmpZR" => "ZF".to_string(),
317        // N/C/V kept distinct from canonical SF/CF/OF (ARM polarity
318        // differs from the per-mnemonic model — see module docs).
319        "NG" | "tmpNG" => "pc_ng".to_string(),
320        "CY" | "tmpCY" => "pc_cy".to_string(),
321        "OV" | "tmpOV" => "pc_ov".to_string(),
322        // `wN` (32-bit) and `xN` (64-bit) are kept as distinct vars.
323        // P-code is explicit about the relationship — every w↔x
324        // transition is materialised by its own `INT_ZEXT` /
325        // `SUBPIECE` op — so faithfully mirroring the named varnodes
326        // is sound; an unmaterialised alias merely leaves a free
327        // input (→ sound `BothPossible`), never a wrong verdict.
328        _ => r.to_string(),
329    }
330}
331
332/// Natural width (bits) of a varnode before any defining op overrides.
333fn varnode_bits(vn: &Varnode, arch: Arch) -> u16 {
334    match vn {
335        Varnode::Unique { size, .. } => u16::from(*size).saturating_mul(8).max(1),
336        Varnode::Const { size, .. } => size.map_or(64, |s| u16::from(s).saturating_mul(8)).max(1),
337        Varnode::Register(r) => register_bits(r, arch),
338        Varnode::Ram(_) | Varnode::CodeAddr(_) => 64,
339    }
340}
341
342fn register_bits(r: &str, arch: Arch) -> u16 {
343    // The GPR / stack / link / program-counter names carry the
344    // architecture's pointer width: 32 on `Arch::Arm` (ARM32 `r0..r15`,
345    // `sp`/`lr`/`pc` are 32-bit), 64 on `Aarch64` and `x86_64`. Minting
346    // an ARM32 `r0` as a 64-bit varnode is a wrong *value*, not a lost
347    // one — a signed compare of a 32-bit-negative value zero-extended
348    // into 64 bits reads as positive. The `xN` / `wN` names are AArch64
349    // only and keep their fixed 64 / 32.
350    let ptr = arch.pointer_bits();
351    match r {
352        "ZR" | "tmpZR" | "NG" | "tmpNG" | "CY" | "tmpCY" | "OV" | "tmpOV" => 1,
353        "sp" | "lr" | "fp" | "pc" => ptr,
354        _ if r.starts_with('x') && r[1..].chars().all(|c| c.is_ascii_digit()) => 64,
355        _ if r.starts_with('w') && r[1..].chars().all(|c| c.is_ascii_digit()) => 32,
356        _ if r.starts_with('r') && r[1..].chars().all(|c| c.is_ascii_digit()) => ptr,
357        _ => ptr,
358    }
359}
360
361fn expr_bits(expr: &Expr, fallback: u16) -> u16 {
362    match expr {
363        Expr::Const { bits, .. } => *bits,
364        Expr::Var(v) => v.bits,
365        Expr::ZeroExtend { to_bits, .. } | Expr::SignExtend { to_bits, .. } => *to_bits,
366        Expr::Eq(..)
367        | Expr::Ne(..)
368        | Expr::Ult(..)
369        | Expr::Ule(..)
370        | Expr::Slt(..)
371        | Expr::Sle(..) => 1,
372        Expr::Ite { then_expr, .. } => expr_bits(then_expr, fallback),
373        Expr::Extract { hi, lo, .. } => hi.saturating_sub(*lo).saturating_add(1),
374        _ => fallback,
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
381
382    use super::*;
383
384    #[test]
385    fn test_lift_int_arith_chain_to_assigns() {
386        let txt = "\
3870x100: mul w8, w8, w9
388    (unique,0x2ae80,4) = INT_MULT w8, w9
389    x8 = INT_ZEXT (unique,0x2ae80,4)";
390        let lift = lift_pcode(txt, Arch::Aarch64).unwrap();
391        assert_eq!(lift.statements.len(), 2);
392        match &lift.statements[0] {
393            IrStmt::Assign { dst, src } => {
394                assert_eq!(dst.name, "u_2ae80_4");
395                assert_eq!(dst.bits, 32);
396                assert_eq!(
397                    *src,
398                    Expr::mul(Expr::Var(Var::new("w8", 32)), Expr::Var(Var::new("w9", 32)))
399                );
400            }
401            other => panic!("expected Assign, got {other:?}"),
402        }
403    }
404
405    #[test]
406    fn size_less_shift_amount_takes_the_register_width_not_pointer_width() {
407        // `asr w8, w9, #4` -> `INT_SRIGHT w9, 0x4`. The size-less shift
408        // amount must be 32-bit (w9's width): at the 64-bit pointer width
409        // the encoder zero-extends w9 to 64 and the arithmetic shift loses
410        // its sign bit (`0x80000000 asr 4` gives 0x08000000, not 0xF8000000).
411        let txt = "\
4120x100: asr w8, w9, #4
413    w8 = INT_SRIGHT w9, 0x4";
414        let lift = lift_pcode(txt, Arch::Aarch64).unwrap();
415        let IrStmt::Assign { src, .. } = &lift.statements[0] else {
416            panic!("expected Assign");
417        };
418        assert_eq!(
419            *src,
420            Expr::ashr(Expr::Var(Var::new("w9", 32)), Expr::konst(4, 32))
421        );
422    }
423
424    #[test]
425    fn size_less_signed_compare_immediate_takes_the_register_width() {
426        // `INT_SLESS w8, 0xffffffff`: the immediate must be 32-bit so it
427        // reads as -1, not the 64-bit positive 0xffffffff. At the pointer
428        // width the signed compare flips for w8 = 0.
429        let txt = "\
4300x100: cmp w8, w9
431    (unique,0x10,1) = INT_SLESS w8, 0xffffffff";
432        let lift = lift_pcode(txt, Arch::Aarch64).unwrap();
433        let IrStmt::Assign { src, .. } = &lift.statements[0] else {
434            panic!("expected Assign");
435        };
436        assert_eq!(
437            *src,
438            Expr::slt(Expr::Var(Var::new("w8", 32)), Expr::konst(0xffff_ffff, 32))
439        );
440    }
441
442    #[test]
443    fn arm32_gpr_is_32_bit_not_defaulted_to_64() {
444        // On `Arch::Arm`, `r0..r15` / `sp` / `lr` / `pc` are 32-bit.
445        // Minting them at the old hardcoded 64 is a wrong signed value:
446        // a 32-bit-negative operand zero-extended to 64 reads as
447        // positive under a later signed compare.
448        let txt = "\
4490x100: add r0, r0, r1
450    r0 = INT_ADD r0, r1";
451        let lift = lift_pcode(txt, Arch::Arm).unwrap();
452        let IrStmt::Assign { dst, src } = &lift.statements[0] else {
453            panic!("expected Assign");
454        };
455        assert_eq!(dst.bits, 32);
456        assert_eq!(
457            *src,
458            Expr::add(Expr::Var(Var::new("r0", 32)), Expr::Var(Var::new("r1", 32)))
459        );
460    }
461
462    #[test]
463    fn test_lift_z_flag_maps_to_canonical_zf() {
464        // `tmpZR = INT_EQUAL (sub), 0 ; ZR = COPY tmpZR` — both must
465        // resolve to canonical `ZF` so the branch composer reads it.
466        let txt = "\
4670x100: subs w8, w8, #2
468    (unique,0x10,4) = INT_SUB w8, 0x2
469    tmpZR = INT_EQUAL (unique,0x10,4), 0x0
470    ZR = COPY tmpZR";
471        let lift = lift_pcode(txt, Arch::Aarch64).unwrap();
472        let IrStmt::Assign { dst, .. } = &lift.statements[2] else {
473            panic!("expected Assign");
474        };
475        assert_eq!(dst.name, "ZF");
476        assert_eq!(dst.bits, 1);
477    }
478
479    #[test]
480    fn test_lift_ncv_flags_stay_non_canonical() {
481        // `CY` must NOT become canonical `CF` (ARM polarity differs).
482        let txt = "\
4830x100: subs w8, w8, #2
484    tmpCY = INT_LESSEQUAL 0x2, w8
485    CY = COPY tmpCY";
486        let lift = lift_pcode(txt, Arch::Aarch64).unwrap();
487        let IrStmt::Assign { dst, .. } = &lift.statements[1] else {
488            panic!("expected Assign");
489        };
490        assert_eq!(dst.name, "pc_cy");
491        assert_ne!(dst.name, "CF");
492    }
493
494    #[test]
495    fn test_lift_load_store_emit_mem_stmts() {
496        let txt = "\
4970x100: ldr w8, [sp, #8]
498    (unique,0x60,8) = INT_ADD sp, 0x8
499    (unique,0x247,4) = LOAD ram[(unique,0x60,8)]
5000x104: str w8, [sp, #8]
501    STORE ram[(unique,0x60,8)] = w8";
502        let lift = lift_pcode(txt, Arch::Aarch64).unwrap();
503        assert!(matches!(
504            lift.statements[1],
505            IrStmt::LoadMem { bits: 32, .. }
506        ));
507        assert!(matches!(
508            lift.statements[2],
509            IrStmt::StoreMem { bits: 32, .. }
510        ));
511    }
512
513    #[test]
514    fn test_unsupported_opcode_errors_for_fallback() {
515        let txt = "\
5160x100: fmul d0, d0, d1
517    d0 = FLOAT_MULT d0, d1";
518        let err = lift_pcode(txt, Arch::Aarch64).unwrap_err();
519        assert_eq!(err, PcodeError::UnsupportedOpcode("FLOAT_MULT".into()));
520    }
521
522    #[test]
523    fn test_parse_error_propagates_as_pcode_error() {
524        let err = lift_pcode("    orphan = INT_ADD a, b", Arch::Aarch64).unwrap_err();
525        assert!(matches!(err, PcodeError::Parse(_)));
526    }
527}