baryl 0.0.2

Public SDK for Baryl, a full-system emulation and introspection engine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
//! Guest virtual memory: translation, typed reads, scans, and the register
//! file.
//!
//! Reached as `ctl.subs.arch`. Every call takes a page-table root — a CR3 on
//! x86-64 — alongside the address: the same virtual address under two roots is
//! two different bytes. Get a root from `regs.cr(3)`, from an
//! `#[arch(address_space_change)]` event, or from `Process::dtb`.
//! `ArchRef::mem` pairs one with the handle as a `GuestMem` for a run of reads.
//!
//! What a register file, a page-table entry, a fault or a guest address is
//! made of is the ISA's to say, so those types arrive only when a feature
//! selects one — `features = ["x86_64"]` brings `X64Regs`, `MemAttr`,
//! `GuestAddr` and `CpuExceptionContext`. Without it this module is the handle
//! and the ISA-neutral vocabulary alone.
//!
//! These calls read the subsystem's table without checking it is there, so
//! write `requires(arch)` on your `#[component]`.

use core::ffi::c_void;

#[cfg(feature = "x86_64")]
use core::ffi::c_int;

/// Re-exported so a caller of the scans below reaches these without a second
/// import.
pub use crate::abi::{BARYL_SCAN_NO_MATCH, ProgressFn, ProgressSink, ScanRange};
#[cfg(feature = "x86_64")]
use crate::abi::{PhysAddr, SbxPtr, VirtAddr, progress_thunk};

// Bindgen output cannot satisfy the workspace lints; the allow stops here.
// Two files, because codegen runs before cargo has resolved a feature.
#[cfg(feature = "x86_64")]
mod generated {
    #![allow(non_camel_case_types, non_upper_case_globals, dead_code)]
    include!("generated-x86_64.rs");
}
#[cfg(not(feature = "x86_64"))]
mod generated {
    #![allow(non_camel_case_types, non_upper_case_globals, dead_code)]
    include!("generated.rs");
}
pub use generated::*;

/// The CPU-exception handler as the ABI calls it: your state, the `Control`,
/// and what the fault carried. `#[arch(cpu_exception)]` writes one for you.
pub type CpuExcCb = unsafe extern "C" fn(*mut c_void, *mut c_void, *const CpuExceptionContext);

/// The address-space-change handler: your state, the `Control`, and the new
/// page-table root. `#[arch(address_space_change)]` writes one for you.
pub type AspaceChgCb = unsafe extern "C" fn(*mut c_void, *mut c_void, u64);

#[cfg(feature = "x86_64")]
mod exception;
#[cfg(feature = "x86_64")]
pub use exception::AccessType;

#[cfg(feature = "x86_64")]
mod mem;
#[cfg(feature = "x86_64")]
pub use mem::{GuestMem, MAX_INLINE_STR};

/// A page-table root as a value: CR3 on x86-64, TTBR0 on aarch64.
///
/// A root is page-aligned, so its low 12 bits carry flags rather than address —
/// on x86-64 that is the PCID. Two reads of the same address space can
/// therefore differ as numbers; [`normalize`](Self::normalize) makes them
/// compare equal.
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct AddressSpace(pub u64);

/// The low bits of a root that are flags, not address.
const ROOT_FLAG_MASK: u64 = 0xFFF;

impl AddressSpace {
    /// Clear the flag bits, so two reads of one address space compare equal.
    pub fn normalize(self) -> Self {
        Self(self.0 & !ROOT_FLAG_MASK)
    }
}

/// Implemented by each ISA's register-file type, and by nothing else.
///
/// Its only job is to carry an id, which is what `Subsystems::regs` checks
/// against the ISA the run is actually on before handing back a reference.
pub trait Arch {
    /// The `BARYL_ARCH_*` value naming this ISA.
    const ID: u32;
}

