Layer 0 spec — mem family
Technical specification — Version 1.0
Family overview
The air-sys-syscall::mem module exposes the kernel memory-management primitives: mappings (mmap/munmap), protection (mprotect), kernel advice (madvise), in-memory locking (mlock), memfd, and inter-process memory access (process_vm_readv/process_vm_writev).
This is a medium-sized family (13 syscalls) but a critical one: memory management is at the heart of performance and security, particularly for components such as the AirCom data plane (which uses memfd + mmap for zero-copy).
Scope of the family.
Operation categories:
- Mappings:
mmap,munmap,mremap. - Protection:
mprotect. - Kernel advice:
madvise. - Locking:
mlock,mlock2,munlock,mlockall,munlockall. - memfd:
memfd_create,memfd_secret. - Synchronization:
msync. - Inter-process access:
process_vm_readv,process_vm_writev.
Cross-cutting characteristics of the family.
-
RAII
Mappingtype. Mappings returned bymmapare wrapped in aMappingtype that callsmunmapon destruction. No leak of mapped memory is possible. -
Distinction between anonymous and file mappings. The typed API distinguishes the two cases via separate functions (
mmap_anonymous,mmap_file). -
Dynamic page size on ARM64. Raspberry Pi and other ARM64 devices may have different page sizes (4 KB, 16 KB, 64 KB depending on the kernel). The Air wrapper detects the page size at initialization via
sysconf(_SC_PAGESIZE)and exposes it as a runtime constant (notconst). -
Checked alignment. Operations that require page-size alignment are checked up front. No opaque kernel
EINVAL. -
memfd at the heart of AirCom.
memfd_createis the primitive on which the AirCom IPC data plane is built (ADR-001). Detailed spec.
Subsection 1: Mappings
mmap (typed)
Rather than a single wrapper over mmap (which is multiplexed depending on the flags), Air exposes functions typed by use case.
mmap_anonymous
Signature.
#![allow(unused)]
fn main() {
pub fn mmap_anonymous(
length: usize,
prot: ProtectionFlags,
flags: MapFlags,
) -> Result<Mapping, Errno>;
pub struct Mapping {
addr: NonNull<u8>,
length: usize,
// Drop calls munmap automatically
}
impl Mapping {
pub fn as_ptr(&self) -> *const u8;
pub fn as_mut_ptr(&mut self) -> *mut u8;
pub fn len(&self) -> usize;
pub fn as_slice(&self) -> &[u8];
pub unsafe fn as_mut_slice(&mut self) -> &mut [u8];
}
bitflags! {
pub struct ProtectionFlags: i32 {
const NONE = 0;
const READ = 1;
const WRITE = 2;
const EXEC = 4;
}
}
bitflags! {
pub struct MapFlags: i32 {
const SHARED = 0x01;
const PRIVATE = 0x02;
const FIXED = 0x10;
const ANONYMOUS = 0x20;
const GROWSDOWN = 0x0100;
const DENYWRITE = 0x0800;
const EXECUTABLE = 0x1000;
const LOCKED = 0x2000;
const NORESERVE = 0x4000;
const POPULATE = 0x8000;
const NONBLOCK = 0x10000;
const STACK = 0x20000;
const HUGETLB = 0x40000;
const SYNC = 0x80000;
const FIXED_NOREPLACE = 0x100000;
}
}
}
Underlying syscall. mmap (x86_64 no. 9, ARM64 no. 222).
Behavior.
Creates an anonymous mapping (no underlying file). The wrapper adds MAP_ANONYMOUS | MAP_PRIVATE to the flags by default.
len is rounded up to the page size by the kernel. The Air wrapper does not perform the rounding explicitly (the caller can do it if it wants to know the effective size).
Pattern.
#![allow(unused)]
fn main() {
let mapping = mmap_anonymous(
4096,
ProtectionFlags::READ | ProtectionFlags::WRITE,
MapFlags::PRIVATE, // ANONYMOUS implicit
)?;
// Use the mapping
// munmap happens automatically on drop
}
Errors.
EINVAL: invalid size or invalid flags.ENOMEM: no memory available.EACCES: insufficient permissions.
Performance. Highly variable. ~10-100 µs depending on size and system memory.
mmap_file
Signature.
#![allow(unused)]
fn main() {
pub fn mmap_file(
fd: BorrowedFd<'_>,
length: usize,
offset: u64,
prot: ProtectionFlags,
flags: MapFlags,
) -> Result<Mapping, Errno>;
}
Behavior.
Creates a file-backed mapping. offset must be page-size aligned.
Typical use case.
#![allow(unused)]
fn main() {
let fd = openat2(DirFd::Cwd, c"./data.bin", OpenHow::read_only())?;
let stat = statx(DirFd::Fd(fd.as_fd()), c"", StatxFlags::EMPTY_PATH, StatxMask::SIZE)?;
let mapping = mmap_file(
fd.as_fd(),
stat.size as usize,
0,
ProtectionFlags::READ,
MapFlags::PRIVATE,
)?;
let data: &[u8] = mapping.as_slice();
}
mmap_fixed
Signature.
#![allow(unused)]
fn main() {
pub unsafe fn mmap_fixed(
addr: NonNull<u8>,
length: usize,
prot: ProtectionFlags,
flags: MapFlags,
fd: Option<BorrowedFd<'_>>,
offset: u64,
) -> Result<MappingPointer, Errno>;
}
Behavior.
Maps at a specific address. unsafe because it can overwrite existing mappings if MAP_FIXED_NOREPLACE is not used.
Rare and advanced use case: runtime implementation, custom allocator, JIT.
munmap
Signature.
#![allow(unused)]
fn main() {
pub fn munmap(mapping: Mapping) -> Result<(), Errno>;
}
Behavior.
Explicitly unmaps. Consumes the Mapping. The automatic Drop also calls munmap (ignoring the error); the explicit function lets you recover a possible error.
mremap
Signature.
#![allow(unused)]
fn main() {
pub fn mremap(
mapping: Mapping,
new_length: usize,
flags: MremapFlags,
) -> Result<Mapping, Errno>;
bitflags! {
pub struct MremapFlags: i32 {
const MAYMOVE = 1;
const FIXED = 2;
const DONTUNMAP = 4;
}
}
}
Behavior.
Resizes a mapping. With MAYMOVE, the kernel may relocate the mapping in memory if in-place resizing is impossible.
Subsection 2: Protection
mprotect
Signature.
#![allow(unused)]
fn main() {
pub fn mprotect(
addr: NonNull<u8>,
length: usize,
prot: ProtectionFlags,
) -> Result<(), Errno>;
}
Behavior.
Changes the permissions of a mapped memory range. Typical pattern: allocate as READ+WRITE, initialize, then switch to READ only for constant data.
Errors.
EINVAL: invalid or unaligned range.EACCES: attempt to gain a forbidden permission (typically EXEC on memory mapped from a no-exec FS).ENOMEM: insufficient kernel resources.
Subsection 3: Kernel advice
madvise
Signature.
#![allow(unused)]
fn main() {
pub fn madvise(
addr: NonNull<u8>,
length: usize,
advice: MadviseAdvice,
) -> Result<(), Errno>;
#[derive(Debug, Clone, Copy)]
pub enum MadviseAdvice {
Normal,
Random,
Sequential,
WillNeed,
DontNeed,
Free,
Remove,
DontFork,
DoFork,
Mergeable,
Unmergeable,
HugePage,
NoHugePage,
DontDump,
DoDump,
WipeOnFork,
KeepOnFork,
Cold,
PageOut,
PopulateRead,
PopulateWrite,
DontNeedLocked,
Collapse,
}
}
Behavior.
Informs the kernel about the expected access pattern for a memory range. The kernel can then optimize (prefetch, swapping, transparent huge pages, etc.).
Common cases:
Sequential+WillNeedfor intensive sequential reads.Randomfor random access (avoids counterproductive prefetch).DontNeedto free pages that will no longer be needed without unmapping.
Subsection 4: Locking
mlock, mlock2, munlock, mlockall, munlockall
#![allow(unused)]
fn main() {
pub fn mlock(addr: NonNull<u8>, length: usize) -> Result<(), Errno>;
pub fn mlock2(addr: NonNull<u8>, length: usize, flags: MlockFlags) -> Result<(), Errno>;
pub fn munlock(addr: NonNull<u8>, length: usize) -> Result<(), Errno>;
pub fn mlockall(flags: MlockallFlags) -> Result<(), Errno>;
pub fn munlockall() -> Result<(), Errno>;
bitflags! {
pub struct MlockFlags: u32 {
const ONFAULT = 1;
}
}
bitflags! {
pub struct MlockallFlags: i32 {
const CURRENT = 1;
const FUTURE = 2;
const ONFAULT = 4;
}
}
}
Behavior.
Locks memory pages to prevent them from being swapped out. Use cases: security (sensitive data that must not end up on the swap disk), real time (predictable latencies).
Limits: RLIMIT_MEMLOCK bounds what an unprivileged process can lock.
Subsection 5: memfd
memfd_create
Signature.
#![allow(unused)]
fn main() {
pub fn memfd_create(name: &CStr, flags: MemfdFlags) -> Result<OwnedFd, Errno>;
bitflags! {
pub struct MemfdFlags: u32 {
const CLOEXEC = 1;
const ALLOW_SEALING = 2;
const HUGETLB = 4;
const NOEXEC_SEAL = 8;
const EXEC = 0x10;
}
}
}
Underlying syscall. memfd_create (x86_64 no. 319, ARM64 no. 279). Available since Linux 3.17.
Behavior.
Creates an FD to an anonymous memory region (a “memory file”). The FD can be:
- Extended with
ftruncate. - Mmapped for sharing between processes.
- Passed between processes via SCM_RIGHTS over a Unix socket.
- Sealed with
fcntl(F_ADD_SEALS)to forbid certain operations.
This is the central primitive for modern shared memory on Linux. At the heart of the AirCom data plane (ADR-001).
Pattern: sharing data between processes.
#![allow(unused)]
fn main() {
// Process A: create a memfd, fill it, share it
let fd = memfd_create(c"shared-data", MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING)?;
ftruncate(fd.as_fd(), 4096)?;
let mapping = mmap_file(fd.as_fd(), 4096, 0, ProtectionFlags::READ | ProtectionFlags::WRITE, MapFlags::SHARED)?;
// ... fill the mapping ...
// Seal to forbid further modifications
add_seals(fd.as_fd(), Seals::SEAL_WRITE | Seals::SEAL_SHRINK | Seals::SEAL_GROW)?;
// Send the FD over a Unix socket
sendmsg(socket, &[fd.as_fd()], ...)?;
// Process B: receive the FD, mmap it for reading
let received_fd: OwnedFd = recvmsg(socket, ...)?;
let mapping = mmap_file(received_fd.as_fd(), 4096, 0, ProtectionFlags::READ, MapFlags::SHARED)?;
// Read the shared data
}
memfd_secret
Signature.
#![allow(unused)]
fn main() {
pub fn memfd_secret(flags: u32) -> Result<OwnedFd, Errno>;
}
Underlying syscall. memfd_secret (x86_64 no. 447, ARM64 no. 447). Available since Linux 5.14.
Behavior.
A variant of memfd_create that creates a memory region not accessible to the kernel after the mapping. Used for high-value cryptographic secrets.
Subsection 6: Synchronization
msync
#![allow(unused)]
fn main() {
pub fn msync(
addr: NonNull<u8>,
length: usize,
flags: MsyncFlags,
) -> Result<(), Errno>;
bitflags! {
pub struct MsyncFlags: i32 {
const ASYNC = 1;
const SYNC = 4;
const INVALIDATE = 2;
}
}
}
Behavior.
Forces synchronization of a mapping’s modifications to the underlying file. For MAP_SHARED file-backed mappings.
Subsection 7: Inter-process access
process_vm_readv, process_vm_writev
#![allow(unused)]
fn main() {
pub fn process_vm_readv(
pid: Pid,
local_iov: &mut [IoSliceMut<'_>],
remote_iov: &[RemoteIoSlice],
flags: u32,
) -> Result<usize, Errno>;
pub fn process_vm_writev(
pid: Pid,
local_iov: &[IoSlice<'_>],
remote_iov: &[RemoteIoSlice],
flags: u32,
) -> Result<usize, Errno>;
pub struct RemoteIoSlice {
pub addr: u64,
pub len: usize,
}
}
Behavior.
Allows reading/writing the memory of another process without ptrace. Requires permissions (typically CAP_SYS_PTRACE or the same user as the target).
Specialized use case: debuggers, monitoring tools, buffer transfer between cooperating processes without copying.
mem family summary
Exposed functions:
| Category | Main functions |
|---|---|
| Mappings | mmap_anonymous, mmap_file, mmap_fixed, munmap, mremap |
| Protection | mprotect |
| Advice | madvise |
| Locking | mlock, mlock2, munlock, mlockall, munlockall |
| memfd | memfd_create, memfd_secret |
| Sync | msync |
| Inter-process | process_vm_readv, process_vm_writev |
Total: 13 wrapped syscalls.
Non-wrapped syscalls (listed in UNSUPPORTED.md):
brk,sbrk: legacy heap management, handled by the allocator, not exposed.pkey_alloc,pkey_free,pkey_mprotect: Memory Protection Keys, marginal in practice.mbind,migrate_pages,move_pages: NUMA-specific, to be evaluated later if Air needs fine-grained NUMA support.set_mempolicy,get_mempolicy: likewise NUMA.
Distribution of types between the two crates
Types in air-sys-types (pure, no syscall)
MappingPointer— pointer + length of a mapping, without ownership semantics (frees nothing on drop). This is the return type ofmmap_fixed, where the caller manages the lifecycle itself.ProtectionFlags,MapFlags,MremapFlagsMadviseAdviceMlockFlags,MlockallFlagsMemfdFlagsMsyncFlagsRemoteIoSlice
RAII type in air-sys-syscall::mem (and not air-sys-types)
Mapping— see core decision no. 2 below.
Architecture decision (Q1, validated on 2026-05-31). The initial drafting of this spec placed
Mappinginair-sys-types. This is incorrect: theDropofMappingmust callmunmap, which is a syscall. Butair-sys-typescontains only pure types and never calls a syscall (onlyair-sys-syscalldoes, viacore::arch::asm!). MakingMappinglive inair-sys-typeswould force that crate to embedasm!, breaking the separation of responsibilities between the two layer 0 crates. As a result,Mapping(a RAII type whoseDropcalls a syscall) is defined inair-sys-syscall::mem, alongside the functions that produce it (mmap_anonymous,mmap_file,mremap). Only the types without a syscall (MappingPointerand thebitflags/enums above) remain inair-sys-types. The same rule applies toLandlockRuleseton thesecurityfamily side (cf.family-security.md).
Core decisions that emerged in the mem family
1. mmap split into typed functions.
Rather than a single wrapper that would take all the flags and arguments, splitting into mmap_anonymous, mmap_file, mmap_fixed. Clearer for the developer, fewer errors (forgetting MAP_ANONYMOUS when there is no FD, for example).
2. Strict RAII Mapping — and defined in air-sys-syscall::mem.
A Mapping cannot be copied, only moved. The automatic munmap on
drop avoids leaks. For cases where you want a “long-lived” mapping that survives
several scopes, you move ownership explicitly.
Because its Drop calls the munmap syscall, Mapping lives in
air-sys-syscall::mem, not in air-sys-types (cf. the “Distribution of
types” section above). General layer 0 rule: a RAII type whose
destruction triggers a syscall lives in the wrappers crate (air-sys-syscall),
never in the pure-types crate (air-sys-types). The MappingPointer type
(bare pointer, without ownership, returned by mmap_fixed) on the other hand stays in
air-sys-types since it frees nothing.
3. Page size detected at runtime.
On Raspberry Pi 4, the page size can be 4 KB or 16 KB depending on the kernel configuration. Air detects it at init and exposes it as a static. No compile-time constant.
4. memfd_create central to AirCom.
In accordance with ADR-001, the AirCom data plane uses memfd + mmap shared. The memfd spec here is deliberately detailed because it determines IPC performance.
5. No Air allocator in layer 0.
Air does not expose a custom allocator at the syscall level. The system global allocator (often jemalloc or mimalloc) is used by default. The allocator can be configured at the application level, outside the layer 0 scope.
Document license: MPL 2.0
Status: Technical specification of the air-sys-syscall::mem module (layer 0).