FreeBSD MAP_GUARD: Reserve Address Space Without Accidental Replacement
Use FreeBSD MAP_GUARD for explicit virtual-address reservations, then replace only ranges your process owns and release them with munmap.
An mmap(2) address is usually a hint: the kernel may choose a different range, and the returned pointer is the address your program must use. FreeBSD also has MAP_GUARD, a way to reserve a range in the process address space without creating an ordinary accessible mapping there. A later mmap can deliberately replace an owned part of that reservation. This is useful when a program needs a known window for mappings it will create later, but it makes the replacement rules important: MAP_FIXED can destroy an existing mapping if the address range is not truly yours.
The details below follow the FreeBSD 15.1 mmap(2) manual. MAP_GUARD is a FreeBSD interface; do not copy this flag into portable POSIX code and assume another system implements the same reservation contract.
Reserve first, map later
The essential MAP_GUARD argument combination is deliberately small: PROT_NONE, MAP_GUARD, descriptor -1, and offset 0. Unlike MAP_ANON, MAP_PRIVATE, or MAP_STACK, MAP_GUARD is not combined with an ordinary mapping type. The kernel returns the chosen base address; save that address and the exact nonzero length as one owned range.
/* Assume length is a nonzero page-multiple selected by the caller. */
void *guard = mmap(NULL, length, PROT_NONE, MAP_GUARD, -1, 0);
if (guard == MAP_FAILED) {
perror("mmap MAP_GUARD");
return 1;
}
The guard is an address-space reservation, not a writable buffer. FreeBSD documents that an access to the guarded range sends SIGSEGV to the accessing thread. Do not test the reservation by dereferencing it; retain the pointer and length as bookkeeping instead. To release an unused reservation, call munmap(guard, length). Closing an unrelated descriptor is not the cleanup operation for this range.
When the application is ready to put a real mapping at that address, it must opt into replacing the guard. For example, a component that owns the entire reserved range could replace it with anonymous read/write memory:
void *region = mmap(guard, length, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
if (region == MAP_FAILED) {
perror("mmap replacement");
/* Resolve the failure before retrying or releasing the owned range. */
return 1;
}
/* region is the requested address when this MAP_FIXED call succeeds. */
if (munmap(region, length) == -1) {
perror("munmap mapped region");
return 1;
}
This example intentionally consumes the full reservation. A staged allocator can replace smaller page-aligned subranges, but it must track which pages are still guards and which are now ordinary mappings. Keep mapping lengths within the reservation and serialize changes with any other code that can map or unmap in the same process. The manual states that mmap will not create mappings in a guard’s range unless the request specifies MAP_FIXED; that flag is the opt-in to replace, not a safe address-discovery mechanism.
MAP_FIXED is destructive; MAP_EXCL changes the contract
With MAP_FIXED, FreeBSD does not move the request to a nearby address. The address must be page-aligned, and a successful request replaces previous mappings in the requested range. That behavior is appropriate when replacing a reservation the same component owns. It is dangerous when an address is guessed, stale, or owned by another subsystem: success can remove that subsystem’s mapping.
For a fixed-address request that must fail rather than replace an existing mapping, add MAP_EXCL with MAP_FIXED. The FreeBSD manual documents that combination as a collision check: if a mapping already exists in the requested range, the call fails. Do not add MAP_EXCL mechanically to the replacement example above; replacement of a range already reserved by your component is intentionally different from claiming an arbitrary occupied address. Use one policy per operation: “reserve this free exact range or fail,” or “replace this range I own.”
If the application does not need an exact address chosen in advance, omit MAP_FIXED and let mmap(NULL, ...) choose the location. The returned base is authoritative. If a fixed address is required, establish ownership before any replacement and keep the base/length pair synchronized with the mapping table; MAP_FIXED cannot tell whether an existing range belongs to your code or another library.
Keep guards distinct from other memory controls
PROT_NONE describes access permissions for a mapping, while MAP_GUARD creates a reservation instead of an ordinary mapping. Both make access fail, but the documented lifecycle differs: a guard blocks later mappings into its range unless they explicitly use MAP_FIXED, and munmap destroys the guard. This is why a guard can mark a future layout slot rather than merely leaving a mapped page inaccessible.
Do not substitute MAP_STACK when you need a fixed reservation window. On FreeBSD, MAP_STACK creates a downward-growing anonymous region together with an adjoining guard; the mapped part grows into the guard on access, subject to security.bsd.stack_guard_page. That is a stack-growth contract, not a general arena allocator. The current manual also rejects invalid MAP_GUARD combinations with EINVAL: its protection must be exactly PROT_NONE, the descriptor must be -1, the offset must be zero, and it cannot be combined with MAP_ANON, MAP_PREFAULT, MAP_PREFAULT_READ, MAP_PRIVATE, MAP_SHARED, or MAP_STACK.
The practical diagnostic path is therefore short: if creation returns MAP_FAILED, inspect errno and verify the special guard argument combination; if later mapping fails, do not assume the old range was replaced; if a process faults at the address, remember that SIGSEGV is the guard’s documented response to access. Log the requested range, the returned base, each successful replacement, and each munmap so a failed transition can be distinguished from an accidental address collision.
Related:
- FreeBSD minherit: Control Which Memory Pages Survive fork()
- FreeBSD POSIX Shared Memory and Named Semaphores: Lifecycle Without Races
Sources: