Skip to content

Research notes / File I/O

Memory-mapped files: visibility, size and synchronization

Published / Measured API behavior

A mapped address, a visible byte and a durable record are different promises. In seven Linux checks repeated five times, a private edit stayed private after a successful sync, while shrinking a mapped file made an address inside the original mapping fault.

Seven checks, three independent boundaries

Each file started with byte A. The harness used ordinary file-backed mappings with a 4,096-byte system page. All 5 repetitions produced identical observations. These are deterministic API examples, not 35 independent samples from a storage fleet.

Observed on Linux 6.8.0-146-generic; all byte reads could use cache.
CaseOperationObserved resultWhat it establishes
Private modificationStore P, then MS_SYNCMapping P; file ASuccessful sync did not publish the private edit.
Shared modificationStore S; read before and after MS_SYNCS before; S afterA matching read did not require an explicit sync first.
Descriptor closureClose fd; read the existing mappingAThe mapping outlived its original descriptor.
Larger mappingMap twice the existing file length8192 mapped bytes; 4096 file bytesThe larger mapping did not extend the file.
Partial final pageStore T at EOF within the last page; MS_SYNCTail byte 84 before sync; read at EOF returns 0 bytesThe accessible tail did not become file content.
Unaligned syncMS_SYNC from mapping start + 1-1 / EINVALAn argument error is not evidence of media failure.
Mapped-file shrinkShrink two pages to one; touch page twoSIGBUSThe child accessed a discarded whole page.

Both private and shared msync(MS_SYNC) calls returned zero. The return value cannot be interpreted without the mapping mode. The shared before-sync read establishes visibility in this test; it tells us neither that storage had persisted the byte nor that it had not.

Private edits are not deferred shared writes

MAP_SHARED permits updates to the backing file. MAP_PRIVATE uses copy-on-write for local modifications; it is not a promise that later external file changes are invisible. Closing the original descriptor does not invalidate either mapping. [1]

The private case deliberately invokes synchronization after its edit. The unchanged file byte is therefore not an omitted-sync bug in the experiment. It demonstrates a different update contract. A memory-mapped database's transaction semantics must come from its own protocol, not the mapping flag alone. Compare the LMDB reader-lifetime experiment.

A valid mapping is not a promise of valid file bytes

Mapping length does not enlarge the underlying object. POSIX distinguishes the unused tail of the last partial page from whole pages outside the object; the latter can raise SIGBUS. Do not use apparent accessibility beyond the file end as a file-growth API. Bytes in the unused final-page tail are initially zero-filled; modifications beyond EOF are not written to the file. Linux documents page-cache caveats for that tail. The experiment below checks a fresh partial page, not the remapping caveat. [2] [1]

ftruncate() changes file length without moving the descriptor's file offset. An extension reads as zeros on supporting Linux filesystems. [5] For a shrink, POSIX.1-2017 specifically requires previously mapped whole pages beyond the new end to be discarded, with later accesses generating SIGBUS. [6]

Our child touched offset 4,096 after a file of 8,192 bytes was shortened to 4,096. That is the first byte of a discarded whole page, not a partial-page edge case. The parent checked the child's terminating signal, so an arbitrary nonzero exit could not pass.

Engineering implication: a reader needs a lifetime and resizing contract for the object it maps. A size check followed by an uncoordinated access can race with a writer. This experiment had no such race: shrinking finished before the child was created.

The last byte of a page need not belong to the file

For the added case, the file is 4,095 bytes long but the mapping covers 4,096 bytes. The byte at offset equal to the file length initially reads as 0. Storing T there is observable in the mapping before synchronization. After MS_SYNC returns zero, the file has not grown and a one-byte pread() at that offset returns 0 bytes: end of file.

This is a partial-page tail, unlike the discarded whole page in the SIGBUS case. Linux documents that tail modifications are not written to the file, and warns that later mappings can sometimes still see cached tail data. [1, Notes and Bugs] We did not test later remapping or file growth. The operational lesson is narrower: an accessible address and a successful sync do not turn an out-of-file store into an append.

Synchronization is neither isolation nor a crash test

MS_SYNC waits for synchronization. On Linux, MS_ASYNC has been a no-op since 2.6.19; unmapping is not a substitute completion barrier. [3] POSIX also permits background writeback before a caller requests synchronization. A sync call therefore cannot provide transaction isolation or an all-or-nothing multi-page commit. [4]

The harness neither interrupted power nor evicted caches. It cannot validate device flush honesty, recovery ordering, directory persistence or hardware protection. Those questions require different tests. See writeback errors and synchronization and durable pathname publication.

Ordinary file mapping is also distinct from DAX, which can bypass the page cache for CPU-addressable storage. The kernel documents media-error handling for that path, including SIGBUS; a signal alone is not a unique diagnosis of truncation. No DAX device was used here. [7]

An invalid sync request is not a failed storage write

The argument-error case calls msync() one byte past the page-aligned mapping start. It records -1 and checks the saved error specifically for EINVAL; any other failure fails the harness. Linux requires a page-aligned starting address. [3] This checks an API precondition, not the device's health or persistence.

Rebello et al.'s ATC 2020 study makes a useful methodological distinction: its filesystem experiments exclude input-validation failures and inject transient sector or block write faults after synchronization begins. The authors tested ext4, XFS and Btrfs on Linux 5.2.11, then used CuttleFS for application-level cases. Their cache-sensitive recovery findings concern that fault model, not every nonzero return from a synchronization API. [8, sections 3-5] Our checks do not reproduce those faults.

Reproduce and inspect the evidence

Download the C harness and Python runner into one directory, then run:

python3 run-mmap-boundaries.py --repetitions 5 > observations.json

The runner needs Linux, Python 3, a C compiler and GNU stat. It creates a private temporary directory, immediately unlinks each test file, checks every API result and removes temporary artifacts. The deliberately faulting child disables core dumps and restores the default SIGBUS disposition. No existing file or block device is opened.

Use --temp-dir /path/to/disposable/test-area to choose the filesystem. The published run used cc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0; GNU stat -f reported ext2/ext3. That filesystem-magic label does not distinguish ext-family versions or expose mount options. This is one host, not a comparison of filesystems.

The raw JSON retains every repetition, compiler flags, kernel version, page size and SHA-256 hashes of both source files. The displayed table is generated from the same observations. No random seed is needed: the workload is deterministic.

Excluded cases include concurrent writers, huge pages, partial-tail remapping, file-growth remapping, performance measurement and recovery after failure. Repetition checks consistency of this run, not a universal probability.

Corpus: memory-mapped file, file truncation, and msync.

Primary sources

  1. Linux man-pages: mmap(2), mapping modes and lifetime
  2. POSIX.1-2017: mmap(), file extent and page boundaries
  3. Linux man-pages: msync(2), completion flags
  4. POSIX.1-2017: msync(), private mappings and background writeback
  5. Linux man-pages: truncate(2), file length and offset
  6. POSIX.1-2017: ftruncate(), discarded whole mapped pages
  7. Linux kernel documentation: Direct Access for files
  8. Rebello et al., Can Applications Recover from fsync Failures? USENIX ATC 2020, sections 3-5

Checked 17 October 2026. POSIX extracts cited here are explicitly the 2017 edition.