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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 SIGBUS signal 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 the SA_SIGINFO flag. The siginfo_t structure passed to the handler contains si_addr, which identifies the exact memory address that caused the fault.
  • Returning from the Handler: Simply returning from a SIGBUS handler 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 with sigsetjmp().

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 or flock() 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_t configured with PTHREAD_PROCESS_SHARED) offer more controllable synchronization than mapped dynamic files.