1 //! Copy-on-write initialization support: creation of backing images for
2 //! modules, and logic to support mapping these backing images into memory.
3 
4 use super::sys::DecommitBehavior;
5 use crate::Engine;
6 use crate::prelude::*;
7 use crate::runtime::vm::sys::vm::{self, MemoryImageSource, PageMap, reset_with_pagemap};
8 use crate::runtime::vm::{
9     HostAlignedByteCount, MmapOffset, ModuleMemoryImageSource, host_page_size,
10 };
11 use alloc::sync::Arc;
12 use core::fmt;
13 use core::ops::Range;
14 use wasmtime_environ::prelude::TryPrimaryMap;
15 use wasmtime_environ::{DefinedMemoryIndex, MemoryInitialization, Module, Tunables};
16 
17 /// Backing images for memories in a module.
18 ///
19 /// This is meant to be built once, when a module is first loaded/constructed,
20 /// and then used many times for instantiation.
21 pub struct ModuleMemoryImages {
22     memories: TryPrimaryMap<DefinedMemoryIndex, Option<Arc<MemoryImage>>>,
23 }
24 
25 impl ModuleMemoryImages {
26     /// Get the MemoryImage for a given memory.
get_memory_image(&self, defined_index: DefinedMemoryIndex) -> Option<&Arc<MemoryImage>>27     pub fn get_memory_image(&self, defined_index: DefinedMemoryIndex) -> Option<&Arc<MemoryImage>> {
28         self.memories[defined_index].as_ref()
29     }
30 }
31 
32 /// One backing image for one memory.
33 pub struct MemoryImage {
34     /// The platform-specific source of this image.
35     ///
36     /// This might be a mapped `*.cwasm` file or on Unix it could also be a
37     /// `Memfd` as an anonymous file in memory on Linux. In either case this is
38     /// used as the backing-source for the CoW image.
39     source: MemoryImageSource,
40 
41     /// Length of image, in bytes.
42     ///
43     /// Note that initial memory size may be larger; leading and trailing zeroes
44     /// are truncated (handled by backing fd).
45     ///
46     /// Must be a multiple of the system page size.
47     len: HostAlignedByteCount,
48 
49     /// Image starts this many bytes into `source`.
50     ///
51     /// This is 0 for anonymous-backed memfd files and is the offset of the
52     /// data section in a `*.cwasm` file for `*.cwasm`-backed images.
53     ///
54     /// Must be a multiple of the system page size.
55     ///
56     /// ## Notes
57     ///
58     /// This currently isn't a `HostAlignedByteCount` because that's a usize and
59     /// this, being a file offset, is a u64.
60     source_offset: u64,
61 
62     /// Image starts this many bytes into heap space.
63     ///
64     /// Must be a multiple of the system page size.
65     linear_memory_offset: HostAlignedByteCount,
66 
67     /// The original source of data that this image is derived from.
68     module_source: Arc<dyn ModuleMemoryImageSource>,
69 
70     /// The offset, within `module_source.wasm_data()`, that this image starts
71     /// at.
72     module_source_offset: usize,
73 }
74 
75 impl MemoryImage {
new( engine: &Engine, page_size: u32, linear_memory_offset: HostAlignedByteCount, module_source: &Arc<impl ModuleMemoryImageSource>, data_range: Range<usize>, ) -> Result<Option<MemoryImage>>76     fn new(
77         engine: &Engine,
78         page_size: u32,
79         linear_memory_offset: HostAlignedByteCount,
80         module_source: &Arc<impl ModuleMemoryImageSource>,
81         data_range: Range<usize>,
82     ) -> Result<Option<MemoryImage>> {
83         let assert_page_aligned = |val: usize| {
84             assert_eq!(val % (page_size as usize), 0);
85         };
86         // Sanity-check that various parameters are page-aligned.
87         let len =
88             HostAlignedByteCount::new(data_range.len()).expect("memory image data is page-aligned");
89 
90         // If a backing `mmap` is present then `data` should be a sub-slice of
91         // the `mmap`. The sanity-checks here double-check that. Additionally
92         // compilation should have ensured that the `data` section is
93         // page-aligned within `mmap`, so that's also all double-checked here.
94         //
95         // Finally if the `mmap` itself comes from a backing file on disk, such
96         // as a `*.cwasm` file, then that's a valid source of data for the
97         // memory image so we simply return referencing that.
98         //
99         // Note that this path is platform-agnostic in the sense of all
100         // platforms we support support memory mapping copy-on-write data from
101         // files, but for now this is still a Linux-specific region of Wasmtime.
102         // Some work will be needed to get this file compiling for macOS and
103         // Windows.
104         let data = &module_source.wasm_data()[data_range.clone()];
105         if !engine.config().force_memory_init_memfd {
106             if let Some(mmap) = module_source.mmap() {
107                 let start = mmap.as_ptr() as usize;
108                 let end = start + mmap.len();
109                 let data_start = data.as_ptr() as usize;
110                 let data_end = data_start + data.len();
111                 assert!(start <= data_start && data_end <= end);
112                 assert_page_aligned(start);
113                 assert_page_aligned(data_start);
114                 assert_page_aligned(data_end);
115 
116                 #[cfg(feature = "std")]
117                 if let Some(file) = mmap.original_file() {
118                     if let Some(source) = MemoryImageSource::from_file(file) {
119                         return Ok(Some(MemoryImage {
120                             source,
121                             source_offset: u64::try_from(data_start - start).unwrap(),
122                             linear_memory_offset,
123                             len,
124                             module_source: module_source.clone(),
125                             module_source_offset: data_range.start,
126                         }));
127                     }
128                 }
129             }
130         }
131 
132         // If `mmap` doesn't come from a file then platform-specific mechanisms
133         // may be used to place the data in a form that's amenable to an mmap.
134         if let Some(source) = MemoryImageSource::from_data(data)? {
135             return Ok(Some(MemoryImage {
136                 source,
137                 source_offset: 0,
138                 linear_memory_offset,
139                 len,
140                 module_source: module_source.clone(),
141                 module_source_offset: data_range.start,
142             }));
143         }
144 
145         Ok(None)
146     }
147 
map_at(&self, mmap_base: &MmapOffset) -> Result<()>148     unsafe fn map_at(&self, mmap_base: &MmapOffset) -> Result<()> {
149         unsafe {
150             mmap_base.map_image_at(
151                 &self.source,
152                 self.source_offset,
153                 self.linear_memory_offset,
154                 self.len,
155             )
156         }
157     }
158 
remap_as_zeros_at(&self, base: *mut u8) -> Result<()>159     unsafe fn remap_as_zeros_at(&self, base: *mut u8) -> Result<()> {
160         unsafe {
161             self.source.remap_as_zeros_at(
162                 base.add(self.linear_memory_offset.byte_count()),
163                 self.len.byte_count(),
164             )?;
165         }
166         Ok(())
167     }
168 }
169 
170 impl ModuleMemoryImages {
171     /// Create a new `ModuleMemoryImages` for the given module. This can be
172     /// passed in as part of a `InstanceAllocationRequest` to speed up
173     /// instantiation and execution by using copy-on-write-backed memories.
new( engine: &Engine, module: &Module, source: &Arc<impl ModuleMemoryImageSource>, ) -> Result<Option<ModuleMemoryImages>>174     pub fn new(
175         engine: &Engine,
176         module: &Module,
177         source: &Arc<impl ModuleMemoryImageSource>,
178     ) -> Result<Option<ModuleMemoryImages>> {
179         let map = match &module.memory_initialization {
180             MemoryInitialization::Static { map } => map,
181             _ => return Ok(None),
182         };
183         let mut memories = TryPrimaryMap::with_capacity(map.len())?;
184         let page_size = crate::runtime::vm::host_page_size();
185         let page_size = u32::try_from(page_size).unwrap();
186         for (memory_index, init) in map {
187             // mmap-based-initialization only works for defined memories with a
188             // known starting point of all zeros, so bail out if the mmeory is
189             // imported.
190             let defined_memory = match module.defined_memory_index(memory_index) {
191                 Some(idx) => idx,
192                 None => return Ok(None),
193             };
194 
195             // If there's no initialization for this memory known then we don't
196             // need an image for the memory so push `None` and move on.
197             let init = match init {
198                 Some(init) => init,
199                 None => {
200                     memories.push(None)?;
201                     continue;
202                 }
203             };
204 
205             let data_range = init.data.start as usize..init.data.end as usize;
206             if module.memories[memory_index]
207                 .minimum_byte_size()
208                 .map_or(false, |mem_initial_len| {
209                     init.offset + u64::try_from(data_range.len()).unwrap() > mem_initial_len
210                 })
211             {
212                 // The image is rounded up to multiples of the host OS page
213                 // size. But if Wasm is using a custom page size, the Wasm page
214                 // size might be smaller than the host OS page size, and that
215                 // rounding might have made the image larger than the Wasm
216                 // memory's initial length. This is *probably* okay, since the
217                 // rounding would have just introduced new runs of zeroes in the
218                 // image, but out of an abundance of caution we don't generate
219                 // CoW images in this scenario.
220                 return Ok(None);
221             }
222 
223             let offset_usize = match usize::try_from(init.offset) {
224                 Ok(offset) => offset,
225                 Err(_) => return Ok(None),
226             };
227             let offset = HostAlignedByteCount::new(offset_usize)
228                 .expect("memory init offset is a multiple of the host page size");
229 
230             // If this creation fails then we fail creating
231             // `ModuleMemoryImages` since this memory couldn't be represented.
232             let image = match MemoryImage::new(engine, page_size, offset, source, data_range)? {
233                 Some(image) => image,
234                 None => return Ok(None),
235             };
236 
237             let idx = memories.push(Some(try_new::<Arc<_>>(image)?))?;
238             assert_eq!(idx, defined_memory);
239         }
240 
241         Ok(Some(ModuleMemoryImages { memories }))
242     }
243 }
244 
245 /// Slot management of a copy-on-write image which can be reused for the pooling
246 /// allocator.
247 ///
248 /// This data structure manages a slot of linear memory, primarily in the
249 /// pooling allocator, which optionally has a contiguous memory image in the
250 /// middle of it. Pictorially this data structure manages a virtual memory
251 /// region that looks like:
252 ///
253 /// ```text
254 ///   +--------------------+-------------------+--------------+--------------+
255 ///   |   anonymous        |      optional     |   anonymous  |    PROT_NONE |
256 ///   |     zero           |       memory      |     zero     |     memory   |
257 ///   |    memory          |       image       |    memory    |              |
258 ///   +--------------------+-------------------+--------------+--------------+
259 ///   |                     <------+---------->
260 ///   |<-----+------------>         \
261 ///   |      \                   image.len
262 ///   |       \
263 ///   |  image.linear_memory_offset
264 ///   |
265 ///   \
266 ///  self.base is this virtual address
267 ///
268 ///    <------------------+------------------------------------------------>
269 ///                        \
270 ///                      static_size
271 ///
272 ///    <------------------+---------------------------------->
273 ///                        \
274 ///                      accessible
275 /// ```
276 ///
277 /// When a `MemoryImageSlot` is created it's told what the `static_size` and
278 /// `accessible` limits are. Initially there is assumed to be no image in linear
279 /// memory.
280 ///
281 /// When `MemoryImageSlot::instantiate` is called then the method will perform
282 /// a "synchronization" to take the image from its prior state to the new state
283 /// for the image specified. The first instantiation for example will mmap the
284 /// heap image into place. Upon reuse of a slot nothing happens except possibly
285 /// shrinking `self.accessible`. When a new image is used then the old image is
286 /// mapped to anonymous zero memory and then the new image is mapped in place.
287 ///
288 /// A `MemoryImageSlot` is either `dirty` or it isn't. When a `MemoryImageSlot`
289 /// is dirty then it is assumed that any memory beneath `self.accessible` could
290 /// have any value. Instantiation cannot happen into a `dirty` slot, however, so
291 /// the `MemoryImageSlot::clear_and_remain_ready` returns this memory back to
292 /// its original state to mark `dirty = false`. This is done by resetting all
293 /// anonymous memory back to zero and the image itself back to its initial
294 /// contents.
295 ///
296 /// On Linux this is achieved with the `madvise(MADV_DONTNEED)` syscall. This
297 /// syscall will release the physical pages back to the OS but retain the
298 /// original mappings, effectively resetting everything back to its initial
299 /// state. Non-linux platforms will replace all memory below `self.accessible`
300 /// with a fresh zero'd mmap, meaning that reuse is effectively not supported.
301 pub struct MemoryImageSlot {
302     /// The mmap and offset within it that contains the linear memory for this
303     /// slot.
304     base: MmapOffset,
305 
306     /// The maximum static memory size which `self.accessible` can grow to.
307     static_size: usize,
308 
309     /// An optional image that is currently being used in this linear memory.
310     ///
311     /// This can be `None` in which case memory is originally all zeros. When
312     /// `Some` the image describes where it's located within the image.
313     image: Option<Arc<MemoryImage>>,
314 
315     /// The size of the heap that is readable and writable.
316     ///
317     /// Note that this may extend beyond the actual linear memory heap size in
318     /// the case of dynamic memories in use. Memory accesses to memory below
319     /// `self.accessible` may still page fault as pages are lazily brought in
320     /// but the faults will always be resolved by the kernel.
321     ///
322     /// Also note that this is always page-aligned.
323     accessible: HostAlignedByteCount,
324 
325     /// Whether this slot may have "dirty" pages (pages written by an
326     /// instantiation). Set by `instantiate()` and cleared by
327     /// `clear_and_remain_ready()`, and used in assertions to ensure
328     /// those methods are called properly.
329     ///
330     /// Invariant: if !dirty, then this memory slot contains a clean
331     /// CoW mapping of `image`, if `Some(..)`, and anonymous-zero
332     /// memory beyond the image up to `static_size`. The addresses
333     /// from offset 0 to `self.accessible` are R+W and set to zero or the
334     /// initial image content, as appropriate. Everything between
335     /// `self.accessible` and `self.static_size` is inaccessible.
336     dirty: bool,
337 }
338 
339 impl fmt::Debug for MemoryImageSlot {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result340     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341         f.debug_struct("MemoryImageSlot")
342             .field("base", &self.base)
343             .field("static_size", &self.static_size)
344             .field("accessible", &self.accessible)
345             .field("dirty", &self.dirty)
346             .finish_non_exhaustive()
347     }
348 }
349 
350 impl MemoryImageSlot {
351     /// Create a new MemoryImageSlot. Assumes that there is an anonymous
352     /// mmap backing in the given range to start.
353     ///
354     /// The `accessible` parameter describes how much of linear memory is
355     /// already mapped as R/W with all zero-bytes. The `static_size` value is
356     /// the maximum size of this image which `accessible` cannot grow beyond,
357     /// and all memory from `accessible` from `static_size` should be mapped as
358     /// `PROT_NONE` backed by zero-bytes.
create( base: MmapOffset, accessible: HostAlignedByteCount, static_size: usize, ) -> Self359     pub(crate) fn create(
360         base: MmapOffset,
361         accessible: HostAlignedByteCount,
362         static_size: usize,
363     ) -> Self {
364         MemoryImageSlot {
365             base,
366             static_size,
367             accessible,
368             image: None,
369             dirty: false,
370         }
371     }
372 
set_heap_limit(&mut self, size_bytes: usize) -> Result<()>373     pub(crate) fn set_heap_limit(&mut self, size_bytes: usize) -> Result<()> {
374         let size_bytes_aligned = HostAlignedByteCount::new_rounded_up(size_bytes)?;
375         assert!(size_bytes <= self.static_size);
376         assert!(size_bytes_aligned.byte_count() <= self.static_size);
377 
378         // If the heap limit already addresses accessible bytes then no syscalls
379         // are necessary since the data is already mapped into the process and
380         // waiting to go.
381         //
382         // This is used for "dynamic" memories where memory is not always
383         // decommitted during recycling (but it's still always reset).
384         if size_bytes_aligned <= self.accessible {
385             return Ok(());
386         }
387 
388         // Otherwise use `mprotect` to make the new pages read/write.
389         self.set_protection(self.accessible..size_bytes_aligned, true)?;
390         self.accessible = size_bytes_aligned;
391 
392         Ok(())
393     }
394 
395     /// Prepares this slot for the instantiation of a new instance with the
396     /// provided linear memory image.
397     ///
398     /// The `initial_size_bytes` parameter indicates the required initial size
399     /// of the heap for the instance. The `maybe_image` is an optional initial
400     /// image for linear memory to contains. The `style` is the way compiled
401     /// code will be accessing this memory.
402     ///
403     /// The purpose of this method is to take a previously pristine slot
404     /// (`!self.dirty`) and transform its prior state into state necessary for
405     /// the given parameters. This could include, for example:
406     ///
407     /// * More memory may be made read/write if `initial_size_bytes` is larger
408     ///   than `self.accessible`.
409     /// * For `MemoryStyle::Static` linear memory may be made `PROT_NONE` if
410     ///   `self.accessible` is larger than `initial_size_bytes`.
411     /// * If no image was previously in place or if the wrong image was
412     ///   previously in place then `mmap` may be used to setup the initial
413     ///   image.
instantiate( &mut self, initial_size_bytes: usize, maybe_image: Option<&Arc<MemoryImage>>, ty: &wasmtime_environ::Memory, tunables: &Tunables, ) -> Result<()>414     pub(crate) fn instantiate(
415         &mut self,
416         initial_size_bytes: usize,
417         maybe_image: Option<&Arc<MemoryImage>>,
418         ty: &wasmtime_environ::Memory,
419         tunables: &Tunables,
420     ) -> Result<()> {
421         assert!(!self.dirty);
422         assert!(
423             initial_size_bytes <= self.static_size,
424             "initial_size_bytes <= self.static_size failed: \
425              initial_size_bytes={initial_size_bytes}, self.static_size={}",
426             self.static_size
427         );
428         let initial_size_bytes_page_aligned =
429             HostAlignedByteCount::new_rounded_up(initial_size_bytes)?;
430 
431         // First order of business is to blow away the previous linear memory
432         // image if it doesn't match the image specified here. If one is
433         // detected then it's reset with anonymous memory which means that all
434         // of memory up to `self.accessible` will now be read/write and zero.
435         //
436         // Note that this intentionally a "small mmap" which only covers the
437         // extent of the prior initialization image in order to preserve
438         // resident memory that might come before or after the image.
439         let images_equal = match (self.image.as_ref(), maybe_image) {
440             (Some(a), Some(b)) if Arc::ptr_eq(a, b) => true,
441             (None, None) => true,
442             _ => false,
443         };
444         if !images_equal {
445             self.remove_image()?;
446         }
447 
448         // The next order of business is to ensure that `self.accessible` is
449         // appropriate. First up is to grow the read/write portion of memory if
450         // it's not large enough to accommodate `initial_size_bytes`.
451         if self.accessible < initial_size_bytes_page_aligned {
452             self.set_protection(self.accessible..initial_size_bytes_page_aligned, true)?;
453             self.accessible = initial_size_bytes_page_aligned;
454         }
455 
456         // If (1) the accessible region is not in its initial state, and (2) the
457         // memory relies on virtual memory at all (i.e. has offset guard
458         // pages), then we need to reset memory protections. Put another way,
459         // the only time it is safe to not reset protections is when we are
460         // using dynamic memory without any guard pages.
461         let host_page_size_log2 = u8::try_from(host_page_size().ilog2()).unwrap();
462         if initial_size_bytes_page_aligned < self.accessible
463             && (tunables.memory_guard_size > 0
464                 || ty.can_use_virtual_memory(tunables, host_page_size_log2))
465         {
466             self.set_protection(initial_size_bytes_page_aligned..self.accessible, false)?;
467             self.accessible = initial_size_bytes_page_aligned;
468         }
469 
470         // Now that memory is sized appropriately the final operation is to
471         // place the new image into linear memory. Note that this operation is
472         // skipped if `self.image` matches `maybe_image`.
473         assert!(initial_size_bytes <= self.accessible.byte_count());
474         assert!(initial_size_bytes_page_aligned <= self.accessible);
475         if !images_equal {
476             if let Some(image) = maybe_image.as_ref() {
477                 assert!(
478                     image
479                         .linear_memory_offset
480                         .checked_add(image.len)
481                         .unwrap()
482                         .byte_count()
483                         <= initial_size_bytes
484                 );
485                 if !image.len.is_zero() {
486                     unsafe {
487                         image.map_at(&self.base)?;
488                     }
489                 }
490             }
491             self.image = maybe_image.cloned();
492         }
493 
494         // Flag ourselves as `dirty` which means that the next operation on this
495         // slot is required to be `clear_and_remain_ready`.
496         self.dirty = true;
497 
498         Ok(())
499     }
500 
remove_image(&mut self) -> Result<()>501     pub(crate) fn remove_image(&mut self) -> Result<()> {
502         if let Some(image) = &self.image {
503             unsafe {
504                 image.remap_as_zeros_at(self.base.as_mut_ptr())?;
505             }
506             self.image = None;
507         }
508         Ok(())
509     }
510 
511     /// Resets this linear memory slot back to a "pristine state".
512     ///
513     /// This will reset the memory back to its original contents on Linux or
514     /// reset the contents back to zero on other platforms. The `keep_resident`
515     /// argument is the maximum amount of memory to keep resident in this
516     /// process's memory on Linux. Up to that much memory will be `memset` to
517     /// zero where the rest of it will be reset or released with `madvise`.
518     ///
519     /// Returns the number of bytes still resident in memory after this function
520     /// has returned.
521     #[allow(dead_code, reason = "only used in some cfgs")]
clear_and_remain_ready( &mut self, pagemap: Option<&PageMap>, keep_resident: HostAlignedByteCount, decommit: impl FnMut(*mut u8, usize), ) -> Result<usize>522     pub(crate) fn clear_and_remain_ready(
523         &mut self,
524         pagemap: Option<&PageMap>,
525         keep_resident: HostAlignedByteCount,
526         decommit: impl FnMut(*mut u8, usize),
527     ) -> Result<usize> {
528         assert!(self.dirty);
529 
530         let bytes_resident =
531             unsafe { self.reset_all_memory_contents(pagemap, keep_resident, decommit)? };
532 
533         self.dirty = false;
534         Ok(bytes_resident)
535     }
536 
537     #[allow(dead_code, reason = "only used in some cfgs")]
reset_all_memory_contents( &mut self, pagemap: Option<&PageMap>, keep_resident: HostAlignedByteCount, decommit: impl FnMut(*mut u8, usize), ) -> Result<usize>538     unsafe fn reset_all_memory_contents(
539         &mut self,
540         pagemap: Option<&PageMap>,
541         keep_resident: HostAlignedByteCount,
542         decommit: impl FnMut(*mut u8, usize),
543     ) -> Result<usize> {
544         match vm::decommit_behavior() {
545             DecommitBehavior::Zero => {
546                 // If we're not on Linux then there's no generic platform way to
547                 // reset memory back to its original state, so instead reset memory
548                 // back to entirely zeros with an anonymous backing.
549                 //
550                 // Additionally the previous image, if any, is dropped here
551                 // since it's no longer applicable to this mapping.
552                 self.reset_with_anon_memory()?;
553                 Ok(0)
554             }
555             DecommitBehavior::RestoreOriginalMapping => {
556                 let bytes_resident =
557                     unsafe { self.reset_with_original_mapping(pagemap, keep_resident, decommit) };
558                 Ok(bytes_resident)
559             }
560         }
561     }
562 
563     #[allow(dead_code, reason = "only used in some cfgs")]
reset_with_original_mapping( &mut self, pagemap: Option<&PageMap>, keep_resident: HostAlignedByteCount, decommit: impl FnMut(*mut u8, usize), ) -> usize564     unsafe fn reset_with_original_mapping(
565         &mut self,
566         pagemap: Option<&PageMap>,
567         keep_resident: HostAlignedByteCount,
568         decommit: impl FnMut(*mut u8, usize),
569     ) -> usize {
570         assert_eq!(
571             vm::decommit_behavior(),
572             DecommitBehavior::RestoreOriginalMapping
573         );
574 
575         unsafe {
576             return match &self.image {
577                 // If there's a backing image then manually resetting a region
578                 // is a bit trickier than without an image, so delegate to the
579                 // helper function below.
580                 Some(image) => reset_with_pagemap(
581                     pagemap,
582                     self.base.as_mut_ptr(),
583                     self.accessible,
584                     keep_resident,
585                     |region| manually_reset_region(self.base.as_mut_ptr().addr(), image, region),
586                     decommit,
587                 ),
588 
589                 // If there's no memory image for this slot then pages are always
590                 // manually reset back to zero or given to `decommit`.
591                 None => reset_with_pagemap(
592                     pagemap,
593                     self.base.as_mut_ptr(),
594                     self.accessible,
595                     keep_resident,
596                     |region| region.fill(0),
597                     decommit,
598                 ),
599             };
600         }
601 
602         /// Manually resets `region` back to its original contents as specified
603         /// in `image`.
604         ///
605         /// This assumes that the original mmap starts at `base_addr` and
606         /// `region` is a subslice within the original mmap.
607         ///
608         /// # Panics
609         ///
610         /// Panics if `base_addr` is not the right index due to the various
611         /// indexing calculations below.
612         fn manually_reset_region(base_addr: usize, image: &MemoryImage, mut region: &mut [u8]) {
613             let image_start = image.linear_memory_offset.byte_count();
614             let image_end = image_start + image.len.byte_count();
615             let mut region_start = region.as_ptr().addr() - base_addr;
616             let region_end = region_start + region.len();
617             let image_bytes = image.module_source.wasm_data();
618             let image_bytes = &image_bytes[image.module_source_offset..][..image.len.byte_count()];
619 
620             // 1. Zero out the part before the image (if any).
621             if let Some(len_before_image) = image_start.checked_sub(region_start) {
622                 let len = len_before_image.min(region.len());
623                 let (a, b) = region.split_at_mut(len);
624                 a.fill(0);
625                 region = b;
626                 region_start += len;
627 
628                 if region.is_empty() {
629                     return;
630                 }
631             }
632 
633             debug_assert_eq!(region_end - region_start, region.len());
634             debug_assert!(region_start >= image_start);
635 
636             // 2. Copy the original bytes from the image for the part that
637             //    overlaps with the image.
638             if let Some(len_in_image) = image_end.checked_sub(region_start) {
639                 let len = len_in_image.min(region.len());
640                 let (a, b) = region.split_at_mut(len);
641                 a.copy_from_slice(&image_bytes[region_start - image_start..][..len]);
642                 region = b;
643                 region_start += len;
644 
645                 if region.is_empty() {
646                     return;
647                 }
648             }
649 
650             debug_assert_eq!(region_end - region_start, region.len());
651             debug_assert!(region_start >= image_end);
652 
653             // 3. Zero out the part after the image.
654             region.fill(0);
655         }
656     }
657 
set_protection(&self, range: Range<HostAlignedByteCount>, readwrite: bool) -> Result<()>658     fn set_protection(&self, range: Range<HostAlignedByteCount>, readwrite: bool) -> Result<()> {
659         let len = range
660             .end
661             .checked_sub(range.start)
662             .expect("range.start <= range.end");
663         assert!(range.end.byte_count() <= self.static_size);
664         if len.is_zero() {
665             return Ok(());
666         }
667 
668         // TODO: use Mmap to change memory permissions instead of these free
669         // functions.
670         unsafe {
671             let start = self.base.as_mut_ptr().add(range.start.byte_count());
672             if readwrite {
673                 vm::expose_existing_mapping(start, len.byte_count())?;
674             } else {
675                 vm::hide_existing_mapping(start, len.byte_count())?;
676             }
677         }
678 
679         Ok(())
680     }
681 
has_image(&self) -> bool682     pub(crate) fn has_image(&self) -> bool {
683         self.image.is_some()
684     }
685 
686     #[allow(dead_code, reason = "only used in some cfgs")]
is_dirty(&self) -> bool687     pub(crate) fn is_dirty(&self) -> bool {
688         self.dirty
689     }
690 
691     /// Map anonymous zeroed memory across the whole slot,
692     /// inaccessible. Used both during instantiate and during drop.
reset_with_anon_memory(&mut self) -> Result<()>693     pub(crate) fn reset_with_anon_memory(&mut self) -> Result<()> {
694         if self.static_size == 0 {
695             assert!(self.image.is_none());
696             assert_eq!(self.accessible, 0);
697             return Ok(());
698         }
699 
700         unsafe {
701             vm::erase_existing_mapping(self.base.as_mut_ptr(), self.static_size)?;
702         }
703 
704         self.image = None;
705         self.accessible = HostAlignedByteCount::ZERO;
706 
707         Ok(())
708     }
709 }
710 
711 #[cfg(all(test, target_os = "linux", not(miri)))]
712 mod test {
713     use super::*;
714     use crate::runtime::vm::mmap::{AlignedLength, Mmap};
715     use crate::runtime::vm::sys::vm::decommit_pages;
716     use crate::runtime::vm::{HostAlignedByteCount, MmapVec, host_page_size};
717     use std::sync::Arc;
718     use wasmtime_environ::{IndexType, Limits, Memory};
719 
create_memfd_with_data(offset: usize, data: &[u8]) -> Result<MemoryImage>720     fn create_memfd_with_data(offset: usize, data: &[u8]) -> Result<MemoryImage> {
721         // offset must be a multiple of the page size.
722         let linear_memory_offset =
723             HostAlignedByteCount::new(offset).expect("offset is page-aligned");
724         // The image length is rounded up to the nearest page size
725         let image_len = HostAlignedByteCount::new_rounded_up(data.len()).unwrap();
726 
727         let mut source = TestDataSource {
728             data: vec![0; image_len.byte_count()],
729         };
730         source.data[..data.len()].copy_from_slice(data);
731 
732         return Ok(MemoryImage {
733             source: MemoryImageSource::from_data(data)?.unwrap(),
734             len: image_len,
735             source_offset: 0,
736             linear_memory_offset,
737             module_source: Arc::new(source),
738             module_source_offset: 0,
739         });
740 
741         struct TestDataSource {
742             data: Vec<u8>,
743         }
744 
745         impl ModuleMemoryImageSource for TestDataSource {
746             fn wasm_data(&self) -> &[u8] {
747                 &self.data
748             }
749             fn mmap(&self) -> Option<&MmapVec> {
750                 None
751             }
752         }
753     }
754 
dummy_memory() -> Memory755     fn dummy_memory() -> Memory {
756         Memory {
757             idx_type: IndexType::I32,
758             limits: Limits { min: 0, max: None },
759             shared: false,
760             page_size_log2: Memory::DEFAULT_PAGE_SIZE_LOG2,
761         }
762     }
763 
mmap_4mib_inaccessible() -> Arc<Mmap<AlignedLength>>764     fn mmap_4mib_inaccessible() -> Arc<Mmap<AlignedLength>> {
765         let four_mib = HostAlignedByteCount::new(4 << 20).expect("4 MiB is page aligned");
766         Arc::new(Mmap::accessible_reserved(HostAlignedByteCount::ZERO, four_mib).unwrap())
767     }
768 
769     /// Presents a part of an mmap as a mutable slice within a callback.
770     ///
771     /// The callback ensures that the reference no longer lives after the
772     /// function is done.
773     ///
774     /// # Safety
775     ///
776     /// The caller must ensure that during this function call, the only way this
777     /// region of memory is not accessed by (read from or written to) is via the
778     /// reference. Making the callback `'static` goes some way towards ensuring
779     /// that, but it's still possible to squirrel away a reference into global
780     /// state. So don't do that.
with_slice_mut( mmap: &Arc<Mmap<AlignedLength>>, range: Range<usize>, f: impl FnOnce(&mut [u8]) + 'static, )781     unsafe fn with_slice_mut(
782         mmap: &Arc<Mmap<AlignedLength>>,
783         range: Range<usize>,
784         f: impl FnOnce(&mut [u8]) + 'static,
785     ) {
786         let ptr = mmap.as_ptr().cast_mut();
787         let slice = unsafe {
788             core::slice::from_raw_parts_mut(ptr.add(range.start), range.end - range.start)
789         };
790         f(slice);
791     }
792 
793     #[test]
instantiate_no_image()794     fn instantiate_no_image() {
795         let ty = dummy_memory();
796         let tunables = Tunables {
797             memory_reservation: 4 << 30,
798             ..Tunables::default_miri()
799         };
800         // 4 MiB mmap'd area, not accessible
801         let mmap = mmap_4mib_inaccessible();
802         // Create a MemoryImageSlot on top of it
803         let mut memfd =
804             MemoryImageSlot::create(mmap.zero_offset(), HostAlignedByteCount::ZERO, 4 << 20);
805         assert!(!memfd.is_dirty());
806         // instantiate with 64 KiB initial size
807         memfd.instantiate(64 << 10, None, &ty, &tunables).unwrap();
808         assert!(memfd.is_dirty());
809 
810         // We should be able to access this 64 KiB (try both ends) and
811         // it should consist of zeroes.
812         unsafe {
813             with_slice_mut(&mmap, 0..65536, |slice| {
814                 assert_eq!(0, slice[0]);
815                 assert_eq!(0, slice[65535]);
816                 slice[1024] = 42;
817                 assert_eq!(42, slice[1024]);
818             });
819         }
820 
821         // grow the heap
822         memfd.set_heap_limit(128 << 10).unwrap();
823         let slice = unsafe { mmap.slice(0..1 << 20) };
824         assert_eq!(42, slice[1024]);
825         assert_eq!(0, slice[131071]);
826         // instantiate again; we should see zeroes, even as the
827         // reuse-anon-mmap-opt kicks in
828         memfd
829             .clear_and_remain_ready(None, HostAlignedByteCount::ZERO, |ptr, len| unsafe {
830                 decommit_pages(ptr, len).unwrap()
831             })
832             .unwrap();
833         assert!(!memfd.is_dirty());
834         memfd.instantiate(64 << 10, None, &ty, &tunables).unwrap();
835         let slice = unsafe { mmap.slice(0..65536) };
836         assert_eq!(0, slice[1024]);
837     }
838 
839     #[test]
instantiate_image()840     fn instantiate_image() {
841         let page_size = host_page_size();
842         let ty = dummy_memory();
843         let tunables = Tunables {
844             memory_reservation: 4 << 30,
845             ..Tunables::default_miri()
846         };
847         // 4 MiB mmap'd area, not accessible
848         let mmap = mmap_4mib_inaccessible();
849         // Create a MemoryImageSlot on top of it
850         let mut memfd =
851             MemoryImageSlot::create(mmap.zero_offset(), HostAlignedByteCount::ZERO, 4 << 20);
852         // Create an image with some data.
853         let image = Arc::new(create_memfd_with_data(page_size, &[1, 2, 3, 4]).unwrap());
854         // Instantiate with this image
855         memfd
856             .instantiate(64 << 10, Some(&image), &ty, &tunables)
857             .unwrap();
858         assert!(memfd.has_image());
859 
860         unsafe {
861             with_slice_mut(&mmap, 0..65536, move |slice| {
862                 assert_eq!(&[1, 2, 3, 4], &slice[page_size..][..4]);
863                 slice[page_size] = 5;
864             });
865         }
866 
867         // Clear and re-instantiate same image
868         memfd
869             .clear_and_remain_ready(None, HostAlignedByteCount::ZERO, |ptr, len| unsafe {
870                 decommit_pages(ptr, len).unwrap()
871             })
872             .unwrap();
873         memfd
874             .instantiate(64 << 10, Some(&image), &ty, &tunables)
875             .unwrap();
876         let slice = unsafe { mmap.slice(0..65536) };
877         assert_eq!(&[1, 2, 3, 4], &slice[page_size..][..4]);
878 
879         // Clear and re-instantiate no image
880         memfd
881             .clear_and_remain_ready(None, HostAlignedByteCount::ZERO, |ptr, len| unsafe {
882                 decommit_pages(ptr, len).unwrap()
883             })
884             .unwrap();
885         memfd.instantiate(64 << 10, None, &ty, &tunables).unwrap();
886         assert!(!memfd.has_image());
887         let slice = unsafe { mmap.slice(0..65536) };
888         assert_eq!(&[0, 0, 0, 0], &slice[page_size..][..4]);
889 
890         // Clear and re-instantiate image again
891         memfd
892             .clear_and_remain_ready(None, HostAlignedByteCount::ZERO, |ptr, len| unsafe {
893                 decommit_pages(ptr, len).unwrap()
894             })
895             .unwrap();
896         memfd
897             .instantiate(64 << 10, Some(&image), &ty, &tunables)
898             .unwrap();
899         let slice = unsafe { mmap.slice(0..65536) };
900         assert_eq!(&[1, 2, 3, 4], &slice[page_size..][..4]);
901 
902         // Create another image with different data.
903         let image2 = Arc::new(create_memfd_with_data(page_size, &[10, 11, 12, 13]).unwrap());
904         memfd
905             .clear_and_remain_ready(None, HostAlignedByteCount::ZERO, |ptr, len| unsafe {
906                 decommit_pages(ptr, len).unwrap()
907             })
908             .unwrap();
909         memfd
910             .instantiate(128 << 10, Some(&image2), &ty, &tunables)
911             .unwrap();
912         let slice = unsafe { mmap.slice(0..65536) };
913         assert_eq!(&[10, 11, 12, 13], &slice[page_size..][..4]);
914 
915         // Instantiate the original image again; we should notice it's
916         // a different image and not reuse the mappings.
917         memfd
918             .clear_and_remain_ready(None, HostAlignedByteCount::ZERO, |ptr, len| unsafe {
919                 decommit_pages(ptr, len).unwrap()
920             })
921             .unwrap();
922         memfd
923             .instantiate(64 << 10, Some(&image), &ty, &tunables)
924             .unwrap();
925         let slice = unsafe { mmap.slice(0..65536) };
926         assert_eq!(&[1, 2, 3, 4], &slice[page_size..][..4]);
927     }
928 
929     #[test]
930     #[cfg(target_os = "linux")]
memset_instead_of_madvise()931     fn memset_instead_of_madvise() {
932         let page_size = host_page_size();
933         let ty = dummy_memory();
934         let tunables = Tunables {
935             memory_reservation: 100 << 16,
936             ..Tunables::default_miri()
937         };
938         let mmap = mmap_4mib_inaccessible();
939         let mut memfd =
940             MemoryImageSlot::create(mmap.zero_offset(), HostAlignedByteCount::ZERO, 4 << 20);
941 
942         // Test basics with the image
943         for image_off in [0, page_size, page_size * 2] {
944             let image = Arc::new(create_memfd_with_data(image_off, &[1, 2, 3, 4]).unwrap());
945             for amt_to_memset in [0, page_size, page_size * 10, 1 << 20, 10 << 20] {
946                 let amt_to_memset = HostAlignedByteCount::new(amt_to_memset).unwrap();
947                 memfd
948                     .instantiate(64 << 10, Some(&image), &ty, &tunables)
949                     .unwrap();
950                 assert!(memfd.has_image());
951 
952                 unsafe {
953                     with_slice_mut(&mmap, 0..64 << 10, move |slice| {
954                         if image_off > 0 {
955                             assert_eq!(slice[image_off - 1], 0);
956                         }
957                         assert_eq!(slice[image_off + 5], 0);
958                         assert_eq!(&[1, 2, 3, 4], &slice[image_off..][..4]);
959                         slice[image_off] = 5;
960                         assert_eq!(&[5, 2, 3, 4], &slice[image_off..][..4]);
961                     })
962                 };
963 
964                 memfd
965                     .clear_and_remain_ready(None, amt_to_memset, |ptr, len| unsafe {
966                         decommit_pages(ptr, len).unwrap()
967                     })
968                     .unwrap();
969             }
970         }
971 
972         // Test without an image
973         for amt_to_memset in [0, page_size, page_size * 10, 1 << 20, 10 << 20] {
974             let amt_to_memset = HostAlignedByteCount::new(amt_to_memset).unwrap();
975             memfd.instantiate(64 << 10, None, &ty, &tunables).unwrap();
976 
977             unsafe {
978                 with_slice_mut(&mmap, 0..64 << 10, |slice| {
979                     for chunk in slice.chunks_mut(1024) {
980                         assert_eq!(chunk[0], 0);
981                         chunk[0] = 5;
982                     }
983                 });
984             }
985             memfd
986                 .clear_and_remain_ready(None, amt_to_memset, |ptr, len| unsafe {
987                     decommit_pages(ptr, len).unwrap()
988                 })
989                 .unwrap();
990         }
991     }
992 
993     #[test]
994     #[cfg(target_os = "linux")]
dynamic()995     fn dynamic() {
996         let page_size = host_page_size();
997         let ty = dummy_memory();
998         let tunables = Tunables {
999             memory_reservation: 0,
1000             memory_reservation_for_growth: 200,
1001             ..Tunables::default_miri()
1002         };
1003 
1004         let mmap = mmap_4mib_inaccessible();
1005         let mut memfd =
1006             MemoryImageSlot::create(mmap.zero_offset(), HostAlignedByteCount::ZERO, 4 << 20);
1007         let image = Arc::new(create_memfd_with_data(page_size, &[1, 2, 3, 4]).unwrap());
1008         let initial = 64 << 10;
1009 
1010         // Instantiate the image and test that memory remains accessible after
1011         // it's cleared.
1012         memfd
1013             .instantiate(initial, Some(&image), &ty, &tunables)
1014             .unwrap();
1015         assert!(memfd.has_image());
1016 
1017         unsafe {
1018             with_slice_mut(&mmap, 0..(64 << 10) + page_size, move |slice| {
1019                 assert_eq!(&[1, 2, 3, 4], &slice[page_size..][..4]);
1020                 slice[page_size] = 5;
1021                 assert_eq!(&[5, 2, 3, 4], &slice[page_size..][..4]);
1022             });
1023         }
1024 
1025         memfd
1026             .clear_and_remain_ready(None, HostAlignedByteCount::ZERO, |ptr, len| unsafe {
1027                 decommit_pages(ptr, len).unwrap()
1028             })
1029             .unwrap();
1030         let slice = unsafe { mmap.slice(0..(64 << 10) + page_size) };
1031         assert_eq!(&[1, 2, 3, 4], &slice[page_size..][..4]);
1032 
1033         // Re-instantiate make sure it preserves memory. Grow a bit and set data
1034         // beyond the initial size.
1035         memfd
1036             .instantiate(initial, Some(&image), &ty, &tunables)
1037             .unwrap();
1038         assert_eq!(&[1, 2, 3, 4], &slice[page_size..][..4]);
1039 
1040         memfd.set_heap_limit(initial * 2).unwrap();
1041 
1042         unsafe {
1043             with_slice_mut(&mmap, 0..(64 << 10) + page_size, move |slice| {
1044                 assert_eq!(&[0, 0], &slice[initial..initial + 2]);
1045                 slice[initial] = 100;
1046                 assert_eq!(&[100, 0], &slice[initial..initial + 2]);
1047             });
1048         }
1049 
1050         memfd
1051             .clear_and_remain_ready(None, HostAlignedByteCount::ZERO, |ptr, len| unsafe {
1052                 decommit_pages(ptr, len).unwrap()
1053             })
1054             .unwrap();
1055 
1056         // Test that memory is still accessible, but it's been reset
1057         assert_eq!(&[0, 0], &slice[initial..initial + 2]);
1058 
1059         // Instantiate again, and again memory beyond the initial size should
1060         // still be accessible. Grow into it again and make sure it works.
1061         memfd
1062             .instantiate(initial, Some(&image), &ty, &tunables)
1063             .unwrap();
1064         assert_eq!(&[0, 0], &slice[initial..initial + 2]);
1065         memfd.set_heap_limit(initial * 2).unwrap();
1066 
1067         unsafe {
1068             with_slice_mut(&mmap, 0..(64 << 10) + page_size, move |slice| {
1069                 assert_eq!(&[0, 0], &slice[initial..initial + 2]);
1070                 slice[initial] = 100;
1071                 assert_eq!(&[100, 0], &slice[initial..initial + 2]);
1072             });
1073         }
1074 
1075         memfd
1076             .clear_and_remain_ready(None, HostAlignedByteCount::ZERO, |ptr, len| unsafe {
1077                 decommit_pages(ptr, len).unwrap()
1078             })
1079             .unwrap();
1080 
1081         // Reset the image to none and double-check everything is back to zero
1082         memfd.instantiate(64 << 10, None, &ty, &tunables).unwrap();
1083         assert!(!memfd.has_image());
1084         assert_eq!(&[0, 0, 0, 0], &slice[page_size..][..4]);
1085         assert_eq!(&[0, 0], &slice[initial..initial + 2]);
1086     }
1087 
1088     #[test]
reset_with_pagemap()1089     fn reset_with_pagemap() {
1090         let page_size = host_page_size();
1091         let ty = dummy_memory();
1092         let tunables = Tunables {
1093             memory_reservation: 100 << 16,
1094             ..Tunables::default_miri()
1095         };
1096         let mmap = mmap_4mib_inaccessible();
1097         let mmap_len = page_size * 9;
1098         let mut memfd =
1099             MemoryImageSlot::create(mmap.zero_offset(), HostAlignedByteCount::ZERO, mmap_len);
1100         let pagemap = PageMap::new();
1101         let pagemap = pagemap.as_ref();
1102 
1103         let mut data = vec![0; 3 * page_size];
1104         for (i, chunk) in data.chunks_mut(page_size).enumerate() {
1105             for slot in chunk {
1106                 *slot = u8::try_from(i + 1).unwrap();
1107             }
1108         }
1109         let image = Arc::new(create_memfd_with_data(3 * page_size, &data).unwrap());
1110 
1111         memfd
1112             .instantiate(mmap_len, Some(&image), &ty, &tunables)
1113             .unwrap();
1114 
1115         let keep_resident = HostAlignedByteCount::new(mmap_len).unwrap();
1116         let assert_pristine_after_reset = |memfd: &mut MemoryImageSlot| unsafe {
1117             // Wipe the image, keeping some bytes resident.
1118             memfd
1119                 .clear_and_remain_ready(pagemap, keep_resident, |ptr, len| {
1120                     decommit_pages(ptr, len).unwrap()
1121                 })
1122                 .unwrap();
1123 
1124             // Double check that the contents of memory are as expected after
1125             // reset.
1126             with_slice_mut(&mmap, 0..mmap_len, move |slice| {
1127                 for (i, chunk) in slice.chunks(page_size).enumerate() {
1128                     let expected = match i {
1129                         0..3 => 0,
1130                         3..6 => u8::try_from(i).unwrap() - 2,
1131                         6..9 => 0,
1132                         _ => unreachable!(),
1133                     };
1134                     for slot in chunk {
1135                         assert_eq!(*slot, expected);
1136                     }
1137                 }
1138             });
1139 
1140             // Re-instantiate, but then wipe the image entirely by keeping
1141             // nothing resident.
1142             memfd
1143                 .instantiate(mmap_len, Some(&image), &ty, &tunables)
1144                 .unwrap();
1145             memfd
1146                 .clear_and_remain_ready(pagemap, HostAlignedByteCount::ZERO, |ptr, len| {
1147                     decommit_pages(ptr, len).unwrap()
1148                 })
1149                 .unwrap();
1150 
1151             // Next re-instantiate a final time to get used for the next test.
1152             memfd
1153                 .instantiate(mmap_len, Some(&image), &ty, &tunables)
1154                 .unwrap();
1155         };
1156 
1157         let write_page = |_memfd: &mut MemoryImageSlot, page: usize| unsafe {
1158             with_slice_mut(
1159                 &mmap,
1160                 page * page_size..(page + 1) * page_size,
1161                 move |slice| slice.fill(0xff),
1162             );
1163         };
1164 
1165         // Test various combinations of dirty pages and regions. For example
1166         // test a dirty region of memory entirely in the zero-initialized zone
1167         // before/after the image and also test when the dirty region straddles
1168         // just the start of the image, just the end of the image, both ends,
1169         // and is entirely contained in just the image.
1170         assert_pristine_after_reset(&mut memfd);
1171 
1172         for i in 0..9 {
1173             write_page(&mut memfd, i);
1174             assert_pristine_after_reset(&mut memfd);
1175         }
1176         write_page(&mut memfd, 0);
1177         write_page(&mut memfd, 1);
1178         assert_pristine_after_reset(&mut memfd);
1179         write_page(&mut memfd, 1);
1180         assert_pristine_after_reset(&mut memfd);
1181         write_page(&mut memfd, 2);
1182         write_page(&mut memfd, 3);
1183         assert_pristine_after_reset(&mut memfd);
1184         write_page(&mut memfd, 3);
1185         write_page(&mut memfd, 4);
1186         write_page(&mut memfd, 5);
1187         assert_pristine_after_reset(&mut memfd);
1188         write_page(&mut memfd, 0);
1189         write_page(&mut memfd, 1);
1190         write_page(&mut memfd, 2);
1191         assert_pristine_after_reset(&mut memfd);
1192         write_page(&mut memfd, 0);
1193         write_page(&mut memfd, 3);
1194         write_page(&mut memfd, 6);
1195         assert_pristine_after_reset(&mut memfd);
1196         write_page(&mut memfd, 2);
1197         write_page(&mut memfd, 3);
1198         write_page(&mut memfd, 4);
1199         write_page(&mut memfd, 5);
1200         write_page(&mut memfd, 6);
1201         assert_pristine_after_reset(&mut memfd);
1202         write_page(&mut memfd, 4);
1203         write_page(&mut memfd, 5);
1204         write_page(&mut memfd, 6);
1205         write_page(&mut memfd, 7);
1206         assert_pristine_after_reset(&mut memfd);
1207         write_page(&mut memfd, 4);
1208         write_page(&mut memfd, 5);
1209         write_page(&mut memfd, 8);
1210         assert_pristine_after_reset(&mut memfd);
1211     }
1212 }
1213