# How do we update a memory-mapped file safely when truncation, writer races, and partial persistence can crash readers?

> Safe mmap updates depend on avoiding live truncation, isolating readers from in-place mutation, and defining a real commit boundary for visibility and durability.

Canonical URL: https://www.devobs.io/articles/qa-ge50-memory-mapped-file-safe-updates/
By: Amara Okafor
Published: 2026-03-07T05:27:04.590Z
Updated: 2026-09-06T08:31:04.426Z
Section: Architecture

Treat a mapped file like a shared on-disk data structure, not a convenient byte array. If readers must not crash or see torn state, do not resize or mutate the live file in place. The safest design is write a new file, flush it, and replace the pathname atomically; the next best option is append-only data with a versioned root or index written last. Use in-place updates only when you can define retryable reader behavior and an explicit commit marker.

## Why is truncation the first thing to ban?

Resizing under active mappings breaks the contract you want. POSIX says that if the mapped file changes size after `mmap()`, access to mapped bytes corresponding to added or removed file portions has unspecified effects ([Open Group mmap specification](https://pubs.opengroup.org/onlinepubs/9699919799/functions/mmap.html)). Linux documents the same family of visibility rules and notes that shared mappings propagate updates to the file, with `msync(2)` needed when you need precise control of writeback timing ([Linux mmap(2)](https://man7.org/linux/man-pages/man2/mmap.2.html)).

On Windows, mapping size must be chosen up front: the documentation says file mapping objects are static in size once created ([Microsoft Creating a File Mapping Object](https://learn.microsoft.com/en-us/windows/win32/memory/creating-a-file-mapping-object)). That makes the decision straightforward: never shrink a file that existing readers may still have mapped, and do not rely on growing an already-published mapping to extend a live structure safely.

## What update pattern gives readers a real safety boundary?

Prefer immutable replacement. Write the next generation to a temporary file, including a header with magic, format version, declared length, and checksum. Flush file contents, then atomically replace the published pathname. Readers reopen, check size with `fstat` or equivalent, map the file, validate the header, and only then switch to the new generation.

That pattern gives a real ordering boundary: payload first, publication second. Readers either use the old complete file or the new complete file.

If full replacement is too expensive, use append-only layout plus a small committed root record. The key is still ordering: write new payload pages first, flush them, then write the new root offset or generation field last. A check-then-write protocol without that final publication boundary does not prevent readers from following half-built pointers.

## What does `msync()` or a view flush actually guarantee?

Visibility is not durability. On Linux, `MAP_SHARED` means updates are shared and carried through to the underlying file, but `mmap(2)` explicitly points you to `msync(2)` for precise control ([Linux mmap(2)](https://man7.org/linux/man-pages/man2/mmap.2.html)). The `msync(2)` page says it flushes changes from the in-core copy back to the filesystem, and without it there is no guarantee changes are written back before `munmap(2)` ([Linux msync(2)](https://man7.org/linux/man-pages/man2/msync.2.html)).

That still does not magically make your format crash-safe. You need both a flush sequence and a recoverable layout. In practice:

- mapped writes make data visible;
- `msync(MS_SYNC)` or the Windows view-flush call pushes dirty mapped pages;
- a file-level flush is still the boundary for durable file state and metadata.

## What should every mapped-file reader validate?

Use this checklist:

- fixed header: magic, schema version, file length, generation;
- bounds-check every offset before dereference;
- commit marker, root pointer, or generation written last;
- checksum for committed region or footer;
- remap only after reopening and re-reading current size;
- startup recovery rejects incomplete generations.

## Follow-up Q&A

**Can I safely patch a few fields in place?**  
Yes, but only if each field update is independently valid or readers can detect an in-progress state and retry using version counters or state flags.

**When should we avoid `mmap` entirely?**  
Skip it when files resize frequently, update visibility must be tightly staged, or crash consistency spans multiple records. Buffered I/O with an explicit commit log is easier to reason about.

Next step: write a one-page reader contract that states exactly what readers may observe during updates—old snapshot, new snapshot, or retry—and then choose replacement, append-only, or in-place format rules to match it.

Reviewed: 2026-09-05.

## Source references

- <https://pubs.opengroup.org/onlinepubs/9699919799/functions/mmap.html>
- <https://man7.org/linux/man-pages/man2/mmap.2.html>
- <https://man7.org/linux/man-pages/man2/msync.2.html>
- <https://learn.microsoft.com/en-us/windows/win32/memory/creating-a-file-mapping-object>
