Spec couche 0 — Famille ebpf
Spécification technique — Version 1.0
Vue d’ensemble de la famille
Le module air-sys-syscall::ebpf expose les primitives de programmation du
sous-système eBPF du kernel Linux : le syscall bpf() (création et
manipulation de cartes, chargement et attache de programmes, introspection,
épinglage, BTF, itérateurs, statistiques) et le syscall perf_event_open()
(compteurs de performance, échantillonnage, points de trace), qui est le point
d’accroche canonique des programmes eBPF de type tracing.
eBPF est, avec seccomp-BPF (famille security) et Landlock, l’un des piliers
techniques assumés du choix Linux-only d’Air (ADR-004). Il sert l’observabilité
(profiling, traçage), la sécurité (filtrage réseau/LSM), et l’instrumentation
fine du système — capacités exploitées par les services système de la couche 5.
Frontière couche 0 ↔ couche 1 : on charge, on ne fabrique pas
Décision d’architecture (alignée sur la famille
security, Q4). Comme pour seccomp, la couche 0 n’assemble pas et ne compile pas de programme eBPF. Fabriquer du bytecode eBPF (encodage desstruct bpf_insn, allocation de registres, relocations CO-RE, génération de BTF, résolution des helpers) est de la logique générative, pas un wrapper de syscall. La couche 0 expose la primitive : charger dans le kernel un programme déjà assemblé (une tranche deBpfInstructionfournie par l’appelant), créer/interroger des cartes, attacher, épingler, introspecter.Sont donc hors couche 0 (spécifiés en couche 1, futur crate
air-bpf) : l’assembleur d’instructions, le chargeur façon libbpf (parsing ELF, sections, relocations), la génération et la manipulation riche de BTF, le décodage des enregistrements de l’anneau d’échantillonnageperf/ring-buffer. La couche 0 fournit les briques sur lesquelles cette logique s’appuie.
Périmètre de la famille
Couverture exhaustive du syscall bpf() : les 37 sous-commandes de
enum bpf_cmd (cible kernel 6.12 LTS, comme io_uring) sont chacune exposées
par une fonction dédiée et typée, conformément à l’ADR-021 convention 3 —
aucun wrapper bpf(cmd, attr, size) générique n’est offert. Plus le syscall
perf_event_open() et ses ioctl de contrôle, eux aussi en fonctions dédiées.
Caractéristiques transverses de la famille.
-
CLOEXEC universel. Tous les FDs eBPF (cartes, programmes, liens, BTF) et les FDs
perf_eventsont créésCLOEXEC(drapeauBPF_F_*CLOEXEC/PERF_FLAG_FD_CLOEXECposé par le wrapper). -
Pas de syscall multiplexé générique (ADR-021 conv. 3).
bpf()multiplexe 37 opérations,ioctlsur perf en multiplexe une douzaine : chacune devient une fonction typée. La complexité est concentrée côté Air, pas côté appelant. -
Sentinelles kernel →
Option<T>/ enums (ADR-021 conv. 1). Les-1« tous CPU / tout processus » deperf_event_open, lesid0 « début d’itération », les fd optionnels deviennent desOption<T>ou des enums. -
RAII pour toutes les ressources.
BpfMap,BpfProgram,BpfLink,Btf,PerfEventpossèdent unOwnedFdet le ferment auDrop. Aucune fuite de FD. -
EINTR remonté tel quel (ADR-021 conv. 2). Pas de retry automatique.
-
Privilèges. La plupart des opérations exigent
CAP_BPF(+CAP_PERFMONpour le tracing,CAP_NET_ADMINpour le réseau) depuis Linux 5.8, ouCAP_SYS_ADMINavant. Le BPF non privilégié est souvent désactivé (kernel.unprivileged_bpf_disabled). Documenté par fonction ; non masqué. -
Tranches typées plutôt que pointeurs bruts. Les clés/valeurs de cartes, les instructions de programme, les buffers de log du vérifieur sont passés en
&[u8]/&[BpfInstruction]dont la longueur est validée contre la géométrie de la ressource (key_size,value_size…) avant l’appel (Principe 4, « valider en amont »). Mismatch ⇒EINVALcôté Air, sans toucher le kernel.
Sous-section 1 : Types fondamentaux
Ces types sont purs (aucun syscall) et vivent dans air-sys-types::ebpf,
sauf les RAII qui appellent des syscalls (placés dans air-sys-syscall::ebpf).
Instruction et programme
#![allow(unused)]
fn main() {
/// Miroir `#[repr(C)]` de `struct bpf_insn` (8 octets). Type « miroir » :
/// nom de type explicite (ADR-029), champs aux noms kernel conservés.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BpfInstruction {
pub code: u8,
/// 4 bits `dst_reg` + 4 bits `src_reg` (layout kernel).
pub registers: u8,
pub offset: i16,
pub immediate: i32,
}
}
Le programme est une tranche &[BpfInstruction] appartenant à l’appelant.
La couche 0 ne l’inspecte ni ne le transforme : le vérifieur kernel s’en
charge au chargement. La limite BPF_MAXINSNS (4096) ne concerne que le BPF
classique (seccomp) ; un programme eBPF est borné par la complexité que le
vérifieur accepte (jusqu’à ~1M d’instructions analysées, privilégié).
Énumérations typées
#![allow(unused)]
fn main() {
/// `enum bpf_map_type`. Variante `Other(u32)` pour les types non encore nommés.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BpfMapType {
Hash, Array, ProgramArray, PerfEventArray, PercpuHash, PercpuArray,
StackTrace, CgroupArray, LruHash, LruPercpuHash, LpmTrie, ArrayOfMaps,
HashOfMaps, DevMap, SockMap, Cpumap, XskMap, SockHash, CgroupStorage,
ReuseportSockArray, PercpuCgroupStorage, Queue, Stack, SkStorage, DevmapHash, StructOps,
RingBuf, InodeStorage, TaskStorage, BloomFilter, UserRingBuf, CgrpStorage,
Arena,
Other(u32),
}
/// `enum bpf_prog_type`. Variante `Other(u32)` de repli.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BpfProgramType {
SocketFilter, Kprobe, SchedCls, SchedAct, Tracepoint, Xdp, PerfEvent,
CgroupSkb, CgroupSock, LwtIn, LwtOut, LwtXmit, SockOps, SkSkb, CgroupDevice,
SkMsg, RawTracepoint, CgroupSockAddr, LwtSeg6local, LircMode2, SkReuseport,
FlowDissector, CgroupSysctl, RawTracepointWritable, CgroupSockopt, Tracing,
StructOps, Ext, Lsm, SkLookup, Syscall, Netfilter,
Other(u32),
}
/// `enum bpf_attach_type` (points d'attache des programmes et des liens).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BpfAttachType {
CgroupInetIngress, CgroupInetEgress, CgroupInetSockCreate, CgroupSockOps,
SkSkbStreamParser, SkSkbStreamVerdict, CgroupDevice, SkMsgVerdict,
CgroupInet4Bind, CgroupInet6Bind, CgroupInet4Connect, CgroupInet6Connect,
TraceFentry, TraceFexit, ModifyReturn, LsmMac, TraceIter, XdpDevmap,
XdpCpumap, SkLookup, Xdp, /* … */ Other(u32),
}
}
Nommage (ADR-029). Les noms de types sont explicites (
BpfMapType, pasBpfMt). Les variantes miroitent les constantes kernelBPF_MAP_TYPE_*/BPF_PROG_TYPE_*(autorité kernel, que le développeur retrouve dansuapi/linux/bpf.h), en CamelCase, avec les abréviations d’usage universel tolérées (Lru,Xdp,Lsm). La varianteOther(u32)évite de bloquer un type ajouté par un kernel plus récent que 6.12.
Handles RAII (dans air-sys-syscall::ebpf)
#![allow(unused)]
fn main() {
pub struct BpfMap { /* OwnedFd */ }
pub struct BpfProgram { /* OwnedFd */ }
pub struct BpfLink { /* OwnedFd */ }
pub struct Btf { /* OwnedFd, objet BTF chargé */ }
pub struct PerfEvent { /* OwnedFd */ }
// Chacun expose : as_fd(&self) -> BorrowedFd<'_>, into_fd(self) -> OwnedFd,
// et un constructeur from_fd(OwnedFd) pour ré-adopter un fd obtenu *_get_fd_by_id.
}
Sous-section 2 : Cartes (maps)
Création
#![allow(unused)]
fn main() {
pub fn bpf_map_create(request: &BpfMapCreateRequest<'_>) -> Result<BpfMap, Errno>;
pub struct BpfMapCreateRequest<'a> {
pub map_type: BpfMapType,
pub key_size: u32,
pub value_size: u32,
pub max_entries: u32,
pub flags: BpfMapCreateFlags,
/// Nom (≤ 15 octets utiles + NUL), pour l'introspection. `None` = anonyme.
pub name: Option<&'a CStr>,
/// Pour les map-of-maps : le fd du gabarit de carte interne.
pub inner_map: Option<BorrowedFd<'a>>,
/// Nœud NUMA préféré (`None` = pas de préférence).
pub numa_node: Option<u32>,
/// Infos BTF optionnelles (clé/valeur typées).
pub btf: Option<BpfMapBtfInfo<'a>>,
}
bitflags! {
pub struct BpfMapCreateFlags: u32 {
const NO_PREALLOC = 1 << 0;
const NO_COMMON_LRU = 1 << 1;
const NUMA_NODE = 1 << 2;
const READ_ONLY = 1 << 3;
const WRITE_ONLY = 1 << 4;
const STACK_BUILD_ID = 1 << 5;
const ZERO_SEED = 1 << 6;
const READ_ONLY_PROG = 1 << 7;
const WRITE_ONLY_PROG = 1 << 8;
const CLONE = 1 << 9;
const MMAPABLE = 1 << 10;
const PRESERVE_ELEMS = 1 << 11;
const INNER_LOCK = 1 << 12;
}
}
}
Syscall sous-jacent. bpf(BPF_MAP_CREATE, &attr, size). bpf :
x86_64 n°321, ARM64 n°280. Disponible depuis Linux 3.18 ; types et drapeaux
selon version (6.12 pour le périmètre figé).
Préconditions (validées en amont, Principe 4). key_size/value_size
cohérents avec le map_type (certains types imposent une taille de clé/valeur
fixe ; p. ex. Array ⇒ key_size == 4). max_entries > 0 sauf types sans
entrées. READ_ONLY/WRITE_ONLY mutuellement exclusifs.
Erreurs. EPERM/EACCES (privilège, BPF non privilégié désactivé),
EINVAL (géométrie invalide), E2BIG (carte trop grande), ENOMEM.
Accès élément par élément
#![allow(unused)]
fn main() {
pub fn bpf_map_lookup_element(
map: BorrowedFd<'_>, key: &[u8], value_out: &mut [u8], flags: BpfMapLookupFlags,
) -> Result<(), Errno>;
pub fn bpf_map_update_element(
map: BorrowedFd<'_>, key: &[u8], value: &[u8], flags: BpfMapUpdateFlags,
) -> Result<(), Errno>;
pub fn bpf_map_delete_element(map: BorrowedFd<'_>, key: &[u8]) -> Result<(), Errno>;
/// `None` pour `key` = « première clé » (sentinelle kernel `NULL`, ADR-021 c.1).
pub fn bpf_map_get_next_key(
map: BorrowedFd<'_>, key: Option<&[u8]>, next_key_out: &mut [u8],
) -> Result<bool, Errno>; // false si fin d'itération (ENOENT)
/// `BPF_MAP_LOOKUP_AND_DELETE_ELEM` (cartes Queue/Stack notamment).
pub fn bpf_map_lookup_and_delete_element(
map: BorrowedFd<'_>, key: Option<&[u8]>, value_out: &mut [u8], flags: BpfMapLookupFlags,
) -> Result<(), Errno>;
bitflags! { pub struct BpfMapUpdateFlags: u64 {
const ANY = 0; const NO_EXIST = 1; const EXIST = 2; const F_LOCK = 4;
}}
bitflags! { pub struct BpfMapLookupFlags: u64 { const F_LOCK = 4; }}
}
Validation des longueurs. key.len() doit valoir key_size,
value.len()/value_out.len() valoir value_size (ou value_size * num_possible_cpus pour les cartes per-CPU). Le wrapper vérifie via
BPF_OBJ_GET_INFO_BY_FD mis en cache, ou impose à l’appelant de fournir des
tranches correctes et renvoie EINVAL si le kernel les rejette. Choix retenu :
ne pas introspecter implicitement (coût caché) — l’appelant garantit la
taille ; mismatch ⇒ EINVAL remonté du kernel. Documenté.
Opérations par lot (batch)
#![allow(unused)]
fn main() {
pub fn bpf_map_lookup_batch(map: BorrowedFd<'_>, request: &mut BpfMapBatchRequest<'_>)
-> Result<u32, Errno>; // nombre d'éléments traités
pub fn bpf_map_lookup_and_delete_batch(map: BorrowedFd<'_>, request: &mut BpfMapBatchRequest<'_>)
-> Result<u32, Errno>;
pub fn bpf_map_update_batch(map: BorrowedFd<'_>, request: &BpfMapBatchInput<'_>)
-> Result<u32, Errno>;
pub fn bpf_map_delete_batch(map: BorrowedFd<'_>, request: &BpfMapBatchInput<'_>)
-> Result<u32, Errno>;
}
BpfMapBatchRequest porte les tampons de clés/valeurs, le curseur
in_batch/out_batch (opaque, géré par le kernel), le count demandé et les
flags. Les opérations batch amortissent le coût syscall sur de grandes cartes.
Gel
#![allow(unused)]
fn main() {
/// `BPF_MAP_FREEZE` — rend la carte non modifiable depuis l'espace utilisateur
/// (le programme eBPF peut encore l'écrire selon ses droits). Irréversible.
pub fn bpf_map_freeze(map: BorrowedFd<'_>) -> Result<(), Errno>;
}
Performance (cartes). lookup/update/delete unitaires : ~1-3 µs (dominés par le syscall). Les variantes batch divisent ce coût par le facteur de lot.
Sous-section 3 : Programmes
Chargement (programme déjà assemblé)
#![allow(unused)]
fn main() {
pub fn bpf_program_load(
request: &BpfProgramLoadRequest<'_>,
verifier_log: Option<&mut [u8]>,
) -> Result<BpfProgram, Errno>;
pub struct BpfProgramLoadRequest<'a> {
pub program_type: BpfProgramType,
/// Programme **déjà assemblé** (la couche 0 ne le fabrique pas).
pub instructions: &'a [BpfInstruction],
/// Licence du programme (`GPL`, `Dual BSD/GPL`…). Conditionne l'accès aux
/// helpers marqués GPL-only par le kernel.
pub license: &'a CStr,
pub name: Option<&'a CStr>,
pub expected_attach_type: Option<BpfAttachType>,
/// Pour fentry/fexit/LSM/tracing : BTF cible + id du point d'attache.
pub attach_btf: Option<BorrowedFd<'a>>,
pub attach_btf_id: Option<u32>,
/// Programme à étendre (`Ext` / freplace).
pub attach_program: Option<BorrowedFd<'a>>,
pub flags: BpfProgramLoadFlags,
pub log_level: BpfVerifierLogLevel,
}
bitflags! { pub struct BpfProgramLoadFlags: u32 {
const STRICT_ALIGNMENT = 1 << 0;
const ANY_ALIGNMENT = 1 << 1;
const TEST_RND_HI32 = 1 << 2;
const TEST_STATE_FREQ = 1 << 3;
const SLEEPABLE = 1 << 4;
const XDP_HAS_FRAGS = 1 << 5;
}}
#[derive(Debug, Clone, Copy)]
pub enum BpfVerifierLogLevel { Disabled, Basic, Verbose, Stats }
}
Syscall sous-jacent. bpf(BPF_PROG_LOAD, …). Le vérifieur kernel analyse
le programme : sûreté mémoire, terminaison (pas de boucle non bornée), types.
En cas de rejet, le wrapper renvoie l’Errno kernel (souvent EACCES/EINVAL)
et remplit verifier_log avec le diagnostic textuel du vérifieur — élément
indispensable au débogage. Sur log trop court : ENOSPC (et le log tronqué).
Sûreté.
bpf_program_loadn’est pasunsafeau sens Rust : le vérifieur kernel garantit la sûreté mémoire du programme chargé. Le wrapper n’expose aucun pointeur brut. (L’effet d’un programme attaché sur le système est une autre question, traitée à l’attache.)
Test et binding
#![allow(unused)]
fn main() {
/// `BPF_PROG_TEST_RUN` / `BPF_PROG_RUN` — exécute le programme sur un contexte
/// et des données fournis, sans l'attacher. Retourne valeur de retour + durée.
pub fn bpf_program_test_run(
program: BorrowedFd<'_>, request: &mut BpfProgramTestRunRequest<'_>,
) -> Result<BpfProgramTestRunResult, Errno>;
/// `BPF_PROG_BIND_MAP` — lie explicitement une carte à un programme (durée de vie).
pub fn bpf_program_bind_map(
program: BorrowedFd<'_>, map: BorrowedFd<'_>, flags: u32,
) -> Result<(), Errno>;
}
Sous-section 4 : Attache et liens
Deux modèles coexistent : l’attache historique (PROG_ATTACH/DETACH,
surtout cgroup et sockmap, attache « anonyme » sans objet) et le modèle moderne
bpf_link (objet FD représentant l’attache, détachée au Drop).
#![allow(unused)]
fn main() {
/// `BPF_PROG_ATTACH` — attache historique (cgroup, sk_skb, flow_dissector…).
pub fn bpf_program_attach(
program: BorrowedFd<'_>, target: BorrowedFd<'_>,
attach_type: BpfAttachType, flags: BpfAttachFlags,
) -> Result<(), Errno>;
/// `BPF_PROG_DETACH`.
pub fn bpf_program_detach(
target: BorrowedFd<'_>, attach_type: BpfAttachType,
program: Option<BorrowedFd<'_>>,
) -> Result<(), Errno>;
/// `BPF_LINK_CREATE` — crée un lien moderne (retourne un FD RAII).
pub fn bpf_link_create(request: &BpfLinkCreateRequest<'_>) -> Result<BpfLink, Errno>;
/// `BPF_LINK_UPDATE` — remplace à chaud le programme d'un lien.
pub fn bpf_link_update(
link: BorrowedFd<'_>, new_program: BorrowedFd<'_>,
old_program: Option<BorrowedFd<'_>>, flags: u32,
) -> Result<(), Errno>;
/// `BPF_LINK_DETACH` — détache le programme tout en gardant le lien (auto-détache).
pub fn bpf_link_detach(link: BorrowedFd<'_>) -> Result<(), Errno>;
/// `BPF_RAW_TRACEPOINT_OPEN` — attache un programme à un tracepoint brut.
pub fn bpf_raw_tracepoint_open(
name: &CStr, program: BorrowedFd<'_>,
) -> Result<BpfLink, Errno>;
/// `BPF_ITER_CREATE` — crée un itérateur BPF à partir d'un `BpfLink` d'itération.
/// Retourne un FD lisible qui produit la sortie de l'itérateur.
pub fn bpf_iterator_create(link: BorrowedFd<'_>, flags: u32) -> Result<OwnedFd, Errno>;
bitflags! { pub struct BpfAttachFlags: u32 {
const ALLOW_OVERRIDE = 1 << 0;
const ALLOW_MULTI = 1 << 1;
const REPLACE = 1 << 2;
}}
}
Effet système — note de sûreté (pas
unsafe, mais documentée). Attacher un programme modifie le comportement du système (filtrage réseau, hooks LSM, traçage). Ces fonctions ne sont pasunsafeau sens mémoire, mais leur documentation# Effetsrappelle qu’un programme attaché s’exécute sur des chemins critiques. La couche 5 (services système) orchestre ces attaches sous contrôle d’entitlements (ADR-010).
Performance. Création d’un lien / attache : ~10-50 µs. Le Drop d’un
BpfLink détache automatiquement.
Sous-section 5 : Épinglage (système de fichiers bpf)
#![allow(unused)]
fn main() {
/// `BPF_OBJ_PIN` — épingle un objet (carte/programme/lien) sous un chemin du
/// montage `bpffs` (`/sys/fs/bpf/...`), pour le faire survivre au processus.
pub fn bpf_object_pin(object: BorrowedFd<'_>, path: &CStr) -> Result<(), Errno>;
/// `BPF_OBJ_GET` — récupère un FD vers un objet précédemment épinglé.
pub fn bpf_object_get(path: &CStr, flags: BpfObjectGetFlags) -> Result<OwnedFd, Errno>;
bitflags! { pub struct BpfObjectGetFlags: u32 {
const RDONLY = 1 << 3; const WRONLY = 1 << 4;
}}
}
Le FD retourné par bpf_object_get est ré-adoptable en BpfMap::from_fd /
BpfProgram::from_fd / BpfLink::from_fd selon le type (vérifiable via
bpf_object_get_info_by_fd).
Sous-section 6 : Introspection par identifiant
Le kernel attribue un id stable à chaque objet vivant. Ces commandes itèrent
les ids et convertissent id → FD (avec privilège). La sentinelle « commencer au
début » (start_id = 0) devient Option<u32> (ADR-021 conv. 1).
#![allow(unused)]
fn main() {
pub fn bpf_program_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno>;
pub fn bpf_map_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno>;
pub fn bpf_btf_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno>;
pub fn bpf_link_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno>;
// `Ok(None)` = fin d'itération (le kernel renvoie ENOENT).
pub fn bpf_program_get_fd_by_id(id: u32) -> Result<BpfProgram, Errno>;
pub fn bpf_map_get_fd_by_id(id: u32) -> Result<BpfMap, Errno>;
pub fn bpf_btf_get_fd_by_id(id: u32) -> Result<Btf, Errno>;
pub fn bpf_link_get_fd_by_id(id: u32) -> Result<BpfLink, Errno>;
/// `BPF_OBJ_GET_INFO_BY_FD` — remplit une structure d'info kernel pour un objet.
/// `info_out` est un buffer dont la taille dépend du type d'objet interrogé.
pub fn bpf_object_get_info_by_fd(
object: BorrowedFd<'_>, info_out: &mut [u8],
) -> Result<u32, Errno>; // octets effectivement écrits
/// `BPF_PROG_QUERY` — liste les programmes attachés à une cible (cgroup…).
pub fn bpf_program_query(request: &mut BpfProgramQueryRequest<'_>) -> Result<u32, Errno>;
/// `BPF_TASK_FD_QUERY` — interroge le programme derrière un fd perf/tracepoint
/// d'une tâche donnée (introspection de kprobe/uprobe).
pub fn bpf_task_fd_query(request: &mut BpfTaskFdQueryRequest<'_>) -> Result<(), Errno>;
}
Privilège. *_get_fd_by_id et PROG_QUERY exigent typiquement
CAP_SYS_ADMIN/CAP_BPF. Documenté.
Sous-section 7 : BTF (BPF Type Format)
#![allow(unused)]
fn main() {
/// `BPF_BTF_LOAD` — charge un blob BTF **déjà formé** dans le kernel.
/// (La *génération* de BTF est de la logique → couche 1.)
pub fn bpf_btf_load(
btf_blob: &[u8],
verifier_log: Option<&mut [u8]>,
flags: BpfBtfLoadFlags,
) -> Result<Btf, Errno>;
bitflags! { pub struct BpfBtfLoadFlags: u32 { const TOKEN_FD = 1 << 0; }}
}
bpf_btf_get_fd_by_id / bpf_btf_get_next_id sont en sous-section 6. La couche
0 charge et référence un blob BTF ; le décodage du format BTF (types, noms,
relocations CO-RE) est de la logique de couche 1.
Sous-section 8 : Statistiques et jetons
#![allow(unused)]
fn main() {
/// `BPF_ENABLE_STATS` — active globalement un type de statistiques (temps CPU
/// passé dans les programmes…). Retourne un FD : les stats restent actives tant
/// qu'il est ouvert (RAII).
pub fn bpf_enable_statistics(kind: BpfStatsType) -> Result<OwnedFd, Errno>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BpfStatsType { RunTime }
/// `BPF_TOKEN_CREATE` (Linux 6.9+) — crée un jeton de délégation de privilèges
/// BPF depuis un montage `bpffs`, pour autoriser des opérations BPF dans un
/// conteneur/namespace sans `CAP_SYS_ADMIN` global. Cohérent avec le modèle de
/// confinement d'Air.
pub fn bpf_token_create(bpffs: BorrowedFd<'_>, flags: u32) -> Result<OwnedFd, Errno>;
}
BpfStatsType est extensible (#[non_exhaustive] ou variante de repli) pour les
types de stats ajoutés après 6.12.
Sous-section 9 : perf_event_open et contrôle des événements
perf_event_open ouvre un compteur/échantillonneur de performance. C’est le
point d’accroche canonique des programmes eBPF de tracing : on ouvre un
perf_event sur un kprobe/uprobe/tracepoint, puis on y attache un programme via
perf_event_set_bpf_program.
Ouverture
#![allow(unused)]
fn main() {
pub fn perf_event_open(
attr: &PerfEventAttr,
scope: PerfEventScope<'_>,
group_leader: Option<BorrowedFd<'_>>,
flags: PerfEventOpenFlags,
) -> Result<PerfEvent, Errno>;
/// `pid`/`cpu` du kernel encodés sans sentinelle `-1` (ADR-021 conv. 1).
pub enum PerfEventScope<'a> {
/// Processus courant, sur n'importe quel CPU (`pid=0, cpu=-1`).
CallingProcessAnyCpu,
/// Processus courant, sur un CPU précis (`pid=0, cpu=N`).
CallingProcessOnCpu(u32),
/// Un processus précis, n'importe quel CPU (`pid=P, cpu=-1`).
ProcessAnyCpu(Pid),
/// Un processus précis sur un CPU précis (`pid=P, cpu=N`).
ProcessOnCpu { process: Pid, cpu: u32 },
/// Tous les processus sur un CPU (échantillonnage système, `pid=-1, cpu=N`).
AllProcessesOnCpu(u32),
/// Surveillance par cgroup (`flags |= PID_CGROUP`, `pid = cgroup_fd`).
Cgroup { cgroup: BorrowedFd<'a>, cpu: u32 },
}
bitflags! { pub struct PerfEventOpenFlags: u64 {
const FD_NO_GROUP = 1 << 0;
const FD_OUTPUT = 1 << 1;
// PID_CGROUP (1<<2) est géré via PerfEventScope::Cgroup, pas exposé ici.
const FD_CLOEXEC = 1 << 3; // toujours posé par le wrapper
}}
}
Syscall sous-jacent. perf_event_open : x86_64 n°298, ARM64 n°241.
Disponible depuis Linux 2.6.31.
PerfEventAttr est un miroir #[repr(C)] de struct perf_event_attr
(~120 octets) : nom de type explicite, champs aux noms kernel (type, config,
sample_period, sample_type, read_format, bitfields disabled, inherit,
exclude_kernel…). Il implémente Default (tout à zéro = valide). Des enums
d’aide nomment les valeurs courantes du champ type
(PerfTypeId::{Hardware, Software, Tracepoint, HwCache, Raw, Breakpoint}) et des
config matériels (PerfHardwareCounter::{CpuCycles, Instructions, …}), sans
masquer la structure brute. Un builder ergonomique (assemblage haut niveau)
relève de la couche 1.
Préconditions / erreurs. EACCES/EPERM (privilège, perf_event_paranoid),
EINVAL (attr incohérent), ENOENT (type/config inconnu), EMFILE,
ENODEV, EOPNOTSUPP.
Lecture des compteurs
#![allow(unused)]
fn main() {
impl PerfEvent {
pub fn as_fd(&self) -> BorrowedFd<'_>;
pub fn into_fd(self) -> OwnedFd;
/// Lit la valeur scalaire du compteur (`read_format` simple).
pub fn read_count(&self) -> Result<u64, Errno>;
}
}
La lecture en mode groupe (PERF_FORMAT_GROUP, plusieurs compteurs d’un
coup) et le décodage de l’anneau d’échantillonnage (mmap du ring buffer,
parcours des struct perf_event_header / enregistrements PERF_RECORD_*) sont
de la logique → couche 1. La couche 0 fournit le FD : l’anneau se mmap via
mem::mmap (famille mem), son interprétation est faite au-dessus.
Contrôle (ioctls dédiés — pas d’ioctl générique, ADR-021 c.3)
#![allow(unused)]
fn main() {
pub fn perf_event_enable(event: BorrowedFd<'_>) -> Result<(), Errno>; // IOC_ENABLE
pub fn perf_event_disable(event: BorrowedFd<'_>) -> Result<(), Errno>; // IOC_DISABLE
pub fn perf_event_reset(event: BorrowedFd<'_>) -> Result<(), Errno>; // IOC_RESET
pub fn perf_event_refresh(event: BorrowedFd<'_>, count: u32) -> Result<(), Errno>; // IOC_REFRESH
pub fn perf_event_set_period(event: BorrowedFd<'_>, period: u64) -> Result<(), Errno>; // IOC_PERIOD
pub fn perf_event_set_filter(event: BorrowedFd<'_>, filter: &CStr) -> Result<(), Errno>; // IOC_SET_FILTER
pub fn perf_event_id(event: BorrowedFd<'_>) -> Result<u64, Errno>; // IOC_ID
/// `PERF_EVENT_IOC_SET_BPF` — attache un programme eBPF à ce perf_event
/// (kprobe/uprobe/tracepoint). LE PONT eBPF ↔ perf.
pub fn perf_event_set_bpf_program(
event: BorrowedFd<'_>, program: BorrowedFd<'_>,
) -> Result<(), Errno>;
/// `PERF_EVENT_IOC_SET_OUTPUT` — redirige la sortie vers l'anneau d'un autre
/// event ; `None` détache la redirection (sentinelle `-1`, ADR-021 c.1).
pub fn perf_event_set_output(
event: BorrowedFd<'_>, output: Option<BorrowedFd<'_>>,
) -> Result<(), Errno>;
pub fn perf_event_pause_output(event: BorrowedFd<'_>, pause: bool) -> Result<(), Errno>; // IOC_PAUSE_OUTPUT
pub fn perf_event_query_bpf(event: BorrowedFd<'_>, ids_out: &mut [u32]) -> Result<u32, Errno>; // IOC_QUERY_BPF
pub fn perf_event_modify_attributes(event: BorrowedFd<'_>, attr: &PerfEventAttr) -> Result<(), Errno>; // IOC_MODIFY_ATTRIBUTES
}
Syscall sous-jacent (contrôle). ioctl sur le FD perf, une constante
PERF_EVENT_IOC_* par fonction. Aucun ioctl générique exposé.
Performance. Ouverture : ~10-30 µs. enable/disable/reset : ~1-2 µs.
read_count : ~1-2 µs.
Récapitulatif de la famille ebpf
Fonctions exposées, par catégorie :
| Catégorie | Fonctions principales |
|---|---|
| Cartes — vie | bpf_map_create, bpf_map_freeze |
| Cartes — éléments | bpf_map_lookup_element, bpf_map_update_element, bpf_map_delete_element, bpf_map_get_next_key, bpf_map_lookup_and_delete_element |
| Cartes — batch | bpf_map_lookup_batch, bpf_map_lookup_and_delete_batch, bpf_map_update_batch, bpf_map_delete_batch |
| Programmes | bpf_program_load, bpf_program_test_run, bpf_program_bind_map |
| Attache / liens | bpf_program_attach, bpf_program_detach, bpf_link_create, bpf_link_update, bpf_link_detach, bpf_raw_tracepoint_open, bpf_iterator_create |
| Épinglage | bpf_object_pin, bpf_object_get |
| Introspection | bpf_program_get_next_id, bpf_map_get_next_id, bpf_btf_get_next_id, bpf_link_get_next_id, bpf_program_get_fd_by_id, bpf_map_get_fd_by_id, bpf_btf_get_fd_by_id, bpf_link_get_fd_by_id, bpf_object_get_info_by_fd, bpf_program_query, bpf_task_fd_query |
| BTF | bpf_btf_load |
| Stats / jetons | bpf_enable_statistics, bpf_token_create |
| perf — vie | perf_event_open, PerfEvent::read_count |
| perf — contrôle | perf_event_enable, perf_event_disable, perf_event_reset, perf_event_refresh, perf_event_set_period, perf_event_set_filter, perf_event_id, perf_event_set_bpf_program, perf_event_set_output, perf_event_pause_output, perf_event_query_bpf, perf_event_modify_attributes |
Total : 37 fonctions bpf_* (couverture exhaustive des 37 sous-commandes de
enum bpf_cmd, cible 6.12) + ~14 fonctions perf_event_*, soit ~51
fonctions publiques principales.
Reporté en couche 1 (hors périmètre) : assembleur d’instructions eBPF,
chargeur ELF/libbpf, génération et décodage riche de BTF, relocations CO-RE,
décodage de l’anneau d’échantillonnage perf / ring-buffer, builders ergonomiques
de PerfEventAttr et de programmes.
Syscalls / opérations non-wrappés (listés dans UNSUPPORTED.md) :
bpf(): aucune sous-commande omise — la couverture est exhaustive pour 6.12. Les sous-commandes ajoutées par un kernel postérieur à 6.12 seront ajoutées par RFC, sans casser l’API (variantesOther(u32)déjà prévues).perf_event_open: le décodage du ring-buffer d’échantillonnage est en couche 1 (logique), pas un syscall manquant.
Répartition des types entre les deux crates
Dans air-sys-types (purs, sans syscall)
BpfInstruction— miroirstruct bpf_insn.BpfMapType,BpfProgramType,BpfAttachType,BpfStatsType,BpfVerifierLogLevel,PerfTypeId,PerfHardwareCounter,PerfEventScope,EventClock(réutilisé dedevicesi pertinent) — enums typés.BpfMapCreateFlags,BpfMapUpdateFlags,BpfMapLookupFlags,BpfProgramLoadFlags,BpfAttachFlags,BpfObjectGetFlags,BpfBtfLoadFlags,PerfEventOpenFlags— bitflags.PerfEventAttr— miroirstruct perf_event_attr(champs aux noms kernel).- Les structs-requête (
BpfMapCreateRequest,BpfProgramLoadRequest,BpfLinkCreateRequest,BpfMapBatchRequest, …) — vues/agrégats purs passés aux wrappers.
Dans air-sys-syscall::ebpf (RAII appelant des syscalls)
BpfMap,BpfProgram,BpfLink,Btf,PerfEvent— possèdent unOwnedFd, ferment auDrop. Même règle queSignalFd/LandlockRuleset/UEventSocket: un type qui appelle un syscall ne vit pas dansair-sys-types.
Soit ~25 types ajoutés à air-sys-types (dont 2 miroirs lourds) + 5 RAII.
Tests (stratégie)
- Cartes — round-trip pur kernel : créer une
Hash/Array,updatepuislookupune clé,get_next_keypour itérer,delete, vérifierENOENT. Déterministe sans programme, mais requiertCAP_BPF→ harnais qui skippe proprement si privilège absent, et le marque enCOVERAGE-EXCEPTIONS.md(catégorie « privilège »). - Programme minimal : charger un programme
SocketFiltertrivial (« retourne 0 ») pré-assemblé en constante de test (quelquesBpfInstructionécrites à la main), vérifier le succès ; charger un programme invalide (registre non initialisé) et vérifier queverifier_logcontient le diagnostic. Cela teste le chemin de log sans assembleur. - Attache/liens : attacher un programme
Tracepoint/Kprobeviaperf_event_open+perf_event_set_bpf_program, déclencher l’événement, vérifier un compteur de carte ;Dropdétache. - Introspection :
*_get_next_id+*_get_fd_by_idsur un objet créé dans le test. - perf : ouvrir un compteur
Software/PERF_COUNT_SW_TASK_CLOCKsur le processus courant,enable, faire du travail,read_count> 0,disable. - Property-based (proptest) : encodage/décodage des miroirs (
BpfInstruction,PerfEventAttr,input-like) round-trip ; tout&[u8]passé en clé/valeur ne panique jamais (validation de longueur en amont). - Fuzzing (
cargo-fuzz) : le décodage des structures d’info retournées parBPF_OBJ_GET_INFO_BY_FDaccepte des données externes (kernel) → harnais de fuzz sur les décodeurs d’info. - Couverture : les branches privilégiées non atteignables en CI non root sont
consignées dans
COVERAGE-EXCEPTIONS.md(catégories « privilège », « feature/kernel »). Validation cross-arch x86_64 + aarch64.
Décisions de fond émergées dans la famille ebpf
1. Couverture exhaustive de bpf(), une fonction par sous-commande.
Les 37 sous-commandes de enum bpf_cmd (6.12) sont toutes exposées en fonctions
dédiées typées. Aucun bpf(cmd, attr, size) générique (ADR-021 conv. 3). La
famille est volumineuse, c’est assumé : la complexité va côté Air.
2. On charge des programmes/BTF déjà formés (frontière couche 1).
Comme seccomp (Q4 famille security), la couche 0 charge un programme ou un
blob BTF déjà assemblé ; elle n’assemble pas, ne compile pas, ne relocalise pas.
L’assembleur, le chargeur libbpf, la génération BTF, le décodage du ring-buffer
perf sont en couche 1.
3. bpf_program_load n’est pas unsafe ; il remplit le log du vérifieur.
Le vérifieur kernel garantit la sûreté mémoire ⇒ pas d’unsafe. Le buffer de log
est un paramètre de premier ordre : sans lui, déboguer un rejet est impossible.
4. Sentinelles kernel → Option/enums partout.
start_id = 0, pid/cpu = -1, key = NULL (première clé), fd de sortie nul :
tous remplacés par Option<T> ou des enums (PerfEventScope), conformément à
l’ADR-021 conv. 1.
5. RAII pour cartes, programmes, liens, BTF, perf_event.
Aucune fuite de FD ; Drop détache les liens. *_get_fd_by_id / obj_get
rendent des FDs ré-adoptables via from_fd.
6. perf_event dans la même famille qu’eBPF.
Parce que perf_event_open + PERF_EVENT_IOC_SET_BPF est le pont d’attache
des programmes de tracing. Les regrouper évite une famille perf orpheline et
reflète l’usage réel.
7. Variantes Other(u32) / #[non_exhaustive] pour la pérennité.
Les enums de types (map/prog/attach/stats) prévoient un repli pour ne pas casser quand un kernel postérieur à 6.12 ajoute une valeur, sans renoncer au typage.
Bilan : la couche 0 est complète
Avec les familles device et ebpf, les onze familles de la couche 0 sont
spécifiées : process, fs, mem, signal, time, net, ipc,
security, system, device, ebpf, plus le module transverse io_uring et
la crate de types air-sys-types. Il ne reste, pour livrer la couche 0
entièrement, que l’implémentation de device, ebpf et des Temps io_uring
restants (2a→4, 3a–3f), selon la méthode squelette-documenté-d’abord.
Licence du document : MPL 2.0
Statut : Spécification technique du module air-sys-syscall::ebpf (couche 0).