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}