Skip to main content

qcode_vm/
hook.rs

1//! Hooks as rewrites of the lifted code.
2//!
3//! A [`Hook`] names the *sites* in a block it cares about — the block's entry,
4//! a guest address, every store to guest memory, every comparison — and, for
5//! each, emits QCode through an [`Emitter`]. What it emits is ordinary IR: a
6//! [`VM_INTERRUPT`] where the host must act, arithmetic to decide whether it
7//! must, loads and stores that count. The interpreter and the JIT run the
8//! result like any other code, so a hook whose condition is false costs a few
9//! native instructions and never leaves compiled code.
10//!
11//! The two ways to stop:
12//!
13//! - [`Emitter::interrupt`] places an unconditional interrupt before the site.
14//! - [`Emitter::interrupt_if`] places the interrupt on a detour: the block is
15//!   split before the site, and a conditional branch chooses between a small
16//!   block holding the interrupt and the rest of the code. Only the condition
17//!   is evaluated on the fast path.
18//!
19//! Both pass *values* to the host — literals, or anything the block computes,
20//! such as the address and datum of a store — which the exit reports as the
21//! interrupt's arguments.
22//!
23//! [`HookInjector`] adapts a hook to the [`CodeInjector`] the machine runs,
24//! and handles idempotence: a site is instrumented once, remembered by its
25//! anchor instruction, which survives absorption and disappears with a
26//! re-lift.
27
28use qcode::{
29    context::Context,
30    space::MemorySpaceId,
31    value::{
32        BasicBlock, BlockId, InstructionId, ValueId,
33        insn::{Binop, IntBinop, Mnemonic, Store, VM_INTERRUPT},
34    },
35};
36use rustc_hash::FxHashSet;
37
38use crate::inject::CodeInjector;
39
40/// A point in a block a hook may instrument. Every site is anchored to the
41/// instruction it precedes, which is where the hook's code is inserted.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Site {
44    /// The block's first instruction; `address` is the block's.
45    BlockEntry { address: u64, anchor: InstructionId },
46    /// The first instruction lifted from a guest instruction.
47    Address { address: u64, anchor: InstructionId },
48    /// A store to guest memory.
49    Store { insn: InstructionId },
50    /// A load from guest memory.
51    Load { insn: InstructionId },
52    /// An integer comparison.
53    Compare { insn: InstructionId },
54}
55
56impl Site {
57    /// The instruction the hook's code goes before.
58    pub fn anchor(&self) -> InstructionId {
59        match self {
60            Self::BlockEntry { anchor, .. } | Self::Address { anchor, .. } => *anchor,
61            Self::Store { insn } | Self::Load { insn } | Self::Compare { insn } => *insn,
62        }
63    }
64}
65
66/// A read-only look at a block, for choosing sites.
67pub struct BlockView<'a> {
68    pub ctx: &'a Context<'static>,
69    pub block: BlockId,
70}
71
72impl BlockView<'_> {
73    /// The block's guest address, if it starts at one.
74    pub fn address(&self) -> Option<u64> {
75        BasicBlock::from_id(self.ctx, self.block).address()
76    }
77
78    /// The site at the block's entry, if the block starts at a guest address.
79    ///
80    /// Anchored on the first instruction that is not an interrupt an earlier
81    /// hook placed at the entry, so hooks on the same entry fire in
82    /// registration order and a hook recognises its own anchor when asked
83    /// again.
84    pub fn entry(&self) -> Option<Site> {
85        let address = self.address()?;
86        let anchor = BasicBlock::from_id(self.ctx, self.block)
87            .instructions()
88            .find(|insn| !is_interrupt_op(self.ctx, insn.id))
89            .map(|insn| insn.id)?;
90        Some(Site::BlockEntry { address, anchor })
91    }
92
93    /// Every guest instruction that *starts* in the block, in order, as a
94    /// site on the first instruction lifted from it.
95    ///
96    /// One guest instruction's p-code may branch within itself, so a block
97    /// with no address of its own can open with the tail of an instruction
98    /// begun in its predecessor. Those instructions carry the predecessor's
99    /// last address; they are a continuation, not a start, and get no site.
100    pub fn addresses(&self) -> Vec<Site> {
101        let block = BasicBlock::from_id(self.ctx, self.block);
102        let continued = if block.address().is_none() {
103            block
104                .predecessors()
105                .filter_map(|(_, pred)| {
106                    BasicBlock::from_id(self.ctx, pred)
107                        .instructions()
108                        .last()
109                        .and_then(|insn| insn.address())
110                })
111                .collect::<Vec<u64>>()
112        } else {
113            Vec::new()
114        };
115        let mut sites = Vec::new();
116        let mut seen = None;
117        for insn in block.instructions() {
118            // An interrupt a hook placed carries the site's address so the
119            // stop reports it, but it is the hook's instruction, not the
120            // guest's: never the start of a run, never an anchor.
121            if is_interrupt_op(self.ctx, insn.id) {
122                continue;
123            }
124            let at = insn.address();
125            if at.is_some() && at != seen {
126                let first_run = seen.is_none();
127                seen = at;
128                let address = at.unwrap_or_default();
129                if first_run && continued.contains(&address) {
130                    continue;
131                }
132                sites.push(Site::Address {
133                    address,
134                    anchor: insn.id,
135                });
136            }
137        }
138        sites
139    }
140
141    /// The instruction addressed by the guest instruction at `address`, as a
142    /// site, if the block covers it.
143    pub fn at(&self, address: u64) -> Option<Site> {
144        self.addresses()
145            .into_iter()
146            .find(|site| matches!(site, Site::Address { address: at, .. } if *at == address))
147    }
148
149    fn is_ram(&self, space: qcode::space::LocalMemorySpaceId) -> bool {
150        space.qualify(self.block.func) == MemorySpaceId::Shared(self.ctx.shared.default_space)
151    }
152
153    /// Every store to guest memory, in order.
154    pub fn stores(&self) -> Vec<Site> {
155        BasicBlock::from_id(self.ctx, self.block)
156            .instructions()
157            .filter(|insn| matches!(insn.mnemonic(), Mnemonic::Store(store) if self.is_ram(store.space)))
158            .map(|insn| Site::Store { insn: insn.id })
159            .collect()
160    }
161
162    /// Every load from guest memory, in order.
163    pub fn loads(&self) -> Vec<Site> {
164        BasicBlock::from_id(self.ctx, self.block)
165            .instructions()
166            .filter(
167                |insn| matches!(insn.mnemonic(), Mnemonic::Load(load) if self.is_ram(load.space)),
168            )
169            .map(|insn| Site::Load { insn: insn.id })
170            .collect()
171    }
172
173    /// Every integer comparison, in order.
174    pub fn compares(&self) -> Vec<Site> {
175        BasicBlock::from_id(self.ctx, self.block)
176            .instructions()
177            .filter(|insn| {
178                matches!(insn.mnemonic(), Mnemonic::Binop(binary) if binary.op.is_comparison()
179                    && matches!(binary.op, Binop::Int(_)))
180            })
181            .map(|insn| Site::Compare { insn: insn.id })
182            .collect()
183    }
184}
185
186/// A rewrite of lifted code at chosen sites. See the [module docs](self).
187pub trait Hook {
188    /// The sites in `block` to instrument. Returned in block order; a hook
189    /// that instruments none returns an empty list.
190    fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site>;
191
192    /// Emits the hook's code at `site`.
193    fn instrument(&mut self, site: &Site, emit: &mut Emitter<'_>);
194}
195
196/// Emits QCode before a site's anchor, on the hook's behalf.
197///
198/// Values are [`ValueId`]s: literals from [`constant`](Self::constant),
199/// operands of the anchor from [`store_operands`](Self::store_operands) and
200/// friends, or the results of arithmetic emitted here. Everything emitted
201/// stays *before* the anchor, in emission order.
202pub struct Emitter<'a> {
203    ctx: &'a mut Context<'static>,
204    /// The block the anchor currently sits in; a split moves it.
205    block: BlockId,
206    anchor: InstructionId,
207    /// The guest address the emitted code is stamped with.
208    address: Option<u64>,
209}
210
211impl<'a> Emitter<'a> {
212    pub fn new(ctx: &'a mut Context<'static>, site: &Site) -> Self {
213        let anchor = site.anchor();
214        let block = qcode::value::Instruction::from_id(ctx, anchor)
215            .parent()
216            .map(|block| block.id)
217            .expect("a site's anchor is in a block");
218        let address = match site {
219            Site::BlockEntry { address, .. } | Site::Address { address, .. } => Some(*address),
220            _ => qcode::value::Instruction::from_id(ctx, anchor).address(),
221        };
222        Self {
223            ctx,
224            block,
225            anchor,
226            address,
227        }
228    }
229
230    pub fn ctx(&self) -> &Context<'static> {
231        self.ctx
232    }
233
234    /// The guest address of the site.
235    pub fn address(&self) -> Option<u64> {
236        self.address
237    }
238
239    /// An integer literal of `size` bytes.
240    pub fn constant(&self, value: u64, size: usize) -> ValueId {
241        self.ctx.shared.get_const(value, size)
242    }
243
244    /// The width in bytes of a value.
245    pub fn size_of(&self, value: ValueId) -> usize {
246        self.ctx
247            .stored_type_of(value)
248            .map(|ty| self.ctx.shared.types.size_of(ty))
249            .unwrap_or(0)
250    }
251
252    /// The anchor's store operands: pointer, width and stored value, if the
253    /// anchor is a store.
254    pub fn store_operands(&self) -> Option<(ValueId, usize, ValueId)> {
255        let insn = self.ctx.instruction(self.anchor);
256        let Mnemonic::Store(Store { ptr, size, src, .. }) = insn.mnemonic() else {
257            return None;
258        };
259        let func = self.anchor.func;
260        Some((ptr.qualify(func), *size, src.qualify(func)))
261    }
262
263    /// The anchor's load operands: pointer and width, if the anchor is a load.
264    pub fn load_operands(&self) -> Option<(ValueId, usize)> {
265        let insn = self.ctx.instruction(self.anchor);
266        let Mnemonic::Load(load) = insn.mnemonic() else {
267            return None;
268        };
269        Some((load.ptr.qualify(self.anchor.func), load.size))
270    }
271
272    /// The anchor's binary operands, if the anchor is a binary operation.
273    pub fn binop_operands(&self) -> Option<(Binop, ValueId, ValueId)> {
274        let insn = self.ctx.instruction(self.anchor);
275        let Mnemonic::Binop(binary) = insn.mnemonic() else {
276            return None;
277        };
278        let func = self.anchor.func;
279        Some((
280            binary.op,
281            binary.lhs.qualify(func),
282            binary.rhs.qualify(func),
283        ))
284    }
285
286    /// Emits an integer binary operation before the anchor.
287    ///
288    /// Emitted arithmetic carries no guest address: it is the hook's, not
289    /// the guest instruction's, and stamping it would make it look like the
290    /// start of that instruction to the next hook choosing sites.
291    pub fn binop(&mut self, op: IntBinop, lhs: ValueId, rhs: ValueId) -> ValueId {
292        let (block, anchor) = (self.block, self.anchor);
293        let mut builder = self.ctx.builder(block);
294        builder.set_insert_point_before(anchor);
295        builder.push_binop(Binop::Int(op), lhs, rhs).id()
296    }
297
298    /// Zero-extends (or truncates) `value` to `size` bytes.
299    pub fn zext(&mut self, value: ValueId, size: usize) -> ValueId {
300        if self.size_of(value) == size {
301            return value;
302        }
303        let (block, anchor) = (self.block, self.anchor);
304        let mut builder = self.ctx.builder(block);
305        builder.set_insert_point_before(anchor);
306        builder.push_zext(value, size).id()
307    }
308
309    /// `value - begin < end - begin + 1`, as a one-byte condition: whether a
310    /// 64-bit `value` lies in `begin..=end`.
311    pub fn in_range(&mut self, value: ValueId, begin: u64, end: u64) -> ValueId {
312        let value = self.zext(value, 8);
313        let offset = self.binop(IntBinop::Sub, value, self.constant(begin, 8));
314        let length = self.constant(end.wrapping_sub(begin).wrapping_add(1), 8);
315        self.binop(IntBinop::Less, offset, length)
316    }
317
318    /// Stops unconditionally before the anchor with `vm.interrupt(code,
319    /// args...)`.
320    pub fn interrupt(&mut self, code: u64, args: &[ValueId]) -> InstructionId {
321        let (block, anchor, address) = (self.block, self.anchor, self.address);
322        crate::inject::insert_interrupt(self.ctx, block, Some(anchor), address, code, args)
323    }
324
325    /// Stops before the anchor only when `cond` is non-zero, without leaving
326    /// compiled code otherwise.
327    ///
328    /// The block is split before the anchor; the code emitted so far stays in
329    /// the first half, which ends in `cbranch cond -> hook, rest`, where
330    /// `hook` holds the interrupt and falls through to `rest`, the second
331    /// half. Later emissions go before the anchor in `rest`.
332    pub fn interrupt_if(&mut self, cond: ValueId, code: u64, args: &[ValueId]) -> InstructionId {
333        let (block, anchor, address) = (self.block, self.anchor, self.address);
334        let func = block.func;
335        let rest = self.ctx.split_block_before(block, anchor);
336        let hook = self.ctx.body_mut(func).make_block();
337        let interrupt = crate::inject::insert_interrupt(self.ctx, hook, None, address, code, args);
338        {
339            let mut builder = self.ctx.builder(hook);
340            if let Some(address) = address {
341                builder.set_address(address);
342            }
343            builder.finalize(rest);
344        }
345        {
346            // No address on the branch either: the rest of the block then
347            // starts the guest instruction it starts, rather than looking
348            // like a continuation of one.
349            let mut builder = self.ctx.builder(block);
350            builder.push_cbranch(cond, hook, rest);
351        }
352        self.block = rest;
353        interrupt
354    }
355}
356
357/// Runs a [`Hook`] as the machine's [`CodeInjector`], instrumenting each site
358/// once.
359pub struct HookInjector<H> {
360    pub hook: H,
361    done: FxHashSet<InstructionId>,
362}
363
364impl<H: Hook> HookInjector<H> {
365    pub fn new(hook: H) -> Self {
366        Self {
367            hook,
368            done: FxHashSet::default(),
369        }
370    }
371}
372
373impl<H: Hook> CodeInjector for HookInjector<H> {
374    fn inject(&mut self, ctx: &mut Context<'static>, block: BlockId) {
375        let sites = self.hook.sites(&BlockView { ctx, block });
376        for site in sites {
377            if !self.done.insert(site.anchor()) {
378                continue;
379            }
380            // The emitter finds the anchor's block itself: an earlier site's
381            // split may have moved this one.
382            let mut emit = Emitter::new(ctx, &site);
383            self.hook.instrument(&site, &mut emit);
384        }
385    }
386}
387
388/// Whether `insn` is a [`VM_INTERRUPT`] op.
389pub fn is_interrupt_op(ctx: &Context<'static>, insn: InstructionId) -> bool {
390    match ctx.instruction(insn).mnemonic() {
391        Mnemonic::PCodeOp(op) => ctx.shared.pcode_ops[op.id].as_ref() == VM_INTERRUPT,
392        _ => false,
393    }
394}
395
396// ---- The built-in hooks: the shapes every hook layer needs, and the pattern
397// ---- for writing more.
398
399/// Whether `addr` lies in `begin..=end`, or anywhere when `begin > end` —
400/// Unicorn's convention for "no range".
401fn in_range(begin: u64, end: u64, addr: u64) -> bool {
402    begin > end || (begin..=end).contains(&addr)
403}
404
405/// Stops at the entry of every block whose address lies in a range, with
406/// `vm.interrupt(code, address)`.
407#[derive(Debug, Clone)]
408pub struct BlockEntryHook {
409    pub begin: u64,
410    pub end: u64,
411    pub code: u64,
412}
413
414impl Hook for BlockEntryHook {
415    fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site> {
416        block
417            .entry()
418            .filter(|site| matches!(site, Site::BlockEntry { address, .. } if in_range(self.begin, self.end, *address)))
419            .into_iter()
420            .collect()
421    }
422
423    fn instrument(&mut self, _site: &Site, emit: &mut Emitter<'_>) {
424        let address = emit.constant(emit.address().unwrap_or_default(), 8);
425        emit.interrupt(self.code, &[address]);
426    }
427}
428
429/// Stops before the guest instruction at each of a set of addresses, with
430/// `vm.interrupt(code, address)`.
431#[derive(Debug, Clone)]
432pub struct AddressHook {
433    pub addresses: FxHashSet<u64>,
434    pub code: u64,
435}
436
437impl AddressHook {
438    pub fn new(addresses: impl IntoIterator<Item = u64>, code: u64) -> Self {
439        Self {
440            addresses: addresses.into_iter().collect(),
441            code,
442        }
443    }
444}
445
446impl Hook for AddressHook {
447    fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site> {
448        block
449            .addresses()
450            .into_iter()
451            .filter(|site| matches!(site, Site::Address { address, .. } if self.addresses.contains(address)))
452            .collect()
453    }
454
455    fn instrument(&mut self, _site: &Site, emit: &mut Emitter<'_>) {
456        let address = emit.constant(emit.address().unwrap_or_default(), 8);
457        emit.interrupt(self.code, &[address]);
458    }
459}
460
461/// Stops before every store to guest memory whose address lies in
462/// `begin..=end`, with `vm.interrupt(code, address, size, value)`.
463///
464/// The range check is emitted as IR before the store, so a store elsewhere
465/// costs three native instructions and no exit. The stored value is passed
466/// when it fits an interrupt argument (8 bytes or fewer).
467#[derive(Debug, Clone)]
468pub struct WriteWatch {
469    pub begin: u64,
470    pub end: u64,
471    pub code: u64,
472}
473
474impl Hook for WriteWatch {
475    fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site> {
476        block.stores()
477    }
478
479    fn instrument(&mut self, _site: &Site, emit: &mut Emitter<'_>) {
480        let Some((ptr, size, value)) = emit.store_operands() else {
481            return;
482        };
483        let cond = emit.in_range(ptr, self.begin, self.end);
484        let mut args = vec![emit.zext(ptr, 8), emit.constant(size as u64, 8)];
485        if size <= 8 {
486            args.push(emit.zext(value, 8));
487        }
488        emit.interrupt_if(cond, self.code, &args);
489    }
490}
491
492/// Stops before every integer comparison with `vm.interrupt(code, lhs, rhs)`:
493/// the operands as the guest computed them, which is what a comparison
494/// logger for a fuzzer wants.
495#[derive(Debug, Clone)]
496pub struct CompareHook {
497    pub code: u64,
498}
499
500impl Hook for CompareHook {
501    fn sites(&mut self, block: &BlockView<'_>) -> Vec<Site> {
502        block.compares()
503    }
504
505    fn instrument(&mut self, _site: &Site, emit: &mut Emitter<'_>) {
506        let Some((_, lhs, rhs)) = emit.binop_operands() else {
507            return;
508        };
509        emit.interrupt(self.code, &[lhs, rhs]);
510    }
511}