File Truncation During Memory-Mapped Access in C
When a file mapped into memory via mmap() is truncated
by another process while a C program attempts to access the affected
address range, the operating system raises a hardware fault that
manifests as a SIGBUS (Bus Error) signal on POSIX-compliant
systems or an EXCEPTION_IN_PAGE_ERROR on Windows. Because
the virtual memory mapping remains valid in the process's page table but
the underlying storage is deallocated, referencing offsets past the new
end-of-file (EOF) results in an immediate crash unless an explicit
signal handler or exception mechanism is implemented.
The Mechanism Behind the Crash
When you create a memory-mapped file using mmap(), the
operating system reserves a range of virtual addresses corresponding to
the file descriptor's contents. However, the OS relies on demand paging;
physical memory pages are mapped dynamically when read or written,
backed by the underlying file system.
If external process B calls ftruncate() or
truncate() to reduce the file's size while process A holds
an active mapping, the following sequence occurs:
- Virtual vs. Backing Mismatch: The virtual address space allocated to process A does not automatically shrink. Process A still sees the original mapped address range.
- Page Fault Generation: When process A attempts to read or write to an address that now corresponds to an offset beyond the new, truncated EOF, the Memory Management Unit (MMU) generates a page fault.
- Kernel Lookup Failure: The kernel's page-fault handler intercepts the fault and inspects the virtual memory area (VMA). It discovers that the requested offset lies beyond the file’s current size on disk.
- Signal Delivery: Because the page cannot be
satisfied by the backing store, the kernel cannot fulfill the read or
write operation. On POSIX systems, the kernel sends a
SIGBUSsignal to the offending thread.
Signal Handling and Recovery
By default, an unhandled SIGBUS terminates the process
and creates a core dump. Attempting to catch and recover from this
condition in C involves several nuances:
- Catching the Signal: You can register a handler
using
sigaction()with theSA_SIGINFOflag. Thesiginfo_tstructure passed to the handler containssi_addr, which identifies the exact memory address that caused the fault. - Returning from the Handler: Simply returning from a
SIGBUShandler is not possible if the fault occurred on a standard read/write instruction. The CPU will re-execute the exact instruction that caused the fault, causing an infinite loop of signal delivery. - Non-Local Jumps: To recover safely, the signal
handler must transfer execution control away from the faulting code
using
siglongjmp()after setting a checkpoint withsigsetjmp().
Mitigating Truncation Conflicts
Handling SIGBUS at runtime is complex and inherently
brittle. Robust applications prevent truncation issues by employing
synchronization strategies:
- File Locking: Use advisory or mandatory locks such
as
fcntl()record locking orflock()before accessing mapped regions to ensure exclusive access or to prevent concurrent truncation while the mapping is active. - Validating File Metadata: Frequently monitor the
file size using
fstat()prior to interacting with edge regions, although this introduces a time-of-check to time-of-use (TOCTOU) race condition if the other process modifies the file concurrently. - Shared Memory Alternatives: If inter-process
communication requires shared buffers that change size, POSIX shared
memory objects (
shm_open) coupled with POSIX mutexes (pthread_mutex_tconfigured withPTHREAD_PROCESS_SHARED) offer more controllable synchronization than mapped dynamic files.