Spec couche 0 — Famille mem
Spécification technique — Version 1.0
Vue d’ensemble de la famille
Le module air-sys-syscall::mem expose les primitives kernel de gestion mémoire : mappings (mmap/munmap), protection (mprotect), conseils kernel (madvise), verrouillage en mémoire (mlock), memfd, et accès mémoire inter-processus (process_vm_readv/process_vm_writev).
C’est une famille moyenne (13 syscalls) mais critique : la gestion mémoire est au cœur de la performance et de la sécurité, particulièrement pour des composants comme le data plane de AirCom (qui utilise memfd + mmap pour le zero-copy).
Périmètre de la famille.
Catégories d’opérations :
- Mappings :
mmap,munmap,mremap. - Protection :
mprotect. - Conseils kernel :
madvise. - Verrouillage :
mlock,mlock2,munlock,mlockall,munlockall. - memfd :
memfd_create,memfd_secret. - Synchronisation :
msync. - Accès inter-processus :
process_vm_readv,process_vm_writev.
Caractéristiques transverses de la famille.
-
Type
MappingRAII. Les mappings retournés parmmapsont encapsulés dans un typeMappingqui appellemunmapà la destruction. Pas de fuite de mémoire mappée possible. -
Distinction entre mappings anonymes et fichiers. L’API typée distingue les deux cas via des fonctions séparées (
mmap_anonymous,mmap_file). -
Page size dynamique sur ARM64. Les Raspberry Pi et autres ARM64 peuvent avoir des page sizes différentes (4 KB, 16 KB, 64 KB selon kernel). Le wrapper Air détecte la page size à l’initialisation via
sysconf(_SC_PAGESIZE)et l’expose comme constante runtime (pasconst). -
Alignement vérifié. Les opérations qui demandent un alignement sur page size sont vérifiées en amont. Pas d’
EINVALopaque kernel. -
memfd au cœur de AirCom.
memfd_createest la primitive sur laquelle est construit le data plane de l’IPC AirCom (ADR-001). Spec détaillée.
Sous-section 1 : Mappings
mmap (typé)
Plutôt qu’un wrapper unique sur mmap (qui est multiplexé selon les flags), Air expose des fonctions typées par cas d’usage.
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 appelle munmap automatiquement
}
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;
}
}
}
Syscall sous-jacent. mmap (x86_64 n°9, ARM64 n°222).
Comportement.
Crée un mapping anonyme (sans fichier sous-jacent). Le wrapper ajoute MAP_ANONYMOUS | MAP_PRIVATE au flags par défaut.
len est arrondi à la page size supérieure par le kernel. Le wrapper Air ne fait pas l’arrondi explicitement (l’appelant peut le faire s’il veut connaître la taille effective).
Pattern.
#![allow(unused)]
fn main() {
let mapping = mmap_anonymous(
4096,
ProtectionFlags::READ | ProtectionFlags::WRITE,
MapFlags::PRIVATE, // ANONYMOUS implicite
)?;
// Utilisation du mapping
// Le munmap est automatique au drop
}
Erreurs.
EINVAL: taille invalide ou flags invalides.ENOMEM: pas de mémoire disponible.EACCES: permissions insuffisantes.
Performance. Très variable. ~10-100 µs selon la taille et la mémoire système.
mmap_file
Signature.
#![allow(unused)]
fn main() {
pub fn mmap_file(
fd: BorrowedFd<'_>,
length: usize,
offset: u64,
prot: ProtectionFlags,
flags: MapFlags,
) -> Result<Mapping, Errno>;
}
Comportement.
Crée un mapping basé sur un fichier. offset doit être aligné sur page size.
Cas d’usage typique.
#![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>;
}
Comportement.
Mappe à une adresse spécifique. unsafe parce que peut écraser des mappings existants si MAP_FIXED_NOREPLACE n’est pas utilisé.
Cas d’usage rare et avancé : implémentation de runtime, allocator custom, JIT.
munmap
Signature.
#![allow(unused)]
fn main() {
pub fn munmap(mapping: Mapping) -> Result<(), Errno>;
}
Comportement.
Démappe explicitement. Consomme le Mapping. Le Drop automatique appelle aussi munmap (en ignorant l’erreur) ; la fonction explicite permet de récupérer une erreur éventuelle.
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;
}
}
}
Comportement.
Redimensionne un mapping. Avec MAYMOVE, le kernel peut déplacer le mapping en mémoire si le redimensionnement sur place est impossible.
Sous-section 2 : Protection
mprotect
Signature.
#![allow(unused)]
fn main() {
pub fn mprotect(
addr: NonNull<u8>,
length: usize,
prot: ProtectionFlags,
) -> Result<(), Errno>;
}
Comportement.
Change les permissions d’une plage mémoire mappée. Pattern typique : allouer en READ+WRITE, initialiser, puis passer en READ uniquement pour les données constantes.
Erreurs.
EINVAL: plage invalide ou pas alignée.EACCES: tentative de gagner une permission interdite (typiquement EXEC sur du mapped depuis un FS no-exec).ENOMEM: ressources kernel insuffisantes.
Sous-section 3 : Conseils kernel
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,
}
}
Comportement.
Informe le kernel sur le pattern d’accès attendu à une plage mémoire. Le kernel peut alors optimiser (préfetch, swapping, transparent huge pages, etc.).
Cas courants :
Sequential+WillNeedpour des reads séquentiels intensifs.Randompour des accès aléatoires (évite le préfetch contre-productif).DontNeedpour libérer des pages dont on n’aura plus besoin sans démapper.
Sous-section 4 : Verrouillage
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;
}
}
}
Comportement.
Verrouille des pages mémoire pour empêcher leur swap. Cas d’usage : sécurité (données sensibles qui ne doivent pas finir sur disque swap), temps réel (latences prévisibles).
Limites : RLIMIT_MEMLOCK borne ce qu’un processus non-privilégié peut verrouiller.
Sous-section 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;
}
}
}
Syscall sous-jacent. memfd_create (x86_64 n°319, ARM64 n°279). Disponible depuis Linux 3.17.
Comportement.
Crée un FD vers une zone mémoire anonyme (un “memory file”). Le FD peut être :
- Étendu avec
ftruncate. - Mmapé pour partage entre processus.
- Passé entre processus via SCM_RIGHTS sur un Unix socket.
- Scellé avec
fcntl(F_ADD_SEALS)pour interdire certaines opérations.
C’est la primitive centrale pour la mémoire partagée moderne sur Linux. Au cœur du data plane de AirCom (ADR-001).
Pattern : partage de données entre processus.
#![allow(unused)]
fn main() {
// Processus A : créer un memfd, le remplir, le partager
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)?;
// ... remplir le mapping ...
// Sceller pour interdire modifications ultérieures
add_seals(fd.as_fd(), Seals::SEAL_WRITE | Seals::SEAL_SHRINK | Seals::SEAL_GROW)?;
// Envoyer le FD via Unix socket
sendmsg(socket, &[fd.as_fd()], ...)?;
// Processus B : recevoir le FD, mmap en lecture
let received_fd: OwnedFd = recvmsg(socket, ...)?;
let mapping = mmap_file(received_fd.as_fd(), 4096, 0, ProtectionFlags::READ, MapFlags::SHARED)?;
// Lire les données partagées
}
memfd_secret
Signature.
#![allow(unused)]
fn main() {
pub fn memfd_secret(flags: u32) -> Result<OwnedFd, Errno>;
}
Syscall sous-jacent. memfd_secret (x86_64 n°447, ARM64 n°447). Disponible depuis Linux 5.14.
Comportement.
Variante de memfd_create qui crée une zone mémoire non accessible au kernel après le mapping. Utilisé pour les secrets cryptographiques de haute valeur.
Sous-section 6 : Synchronisation
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;
}
}
}
Comportement.
Force la synchronisation des modifications d’un mapping vers le fichier sous-jacent. Pour les mappings MAP_SHARED sur fichier.
Sous-section 7 : Accès inter-processus
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,
}
}
Comportement.
Permet de lire/écrire la mémoire d’un autre processus sans ptrace. Demande des permissions (typiquement CAP_SYS_PTRACE ou même utilisateur que la cible).
Cas d’usage spécialisé : debuggers, outils de surveillance, transfert de buffer entre processus coopérant sans copie.
Récapitulatif de la famille mem
Fonctions exposées :
| Catégorie | Fonctions principales |
|---|---|
| Mappings | mmap_anonymous, mmap_file, mmap_fixed, munmap, mremap |
| Protection | mprotect |
| Conseils | madvise |
| Verrouillage | mlock, mlock2, munlock, mlockall, munlockall |
| memfd | memfd_create, memfd_secret |
| Sync | msync |
| Inter-process | process_vm_readv, process_vm_writev |
Total : 13 syscalls wrappés.
Syscalls non-wrappés (listés dans UNSUPPORTED.md) :
brk,sbrk: gestion historique du heap, gérée par l’allocateur, pas exposée.pkey_alloc,pkey_free,pkey_mprotect: Memory Protection Keys, marginal en pratique.mbind,migrate_pages,move_pages: NUMA-specific, à évaluer ultérieurement si Air a besoin de support NUMA fin.set_mempolicy,get_mempolicy: idem NUMA.
Répartition des types entre les deux crates
Types dans air-sys-types (purs, sans syscall)
MappingPointer— pointeur + longueur d’un mapping, sans sémantique de propriété (ne libère rien au drop). C’est le type de retour demmap_fixed, où l’appelant gère lui-même le cycle de vie.ProtectionFlags,MapFlags,MremapFlagsMadviseAdviceMlockFlags,MlockallFlagsMemfdFlagsMsyncFlagsRemoteIoSlice
Type RAII dans air-sys-syscall::mem (et non air-sys-types)
Mapping— voir la décision de fond n° 2 ci-dessous.
Décision d’architecture (Q1, validée le 2026-05-31). La rédaction initiale de cette spec plaçait
Mappingdansair-sys-types. C’est incorrect : leDropdeMappingdoit appelermunmap, qui est un syscall. Orair-sys-typesne contient que des types purs et n’appelle jamais de syscall (seulair-sys-syscallle fait, viacore::arch::asm!). Faire vivreMappingdansair-sys-typesforcerait cette crate à embarquer de l’asm!, brisant la séparation de responsabilités entre les deux crates de la couche 0. En conséquence,Mapping(type RAII dont leDropappelle un syscall) est défini dansair-sys-syscall::mem, aux côtés des fonctions qui le produisent (mmap_anonymous,mmap_file,mremap). Seuls les types sans syscall (MappingPointeret lesbitflags/enums ci-dessus) restent dansair-sys-types. La même règle s’applique àLandlockRulesetcôté famillesecurity(cf.family-security.md).
Décisions de fond émergées dans la famille mem
1. mmap éclaté en fonctions typées.
Plutôt qu’un wrapper unique qui prendrait tous les flags et arguments, séparation en mmap_anonymous, mmap_file, mmap_fixed. Plus clair pour le développeur, moins d’erreurs (oublier MAP_ANONYMOUS quand on n’a pas de FD, par exemple).
2. Mapping RAII strict — et défini dans air-sys-syscall::mem.
Un Mapping ne peut pas être copié, seulement déplacé. Le munmap automatique au
drop évite les fuites. Pour les cas où on veut un mapping « long-lived » qui survit
à plusieurs scopes, on déplace l’ownership explicitement.
Parce que son Drop appelle le syscall munmap, Mapping réside dans
air-sys-syscall::mem, pas dans air-sys-types (cf. la section « Répartition
des types » ci-dessus). Règle générale de la couche 0 : un type RAII dont la
destruction déclenche un syscall vit dans la crate des wrappers (air-sys-syscall),
jamais dans la crate des types purs (air-sys-types). Le type MappingPointer
(pointeur nu, sans propriété, retourné par mmap_fixed) reste en revanche dans
air-sys-types puisqu’il ne libère rien.
3. Page size détectée au runtime.
Sur Raspberry Pi 4, la page size peut être 4 KB ou 16 KB selon la configuration kernel. Air détecte à l’init et expose comme static. Pas de constante de compilation.
4. memfd_create central pour AirCom.
Conformément à l’ADR-001, le data plane de AirCom utilise memfd + mmap shared. La spec memfd ici est délibérément détaillée parce qu’elle conditionne la performance d’IPC.
5. Pas d’allocateur Air en couche 0.
Air n’expose pas d’allocateur custom au niveau syscall. Le système global allocator (souvent jemalloc ou mimalloc) est utilisé par défaut. L’allocateur peut être configuré au niveau de l’application, hors du périmètre couche 0.
Licence du document : MPL 2.0
Statut : Spécification technique du module air-sys-syscall::mem (couche 0).