FreeBSD minherit: Control Which Memory Pages Survive fork()
Use FreeBSD minherit(2) to choose whether forked children share, copy, omit, or zero selected mappings, and test the consequences safely.
fork() normally gives a child a process address space based on its parent. FreeBSD’s minherit(2) lets a program change that rule for a mapped range before the fork: the child can share the range, receive copy-on-write contents, omit it, or receive new zero-filled anonymous pages. This is a narrow process-memory contract, not a general-purpose shared-memory protocol or a sandbox.
The important distinction is between a mapping’s backing and its fork-inheritance policy. mmap() chooses how a process maps an object; minherit() changes how the selected pages appear in a child created by fork(). Confusing these can turn an intended private copy into shared state, unexpectedly discard data in a worker, or leak a mapping into a child that should not have it.
Map inheritance is not the same as MAP_SHARED
MAP_SHARED and MAP_PRIVATE describe a mapping’s relationship to its backing object. minherit() selects how a process’s address-space range is inherited by a child. FreeBSD documents that INHERIT_SHARE can share a range between parent and child even when its original mapping was MAP_PRIVATE; changes are visible between those processes while the original mapped file remains untouched. That is different from MAP_SHARED, where the mapping’s sharing behavior is tied to the backing object.
Most applications do not need to tune this property. If cooperating processes need a stable shared data structure, an explicitly designed shared-memory object with synchronization and a versioned layout is usually clearer. minherit() is useful when a process already has an address range and its fork() child needs a deliberately different inheritance behavior, such as dropping a large cache or receiving a clean scratch region.
Inheritance settings affect children created by fork(). They do not change what exec() does: an executed program replaces the old address space. The call does not rewrite the parent’s bytes merely because the setting changed, although INHERIT_SHARE can make the parent and its later child refer to shared memory. FreeBSD can adjust the characteristic page by page; code should still pass a valid mapped range and test the exact target release.
Select the child’s memory behavior deliberately
The FreeBSD interface uses four inheritance values:
INHERIT_SHAREmakes the selected address space shared between parent and child. Do not mistake it for a lock, atomic update, or durable store; synchronization and crash recovery remain application work.INHERIT_NONEremoves the selected mapping from the child. It is useful only when the child can operate without that address range; dereferencing an address in the omitted range is invalid.INHERIT_COPYgives the child copy-on-write inheritance. There is a consequential caveat: FreeBSD’s manual says the parent’s range also becomes copy-on-write when it forks. If that range was originallyMAP_SHARED, the parent no longer retains that shared-backing behavior after the fork; restoring it requires unmapping and remapping.INHERIT_ZEROgives the child new anonymous pages initialized to zero, while the parent retains its original contents. This was added in FreeBSD 12.0, so confirm availability when supporting older releases.
These options are not interchangeable optimization flags. INHERIT_COPY may preserve initialized data while giving the child private writes; INHERIT_ZERO deliberately discards the child’s view of those prior bytes. INHERIT_NONE removes the range altogether. INHERIT_SHARE makes concurrent access possible but does not define who may write, when data is ready, or how two writers coordinate.
Run a small INHERIT_ZERO probe
The following FreeBSD-only test creates one anonymous private page, puts a marker in it, changes its inheritance to INHERIT_ZERO, and then forks. The child exits successfully only if it sees a zero byte; after waiting for the child, the parent checks that its own marker remains. It exercises the inheritance contract without changing a file or a system-wide setting.
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
int
main(void)
{
long page_size = sysconf(_SC_PAGESIZE);
if (page_size <= 0) {
perror("sysconf(_SC_PAGESIZE)");
return 1;
}
size_t length = (size_t)page_size;
unsigned char *page = mmap(NULL, length, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON, -1, 0);
if (page == MAP_FAILED) {
perror("mmap");
return 1;
}
page[0] = 0x5a;
if (minherit(page, length, INHERIT_ZERO) == -1) {
perror("minherit");
(void)munmap(page, length);
return 1;
}
pid_t child = fork();
if (child == -1) {
perror("fork");
(void)munmap(page, length);
return 1;
}
if (child == 0)
_exit(page[0] == 0 ? 0 : 2);
int status;
pid_t waited;
do {
waited = waitpid(child, &status, 0);
} while (waited == -1 && errno == EINTR);
int ok = waited == child && WIFEXITED(status) &&
WEXITSTATUS(status) == 0 && page[0] == 0x5a;
if (!ok)
fprintf(stderr, "inheritance probe failed\n");
if (munmap(page, length) == -1) {
perror("munmap");
ok = 0;
}
return ok ? 0 : 1;
}
This is a focused regression probe, not a benchmark or proof that every page-size and mapping combination behaves the same way. Compile and run it on each FreeBSD branch and architecture you support. On a non-FreeBSD host, minherit() and its inheritance constants may not exist; a successful build of a different implementation would not validate this FreeBSD-specific result.
Treat INHERIT_COPY as a change to both sides
INHERIT_COPY is easy to misread as “make only the child private.” FreeBSD’s documented behavior is more subtle: the child inherits a copy-on-write range, and the parent range also becomes copy-on-write as part of the fork. If a MAP_SHARED mapping was being used to communicate changes with another process or with the backing file, do not assume that INHERIT_COPY preserves that behavior in the parent after the fork.
Before deploying this setting, identify the full ownership graph: every process that maps the object, every process that can fork, and any external component that expects updates in the backing file. Test parent writes, child writes, and observations through a separately opened mapping or file descriptor. If the original shared mapping must be restored, the manual’s documented recovery is to unmap and map it again; this is a remapping operation with its own synchronization and error paths, not a reversible toggle.
Use INHERIT_SHARE when the intended result is in-memory communication between parent and child, but then define synchronization explicitly. A mutex in a normal private mapping is not automatically a process-shared protocol merely because the mapping’s inheritance changed. Use primitives documented for interprocess sharing, ensure their attributes and lifetime match the mapping, and plan for a process dying while holding a lock. INHERIT_SHARE controls visibility, not consistency.
Validate range and failure handling
Call minherit() only for a range that is currently mapped and whose address and length match the allocation you control. Check the return value: success is zero; failure is -1 with errno set. The manual documents EINVAL for an invalid virtual-address range and EACCES when the requested inheritance flag is not valid for the pages. Do not continue to fork() after a failed call if the child depends on the new policy; that would silently run with the prior inheritance behavior.
Track the original mapping base and length as a pair. Page alignment, partial ranges, and adjacent mappings deserve explicit tests rather than assumptions based on a single allocation. FreeBSD supports page-granular adjustments, but other implementations may apply a larger region granularity. A portability layer should either provide a verified target-specific implementation or reject a requested policy it cannot guarantee.
On cleanup, distinguish child completion from mapping release. waitpid() reports the child status; munmap() removes the caller’s mapping. A child calling _exit() avoids running the parent’s ordinary process-exit handlers in the small probe. Production code should use its normal process lifecycle, but must not let a child return through code that accidentally repeats parent cleanup or unrelated application logic.
Keep the security and portability claims narrow
Inheritance policy can reduce accidental data exposure to a helper process, but it is not a security boundary. The child may still receive file descriptors, credentials, environment values, other mappings, and access through system calls. Review the whole child-launch contract: descriptors, signal state, credentials, working directory, environment, and any sandbox policy. Do not use INHERIT_NONE as a substitute for privilege separation or Capsicum capability mode.
minherit() is a non-POSIX BSD interface documented by FreeBSD, OpenBSD, and NetBSD, but the constants and detailed semantics differ. FreeBSD uses INHERIT_*; OpenBSD and NetBSD use MAP_INHERIT_*. Linux has no drop-in minherit() equivalent; madvise(MADV_DONTFORK) and madvise(MADV_WIPEONFORK) provide narrower analogues, with MADV_WIPEONFORK limited to private anonymous pages. The portable mmap() API does not itself make the four FreeBSD policies portable. Hide calls behind a small operating-system adapter, test behavior rather than just symbol presence, and document the fallback when another platform cannot provide zero-on-fork or omission semantics.
Build a reproducible verification matrix
For each supported FreeBSD release, record freebsd-version -kru, architecture, compiler version, page size, mapping flags, and the exact inheritance operation. In a disposable VM, test one case per mode: shared visibility, copy-on-write behavior including the parent’s view, child omission, and child zero-fill. Include both anonymous mappings and any file-backed mapping used by the application. Check the parent and child independently, then inspect file contents where backing-store behavior matters.
Add negative cases for an invalid address, zero or out-of-range lengths where applicable, and an unsupported flag. Keep tests isolated from production workers because forked children may otherwise inherit live descriptors and application state. Treat a test failure as evidence about the exact kernel and mapping setup; do not “fix” it by replacing one inheritance mode with another without reviewing the data-sharing contract.
The acceptance condition is precise: the parent retains the state it is supposed to own, the child receives only the pages and contents its contract specifies, backing-store behavior is understood, and cleanup is bounded. minherit() can express that fork boundary at page granularity, but it cannot supply the synchronization, isolation, portability, or lifecycle design around it.
Related:
- FreeBSD posix_spawn: Descriptor Actions, Signal State, and Child Completion Contracts
- FreeBSD POSIX Shared Memory and Named Semaphores: Lifecycle Without Races
Sources: