Skip to main content

vmi_core/ctx/
prober.rs

1use std::cell::RefCell;
2
3use indexmap::IndexSet;
4
5use crate::{AddressContext, VmiError};
6
7/// Batches the page faults raised by a sequence of memory reads so they can be
8/// injected together, while skipping pages already known to be unserviceable.
9///
10/// A read that touches a non-resident page returns [`VmiError::Translation`].
11/// Instead of aborting an operation on the first such fault, the prober lets
12/// you run a sequence of reads: each probed read that faults yields `Ok(None)`
13/// and its faulting address is recorded, unless that address is in the
14/// `suppressed` set, in which case it is ignored.
15///
16/// # Protocol
17///
18/// 1. Construct a prober with [`new`], passing the set of pages already known
19///    to be unserviceable (the `suppressed` set).
20/// 2. Run one or more reads through [`probe`] or the [`vmi_probe!`] macro. Each
21///    returns `Ok(Some(value))` when the read succeeded, or `Ok(None)` when it
22///    faulted (the fault is recorded unless it is suppressed).
23/// 3. Call [`error_for_page_faults`]. It returns `Err(VmiError::Translation)`
24///    carrying every recorded, non-suppressed fault, or `Ok(())` if there were
25///    none. Propagate that error to an event loop that injects page faults: for
26///    example, returning it from a `vmi-utils` reactor handler makes the
27///    reactor inject the fault so the guest pages the memory in, after which
28///    the operation is retried.
29/// 4. Faults that turn out to be permanently unserviceable are added to the
30///    `suppressed` set by the caller (for instance from a `KiDispatchException`
31///    handler), so a later attempt skips them instead of re-injecting forever.
32///
33/// [`new`]: Self::new
34/// [`probe`]: Self::probe
35/// [`error_for_page_faults`]: Self::error_for_page_faults
36/// [`vmi_probe!`]: crate::vmi_probe
37///
38/// # Examples
39///
40/// ```ignore
41/// // `suppressed` holds pages a previous round found unserviceable.
42/// let prober = VmiProber::new(&suppressed);
43///
44/// // Each probed read is `Ok(Some(_))` if resident, `Ok(None)` if it faulted.
45/// let header = vmi_probe!(prober, vmi.read_struct::<Header>(address))?;
46/// let name = vmi_probe!(prober, vmi.read_string(name_address))?;
47///
48/// // Surface the accumulated, non-suppressed faults. Returning this error from
49/// // a reactor handler injects the fault and retries the operation.
50/// prober.error_for_page_faults()?;
51///
52/// // No faults pending: `header` and `name` are `Some` (or `None` for a
53/// // suppressed page) and safe to use.
54/// ```
55pub struct VmiProber {
56    /// Pages to suppress.
57    ///
58    /// Addresses already known to be unserviceable, so a probed read that
59    /// faults on one is skipped instead of being reported for injection.
60    suppressed: IndexSet<AddressContext>,
61
62    /// Non-suppressed page faults recorded by the probed reads so far.
63    page_faults: RefCell<IndexSet<AddressContext>>,
64}
65
66impl VmiProber {
67    /// Creates a prober that suppresses faults on the given pages.
68    ///
69    /// `suppressed` is the set of pages already known to be unserviceable
70    /// (typically discovered by an earlier round and recorded by an exception
71    /// handler). Faults on them are ignored rather than reported by
72    /// [`error_for_page_faults`].
73    ///
74    /// [`error_for_page_faults`]: Self::error_for_page_faults
75    pub fn new(suppressed: &IndexSet<AddressContext>) -> Self {
76        Self {
77            suppressed: suppressed.clone(),
78            page_faults: RefCell::new(IndexSet::new()),
79        }
80    }
81
82    /// Runs `f` and records any page fault it raises, returning `Ok(None)` on a
83    /// fault instead of propagating it.
84    ///
85    /// Equivalent to [`check_result`] applied to `f()`. See the type-level
86    /// protocol for how to surface the recorded faults.
87    ///
88    /// [`check_result`]: Self::check_result
89    pub fn probe<T, F>(&self, f: F) -> Result<Option<T>, VmiError>
90    where
91        F: FnOnce() -> Result<T, VmiError>,
92    {
93        self.check_result(f())
94    }
95
96    /// Records a page fault carried by `result` and returns `Ok(None)` instead
97    /// of propagating it, so the caller can keep probing further reads.
98    ///
99    /// A successful `result` becomes `Ok(Some(value))`. A
100    /// [`VmiError::Translation`] is recorded (unless its address is suppressed)
101    /// and becomes `Ok(None)`. Any other error is propagated unchanged. Call
102    /// [`error_for_page_faults`] afterwards to surface the recorded faults for
103    /// injection.
104    ///
105    /// [`error_for_page_faults`]: Self::error_for_page_faults
106    pub fn check_result<T>(&self, result: Result<T, VmiError>) -> Result<Option<T>, VmiError> {
107        match result {
108            Ok(value) => Ok(Some(value)),
109            Err(VmiError::Translation(pf)) => {
110                self.record(pf);
111                Ok(None)
112            }
113            Err(err) => Err(err),
114        }
115    }
116
117    /// Records the page faults that are not in the `suppressed` set.
118    fn record(&self, pf: AddressContext) {
119        let mut page_faults = self.page_faults.borrow_mut();
120        if !self.suppressed.contains(&pf) {
121            tracing::trace!(va = %pf.va, "page fault");
122            page_faults.insert(pf);
123        }
124        else {
125            tracing::trace!(va = %pf.va, "page fault (suppressed)");
126        }
127    }
128
129    /// Returns the recorded page faults that are not in the `suppressed` set.
130    pub fn page_faults(&self) -> IndexSet<AddressContext> {
131        &*self.page_faults.borrow() - &self.suppressed
132    }
133
134    /// Returns `Err(VmiError::Translation)` carrying the recorded, non-suppressed
135    /// page faults, or `Ok(())` if none occurred.
136    ///
137    /// The returned error is meant to be propagated to a page-fault-injecting
138    /// event loop (such as the `vmi-utils` reactor). Injecting these faults
139    /// pages the memory in so the probed operation can be retried.
140    pub fn error_for_page_faults(&self) -> Result<(), VmiError> {
141        let pfs = self.page_faults();
142        if !pfs.is_empty() {
143            tracing::trace!(?pfs);
144            return Err(VmiError::Translation(pfs[0]));
145        }
146
147        Ok(())
148    }
149}
150
151/// Probes a single read expression through a [`VmiProber`], returning
152/// `Ok(None)` if it page-faults.
153///
154/// Shorthand for [`VmiProber::check_result`] over the expression's result.
155///
156/// [`VmiProber`]: crate::VmiProber
157/// [`VmiProber::check_result`]: crate::VmiProber::check_result
158#[macro_export]
159macro_rules! vmi_probe {
160    ($prober:expr, $expr:expr) => {
161        $prober.check_result(|| -> Result<_, $crate::VmiError> { $expr }())
162    };
163}