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