/// One ISA, as a value you can store and compare.
///
/// Holds any `u32`, including one this build has no constant for. The ids it
/// does name are [`ANY`](Self::ANY) and [`X86_64`](Self::X86_64); compare
/// against those, and treat anything else as an ISA you do not handle.
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ArchId(pub u32);

impl ArchId {
    /// No particular ISA — 0, so a zeroed field reads as neutral.
    pub const ANY: Self = Self(0);

    /// x86-64. Present whether or not the `x86_64` feature is on: the id is
    /// just a number, and only the register file it names needs the feature.
    pub const X86_64: Self = Self(BARYL_ARCH_X86_64);
}

/// The x86-64 register file: a table of pointers into wherever the engine
/// keeps its CPU state.
///
/// Get one from `Subsystems::regs`, then reach a register through the accessor
/// named after it. Each accessor answers an `SbxPtr` into the engine's own
/// storage, so a deref reads the live value and an assignment writes it.
///
/// Covers what it takes to decode and address the guest, plus the MSRs that say
/// where the kernel is. x87 and SSE/AVX are not here.
///
/// # Examples
///
/// ```ignore
/// let Ok(regs): Result<&X64Regs, _> = t.subs.regs() else { return };
///
/// let rip = *regs.rip();
/// let rsp = *regs.gpr(X64GPR::Rsp);
/// let cr3 = *regs.cr(3);
/// baryl::logging::info!("rip {rip:#x} rsp {rsp:#x} cr3 {cr3:#x}");
///
/// // Step past a two-byte instruction.
/// let mut pc = regs.rip();
/// *pc = rip + 2;
/// ```
#[cfg(feature = "x86_64")]
pub use generated::RegLayoutX64 as X64Regs;

#[cfg(feature = "x86_64")]
impl Arch for X64Regs {
    const ID: u32 = BARYL_ARCH_X86_64;
}

/// Reading a register is a pointer dereference, not a call across a boundary.
#[cfg(feature = "x86_64")]
impl X64Regs {
    /// The program counter.
    pub fn rip(&self) -> SbxPtr<u64> {
        unsafe { SbxPtr::from_raw(self.rip) }
    }

    /// The flags register — but read it only where the guest stopped on an
    /// instruction boundary: a breakpoint, a CPU exception, a single step. DF
    /// reads clear at a breakpoint, and at a notification exit the arithmetic
    /// flags may read cleared whatever the guest's real state is.
    ///
    /// A write reaches only the bits outside CF, PF, AF, ZF, SF, OF and DF: an
    /// engine with lazy flag evaluation keeps those elsewhere and resumes from
    /// there, so writing them here does nothing.
    pub fn rflags(&self) -> SbxPtr<u64> {
        unsafe { SbxPtr::from_raw(self.rflags) }
    }

    /// One general-purpose register, by name.
    ///
    /// [`X64GPR`] is the only way to index this block, and nothing
    /// bounds-checks. Unlike [`cr`](Self::cr) and [`dr`](Self::dr), there is no
    /// number to get wrong.
    pub fn gpr(&self, r: X64GPR) -> SbxPtr<u64> {
        unsafe { SbxPtr::from_raw((self.gpr as *mut u64).add(r as usize)) }
    }

    /// One segment register with its hidden descriptor cache — base, limit,
    /// flags and selector.
    pub fn seg(&self, s: X64Seg) -> SbxPtr<X64Segment> {
        unsafe { SbxPtr::from_raw(self.seg.add(s as usize)) }
    }

    /// A control register by number, so `cr(3)` is CR3. CR1 is architecturally
    /// reserved, and `n` above 4 reads past the block — this is not checked.
    pub fn cr(&self, n: usize) -> SbxPtr<u64> {
        unsafe { SbxPtr::from_raw(self.cr.add(n)) }
    }

    /// A debug register by number. DR4 and DR5 alias DR6 and DR7, and `n` above
    /// 7 reads past the block — this is not checked.
    pub fn dr(&self, n: usize) -> SbxPtr<u64> {
        unsafe { SbxPtr::from_raw(self.dr.add(n)) }
    }

