A process can retain a valid virtual memory mapping after another actor has shortened the mapped file. The address range still exists in the process, but the backing object may no longer contain every page that range once represented. On POSIX systems, an access to a whole mapped page beyond the new end can deliver SIGBUS rather than behaving like an ordinary failed file read.

That boundary makes file-backed mmap() different from copying bytes into private heap storage. A pointer into a mapping is not proof that the corresponding file extent still exists. The virtual address, mapping lifetime, file identity, and current file size are related state, but they are not one indivisible object.

Mapping lifetime is separate from descriptor lifetime

A successful mmap() establishes an address-space mapping to the memory object represented by a file descriptor. POSIX specifies that the mapping adds its own reference to the associated file. Closing the descriptor afterward does not remove that mapping.

This sequence is therefore valid:

int fd = open("index.bin", O_RDONLY);
void *p = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, 0);
close(fd);

/* p can still refer to the mapping here. */

Subject to successful calls and the relevant access permissions, close(fd) releases the descriptor but not the mapping. munmap() or process teardown governs the mapping’s address-space lifetime.

That separation is useful because mapped access does not require keeping an otherwise unused descriptor open. It also removes a tempting synchronization assumption: descriptor closure does not freeze the file, preserve its size, or detach the mapping into an independent snapshot.

The pathname is another separate layer. Renaming or unlinking the pathname does not make an established mapping resolve the name again. The mapping continues to refer to the opened object. File replacement through a new inode and in-place mutation of the existing inode therefore have materially different effects on existing mappings.

Shrinking the same object changes the accessible extent

ftruncate() can reduce a regular file to a specified length. When that operation removes whole pages that were previously mapped, POSIX requires those pages to be discarded. A later reference to a discarded page generates SIGBUS.

Consider a process that maps an 8 MiB file and retains the mapping while another process truncates that same file to 1 MiB:

mapping:  [0 -------------------------------- 8 MiB)
file:     [0 ---- 1 MiB)

mapped virtual addresses still exist
backing file pages after the new end do not

The mapping length recorded in the process has not automatically become 1 MiB. Yet accesses across the original range no longer all have the same backing-file validity.

On Linux, mmap(2) documents SIGBUS for an attempted access to a page of a mapped buffer that lies beyond the end of the mapped file. This is a memory-access failure, so an API that exposes mapped bytes as an ordinary pointer can surface a file-size race as a synchronous signal in code that appears to perform only loads.

The exact boundary at the final partial page needs care. POSIX specifies zero filling for the partial page at the end of an object and states that modifications beyond the object end in that page are not written out. Whole pages following the object end have the stronger SIGBUS rule. Code should not turn page-rounding details into an application data contract.

A prior size check does not pin the size

Reading st_size before mapping is useful for choosing a mapping length, but it does not establish an enduring invariant unless some separate protocol prevents incompatible size changes.

A common sequence has two distinct observations:

struct stat st;
fstat(fd, &st);

void *p = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);

The fstat() result describes file metadata at that observation point. The mapping operation follows later. Another actor with suitable access can change the object between those operations or after both have completed.

Even if the initial mapping exactly matches the observed file size, subsequent truncation can invalidate the assumption that every mapped page remains backed. Bounds checks against the original st_size protect against indexing beyond the original mapping; they do not prove that the current file still reaches that offset.

This is a time-of-check versus later-use boundary, but the failure mode is specific to mapped storage. Ordinary pread() against an offset beyond current end of file reports end-of-file through its return value. A load through a mapped pointer has no return channel for that condition. The virtual-memory subsystem reports the invalid backing access through the platform’s memory-fault mechanism.

Atomic pathname replacement avoids mutating existing mappings

Publishing a new file by creating a separate object and atomically replacing a pathname has different mapping semantics from truncating and rewriting the object that current readers have mapped.

With replacement, an existing mapping keeps its reference to the old opened object. A later open() through the pathname can reach the replacement object, while the old mapping continues to expose the prior object for as long as its references keep that object alive.

The topology is roughly:

existing mapping ------> old file object

pathname --------------> new file object
new open --------------> new file object

That property can support immutable-generation designs. A writer constructs a complete new file, publishes it through a namespace replacement, and leaves existing readers attached to the old generation until they release it. The publication operation does not resize the object beneath those readers.

This does not make rename a complete durability or concurrency protocol. Persistence across crashes has separate synchronization requirements, and applications still need a rule for detecting or selecting generations. The relevant point here is narrower: replacing the pathname can preserve the size and contents of the object already referenced by existing mappings, whereas in-place truncation changes that same object.

The distinction also depends on using a genuinely separate file object. Rewriting the same inode through a different pathname or hard link does not create generation isolation. Object identity, not spelling of the pathname, determines whether existing mappings share the mutation target.

MAP_PRIVATE is not a full snapshot boundary

MAP_PRIVATE is sometimes read as if it copied the entire file at mapping time. Its contract is copy-on-write visibility for modifications through the mapping, not eager snapshot creation of every mapped byte.

POSIX explicitly leaves effects unspecified when the size of a mapped file changes after mmap() for references corresponding to added or removed portions. Linux documents the beyond-end SIGBUS behavior for file mappings. Choosing MAP_PRIVATE therefore does not justify treating later file truncation as irrelevant to pages that have not become independent private state.

The useful semantic distinction is between modification visibility and backing-object extent. MAP_PRIVATE prevents mapping writes from being carried through to the underlying object as shared updates. It does not convert the mapping into a byte-for-byte heap copy with a lifetime independent of file-size changes.

Applications that require an immutable byte snapshot need a mechanism that actually provides that property under their operating assumptions. Depending on the system, that can mean copying the bytes, mapping an object whose writers follow an immutable-generation protocol, or using another storage primitive with an explicit snapshot contract.

Signal recovery is a narrow and difficult boundary

Installing a SIGBUS handler can make the failure observable, but it does not automatically make arbitrary mapped access recoverable.

A synchronous fault can occur at many machine instructions generated from apparently simple source expressions. Signal handlers also operate under strict constraints: only async-signal-safe operations are generally suitable inside a POSIX signal handler, and returning to the faulting instruction without changing the condition can trigger the same fault again.

A design that intends to recover from invalidated mappings therefore needs an explicit control-transfer and ownership model rather than a generic handler that logs and resumes. Threads concurrently accessing the same mapping further complicate that model because one thread’s recovery does not by itself establish that other threads have stopped touching invalid pages.

For many file formats, preventing destructive in-place resize is a cleaner boundary than attempting to turn arbitrary mapped loads into recoverable I/O operations. That is an architectural constraint, not a universal prescription: some systems deliberately use fault handling as part of their memory model, but doing so requires signal semantics to be part of the interface contract.

Mapping safety depends on a mutation protocol

A mapped file shared across components has at least four independently changing properties: object identity, object size, mapping lifetime, and application-level format state. Correct access depends on the relationships among them.

If writers may shrink the same object while readers retain mappings, readers must account for invalid backing pages. If writers publish separate immutable objects, existing mappings can remain attached to stable generations while pathname resolution moves forward. If a format supports in-place updates without size reduction, the remaining concurrency questions shift toward visibility, ordering, atomicity, and format consistency rather than discarded pages.

These are different protocols even when every reader uses the same mmap() call.

The central constraint is that address-space validity is not equivalent to backing-object validity. A mapping can remain installed while the file extent it once covered changes underneath it. Treating mapped bytes as durable process memory erases that boundary and turns a storage mutation into a memory fault at the point of access.