1 //! Module for Linux pagemap based tracking of dirty pages.
2 //!
3 //! For other platforms, a no-op implementation is provided.
4 
5 #[cfg(feature = "pooling-allocator")]
6 use crate::prelude::*;
7 
8 use self::ioctl::{Categories, PageMapScanBuilder};
9 use crate::runtime::vm::{HostAlignedByteCount, host_page_size};
10 use rustix::ioctl::ioctl;
11 use std::fs::File;
12 use std::mem::MaybeUninit;
13 use std::ptr;
14 
15 /// A static file-per-process which represents this process's page map file.
16 ///
17 /// Note that this is required to be updated on a fork because otherwise this'll
18 /// refer to the parent process's page map instead of the child process's page
19 /// map. Thus when first initializing this file the `pthread_atfork` function is
20 /// used to hook the child process to update this.
21 ///
22 /// Also note that updating this is not done via mutation but rather it's done
23 /// with `dup2` to replace the file descriptor that `File` points to in-place.
24 /// The local copy of of `File` is then closed in the atfork handler.
25 #[cfg(feature = "pooling-allocator")]
26 static PROCESS_PAGEMAP: std::sync::LazyLock<Option<File>> = std::sync::LazyLock::new(|| {
27     use rustix::fd::AsRawFd;
28 
29     let pagemap = File::open("/proc/self/pagemap").ok()?;
30 
31     // SAFETY: all libc functions are unsafe by default, and we're basically
32     // going to do our damndest to make sure this invocation of `pthread_atfork`
33     // is safe, namely the handler registered here is intentionally quite
34     // minimal and only accesses the `PROCESS_PAGEMAP`.
35     let rc = unsafe { libc::pthread_atfork(None, None, Some(after_fork_in_child)) };
36     if rc != 0 {
37         return None;
38     }
39 
40     return Some(pagemap);
41 
42     /// Hook executed as part of `pthread_atfork` in the child process after a
43     /// fork.
44     ///
45     /// # Safety
46     ///
47     /// This function is not safe to call in general and additionally has its
48     /// own stringent safety requirements. This is after a fork but before exec
49     /// so all the safety requirements of `Command::pre_exec` in the standard
50     /// library apply here. Effectively the standard library primitives are
51     /// avoided here as they aren't necessarily safe to execute in this context.
after_fork_in_child()52     unsafe extern "C" fn after_fork_in_child() {
53         let Some(parent_pagemap) = PROCESS_PAGEMAP.as_ref() else {
54             // This should not be reachable, but to avoid panic infrastructure
55             // here this is just skipped instead.
56             return;
57         };
58 
59         // SAFETY: see function documentation.
60         //
61         // Here `/proc/self/pagemap` is opened in the child. If that fails for
62         // whatever reason then the pagemap is replaced with `/dev/null` which
63         // means that all future ioctls for `PAGEMAP_SCAN` will fail. If that
64         // fails then that's left to abort the process for now. If that's
65         // problematic we may want to consider opening a local pipe and then
66         // installing that here? Unsure.
67         //
68         // Once a fd is opened the `dup2` syscall is used to replace the
69         // previous file descriptor stored in `parent_pagemap`. That'll update
70         // the pagemap in-place in this child for all future use in case this is
71         // further used in the child.
72         //
73         // And finally once that's all done the `child_pagemap` is itself
74         // closed since we have no more need for it.
75         unsafe {
76             let flags = libc::O_CLOEXEC | libc::O_RDONLY;
77             let mut child_pagemap = libc::open(c"/proc/self/pagemap".as_ptr(), flags);
78             if child_pagemap == -1 {
79                 child_pagemap = libc::open(c"/dev/null".as_ptr(), flags);
80             }
81             if child_pagemap == -1 {
82                 libc::abort();
83             }
84 
85             let rc = libc::dup2(child_pagemap, parent_pagemap.as_raw_fd());
86             if rc == -1 {
87                 libc::abort();
88             }
89             let rc = libc::close(child_pagemap);
90             if rc == -1 {
91                 libc::abort();
92             }
93         }
94     }
95 });
96 
97 #[derive(Debug)]
98 pub struct PageMap(&'static File);
99 
100 impl PageMap {
101     #[cfg(feature = "pooling-allocator")]
new() -> Option<PageMap>102     pub fn new() -> Option<PageMap> {
103         let file = PROCESS_PAGEMAP.as_ref()?;
104 
105         // Check if the `pagemap_scan` ioctl is supported.
106         let mut regions = vec![MaybeUninit::uninit(); 1];
107         let pm_scan = PageMapScanBuilder::new(ptr::slice_from_raw_parts(ptr::null_mut(), 0))
108             .max_pages(1)
109             .return_mask(Categories::empty())
110             .category_mask(Categories::all())
111             .build(&mut regions);
112 
113         // SAFETY: we did our best in the `ioctl` code below to model this ioctl
114         // safely, and it's safe to issue the ioctl on `/proc/self/pagemap`.
115         unsafe {
116             ioctl(&file, pm_scan).ok()?;
117         }
118         Some(PageMap(file))
119     }
120 }
121 
122 /// Resets `ptr` for `len` bytes.
123 ///
124 /// This function is a dual implementation of this function in the
125 /// `pagemap_disabled` module except it uses the `PAGEMAP_SCAN` [ioctl] on
126 /// Linux to be more clever about calling the `reset_manually` closure.
127 /// Semantically though this still has the same meaning where all of `ptr` for
128 /// `len` bytes will be reset, either through `reset_manually` or `decommit`.
129 /// The optimization here is that `reset_manually` will only be called on
130 /// regions as-necessary and `decommit` can be skipped entirely in some
131 /// situations.
132 ///
133 /// The `PAGEMAP_SCAN` [ioctl] scans a region of memory and reports back
134 /// "regions of interest" as configured by the scan. It also does things with
135 /// uffd and write-protected pages, but that's not leveraged here. Specifically
136 /// this function will perform a scan of `ptr` for `len` bytes which will search
137 /// for pages that:
138 ///
139 /// * Are present.
140 /// * Have been written.
141 /// * Are NOT backed by the "zero" page.
142 /// * Are NOT backed by a "file" page.
143 ///
144 /// By default WebAssembly memories/tables are all accessible virtual memory,
145 /// but paging optimizations on Linux means they don't actually have a backing
146 /// page. For example when an instance starts for the first time its entire
147 /// linear memory will be mapped as anonymous memory where page-table-entries
148 /// don't even exist for the new memory. Most modules will then have an initial
149 /// image mapped in, but that still won't have any page table entries. When
150 /// memory is accessed for the first time a page fault will be generated and
151 /// handled by the kernel.
152 ///
153 /// If memory is read then the page fault will force a PTE to be allocated to
154 /// either zero-backed pages (e.g. ZFOD behavior) or a file-backed page if the
155 /// memory is in the initial image mapping. For ZFOD the kernel uses a single
156 /// page for the entire system of zeros and for files it uses the page map cache
157 /// in the kernel to share the same page across many mappings (as it's all
158 /// read-only anyway). Note that in this situation the PTE allocated will have
159 /// the write permission disabled meaning that a write will later generate a
160 /// page fault.
161 ///
162 /// If memory is written then that will allocate a fresh page from the kernel.
163 /// If the PTE was not previously present then the fresh page is initialized
164 /// either with zeros or a copy of the contents of the file-backed mapping. If
165 /// the PTE was previously present then its previous contents are copied into
166 /// the new page. In all of these cases the final PTE allocate will be a private
167 /// page to just this process which will be reflected nowhere else on the
168 /// system.
169 ///
170 /// Putting this all together this helps explain the search criteria for
171 /// `PAGEMAP_SCAN`, notably:
172 ///
173 /// * `Categories::PRESENT` - we're only interested in present pages, anything
174 ///   unmapped wasn't touched by the guest so no need for the host to touch it
175 ///   either.
176 ///
177 /// * `Categories::WRITTEN` - if a page was only read by the guest no need to
178 ///   take a look at it as the contents aren't changed from the initial image.
179 ///
180 /// * `!Categories::PFNZERO` - if a page is mapped to the zero page then it's
181 ///   guaranteed to be readonly and it means that wasm read the memory but
182 ///   didn't write to it, additionally meaning it doesn't need to be reset.
183 ///
184 /// * `!Categories::FILE` - similar to `!PFNZERO` if a page is mapped to a file
185 ///   then for us that means it's readonly meaning wasm only read the memory,
186 ///   didn't write to it, so the page can be skipped.
187 ///
188 /// The `PAGEMAP_SCAN` will report back a set of contiguous regions of memory
189 /// which match our scan flags that we're looking for. Each of these regions is
190 /// then passed to `reset_manually` as-is. The ioctl will additionally then
191 /// report a "walk_end" address which is the last address it considered before
192 /// the scan was halted. A scan can stop for 3 reasons:
193 ///
194 /// * The end of the region of memory being scanned was reached. In this case
195 ///   the entire region was scanned meaning that all dirty memory was reported
196 ///   through `reset_manually`. This means that `decommit` can be skipped
197 ///   entirely (or invoked with a 0 length here which will also end up with it
198 ///   being skipped).
199 ///
200 /// * The scan's `max_pages` setting was reached. The `keep_resident` argument
201 ///   indicates the maximal amount of memory to pass to `reset_manually` and
202 ///   this translates to the `max_pages` configuration option to the ioctl. The
203 ///   sum total of the size of all regions reported from the ioctl is guaranteed
204 ///   to be less than `max_pages`. This means that if a scan reaches the
205 ///   `keep_resident` limit before reaching the end then the ioctl will bail out
206 ///   early. That means that the wasm module's working set of memory was larger
207 ///   than `keep_resident` and then the rest of it will be `decommit`'d away.
208 ///
209 /// * The scan's returned set of regions exceeds the capacity passed into the
210 ///   ioctl. The `pm_scan_arg` of the ioctl takes a `vec` and `vec_len` which is
211 ///   a region of memory to store a list of `page_region` structures. Below this
212 ///   is always `MAX_REGIONS`. If there are more than this number of disjoint
213 ///   regions of memory that need to be reported then the ioctl will also return
214 ///   early without reaching the end of memory. Note that this means that all
215 ///   further memory will be `decommit`'d with reported regions still going to
216 ///   `reset_manually`. This is arguably something we should detect and improve
217 ///   in Wasmtime, but for now `MAX_REGIONS` is hardcoded.
218 ///
219 /// In the end this ends up being a "more clever" version of this function than
220 /// the one in the `pagemap_disabled` module. By using `PAGEMAP_SCAN` we can
221 /// search for the first `keep_resident` bytes of dirty memory written to by a
222 /// wasm guest instead of assuming the first `keep_resident` bytes of the region
223 /// were modified by the guest. This crucially enables the `decommit` operation
224 /// to a noop if the wasm guest's set of working memory is less than
225 /// `keep_resident` which means that `memcpy` is sufficient to reset a linear
226 /// memory or table. This directly translates to higher throughput as it avoids
227 /// IPIs and synchronization updating page tables and additionally avoids page
228 /// faults on future executions of the same module.
229 ///
230 /// # Safety
231 ///
232 /// Requires that `ptr` is valid to read and write for `len` bytes.
233 ///
234 /// [ioctl]: https://www.man7.org/linux/man-pages/man2/PAGEMAP_SCAN.2const.html
reset_with_pagemap( mut pagemap: Option<&PageMap>, ptr: *mut u8, len: HostAlignedByteCount, mut keep_resident: HostAlignedByteCount, mut reset_manually: impl FnMut(&mut [u8]), mut decommit: impl FnMut(*mut u8, usize), ) -> usize235 pub unsafe fn reset_with_pagemap(
236     mut pagemap: Option<&PageMap>,
237     ptr: *mut u8,
238     len: HostAlignedByteCount,
239     mut keep_resident: HostAlignedByteCount,
240     mut reset_manually: impl FnMut(&mut [u8]),
241     mut decommit: impl FnMut(*mut u8, usize),
242 ) -> usize {
243     keep_resident = keep_resident.min(len);
244     let host_page_size = host_page_size();
245 
246     if pagemap.is_some() {
247         // Nothing to keep resident? fall back to the default behavior.
248         if keep_resident.byte_count() == 0 {
249             pagemap = None;
250         }
251 
252         // Keeping less than one page of memory resident when the original
253         // mapping itself is also less than a page? Also fall back to the
254         // default behavior as this'll just be a simple memcpy.
255         if keep_resident.byte_count() <= host_page_size && len.byte_count() <= host_page_size {
256             pagemap = None;
257         }
258     }
259 
260     let pagemap = match pagemap {
261         Some(pagemap) => pagemap,
262 
263         // Fall back to the default behavior.
264         //
265         // SAFETY: the safety requirement of
266         // `pagemap_disabled::reset_with_pagemap` is the same as this function.
267         _ => unsafe {
268             return crate::runtime::vm::pagemap_disabled::reset_with_pagemap(
269                 None,
270                 ptr,
271                 len,
272                 keep_resident,
273                 reset_manually,
274                 decommit,
275             );
276         },
277     };
278 
279     // For now use a fixed set of regions on the stack, but in the future this
280     // may want to use a dynamically allocated vector for more regions for
281     // example.
282     const MAX_REGIONS: usize = 32;
283     let mut storage = [MaybeUninit::uninit(); MAX_REGIONS];
284 
285     let scan_arg = PageMapScanBuilder::new(ptr::slice_from_raw_parts(ptr, len.byte_count()))
286         .max_pages(keep_resident.byte_count() / host_page_size)
287         // We specifically want pages that are NOT backed by the zero page or
288         // backed by files. Such pages mean that they haven't changed from their
289         // original contents, so they're inverted.
290         .category_inverted(Categories::PFNZERO | Categories::FILE)
291         // Search for pages that are written and present as those are the dirty
292         // pages. Additionally search for the zero page/file page as those are
293         // inverted above meaning we're searching for pages that specifically
294         // don't have those flags.
295         .category_mask(
296             Categories::WRITTEN | Categories::PRESENT | Categories::PFNZERO | Categories::FILE,
297         )
298         // Don't return any categories back. This helps group regions together
299         // since the reported set of categories is always empty and we otherwise
300         // aren't looking for anything in particular.
301         .return_mask(Categories::empty())
302         .build(&mut storage);
303 
304     // SAFETY: this should be a safe ioctl as we control the fd we're operating
305     // on plus all of `scan_arg`, but this relies on `Ioctl` below being the
306     // correct implementation and such.
307     let result = match unsafe { ioctl(&pagemap.0, scan_arg) } {
308         Ok(result) => result,
309 
310         // If the ioctl fails for whatever reason, we at least tried, so fall
311         // back to the default behavior.
312         //
313         // SAFETY: the safety requirement of
314         // `pagemap_disabled::reset_with_pagemap` is the same as this function.
315         Err(err) => unsafe {
316             log::warn!("failed pagemap scan {err}");
317             return crate::runtime::vm::pagemap_disabled::reset_with_pagemap(
318                 None,
319                 ptr,
320                 len,
321                 keep_resident,
322                 reset_manually,
323                 decommit,
324             );
325         },
326     };
327 
328     // For all regions that were written in the scan reset them manually, then
329     // afterwards decommit everything else.
330     let mut bytes_resident = 0;
331     for region in result.regions() {
332         // SAFETY: we're relying on Linux to pass in valid region ranges within
333         // the `ptr/len` we specified to the original syscall.
334         unsafe {
335             reset_manually(&mut *region.region().cast_mut());
336         }
337         bytes_resident += region.len();
338     }
339 
340     // Report everything after `walk_end` to the end of memory as memory that
341     // must be decommitted as the scan didn't reach it. Note that if `walk_end`
342     // is already at the end of memory then the byte size of the decommitted
343     // memory here will be 0 meaning that this is a noop.
344     let scan_size = result.walk_end().addr() - ptr.addr();
345     decommit(result.walk_end().cast_mut(), len.byte_count() - scan_size);
346 
347     bytes_resident
348 }
349 
350 mod ioctl {
351     use rustix::ioctl::*;
352     use std::ffi::c_void;
353     use std::fmt;
354     use std::marker;
355     use std::mem::MaybeUninit;
356     use std::ptr;
357 
358     bitflags::bitflags! {
359         /// Categories that can be filtered with [`PageMapScan`]
360         #[derive(Copy, Clone, PartialEq, Eq)]
361         #[repr(transparent)]
362         pub struct Categories: u64 {
363             /// The page has asynchronous write-protection enabled.
364             const WPALLOWED = 1 << 0;
365             /// The page has been written to from the time it was write protected.
366             const WRITTEN = 1 << 1;
367             /// The page is file backed.
368             const FILE = 1 << 2;
369             /// The page is present in the memory.
370             const PRESENT = 1 << 3;
371             /// The page is swapped.
372             const SWAPPED = 1 << 4;
373             /// The page has zero PFN.
374             const PFNZERO = 1 << 5;
375             /// The page is THP or Hugetlb backed.
376             const HUGE = 1 << 6;
377             // NB: I don't know what this is and I can't find documentation for
378             // it, it's just included here for complete-ness with the API that
379             // `PAGEMAP_SCAN` provides.
380             const SOFT_DIRTY = 1 << 7;
381         }
382     }
383 
384     impl fmt::Debug for Categories {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result385         fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386             bitflags::parser::to_writer(self, f)
387         }
388     }
389 
390     impl fmt::Display for Categories {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result391         fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392             bitflags::parser::to_writer(self, f)
393         }
394     }
395 
396     /// Builder-style structure for building up a [`PageMapScan`] `ioctl` call.
397     pub struct PageMapScanBuilder {
398         pm_scan_arg: pm_scan_arg,
399     }
400 
401     impl PageMapScanBuilder {
402         /// Creates a new page map scan that will scan the provided range of memory.
new(region: *const [u8]) -> PageMapScanBuilder403         pub fn new(region: *const [u8]) -> PageMapScanBuilder {
404             PageMapScanBuilder {
405                 pm_scan_arg: pm_scan_arg {
406                     size: size_of::<pm_scan_arg>() as u64,
407                     flags: 0,
408                     start: region.cast::<u8>().addr() as u64,
409                     end: region.cast::<u8>().addr().wrapping_add(region.len()) as u64,
410                     walk_end: 0,
411                     vec: 0,
412                     vec_len: 0,
413                     max_pages: 0,
414                     category_inverted: Categories::empty(),
415                     category_anyof_mask: Categories::empty(),
416                     category_mask: Categories::empty(),
417                     return_mask: Categories::empty(),
418                 },
419             }
420         }
421 
422         /// Configures the maximum number of returned pages in the output regions.
423         ///
424         /// Setting this to 0 disables this maximum.
max_pages(&mut self, max: usize) -> &mut PageMapScanBuilder425         pub fn max_pages(&mut self, max: usize) -> &mut PageMapScanBuilder {
426             self.pm_scan_arg.max_pages = max.try_into().unwrap();
427             self
428         }
429 
430         /// Configures categories which values must match if 0 instead of 1.
431         ///
432         /// Note that this is a mask which is xor'd to the page's true
433         /// categories before testing for `category_mask`. That means that if a
434         /// bit needs to be zero then it additionally must be specified in one
435         /// of `category_mask` or `category_anyof_mask`.
436         ///
437         /// For more detail see the `pagemap_scan_is_interesting_page` function
438         /// in the Linux kernel source.
category_inverted(&mut self, flags: Categories) -> &mut PageMapScanBuilder439         pub fn category_inverted(&mut self, flags: Categories) -> &mut PageMapScanBuilder {
440             self.pm_scan_arg.category_inverted = flags;
441             self
442         }
443 
444         /// Only consider pages for which all `flags` match.
445         ///
446         /// This mask is applied after `category_inverted` is used to flip bits
447         /// in a page's categories. Only pages which match all bits in `flags`
448         /// will be considered.
449         ///
450         /// For more detail see the `pagemap_scan_is_interesting_page` function
451         /// in the Linux kernel source.
category_mask(&mut self, flags: Categories) -> &mut PageMapScanBuilder452         pub fn category_mask(&mut self, flags: Categories) -> &mut PageMapScanBuilder {
453             self.pm_scan_arg.category_mask = flags;
454             self
455         }
456 
457         /// Only consider pages for which any bit of `flags` matches.
458         ///
459         /// After `category_inverted` and `category_mask` have been applied, if
460         /// this option is specified to a non-empty value, then at least one of
461         /// `flags` must be in a page's flags to be considered. That means that
462         /// flags specified in `category_inverted` will already be inverted for
463         /// consideration here. The page categories are and'd with `flags` and
464         /// some bit must be set for the page to be considered.
465         ///
466         /// For more detail see the `pagemap_scan_is_interesting_page` function
467         /// in the Linux kernel source.
468         #[expect(dead_code, reason = "bindings for the future if we need them")]
category_anyof_mask(&mut self, flags: Categories) -> &mut PageMapScanBuilder469         pub fn category_anyof_mask(&mut self, flags: Categories) -> &mut PageMapScanBuilder {
470             self.pm_scan_arg.category_anyof_mask = flags;
471             self
472         }
473 
474         /// Categories that are to be reported in the regions returned
return_mask(&mut self, flags: Categories) -> &mut PageMapScanBuilder475         pub fn return_mask(&mut self, flags: Categories) -> &mut PageMapScanBuilder {
476             self.pm_scan_arg.return_mask = flags;
477             self
478         }
479 
480         /// Finishes this configuration and flags that the scan results will be
481         /// placed within `dst`. The returned object can be used to perform the
482         /// pagemap scan ioctl.
build<'a>(&self, dst: &'a mut [MaybeUninit<PageRegion>]) -> PageMapScan<'a>483         pub fn build<'a>(&self, dst: &'a mut [MaybeUninit<PageRegion>]) -> PageMapScan<'a> {
484             let mut ret = PageMapScan {
485                 pm_scan_arg: self.pm_scan_arg,
486                 _marker: marker::PhantomData,
487             };
488             ret.pm_scan_arg.vec = dst.as_ptr() as u64;
489             ret.pm_scan_arg.vec_len = dst.len() as u64;
490             return ret;
491         }
492     }
493 
494     /// Return result of [`PageMapScanBuilder::build`] used to perform an `ioctl`.
495     #[repr(transparent)]
496     pub struct PageMapScan<'a> {
497         pm_scan_arg: pm_scan_arg,
498         _marker: marker::PhantomData<&'a mut [MaybeUninit<PageRegion>]>,
499     }
500 
501     #[derive(Copy, Clone)]
502     #[repr(C)]
503     struct pm_scan_arg {
504         size: u64,
505         flags: u64,
506         start: u64,
507         end: u64,
508         walk_end: u64,
509         vec: u64,
510         vec_len: u64,
511         max_pages: u64,
512         category_inverted: Categories,
513         category_mask: Categories,
514         category_anyof_mask: Categories,
515         return_mask: Categories,
516     }
517 
518     /// Return result of a [`PageMapScan`] `ioctl`.
519     ///
520     /// This reports where the kernel stopped walking with
521     /// [`PageMapScanResult::walk_end`] and the description of regions found in
522     /// [`PageMapScanResult::regions`].
523     #[derive(Debug)]
524     pub struct PageMapScanResult<'a> {
525         walk_end: *const u8,
526         regions: &'a mut [PageRegion],
527     }
528 
529     impl PageMapScanResult<'_> {
530         /// Where the kernel stopped walking pages, which may be earlier than the
531         /// end of the requested region
walk_end(&self) -> *const u8532         pub fn walk_end(&self) -> *const u8 {
533             self.walk_end
534         }
535 
536         /// Regions the kernel reported back with categories and such.
regions(&self) -> &[PageRegion]537         pub fn regions(&self) -> &[PageRegion] {
538             self.regions
539         }
540     }
541 
542     /// Return value of [`PageMapScan`], description of regions in the original scan
543     /// with the categories queried.
544     #[repr(transparent)]
545     #[derive(Copy, Clone)]
546     pub struct PageRegion(page_region);
547 
548     #[repr(C)]
549     #[derive(Debug, Copy, Clone)]
550     struct page_region {
551         start: u64,
552         end: u64,
553         categories: Categories,
554     }
555 
556     impl PageRegion {
557         /// Returns the region of memory this represents as `*const [u8]`
558         #[inline]
region(&self) -> *const [u8]559         pub fn region(&self) -> *const [u8] {
560             ptr::slice_from_raw_parts(self.start(), self.len())
561         }
562 
563         /// Returns the base pointer into memory this region represents.
564         #[inline]
start(&self) -> *const u8565         pub fn start(&self) -> *const u8 {
566             self.0.start as *const u8
567         }
568 
569         /// Returns the byte length that this region represents.
570         #[inline]
len(&self) -> usize571         pub fn len(&self) -> usize {
572             usize::try_from(self.0.end - self.0.start).unwrap()
573         }
574 
575         /// Returns the category flags associated with this region.
576         ///
577         /// Note that this will only contain categories specified in
578         /// [`PageMapScanBuilder::return_mask`].
579         #[inline]
580         #[cfg_attr(
581             not(test),
582             expect(dead_code, reason = "bindings for the future if we need them")
583         )]
categories(&self) -> Categories584         pub fn categories(&self) -> Categories {
585             self.0.categories
586         }
587     }
588 
589     impl fmt::Debug for PageRegion {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result590         fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591             f.debug_struct("PageRegion")
592                 .field("start", &self.start())
593                 .field("len", &self.len())
594                 .field("categories", &self.0.categories)
595                 .finish()
596         }
597     }
598 
599     // SAFETY: this implementation should uphold the various requirements that
600     // this trait has, such as `IS_MUTATING` is right, it's only used on the
601     // right platform with the right files, etc.
602     unsafe impl<'a> Ioctl for PageMapScan<'a> {
603         type Output = PageMapScanResult<'a>;
604 
605         const IS_MUTATING: bool = true;
606 
opcode(&self) -> Opcode607         fn opcode(&self) -> Opcode {
608             opcode::read_write::<pm_scan_arg>(b'f', 16)
609         }
610 
as_ptr(&mut self) -> *mut c_void611         fn as_ptr(&mut self) -> *mut c_void {
612             (&raw mut self.pm_scan_arg).cast()
613         }
614 
output_from_ptr( out: IoctlOutput, extract_output: *mut c_void, ) -> rustix::io::Result<Self::Output>615         unsafe fn output_from_ptr(
616             out: IoctlOutput,
617             extract_output: *mut c_void,
618         ) -> rustix::io::Result<Self::Output> {
619             let extract_output = extract_output.cast::<pm_scan_arg>();
620             let len = usize::try_from(out).unwrap();
621             // SAFETY: it's a requirement of this method that
622             // `extract_output` is safe to read and indeed a `pm_scan_arg`.
623             // Additionally the slice returned here originated from a slice
624             // provided to `PageMapScanBuilder::build` threaded through the
625             // `vec` field and it should be safe to thread that back out through
626             // to the result.
627             let regions = unsafe {
628                 assert!((len as u64) <= (*extract_output).vec_len);
629                 std::slice::from_raw_parts_mut((*extract_output).vec as *mut PageRegion, len)
630             };
631             Ok(PageMapScanResult {
632                 regions,
633                 // SAFETY: it's a requirement of this method that
634                 // `extract_output` is safe to read and indeed a `pm_scan_arg`.
635                 walk_end: unsafe { (*extract_output).walk_end as *const u8 },
636             })
637         }
638     }
639 }
640 
641 #[cfg(test)]
642 mod tests {
643     use super::ioctl::*;
644     use crate::prelude::*;
645     use rustix::ioctl::*;
646     use rustix::mm::*;
647     use std::fs::File;
648     use std::ptr;
649 
650     struct MmapAnonymous {
651         ptr: *mut std::ffi::c_void,
652         len: usize,
653     }
654 
655     impl MmapAnonymous {
new(pages: usize) -> MmapAnonymous656         fn new(pages: usize) -> MmapAnonymous {
657             let len = pages * rustix::param::page_size();
658             let ptr = unsafe {
659                 mmap_anonymous(
660                     ptr::null_mut(),
661                     len,
662                     ProtFlags::READ | ProtFlags::WRITE,
663                     MapFlags::PRIVATE,
664                 )
665                 .unwrap()
666             };
667             MmapAnonymous { ptr, len }
668         }
669 
read(&self, page: usize)670         fn read(&self, page: usize) {
671             unsafe {
672                 let offset = page * rustix::param::page_size();
673                 assert!(offset < self.len);
674                 std::ptr::read_volatile(self.ptr.cast::<u8>().add(offset));
675             }
676         }
677 
write(&self, page: usize)678         fn write(&self, page: usize) {
679             unsafe {
680                 let offset = page * rustix::param::page_size();
681                 assert!(offset < self.len);
682                 std::ptr::write_volatile(self.ptr.cast::<u8>().add(offset), 1);
683             }
684         }
685 
region(&self) -> *const [u8]686         fn region(&self) -> *const [u8] {
687             ptr::slice_from_raw_parts(self.ptr.cast(), self.len)
688         }
689 
page_region(&self, pages: std::ops::Range<usize>) -> *const [u8]690         fn page_region(&self, pages: std::ops::Range<usize>) -> *const [u8] {
691             ptr::slice_from_raw_parts(
692                 self.ptr
693                     .cast::<u8>()
694                     .wrapping_add(pages.start * rustix::param::page_size()),
695                 (pages.end - pages.start) * rustix::param::page_size(),
696             )
697         }
698 
end(&self) -> *const u8699         fn end(&self) -> *const u8 {
700             self.ptr.cast::<u8>().wrapping_add(self.len)
701         }
702 
page_end(&self, page: usize) -> *const u8703         fn page_end(&self, page: usize) -> *const u8 {
704             self.ptr
705                 .cast::<u8>()
706                 .wrapping_add((page + 1) * rustix::param::page_size())
707         }
708     }
709 
710     impl Drop for MmapAnonymous {
drop(&mut self)711         fn drop(&mut self) {
712             unsafe {
713                 munmap(self.ptr, self.len).unwrap();
714             }
715         }
716     }
717 
ioctl_supported() -> bool718     fn ioctl_supported() -> bool {
719         let mmap = MmapAnonymous::new(1);
720         let mut results = Vec::with_capacity(1);
721         let fd = File::open("/proc/self/pagemap").unwrap();
722         unsafe {
723             ioctl(
724                 &fd,
725                 PageMapScanBuilder::new(mmap.region())
726                     .category_mask(Categories::WRITTEN)
727                     .return_mask(Categories::all())
728                     .build(results.spare_capacity_mut()),
729             )
730             .is_ok()
731         }
732     }
733 
734     #[test]
no_pages_returned()735     fn no_pages_returned() {
736         if !ioctl_supported() {
737             return;
738         }
739         let mmap = MmapAnonymous::new(10);
740         let mut results = Vec::with_capacity(10);
741         let fd = File::open("/proc/self/pagemap").unwrap();
742 
743         let result = unsafe {
744             ioctl(
745                 &fd,
746                 PageMapScanBuilder::new(mmap.region())
747                     .category_mask(Categories::WRITTEN)
748                     .return_mask(Categories::all())
749                     .build(results.spare_capacity_mut()),
750             )
751             .unwrap()
752         };
753         assert!(result.regions().is_empty());
754         assert_eq!(result.walk_end(), mmap.end());
755     }
756 
757     #[test]
empty_region()758     fn empty_region() {
759         if !ioctl_supported() {
760             return;
761         }
762         let mut results = Vec::with_capacity(10);
763         let fd = File::open("/proc/self/pagemap").unwrap();
764 
765         let empty_region = ptr::slice_from_raw_parts(rustix::param::page_size() as *const u8, 0);
766         let result = unsafe {
767             ioctl(
768                 &fd,
769                 PageMapScanBuilder::new(empty_region)
770                     .return_mask(Categories::all())
771                     .build(results.spare_capacity_mut()),
772             )
773             .unwrap()
774         };
775         assert!(result.regions().is_empty());
776     }
777 
778     #[test]
basic_page_flags()779     fn basic_page_flags() {
780         if !ioctl_supported() {
781             return;
782         }
783         let mmap = MmapAnonymous::new(10);
784         let mut results = Vec::with_capacity(10);
785         let fd = File::open("/proc/self/pagemap").unwrap();
786 
787         mmap.read(0);
788         mmap.write(1);
789         mmap.write(2);
790         mmap.read(3);
791 
792         mmap.read(5);
793         mmap.read(6);
794 
795         let result = unsafe {
796             ioctl(
797                 &fd,
798                 PageMapScanBuilder::new(mmap.region())
799                     .category_mask(Categories::WRITTEN)
800                     .return_mask(Categories::WRITTEN | Categories::PRESENT | Categories::PFNZERO)
801                     .build(results.spare_capacity_mut()),
802             )
803             .unwrap()
804         };
805         assert_eq!(result.regions().len(), 4);
806         assert_eq!(result.walk_end(), mmap.end());
807         assert_eq!(result.regions()[0].region(), mmap.page_region(0..1));
808         assert_eq!(
809             result.regions()[0].categories(),
810             Categories::WRITTEN | Categories::PRESENT | Categories::PFNZERO
811         );
812 
813         assert_eq!(result.regions()[1].region(), mmap.page_region(1..3));
814         assert_eq!(
815             result.regions()[1].categories(),
816             Categories::WRITTEN | Categories::PRESENT
817         );
818 
819         assert_eq!(result.regions()[2].region(), mmap.page_region(3..4));
820         assert_eq!(
821             result.regions()[2].categories(),
822             Categories::WRITTEN | Categories::PRESENT | Categories::PFNZERO
823         );
824 
825         assert_eq!(result.regions()[3].region(), mmap.page_region(5..7));
826         assert_eq!(
827             result.regions()[3].categories(),
828             Categories::WRITTEN | Categories::PRESENT | Categories::PFNZERO
829         );
830     }
831 
832     #[test]
only_written_pages()833     fn only_written_pages() {
834         if !ioctl_supported() {
835             return;
836         }
837         let mmap = MmapAnonymous::new(10);
838         let mut results = Vec::with_capacity(10);
839         let fd = File::open("/proc/self/pagemap").unwrap();
840 
841         mmap.read(0);
842         mmap.write(1);
843         mmap.write(2);
844         mmap.read(3);
845 
846         mmap.read(5);
847         mmap.read(6);
848 
849         let result = unsafe {
850             ioctl(
851                 &fd,
852                 PageMapScanBuilder::new(mmap.region())
853                     .category_inverted(Categories::PFNZERO)
854                     .category_mask(Categories::WRITTEN | Categories::PFNZERO)
855                     .return_mask(Categories::WRITTEN | Categories::PRESENT | Categories::PFNZERO)
856                     .build(results.spare_capacity_mut()),
857             )
858             .unwrap()
859         };
860         assert_eq!(result.regions().len(), 1);
861         assert_eq!(result.walk_end(), mmap.end());
862 
863         assert_eq!(result.regions()[0].region(), mmap.page_region(1..3));
864         assert_eq!(
865             result.regions()[0].categories(),
866             Categories::WRITTEN | Categories::PRESENT
867         );
868     }
869 
870     #[test]
region_limit()871     fn region_limit() {
872         if !ioctl_supported() {
873             return;
874         }
875         let mmap = MmapAnonymous::new(10);
876         let mut results = Vec::with_capacity(1);
877         let fd = File::open("/proc/self/pagemap").unwrap();
878 
879         mmap.read(0);
880         mmap.write(1);
881         mmap.read(2);
882         mmap.write(3);
883 
884         // Ask for written|pfnzero meaning only-read pages. This should return only
885         // a single region of the first page.
886         let result = unsafe {
887             ioctl(
888                 &fd,
889                 PageMapScanBuilder::new(mmap.region())
890                     .return_mask(Categories::WRITTEN | Categories::PFNZERO)
891                     .build(results.spare_capacity_mut()),
892             )
893             .unwrap()
894         };
895         assert_eq!(result.regions().len(), 1);
896         assert_eq!(result.walk_end(), mmap.page_end(0));
897 
898         assert_eq!(result.regions()[0].region(), mmap.page_region(0..1));
899         assert_eq!(
900             result.regions()[0].categories(),
901             Categories::WRITTEN | Categories::PFNZERO
902         );
903 
904         // If we ask for written pages though (which seems synonymous with
905         // present?) then everything should be in one region.
906         let result = unsafe {
907             ioctl(
908                 &fd,
909                 PageMapScanBuilder::new(mmap.region())
910                     .return_mask(Categories::WRITTEN)
911                     .build(results.spare_capacity_mut()),
912             )
913             .unwrap()
914         };
915         assert_eq!(result.regions().len(), 1);
916         assert_eq!(result.walk_end(), mmap.page_end(3));
917 
918         assert_eq!(result.regions()[0].region(), mmap.page_region(0..4));
919         assert_eq!(result.regions()[0].categories(), Categories::WRITTEN);
920     }
921 
922     #[test]
page_limit()923     fn page_limit() {
924         if !ioctl_supported() {
925             return;
926         }
927         let mmap = MmapAnonymous::new(10);
928         let mut results = Vec::with_capacity(10);
929         let fd = File::open("/proc/self/pagemap").unwrap();
930 
931         mmap.read(0);
932         mmap.read(1);
933         mmap.read(2);
934         mmap.read(3);
935 
936         // Ask for written|pfnzero meaning only-read pages. This should return only
937         // a single region of the first page.
938         let result = unsafe {
939             ioctl(
940                 &fd,
941                 PageMapScanBuilder::new(mmap.region())
942                     .return_mask(Categories::WRITTEN | Categories::PFNZERO)
943                     .max_pages(2)
944                     .build(results.spare_capacity_mut()),
945             )
946             .unwrap()
947         };
948         assert_eq!(result.regions().len(), 1);
949         assert_eq!(result.walk_end(), mmap.page_end(1));
950 
951         assert_eq!(result.regions()[0].region(), mmap.page_region(0..2));
952         assert_eq!(
953             result.regions()[0].categories(),
954             Categories::WRITTEN | Categories::PFNZERO
955         );
956     }
957 
958     #[test]
page_limit_with_hole()959     fn page_limit_with_hole() {
960         if !ioctl_supported() {
961             return;
962         }
963         let mmap = MmapAnonymous::new(10);
964         let mut results = Vec::with_capacity(10);
965         let fd = File::open("/proc/self/pagemap").unwrap();
966 
967         mmap.read(0);
968         mmap.read(2);
969         mmap.read(3);
970 
971         // Ask for written|pfnzero meaning only-read pages. This should return only
972         // a single region of the first page.
973         let result = unsafe {
974             ioctl(
975                 &fd,
976                 PageMapScanBuilder::new(mmap.region())
977                     .category_mask(Categories::WRITTEN)
978                     .return_mask(Categories::WRITTEN | Categories::PFNZERO)
979                     .max_pages(2)
980                     .build(results.spare_capacity_mut()),
981             )
982             .unwrap()
983         };
984         assert_eq!(result.regions().len(), 2);
985         assert_eq!(result.walk_end(), mmap.page_end(2));
986 
987         assert_eq!(result.regions()[0].region(), mmap.page_region(0..1));
988         assert_eq!(
989             result.regions()[0].categories(),
990             Categories::WRITTEN | Categories::PFNZERO
991         );
992         assert_eq!(result.regions()[1].region(), mmap.page_region(2..3));
993         assert_eq!(
994             result.regions()[1].categories(),
995             Categories::WRITTEN | Categories::PFNZERO
996         );
997     }
998 }
999