    /// IA32_EFER.
    pub fn efer(&self) -> SbxPtr<u64> {
        unsafe { SbxPtr::from_raw(self.efer) }
    }

    /// IA32_LSTAR: where a `syscall` from ring 3 lands. A reliable pointer into
    /// the kernel, available as soon as the guest has made one syscall.
    pub fn lstar(&self) -> SbxPtr<u64> {
        unsafe { SbxPtr::from_raw(self.lstar) }
    }

    /// IA32_KERNEL_GS_BASE. Holds the per-cpu base only while ring 3 is
    /// running; [`percpu_base`](Self::percpu_base) picks the right one for you.
    pub fn kernel_gs_base(&self) -> SbxPtr<u64> {
        unsafe { SbxPtr::from_raw(self.kernel_gs_base) }
    }

    /// The per-cpu data base, whichever register is holding it right now.
    ///
    /// `swapgs` parks it in the kernel MSR for as long as user code runs, so
    /// this reads GS.base at CPL 0 and IA32_KERNEL_GS_BASE otherwise. Read the
    /// two registers directly and you get the right answer half the time.
    pub fn percpu_base(&self) -> u64 {
        if self.seg(X64Seg::Cs).selector & CS_RPL_MASK == 0 {
            self.seg(X64Seg::Gs).base
        } else {
            *self.kernel_gs_base()
        }
    }
}

/// The bottom two bits of the CS selector, which hold the current privilege
/// level.
#[cfg(feature = "x86_64")]
const CS_RPL_MASK: u32 = 0b11;

// A block key is plain data, so it may be stored in the pool.
#[cfg(feature = "x86_64")]
crate::abi::impl_sandbox_safe!(GuestAddr);

#[cfg(feature = "x86_64")]
impl GuestAddr {
    /// This block's placement hash — what `CoverageRef::record_block` takes.
    ///
    /// The same mix the engine's own probe computes, so a key you build by hand
    /// lands in the slot a live lookup will search. The page-table root is
    /// normalized before mixing, so passing it raw or already stripped gives
    /// the same answer.
    pub fn block_hash(&self) -> u64 {
        // The probe normalizes `cr3` before mixing, and a stored key already is.
        let root = self.cr3 & !ROOT_FLAG_MASK;
        let mut h = self.va ^ root.wrapping_mul(BARYL_BLOCK_HASH_C1 as u64);
        h ^= h >> BARYL_BLOCK_HASH_SHIFT1;
        h = h.wrapping_mul(BARYL_BLOCK_HASH_C2 as u64);
        h ^= h >> BARYL_BLOCK_HASH_SHIFT2;
        h
    }
}

/// What the page walk permits, accumulated across every level rather than read
/// off the last one: U and W are ANDed down the levels and NX is ORed, per the
/// SDM's access-rights rules. So these four answer what the *guest* may do at
/// this address, not what one table entry says.
#[cfg(feature = "x86_64")]
impl MemAttr {
    /// The address translates at all. False here makes the other three
    /// meaningless.
    pub fn present(&self) -> bool {
        self.perms & BARYL_X64_PTE_PRESENT != 0
    }

    /// The guest may write here.
    pub fn writable(&self) -> bool {
        self.perms & BARYL_X64_PTE_WRITE != 0
    }

    /// Ring 3 may reach here — the U bit survived every level. The dividing
    /// line between a user mapping and a kernel one.
    pub fn user(&self) -> bool {
        self.perms & BARYL_X64_PTE_USER != 0
    }

    /// The guest may execute here — NX was clear at every level. What makes a
    /// mapping code rather than data.
    pub fn executable(&self) -> bool {
        self.perms & BARYL_X64_PTE_EXEC != 0
    }
}

#[cfg(feature = "x86_64")]
impl ArchVtable {
    /// A table with no calls in it. Start from this when filling one in, so a
    /// call you did not implement reads as absent rather than as garbage.
    pub const ABSENT: ArchVtable = ArchVtable {
        translate: None,
        read_virt: None,
        write_virt: None,
        tlb_invalidate: None,
        virt_scan_for_pattern: None,
        virt_scan_for_u64: None,
        virt_scan_for_u32: None,
        virt_walk_mappings: None,
    };
}

