Skip to main content

mmap_snapshot/
lib.rs

1#![warn(missing_docs)]
2#![warn(clippy::undocumented_unsafe_blocks)]
3
4/*! **Safe** `mmap()` with **snapshot isolation** and **atomic commits**.
5
6([Linux-only](#os-support), [works best](#performance) on XFS/btrfs.)
7
8## Example
9
10Mmap a file as `&[u8]`:
11
12```rust
13# use mmap_snapshot::Mmap;
14# fn foo() -> std::io::Result<()> {
15# let path = std::path::Path::new("/tmp/doctest_1");
16# std::fs::write(&path, b"Hello world!")?;
17let mmap = Mmap::open(&path)?;
18assert_eq!(mmap.len(), 12);
19assert_eq!(&mmap[..], b"Hello world!");
20# std::fs::remove_file(path)?;
21# Ok(())
22# }
23```
24
25Mmap a file as `&mut [u8]`, committing the changes back to disk:
26
27```rust
28# use mmap_snapshot::MmapMut;
29# fn foo() -> std::io::Result<()> {
30# let path = std::path::Path::new("/tmp/doctest_2");
31# std::fs::write(&path, b"Hello world!")?;
32let mut mmap = MmapMut::open(&path)?;
33mmap[6..11].copy_from_slice(b"sekai");
34mmap.commit()?;
35assert_eq!(std::fs::read_to_string(&path)?, "Hello sekai!");
36# std::fs::remove_file(path)?;
37# Ok(())
38# }
39```
40
41## Safety
42
43The unsafe thing about mmapping a file is that it gives you _volatile memory_:
44when someone modifies the file, the memory changes.  This is not the way a
45respectable `&[u8]` (or even `&mut [u8]`) should behave.
46
47So we use a trick: instead of mapping the file directly, we map a private
48"snapshot" of the file which doesn't change, even when the file is externally
49modified. The *only* way to modify the snapshot is via the mmap, which makes it
50valid according to Rust's rules.
51
52See the SAFETY comments in the code for a more thorough explanation.
53
54## Performance
55
56The cost of safety?
57On my machine, `Mmap::open()` takes just 0.1 ms longer than `File::open()` -
58that's it!
59And it doesn't matter how big the file is.
60A small price to pay.
61
62But there's a catch:
63if the file is on a filesystem which doesn’t support reflinks
64then we have to copy the whole file.
65Therefore, while the semantics are the same on all filesystems,
66the performance characteristics vary wildly.
67
68This table shows whether methods are constant-time or linear-time in
69the size of the file:
70
71Method | XFS | btrfs | ext4 | tmpfs
72-------|-----|-------|------|-------
73[`open()`][`Mmap::open`]                            | O(1) | O(1) | O(n) | O(n)
74[`commit()`][`MmapMut::commit`]                     | O(1) | O(1) | O(n) | O(n)
75[`commit_and_close()`][`MmapMut::commit_and_close`] | O(1) | O(1) | O(1) | O(1)
76
77See the method docs for more details.
78
79If the file is on a reflink-capable filesystem, the overhead is so tiny that
80there's really no reason not to snapshot it.  However, although many distros
81now default to reflink-capable filesystems for new installs[^debian], it will
82obviously be common to encounter ext4 in the wild for many years to come.  So
83be aware that a subset of your users may experience stalls when mmapping large
84files.
85
86[^debian]: The major exceptions are Debian and Ubuntu, which select ext4 by
87    default in the installer.  This is, frankly, a bad decision. From its
88    creation, ext4 was intended as a "stop-gap" to give people more time to
89    migrate away from the ext* family of filesystems.  Encouraging its use on
90    fresh installs is poor.
91
92## Platform support
93
94We make the snapshot by cloning the original file into a private (unlinked) file.
95It's impossible for anyone else to modify this file, which is what makes it safe to mmap.
96On Linux we use `O_TMPFILE` for this.
97I don't know of a race-free way to create an unlinked file on MacOS/Windows;
98if one exists, please open an issue to let me know!
99
100*/
101
102use rustix::{
103    fs::{AtFlags, Mode, OFlags, copy_file_range, ftruncate, ioctl_ficlone, linkat, open, rename},
104    io::Errno,
105    mm::{MapFlags, MremapFlags, MsyncFlags, ProtFlags, mmap, mremap, msync, munmap},
106};
107use std::{
108    ffi::c_void,
109    fs::File,
110    io,
111    ops::{Deref, DerefMut},
112    os::fd::AsFd,
113    path::{Path, PathBuf},
114};
115
116/// Returns whether it fell back
117fn ficlone(fd_out: impl AsFd, fd_in: impl AsFd, len: usize) -> io::Result<bool> {
118    match ioctl_ficlone(&fd_out, &fd_in) {
119        Ok(()) => Ok(false),
120        Err(Errno::OPNOTSUPP) => {
121            ftruncate(&fd_out, len as u64)?;
122            let mut off_in = 0;
123            let mut off_out = 0;
124            while off_in < len as u64 {
125                let rem = len - off_in as usize;
126                let n =
127                    copy_file_range(&fd_in, Some(&mut off_in), &fd_out, Some(&mut off_out), rem)?;
128                assert_eq!(off_in, off_out);
129                assert!(
130                    n <= rem,
131                    "copy_file_range() copied more bytes than requested"
132                );
133                if n == 0 {
134                    Err(io::ErrorKind::UnexpectedEof)?;
135                }
136            }
137            assert_eq!(off_out, len as u64);
138            Ok(true)
139        }
140        Err(e) => Err(e.into()),
141    }
142}
143
144/// A point-in-time snapshot of a file
145///
146/// Read the file contents using the `Deref` impl.  The data you see will
147/// reflect the state of the file at the time `open()` was called; writes by other
148/// process are not reflected.  In other words, `Mmap` will show you a consistent
149/// point-in-time snapshot of the file.
150///
151/// Data is not loaded eagerly into memory.  It will be read in from disk on demand.
152/// For this we rely on the COW capabilities of the underlying filesystem.
153pub struct Mmap {
154    ptr: *mut c_void, // null iff len == 0
155    len: usize,
156}
157
158// SAFETY: `ptr`+`len` are just "plain old memory" (that's the point of the
159// trick with the private unlinked file.)  It can be read from any thread.
160unsafe impl Send for Mmap {}
161// SAFETY: `ptr`+`len` are just "plain old memory" (that's the point of the
162// trick with the private unlinked file.)  It can be read concurrently from
163// multiple threads.
164unsafe impl Sync for Mmap {}
165
166impl Mmap {
167    /// Take a snapshot of the file and map it into memory.
168    ///
169    /// # Performance
170    ///
171    /// If the filesystem _doesn't_ support reflinks (eg. ext4) then this will
172    /// physically duplicate the file on disk.  If the file is large then this
173    /// will be slow and consume disk space.  The duplicate will be deleted when
174    /// the `Mmap` is dropped.
175    ///
176    /// If the filesystem _does_ support reflinks (XFS, btrfs) then we simply
177    /// mark the file as "copy on write" until the `Mmap` is dropped.  This is
178    /// O(1) and fast: on my machine it takes just 0.1 ms longer than a plain
179    /// old `File::open()`.  Disk usage will not increase unless the file is
180    /// externally modified.
181    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
182        let path = path.as_ref();
183        let original = File::open(path)?;
184        let len = original.metadata()?.len() as usize;
185        if len >= isize::MAX as usize {
186            return Err(io::ErrorKind::FileTooLarge.into());
187        }
188        let dir = path.parent().filter(|x| *x != "").unwrap_or(Path::new("."));
189        // Create an unlinked clone of `original`
190        let private: File =
191            open(dir, OFlags::TMPFILE | OFlags::RDWR, Mode::RUSR | Mode::WUSR)?.into();
192        ficlone(&private, &original, len)?;
193
194        let ptr;
195        if len == 0 {
196            ptr = std::ptr::null_mut();
197        } else {
198            // SAFETY:
199            // > If `ptr` is not null, it must be aligned...
200            //
201            // `ptr` is null.
202            //
203            // > If there exist any Rust references referring to the memory region
204            //
205            // We're letting the kernel pick an unused region so there shouldn't be any.
206            //
207            // > or if you subsequently create a Rust reference referring to the
208            // > resulting region,
209            //
210            // We will be doing this.
211            //
212            // > it is your responsibility to ensure that the Rust reference invariants are
213            // > preserved, including ensuring that the memory is not mutated in a way that
214            // > a Rust reference would not expect.
215            //
216            // See the safety comment in the Deref impl.
217            unsafe {
218                ptr = mmap(
219                    std::ptr::null_mut(),
220                    len,
221                    ProtFlags::READ,
222                    MapFlags::SHARED,
223                    &private,
224                    0,
225                )?;
226            }
227        };
228        assert!(ptr.is_null() == (len == 0));
229        // We drop `original` and `private` here, closing both fds.  The mapping
230        // itself keeps `private`'s inode alive.  Unmapping will drop the
231        // linkcount on the inode to zero and destroy it.
232        Ok(Self { ptr, len })
233    }
234}
235
236impl Deref for Mmap {
237    type Target = [u8];
238
239    fn deref(&self) -> &[u8] {
240        if self.len == 0 {
241            &[]
242        } else {
243            // SAFETY:
244            // > `ptr` must be non-null
245            //
246            // We just checked that `len` is non-zero.  This implies that `ptr`
247            // is not null (as per the assert in `open()`).
248            //
249            // > `ptr` must be valid for reads for `len * size_of::<T>()` many bytes
250            // > ...
251            // > The entire memory range of this slice must be contained within a single allocation!
252            //
253            // The whole range comes from a single call to `mmap()` with length
254            // `len`.
255            //
256            // >   * `ptr` must be properly aligned
257            //
258            // The element type is `u8`, so `ptr` is trivially aligned.
259            //
260            // > `ptr` must point to `len` consecutive properly initialized values
261            // > of type `u8`.
262            //
263            // File-backed VMAs count as initialized.  There's no such thing as a
264            // file which contains uninitialised bytes.  (Even sparse regions are
265            // well-defined as containing zeroes.)
266            //
267            // > The memory referenced by the returned slice must not be mutated for
268            // > the duration of lifetime `'a`, except inside an `UnsafeCell`.
269            //
270            // Since we never modify the memory directly, the only way for it to
271            // change is via writes to the underlying file. However, we can be sure
272            // that no such writes will take place. That's because:
273            //
274            // * The file was created with `O_TMPFILE`, which means it's impossible
275            //   to create a new fd for the file via the filesystem.
276            // * We close our fd without ever exposing it, which means it's not
277            //   possible that anyone cloned it.
278            // * Therefore no fds exist referencing the underlying file
279            // * Therefore the memory can only be accessed via the mmap
280            //
281            // > The total size `len * size_of::<T>()` of the slice must be no
282            // > larger than `isize::MAX`
283            //
284            // We check this in open().
285            //
286            // > adding `len * size_of::<T>()` to `ptr` must not "wrap around" the
287            // > address space.
288            //
289            // `mmap()` puts the mapping somewhere where it fits, so
290            // `self.ptr.add(self.len)` will never overflow the address space.
291            unsafe { core::slice::from_raw_parts(self.ptr as *const u8, self.len) }
292        }
293    }
294}
295
296impl Drop for Mmap {
297    fn drop(&mut self) {
298        if self.len != 0 {
299            // SAFETY:
300            // > `ptr` must be aligned to the applicable page size, and the range of memory
301            // > starting at `ptr` and extending for `len` bytes, rounded up to the
302            // > applicable page size, must be valid to mutate with `ptr`'s provenance.
303            //
304            // `self.ptr` comes from an mmap with length `self.len`.
305            //
306            // > And there must be no Rust references referring to that memory.
307            //
308            // The only way to get references to the mapping is via the Deref impl,
309            // which take borrows on the `Mmap`.  Since this method takes `&mut
310            // self`, we know that no such references are live.
311            unsafe {
312                match munmap(self.ptr, self.len) {
313                    Ok(()) => (),
314                    Err(e) => eprintln!("munmap failed: {e}"),
315                }
316            }
317        }
318    }
319}
320
321/// A mutable snapshot of a file
322///
323/// The snapshot can be modified and then atomically committed to disk,
324/// overwriting the contents of file.
325///
326/// ## Reading
327///
328/// Read the file contents using the `Deref` impl.  The data you see will
329/// reflect the state of the file at the time `open()` was called; writes by other
330/// process are not reflected.  In other words, `MmapMut` will show you a consistent
331/// point-in-time snapshot of the file.
332///
333/// Data is not loaded eagerly into memory.  It will be read in from disk on demand.
334/// For this we rely on the COW capabilities of the underlying filesystem.
335///
336/// ## Writing
337///
338/// Modify the contents using the `DerefMut` impl.  Writes will not be visible
339/// to other processes reading the file until you call `commit()`.  Once you
340/// call `commit()`, all your modifications will be atomically visible to other
341/// readers.  If you drop the `MmapMut` without calling `commit()`, your writes
342/// will be lost!
343///
344/// Modifications are written to disk continuously in the background; `commit()`
345/// simply waits for writeback to finish, and then makes the written changes
346/// visible.
347pub struct MmapMut {
348    original: OriginalFile,
349    private: File, // Unlinked; initially a clone of `original`
350    ptr: *mut c_void,
351    len: usize,
352}
353
354enum OriginalFile {
355    /// In this case the file is on a reflink-capable filesystem
356    Fd(File),
357    /// In this case the file is on a reflink-incapable filesystem
358    Path(PathBuf),
359}
360
361// SAFETY:
362// All members of `MmapMut` implement Send except for `ptr`.  `ptr`+`len` are just
363// "plain old memory" (that's the point of the trick with the private unlinked
364// file.)  Accessing it from multiple threads is fine.
365unsafe impl Send for MmapMut {}
366// SAFETY:
367// The mapping (`ptr`+`len`) is fine to read concurrently from multiple threads.
368//
369unsafe impl Sync for MmapMut {}
370
371impl MmapMut {
372    /// Take a snapshot of the file and map it into memory.
373    ///
374    /// Note that changes to the snapshot will be discarded unless you call
375    /// [`MmapMut::commit`].
376    ///
377    /// # Performance
378    ///
379    /// If the filesystem _doesn't_ support reflinks (eg. ext4) then this
380    /// will physically duplicate the file on disk.  If the file is large then
381    /// clearly this will be slow and consume I/O bandwidth.  The duplicate will
382    /// be deleted when the `MmapMut` is dropped.
383    ///
384    /// If the filesystem _does_ support reflinks (XFS, btrfs) then we simply
385    /// mark the file as "copy on write" until the `MmapMut` is dropped.  This is
386    /// O(1) and fast: on my machine it takes just 0.1 ms longer than a plain
387    /// old `File::open()`.  Disk usage will not increase until the file is
388    /// modified.
389    pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
390        let path = path.as_ref();
391        let original = File::options().read(true).write(true).open(path)?;
392        let len = original.metadata()?.len() as usize;
393        if len >= isize::MAX as usize {
394            return Err(io::ErrorKind::FileTooLarge.into());
395        }
396        let dir = path.parent().filter(|x| *x != "").unwrap_or(Path::new("."));
397        let private: File =
398            open(dir, OFlags::TMPFILE | OFlags::RDWR, Mode::RUSR | Mode::WUSR)?.into();
399        let fellback = ficlone(&private, &original, len)?;
400
401        let ptr;
402        if len == 0 {
403            ptr = std::ptr::null_mut();
404        } else {
405            // SAFETY:
406            // > If `ptr` is not null, it must be aligned...
407            //
408            // `ptr` is null.
409            //
410            // > If there exist any Rust references referring to the memory region
411            //
412            // We're letting the kernel pick an unused region so there shouldn't be any.
413            //
414            // > or if you subsequently create a Rust reference referring to the
415            // > resulting region,
416            //
417            // We will be doing this.
418            //
419            // > it is your responsibility to ensure that the Rust reference invariants are
420            // > preserved, including ensuring that the memory is not mutated in a way that
421            // > a Rust reference would not expect.
422            //
423            // See the safety comment in the DerefMut impl.
424            unsafe {
425                ptr = mmap(
426                    std::ptr::null_mut(),
427                    len,
428                    ProtFlags::READ | ProtFlags::WRITE,
429                    MapFlags::SHARED,
430                    &private,
431                    0,
432                )?;
433            }
434        };
435        assert!(ptr.is_null() == (len == 0));
436        Ok(Self {
437            private,
438            ptr,
439            len,
440            original: if fellback {
441                OriginalFile::Path(path.to_owned())
442            } else {
443                OriginalFile::Fd(original)
444            },
445        })
446    }
447
448    /// Create a mapping with zero length.
449    ///
450    /// Use [`MmapMut::resize`] to increase the length.  The file won't be
451    /// created until the first time you call [`MmapMut::commit`].
452    pub fn create(path: impl AsRef<Path>) -> io::Result<Self> {
453        let path = path.as_ref();
454        let original = File::create(path)?;
455        let dir = path.parent().filter(|x| *x != "").unwrap_or(Path::new("."));
456        let private: File =
457            open(dir, OFlags::TMPFILE | OFlags::RDWR, Mode::RUSR | Mode::WUSR)?.into();
458        let ptr = std::ptr::null_mut();
459        let len = 0;
460        // The clone is a no-op of course; we just do this to find out whether
461        // reflinks are available
462        let fellback = matches!(ioctl_ficlone(&private, &original), Err(Errno::OPNOTSUPP));
463        Ok(Self {
464            private,
465            ptr,
466            len,
467            original: if fellback {
468                OriginalFile::Path(path.to_owned())
469            } else {
470                OriginalFile::Fd(original)
471            },
472        })
473    }
474
475    /// Atomically replace the original file with the contents of the snapshot.
476    ///
477    /// You can continue to read/write the mmap after calling `commit()`.
478    ///
479    /// # Performance
480    ///
481    /// It's a similar story to [`MmapMut::open()`]: if the filesystem supports
482    /// reflinks it'll be a fast O(1) (after waiting for writeback to finish);
483    /// otherwise it'll be O(n).
484    ///
485    /// If you're done with the file you can use [`MmapMut::commit_and_close`],
486    /// which is always O(1).
487    pub fn commit(&mut self) -> io::Result<()> {
488        self.sync()?;
489        match &self.original {
490            OriginalFile::Fd(original) => ioctl_ficlone(original, &self.private)?,
491            OriginalFile::Path(path) => {
492                // We can't just copy self.private to self.original, since
493                // this would not be atomic. And we need to keep self.private
494                // unlinked. So we create a new private file, copy over the
495                // contents, and link it.
496                let dir = path.parent().filter(|x| *x != "").unwrap_or(Path::new("."));
497                let private2: File =
498                    open(dir, OFlags::TMPFILE | OFlags::RDWR, Mode::RUSR | Mode::WUSR)?.into();
499                // This is non-atomic but that's fine, since we're holding &mut
500                // self and therefore `self.private` can't receive modifications
501                // while the copy is in-progress
502                ficlone(&private2, &self.private, self.len)?;
503                link(&private2, path)?;
504            }
505        }
506        Ok(())
507    }
508
509    /// Atomically replace the original file with the contents of the snapshot and close it.
510    ///
511    /// Atomic and O(1).
512    pub fn commit_and_close(self) -> io::Result<()> {
513        self.sync()?;
514        match &self.original {
515            OriginalFile::Fd(original) => ioctl_ficlone(original, &self.private)?,
516            OriginalFile::Path(path) => {
517                // `path` is always on the same filesystem as the original file - it
518                // _is_ the original file!  So this is atomic.
519                link(&self.private, path)?;
520            }
521        }
522        Ok(())
523    }
524
525    /// Link this snapshot to the directory tree at the given path.
526    ///
527    /// Atomic and O(1) if `path` is on the same filesystem as the original
528    /// file.
529    pub fn link(self, path: impl AsRef<Path>) -> io::Result<()> {
530        self.sync()?;
531        link(&self.private, path.as_ref())?;
532        Ok(())
533    }
534
535    fn sync(&self) -> io::Result<()> {
536        if self.len != 0 {
537            // SAFETY:
538            // > `addr` must be a valid pointer to memory that is appropriate to call
539            // > `msync` on.
540            //
541            // Given that len is non-zero, `self.ptr` is a pointer which
542            // came from `mmap()`, and `self.len` is the length we passed to
543            // `mmap()`, so together these describe an mmapped region and are
544            // safe to pass to `msync()`.
545            unsafe {
546                msync(self.ptr, self.len, MsyncFlags::SYNC)?;
547            }
548        }
549        Ok(())
550    }
551
552    /// Change the size of the file.  If extending, the extension is filled with zeroes.
553    pub fn resize(&mut self, new_len: usize) -> io::Result<()> {
554        if new_len >= isize::MAX as usize {
555            return Err(io::ErrorKind::FileTooLarge.into());
556        }
557        if new_len == self.len {
558            return Ok(());
559        }
560        ftruncate(&self.private, new_len as u64)?;
561        if new_len == 0 {
562            // SAFETY: See the Drop impl
563            unsafe {
564                munmap(self.ptr, self.len)?;
565            }
566            self.ptr = std::ptr::null_mut();
567        } else if self.len == 0 {
568            // SAFETY: See MmapMut::open()
569            unsafe {
570                self.ptr = mmap(
571                    std::ptr::null_mut(),
572                    new_len,
573                    ProtFlags::READ | ProtFlags::WRITE,
574                    MapFlags::SHARED,
575                    &self.private,
576                    0,
577                )?;
578            }
579        } else {
580            // SAFETY:
581            // > `self.ptr` must be aligned to the applicable page size, and the range of
582            // > memory starting at `self.ptr` and extending for `self.len` bytes,
583            // > rounded up to the applicable page size, must be valid to mutate with
584            // > `self.ptr`'s provenance.
585            //
586            // `self.ptr` comes from an mmap with length `self.len`, so this should
587            // all hold.
588            //
589            // > If `MremapFlags::MAY_MOVE` is set in `flags`,
590            // > there must be no Rust references referring to that the memory.
591            //
592            // This flag is set, so `mremap()` might move the mapping to a
593            // completely new address. The only way to get references to the mapping
594            // is via the Deref/DerefMut impls, which take borrows on the `MmapMut`. Since
595            // this method takes `&mut self`, we know that no such references are
596            // live.
597            //
598            // > If `new_len` is less than `self.len`, than there must be no Rust
599            // > references referring to the memory starting at offset `new_len` and ending
600            // > at `self.len`.
601            //
602            // As per the above, there are no live references at all into the
603            // mapping.
604            unsafe {
605                self.ptr = mremap(self.ptr, self.len, new_len, MremapFlags::MAYMOVE)?;
606            }
607        }
608        self.len = new_len;
609        assert!(self.ptr.is_null() == (self.len == 0));
610        Ok(())
611    }
612}
613
614// Now the most annoying part: you can't just link the file to
615// `path` because it'll fail if `path` already exists (which it
616// does).  So we have to do another little dance, and this one
617// is actually racy :-(
618// TODO: We can avoid all this if/when Linux ever gets `AtFlags::REPLACE`.
619fn link(fd: &File, path: &Path) -> io::Result<()> {
620    let mut tmppath = path.with_added_extension(".tmp");
621    loop {
622        match linkat(fd, "", rustix::fs::CWD, &tmppath, AtFlags::EMPTY_PATH) {
623            Ok(()) => {
624                rename(tmppath, path)?;
625                break; // we did it!
626            }
627            Err(Errno::EXIST) => {
628                tmppath = tmppath.with_added_extension(".tmp");
629                // try again...
630            }
631            Err(e) => Err(e)?,
632        }
633    }
634    Ok(())
635}
636
637impl Deref for MmapMut {
638    type Target = [u8];
639
640    fn deref(&self) -> &[u8] {
641        if self.len == 0 {
642            &[]
643        } else {
644            // SAFETY: See the `DerefMut` impl.
645            unsafe { core::slice::from_raw_parts(self.ptr as *const u8, self.len) }
646        }
647    }
648}
649
650impl DerefMut for MmapMut {
651    fn deref_mut(&mut self) -> &mut [u8] {
652        if self.len == 0 {
653            &mut []
654        } else {
655            // SAFETY:
656            // > `ptr` must be valid for both reads and writes for `len *
657            // >  size_of::<T>()` many bytes ...
658            // > The entire memory range of this slice must be contained within a
659            // >  single allocation!
660            //
661            // The whole range comes from a single call to `mmap()` with length
662            // `len`.
663            //
664            // > `ptr` must be non-null
665            //
666            // So long as len is non-zero, `ptr` is asserted to be non-null wherever
667            // it is modified (`open()` and `resize()`).
668            //
669            // > `ptr` must be properly aligned
670            //
671            // The element type is `u8`, so `ptr` is trivially aligned.
672            //
673            // > `ptr` must point to `len` consecutive properly initialized values
674            // > of type `u8`.
675            //
676            // File-backed VMAs count as initialized.  There's no such thing as a
677            // file which contains uninitialised bytes.  (Even sparse regions are
678            // well-defined as containing zeroes.)
679            //
680            // > The memory referenced by the returned slice must not be accessed
681            // > through any other pointer (not derived from the return value) for
682            // > the duration of lifetime `'a`. Both read and write accesses are
683            // > forbidden.
684            //
685            // This is the big one.  I believe this is satisfied if both of the
686            // following hold true:
687            //
688            // * the only way to mutate the memory is via this `DerefMut` impl
689            // * the only way to read the memory is via this `DerefMut` impl or the
690            //   `Deref` impl
691            //
692            // This memory can be accessed via these impls of course, and also via
693            // operations on the underlying file. However, we can be sure that no
694            // such file operations will take place. That's because:
695            //
696            // * The file was created with `O_TMPFILE`, which means it's impossible
697            //   to create a new fd for the file via the filesystem.
698            // * We never expose our fd, which means it's impossible to create a new
699            //   fd via `clone()`.
700            // * Therefore the _only_ fd referencing the underlying file is
701            //   `self.private`.
702            // * All public methods which access the fd (self.private) take
703            //   `&mut self`.
704            // * Therefore we don't access the file via that fd while `'a` is live.
705            // * Therefore the memory can only be accessed via the mmap
706            //
707            // > The total size `len * size_of::<T>()` of the slice must be no
708            // > larger than `isize::MAX`, and adding that size to `ptr` must not
709            // > "wrap around" the address space. See the safety documentation of
710            // > [`pointer::offset`].
711            //
712            // `mmap()` puts the mapping somewhere where it fits, so
713            // `self.ptr.add(self.len)` will never overflow the address space.
714            // `self.len < isize::MAX` is asserted in open() and resize().
715            unsafe { core::slice::from_raw_parts_mut(self.ptr as *mut u8, self.len) }
716        }
717    }
718}
719
720impl Drop for MmapMut {
721    fn drop(&mut self) {
722        if self.len != 0 {
723            // SAFETY:
724            // > `ptr` must be aligned to the applicable page size, and the range of memory
725            // > starting at `ptr` and extending for `len` bytes, rounded up to the
726            // > applicable page size, must be valid to mutate with `ptr`'s provenance.
727            //
728            // `self.ptr` comes from an mmap with length `self.len`.
729            //
730            // > And there must be no Rust references referring to that memory.
731            //
732            // The only way to get references to the mapping is via the
733            // Deref/DerefMut impls, which take borrows on the `MmapMut`.  Since this
734            // method takes `&mut self`, we know that no such references are live.
735            unsafe {
736                match munmap(self.ptr, self.len) {
737                    Ok(()) => (),
738                    Err(e) => eprintln!("munmap failed: {e}"),
739                }
740            }
741        }
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    // TODO: These could use some improvement.  Ideally I'd mount a bunch
750    // of different filesystems... but that requires root.  Anyway, I should
751    // systematically make sure the files used by different tests don't
752    // conflict, and that they're cleaned up at the end.
753
754    fn paths(name: &str) -> impl Iterator<Item = PathBuf> {
755        ["/tmp", "/var/tmp"].into_iter().map(move |d| {
756            let d = Path::new(d).join("mmap-snapshot");
757            std::fs::create_dir_all(&d).unwrap();
758            d.join(name)
759        })
760    }
761
762    #[test]
763    fn mmap() -> std::io::Result<()> {
764        for p in paths("mmap") {
765            std::fs::write(&p, b"Hello world!")?;
766            let f = Mmap::open(&p)?;
767            std::fs::write(&p, b"Goodbye world!")?;
768            assert_eq!(&*f, b"Hello world!");
769            std::fs::remove_file(&p)?;
770            assert_eq!(&*f, b"Hello world!");
771        }
772        Ok(())
773    }
774
775    #[test]
776    fn mmap_mut() -> std::io::Result<()> {
777        for p in paths("mmap_mut") {
778            std::fs::write(&p, b"Hello world!")?;
779            let mut f = MmapMut::open(&p)?;
780            assert_eq!(&*f, b"Hello world!");
781            f[6..11].copy_from_slice(b"sekai");
782            assert_eq!(&*f, b"Hello sekai!");
783            assert_eq!(std::fs::read_to_string(&p)?, "Hello world!");
784            f.commit()?;
785            std::mem::drop(f);
786            assert_eq!(std::fs::read_to_string(&p)?, "Hello sekai!");
787            std::fs::remove_file(&p)?;
788        }
789        Ok(())
790    }
791
792    #[test]
793    fn zero_len() -> std::io::Result<()> {
794        for p in paths("zero_len") {
795            File::create(&p)?;
796            let f = Mmap::open(&p)?;
797            assert_eq!(&*f, b"");
798            std::fs::remove_file(&p)?;
799            assert_eq!(&*f, b"");
800        }
801        Ok(())
802    }
803
804    #[test]
805    fn zero_len_mut() -> std::io::Result<()> {
806        for p in paths("zero_len_mut") {
807            File::create(&p)?;
808            let mut f = MmapMut::open(&p)?;
809            assert_eq!(&*f, b"");
810            f.resize(12)?;
811            f.copy_from_slice(b"Hello world!");
812            assert_eq!(std::fs::read_to_string(&p)?, "");
813            f.commit()?;
814            assert_eq!(std::fs::read_to_string(&p)?, "Hello world!");
815            f[6..11].copy_from_slice(b"sekai");
816            assert_eq!(&*f, b"Hello sekai!");
817            assert_eq!(std::fs::read_to_string(&p)?, "Hello world!");
818            f.commit()?;
819            std::mem::drop(f);
820            assert_eq!(std::fs::read_to_string(&p)?, "Hello sekai!");
821            std::fs::remove_file(&p)?;
822        }
823        Ok(())
824    }
825}