Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Mappings: mmap, munmap, mremap.
  2. Protection: mprotect.
  3. Kernel advice: madvise.
  4. Locking: mlock, mlock2, munlock, mlockall, munlockall.
  5. memfd: memfd_create, memfd_secret.
  6. Synchronization: msync.
  7. Inter-process access: process_vm_readv, process_vm_writev.

Cross-cutting characteristics of the family.

  1. RAII Mapping type. Mappings returned by mmap are wrapped in a Mapping type that calls munmap on destruction. No leak of mapped memory is possible.

  2. Distinction between anonymous and file mappings. The typed API distinguishes the two cases via separate functions (mmap_anonymous, mmap_file).

  3. 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 (not const).

  4. Checked alignment. Operations that require page-size alignment are checked up front. No opaque kernel EINVAL.

  5. memfd at the heart of AirCom. memfd_create is 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 + WillNeed for intensive sequential reads.
  • Random for random access (avoids counterproductive prefetch).
  • DontNeed to 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:

CategoryMain functions
Mappingsmmap_anonymous, mmap_file, mmap_fixed, munmap, mremap
Protectionmprotect
Advicemadvise
Lockingmlock, mlock2, munlock, mlockall, munlockall
memfdmemfd_create, memfd_secret
Syncmsync
Inter-processprocess_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 of mmap_fixed, where the caller manages the lifecycle itself.
  • ProtectionFlags, MapFlags, MremapFlags
  • MadviseAdvice
  • MlockFlags, MlockallFlags
  • MemfdFlags
  • MsyncFlags
  • RemoteIoSlice

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 Mapping in air-sys-types. This is incorrect: the Drop of Mapping must call munmap, which is a syscall. But air-sys-types contains only pure types and never calls a syscall (only air-sys-syscall does, via core::arch::asm!). Making Mapping live in air-sys-types would force that crate to embed asm!, breaking the separation of responsibilities between the two layer 0 crates. As a result, Mapping (a RAII type whose Drop calls a syscall) is defined in air-sys-syscall::mem, alongside the functions that produce it (mmap_anonymous, mmap_file, mremap). Only the types without a syscall (MappingPointer and the bitflags/enums above) remain in air-sys-types. The same rule applies to LandlockRuleset on the security family 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).