/// A virtual read or write did not happen: some page of the span is unmapped
/// under the root it was made with, or this run offers no such call.
///
/// Carries no detail — the ABI answers with a bare status, so there is nothing
/// more specific to report. Implements `Display` and `std::error::Error`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VirtAccessError;

impl core::fmt::Display for VirtAccessError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "virtual memory access failed")
    }
}

impl core::error::Error for VirtAccessError {}

/// Every call the arch subsystem answers.
///
/// All of them are `unsafe` for one reason: they trust `cr3` to name an address
/// space that exists. Get one from `regs.cr(3)`, from an
/// `#[arch(address_space_change)]` event, or from `Process::dtb`, and the
/// guarantee holds; invent one and the walk reads whatever is at that physical
/// address as though it were a page table.
#[cfg(feature = "x86_64")]
impl ArchRef {
    /// Walk `cr3`'s page tables and answer the guest physical address `va` maps
    /// to; `None` when nothing is mapped there.
    ///
    /// # Safety
    ///
    /// `cr3` must name a live address space, so no earlier than
    /// `#[core(first_vm_entry)]`.
    ///
    /// # Examples
    ///
    /// ```ignore
    /// let Ok(regs): Result<&X64Regs, _> = t.subs.regs() else { return };
    /// let cr3 = *regs.cr(3);
    ///
    /// // SAFETY: cr3 was read from the live register file.
    /// let Some(pa) = (unsafe { t.subs.arch.translate_with_cr3(VirtAddr(*regs.rip()), cr3) })
    /// else {
    ///     return;
    /// };
    /// t.subs.breakpoints.insert(pa, self, on_hit).ok();
    /// ```
    pub unsafe fn translate_with_cr3(&self, va: VirtAddr, cr3: u64) -> Option<PhysAddr> {
        // SAFETY: `vtable` is the arch `.so`'s static table, valid for the process.
        let f = unsafe { (*self.vtable).translate }?;
        let mut pa = 0u64;
        let rc = unsafe { f(*self, va.0, cr3, &raw mut pa, core::ptr::null_mut()) };
        (rc == 0).then_some(PhysAddr(pa))
    }

    /// The same walk, also answering what it permits at the leaf it stopped on.
    ///
    /// One walk answers both, so read permissions here rather than translating
    /// and then asking again.
    ///
    /// # Safety
    ///
    /// As [`translate_with_cr3`](Self::translate_with_cr3).
    pub unsafe fn translate_attr_with_cr3(
        &self,
        va: VirtAddr,
        cr3: u64,
    ) -> Option<(PhysAddr, MemAttr)> {
        // SAFETY: as `translate_with_cr3`; both out-params are ours and live.
        let f = unsafe { (*self.vtable).translate }?;
        let (mut pa, mut attr) = (0u64, MemAttr::default());
        let rc = unsafe { f(*self, va.0, cr3, &raw mut pa, &raw mut attr) };
        (rc == 0).then_some((PhysAddr(pa), attr))
    }

    /// Fill `buf` from guest virtual `va` under `cr3`.
    ///
    /// The span may cross pages: the walk is redone at each boundary, so a
    /// structure straddling two pages reads correctly even where the two are
    /// nowhere near each other physically.
    ///
    /// # Errors
    ///
    /// [`VirtAccessError`] if any page of the span is unmapped. `buf` may hold
    /// bytes from the pages that did resolve — do not read it after an error.
    ///
    /// # Safety
    ///
    /// As [`translate_with_cr3`](Self::translate_with_cr3).
    pub unsafe fn read_virt_into_with_cr3(
        &self,
        buf: &mut [u8],
        va: u64,
        cr3: u64,
    ) -> Result<(), VirtAccessError> {
        // SAFETY: as `translate_with_cr3`; `buf` bounds the write.
        let f = unsafe { (*self.vtable).read_virt }.ok_or(VirtAccessError)?;
        match unsafe { f(*self, va, cr3, buf.as_mut_ptr(), buf.len() as u64) } {
            0 => Ok(()),
            _ => Err(VirtAccessError),
        }
    }

