Haiku POSIX mmap: File Backing, EOF, and Mapping Lifetime
Use Haiku POSIX mmap safely: separate file size from mapping length, coordinate truncation, avoid unsafe MAP_FIXED replacement, and unmap deliberately.
mmap() lets a Haiku process access a file-backed region through ordinary loads and stores. That convenience can obscure two separate lifetimes: the virtual mapping and the file object behind it. Closing the descriptor does not itself remove an established mapping, but another process can still change or truncate the file. A pointer that was valid after mmap() can therefore become unsafe before munmap().
This is not the same API as Haiku’s Kernel Kit create_area() and clone_area(). Areas are explicit kernel objects used for named mappings and cross-team sharing. POSIX mmap() is the familiar file/anonymous mapping interface. Choose the interface for the ownership and sharing problem at hand rather than treating every memory region as interchangeable.
Pick mapping semantics before choosing an address
The reviewed Haiku <sys/mman.h> defines MAP_SHARED, MAP_PRIVATE, MAP_FIXED, MAP_ANONYMOUS, and MAP_NORESERVE. A shared mapping is the mode for updates that are intended to be shared with the underlying object; a private mapping keeps writes made through that mapping private. MAP_PRIVATE is not snapshot isolation: it does not freeze the file against changes or truncation by another writer.
Start with a null address hint unless a real ABI requires placement. MAP_FIXED is materially different: it asks for the requested address rather than offering a hint, and can replace an existing mapping in that range. Haiku’s own VM cut tests exercise this replacement behavior. Use a fixed mapping only inside an address range that the same component has already reserved and whose ownership it controls. The reviewed Haiku header does not define Linux’s MAP_FIXED_NOREPLACE; do not copy code that assumes it is available.
Keep permissions narrow as well. A read-only parser normally needs PROT_READ, not PROT_WRITE or PROT_EXEC. The requested length must be nonzero, size arithmetic must be checked before passing a file size to mmap(), and subrange offsets should be page-aligned for portability. The POSIX specification permits an implementation to require page-aligned offsets. On success, compare the result with MAP_FAILED, not with NULL.
EOF is a mapping boundary, not an allocated-file guarantee
Do not infer accessible bytes from the length argument alone. Obtain the current file size, reject negative or unrepresentable sizes, and handle an empty file without calling mmap() with a zero length. More importantly, fstat() followed by mmap() is not a lock: another process can truncate the file immediately after either operation.
Haiku’s pinned VM regression test makes this hazard concrete. It maps a page from a half-page file, truncates the file to zero, and then deliberately touches the old mapping; the test expects the access to fault with SIGBUS. A read-only application should not try to turn that signal into ordinary EOF handling. Coordinate with writers so the mapped object remains stable, use an immutable replacement-and-rename workflow, or use bounded read()/BFile operations when the producer may resize the file concurrently.
Another Haiku regression test creates a file of two and a half pages, then maps one page at an offset four pages into it. In that fixture, the MAP_PRIVATE mapping call succeeds while the MAP_SHARED call fails. The test does not read from the successful mapping. Treat that as a tested Haiku-specific mapping-creation distinction, not as permission to dereference bytes beyond EOF, extend a file by mapping it, or rely on Linux behavior. Test the exact Haiku release and filesystem you support.
A bounded read-only mapping example
The small program below maps a regular, non-empty file privately, keeps the descriptor alongside the mapping, computes a checksum while the file is expected to remain unchanged, and unmaps the exact returned range. It intentionally avoids MAP_FIXED. The caller must still ensure that another writer will not truncate or mutate the file during the scan; the size check is not a concurrency control mechanism.
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
struct mapped_file {
const unsigned char *data;
size_t length;
int fd;
};
static int
map_readonly_regular_file(const char *path, struct mapped_file *out)
{
int fd = -1;
int savedError;
struct stat info;
void *mapping = MAP_FAILED;
size_t length = 0;
if (path == NULL || out == NULL) {
errno = EINVAL;
return -1;
}
out->data = NULL;
out->length = 0;
out->fd = -1;
fd = open(path, O_RDONLY);
if (fd < 0)
return -1;
if (fstat(fd, &info) != 0)
goto fail;
if (!S_ISREG(info.st_mode) || info.st_size < 0) {
errno = EINVAL;
goto fail;
}
if (info.st_size == 0) {
int closeResult = close(fd);
fd = -1;
return closeResult;
}
if ((uintmax_t)info.st_size > (uintmax_t)SIZE_MAX) {
errno = EOVERFLOW;
goto fail;
}
length = (size_t)info.st_size;
mapping = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapping == MAP_FAILED)
goto fail;
out->data = (const unsigned char *)mapping;
out->length = length;
out->fd = fd;
return 0;
fail:
savedError = errno;
if (fd >= 0)
(void)close(fd);
if (mapping != MAP_FAILED)
(void)munmap(mapping, length);
errno = savedError;
return -1;
}
int
main(int argc, char **argv)
{
struct mapped_file file;
uint64_t checksum = UINT64_C(14695981039346656037);
if (argc != 2) {
fprintf(stderr, "usage: %s FILE\n", argv[0]);
return 2;
}
if (map_readonly_regular_file(argv[1], &file) != 0) {
fprintf(stderr, "cannot map %s: %s\n", argv[1], strerror(errno));
return 1;
}
for (size_t i = 0; i < file.length; i++) {
checksum ^= file.data[i];
checksum *= UINT64_C(1099511628211);
}
printf("bytes=%zu checksum=%" PRIu64 "\n", file.length, checksum);
if (file.data != NULL && munmap((void *)file.data, file.length) != 0) {
fprintf(stderr, "munmap failed: %s\n", strerror(errno));
(void)close(file.fd);
return 1;
}
if (file.fd >= 0 && close(file.fd) != 0) {
fprintf(stderr, "close failed: %s\n", strerror(errno));
return 1;
}
return 0;
}
The example keeps the descriptor open to make ownership visible and keep cleanup simple. Haiku’s mmap_cut_tests.cpp separately demonstrates that a mapping remains usable after its descriptor is closed. Keep the mapping itself alive until every user of its pointer has stopped; do not hand the pointer to deferred work and then unmap it. Also check cleanup paths: munmap() takes the original address and mapped length, and a failed unmap must not be reported as successful release.
Shared writes and durability are separate contracts
MAP_SHARED is useful when writers need mapped updates to reach the backing object, but it does not provide record-level atomicity, mutual exclusion, crash consistency, or an application transaction. A second process can observe an intermediate structure unless the application adds a synchronization and publication protocol. For a persistent index or database page, define checksums, versioning, recovery, and ordering separately.
POSIX msync() gives a caller a way to request synchronization of a mapped range with its backing object. It is not a substitute for a database commit protocol, and it does not by itself prove that a multi-page update is recoverable after power loss. Distinguish three questions in code and operational documentation: whether another mapping can observe a change, whether mapped data has been synchronized with the object, and whether the application can recover a consistent state after interruption.
Test the target OS, not just the happy path
Run mapping tests on the Haiku versions, architectures, and filesystems you actually support. Include an empty file, a non-page-sized file, a negative offset, a file replaced by rename, concurrent truncation, descriptor closure after mapping, MAP_PRIVATE versus MAP_SHARED, and cleanup after each failure. Haiku’s kernel tests are especially useful evidence for its current edge cases, but a regression test is not a promise that every filesystem or future release behaves identically.
Prefer ordinary hints over fixed placement, parse only the file size you validated, and keep the backing object stable for the full period that mapped pointers are used. For mutable or externally controlled files, a bounded streaming reader is often easier to make safe than a mapping. mmap() removes explicit read calls; it does not remove the need for an ownership, size, synchronization, and lifetime contract.
Related:
- Haiku Memory Areas: Shared Mappings, Protection, and Lifetime
- Haiku BFile: Open Modes, Truncation Safety, and Reliable I/O
Sources:
- Haiku
sys/mman.h, reviewed source revision3a93a9f6a03f12e89e7a700f41fc4f696e3709e8 - Haiku VM regression test: invalid and past-EOF mappings, same revision
- Haiku VM regression test: truncation of an active mapping, same revision
- Haiku VM regression tests: fixed mappings and descriptor closure, same revision
- The Open Group Base Specifications Issue 8:
mmap() - The Open Group Base Specifications Issue 8:
msync()