    /// Write `buf` into guest memory at virtual `va` under `cr3`, re-walking at
    /// each page boundary.
    ///
    /// The walk is done for translation, not for permission — a page the guest
    /// itself may only read is still written. Follow a write over executable
    /// memory with `EngRef::jit_flush_page` on the physical page.
    ///
    /// # Errors
    ///
    /// [`VirtAccessError`] if any page of the span is unmapped. A partial write
    /// may have landed.
    ///
    /// # Safety
    ///
    /// As [`translate_with_cr3`](Self::translate_with_cr3).
    pub unsafe fn write_virt_from_with_cr3(
        &self,
        buf: &[u8],
        va: u64,
        cr3: u64,
    ) -> Result<(), VirtAccessError> {
        // SAFETY: as `translate_with_cr3`; `buf` bounds the read.
        let f = unsafe { (*self.vtable).write_virt }.ok_or(VirtAccessError)?;
        match unsafe { f(*self, va, cr3, buf.as_ptr(), buf.len() as u64) } {
            0 => Ok(()),
            _ => Err(VirtAccessError),
        }
    }

    /// Throw away whatever translations the subsystem has memoized.
    ///
    /// Reach for it after writing a page table yourself, so a later translate
    /// does not answer out of a stale entry. A no-op on a run that memoizes
    /// nothing.
    ///
    /// # Safety
    ///
    /// As [`translate_with_cr3`](Self::translate_with_cr3).
    pub unsafe fn tlb_invalidate(&self) {
        // SAFETY: as `translate_with_cr3`.
        let Some(f) = (unsafe { (*self.vtable).tlb_invalidate }) else {
            return;
        };
        unsafe { f(*self) };
    }

    /// The guest virtual address of the first `pat` in `range`, searching under
    /// `cr3`.
    ///
    /// An unmapped page is skipped, not treated as the end of the scan, so a
    /// hole in the address space costs nothing and `ScanRange::WHOLE` over a
    /// 64-bit space is a reasonable thing to ask for.
    ///
    /// `progress` is called with bytes done and bytes total as the sweep
    /// advances; `None` opts out.
    ///
    /// `None` covers no match, an argument the subsystem rejected, and a run
    /// with no pattern scan.
    ///
    /// # Safety
    ///
    /// As [`translate_with_cr3`](Self::translate_with_cr3).
    pub unsafe fn virt_scan_for_pattern_with_cr3(
        &self,
        cr3: u64,
        range: ScanRange,
        pat: &[u8],
        progress: Option<ProgressSink<'_>>,
    ) -> Option<u64> {
        // SAFETY: as `translate_with_cr3`; `pat` bounds the read, and the thunk
        // pair is borrowed from `progress`, which outlives the call.
        let f = unsafe { (*self.vtable).virt_scan_for_pattern }?;
        let mut progress = progress;
        let (obj, cb) = progress_thunk(&mut progress);
        let hit = unsafe {
            f(
                *self,
                cr3,
                range.start,
                range.end,
                range.stride,
                pat.as_ptr(),
                pat.len() as u64,
                obj,
                cb,
            )
        };
        (hit != BARYL_SCAN_NO_MATCH).then_some(hit)
    }

    /// The first little-endian `target` in `range` under `cr3` — the pattern
    /// scan over eight bytes, so this finds a stored pointer rather than its
    /// spelling.
    ///
    /// `None` as for
    /// [`virt_scan_for_pattern_with_cr3`](Self::virt_scan_for_pattern_with_cr3).
    ///
    /// # Safety
    ///
    /// As [`translate_with_cr3`](Self::translate_with_cr3).
    pub unsafe fn virt_scan_for_u64_with_cr3(
        &self,
        cr3: u64,
        range: ScanRange,
        target: u64,
        progress: Option<ProgressSink<'_>>,
    ) -> Option<u64> {
        // SAFETY: as `virt_scan_for_pattern_with_cr3`.
        let f = unsafe { (*self.vtable).virt_scan_for_u64 }?;
        let mut progress = progress;
        let (obj, cb) = progress_thunk(&mut progress);
        let hit = unsafe { f(*self, cr3, range.start, range.end, range.stride, target, obj, cb) };
        (hit != BARYL_SCAN_NO_MATCH).then_some(hit)
    }

    /// The same over four bytes.
    ///
    /// # Safety
    ///
    /// As [`translate_with_cr3`](Self::translate_with_cr3).
    pub unsafe fn virt_scan_for_u32_with_cr3(
        &self,
        cr3: u64,
        range: ScanRange,
        target: u32,
        progress: Option<ProgressSink<'_>>,
    ) -> Option<u64> {
        // SAFETY: as `virt_scan_for_pattern_with_cr3`.
        let f = unsafe { (*self.vtable).virt_scan_for_u32 }?;
        let mut progress = progress;
        let (obj, cb) = progress_thunk(&mut progress);
        let hit = unsafe { f(*self, cr3, range.start, range.end, range.stride, target, obj, cb) };
        (hit != BARYL_SCAN_NO_MATCH).then_some(hit)
    }

    /// Translation run backwards: every virtual address that maps to guest
    /// physical `pa`.
    ///
    /// `cr3` of 0 searches every address space the run knows about, which is
    /// how you find the other processes sharing a page. `f` is called once per
    /// mapping and returning `false` from it stops the walk early. Answers how
    /// many mappings were found.
    ///
    /// # Safety
    ///
    /// As [`translate_with_cr3`](Self::translate_with_cr3), except that 0 is
    /// always allowed.
    ///
    /// # Examples
    ///
    /// ```ignore
    /// // Who else has this page mapped?
    /// let mut sharers: Vec<u64> = Vec::new();
    /// // SAFETY: 0 means "every space", so no cr3 is being asserted.
    /// let found = unsafe {
    ///     t.subs.arch.walk_mappings_of(0, pa, |addr| {
    ///         sharers.push(addr.cr3);
    ///         sharers.len() < 16   // false stops the walk
    ///     })
    /// };
    /// baryl::logging::info!("{found} mappings, kept {}", sharers.len());
    /// ```
    pub unsafe fn walk_mappings_of(
        &self,
        cr3: u64,
        pa: u64,
        f: impl FnMut(GuestAddr) -> bool,
    ) -> u64 {
        let Some(w) = (unsafe { (*self.vtable).virt_walk_mappings }) else {
            return 0;
        };
        // Erased, and borrowed through a place this frame owns: the ABI's `obj`
        // is one word and a `dyn` reference is two.
        let mut f = f;
        let mut sink: &mut dyn FnMut(GuestAddr) -> bool = &mut f;
        // SAFETY: as `translate_with_cr3`; `sink` outlives the walk that borrows it.
        unsafe {
            w(
                *self,
                cr3,
                pa,
                core::ptr::from_mut(&mut sink).cast(),
                Some(visit),
                None,
            )
        }
    }
}

/// Hands one found mapping to the closure `walk_mappings_of` erased.
///
/// # Safety
/// `obj` is the sink that call borrowed, and `addr` is live for this call.
#[cfg(feature = "x86_64")]
unsafe extern "C" fn visit(obj: *mut c_void, addr: *const GuestAddr) -> c_int {
    // SAFETY: the contract above; nothing else holds the sink for this call.
    let f: &mut &mut dyn FnMut(GuestAddr) -> bool = unsafe { &mut *obj.cast() };
    // Nonzero stops the walk, which is what returning `false` asked for.
    c_int::from(!f(unsafe { *addr }))
}