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

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 des struct 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 de BpfInstruction fournie 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’échantillonnage perf/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.

  1. CLOEXEC universel. Tous les FDs eBPF (cartes, programmes, liens, BTF) et les FDs perf_event sont créés CLOEXEC (drapeau BPF_F_*CLOEXEC / PERF_FLAG_FD_CLOEXEC posé par le wrapper).

  2. Pas de syscall multiplexé générique (ADR-021 conv. 3). bpf() multiplexe 37 opérations, ioctl sur perf en multiplexe une douzaine : chacune devient une fonction typée. La complexité est concentrée côté Air, pas côté appelant.

  3. Sentinelles kernel → Option<T> / enums (ADR-021 conv. 1). Les -1 « tous CPU / tout processus » de perf_event_open, les id 0 « début d’itération », les fd optionnels deviennent des Option<T> ou des enums.

  4. RAII pour toutes les ressources. BpfMap, BpfProgram, BpfLink, Btf, PerfEvent possèdent un OwnedFd et le ferment au Drop. Aucune fuite de FD.

  5. EINTR remonté tel quel (ADR-021 conv. 2). Pas de retry automatique.

  6. Privilèges. La plupart des opérations exigent CAP_BPF (+ CAP_PERFMON pour le tracing, CAP_NET_ADMIN pour le réseau) depuis Linux 5.8, ou CAP_SYS_ADMIN avant. Le BPF non privilégié est souvent désactivé (kernel.unprivileged_bpf_disabled). Documenté par fonction ; non masqué.

  7. 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 ⇒ EINVAL cô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, pas BpfMt). Les variantes miroitent les constantes kernel BPF_MAP_TYPE_* / BPF_PROG_TYPE_* (autorité kernel, que le développeur retrouve dans uapi/linux/bpf.h), en CamelCase, avec les abréviations d’usage universel tolérées (Lru, Xdp, Lsm). La variante Other(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. Arraykey_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_load n’est pas unsafe au 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 pas unsafe au sens mémoire, mais leur documentation # Effets rappelle 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égorieFonctions principales
Cartes — viebpf_map_create, bpf_map_freeze
Cartes — élémentsbpf_map_lookup_element, bpf_map_update_element, bpf_map_delete_element, bpf_map_get_next_key, bpf_map_lookup_and_delete_element
Cartes — batchbpf_map_lookup_batch, bpf_map_lookup_and_delete_batch, bpf_map_update_batch, bpf_map_delete_batch
Programmesbpf_program_load, bpf_program_test_run, bpf_program_bind_map
Attache / liensbpf_program_attach, bpf_program_detach, bpf_link_create, bpf_link_update, bpf_link_detach, bpf_raw_tracepoint_open, bpf_iterator_create
Épinglagebpf_object_pin, bpf_object_get
Introspectionbpf_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
BTFbpf_btf_load
Stats / jetonsbpf_enable_statistics, bpf_token_create
perf — vieperf_event_open, PerfEvent::read_count
perf — contrôleperf_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 (variantes Other(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 — miroir struct bpf_insn.
  • BpfMapType, BpfProgramType, BpfAttachType, BpfStatsType, BpfVerifierLogLevel, PerfTypeId, PerfHardwareCounter, PerfEventScope, EventClock (réutilisé de device si pertinent) — enums typés.
  • BpfMapCreateFlags, BpfMapUpdateFlags, BpfMapLookupFlags, BpfProgramLoadFlags, BpfAttachFlags, BpfObjectGetFlags, BpfBtfLoadFlags, PerfEventOpenFlags — bitflags.
  • PerfEventAttr — miroir struct 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 un OwnedFd, ferment au Drop. Même règle que SignalFd/LandlockRuleset/UEventSocket : un type qui appelle un syscall ne vit pas dans air-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, update puis lookup une clé, get_next_key pour itérer, delete, vérifier ENOENT. Déterministe sans programme, mais requiert CAP_BPF → harnais qui skippe proprement si privilège absent, et le marque en COVERAGE-EXCEPTIONS.md (catégorie « privilège »).
  • Programme minimal : charger un programme SocketFilter trivial (« retourne 0 ») pré-assemblé en constante de test (quelques BpfInstruction écrites à la main), vérifier le succès ; charger un programme invalide (registre non initialisé) et vérifier que verifier_log contient le diagnostic. Cela teste le chemin de log sans assembleur.
  • Attache/liens : attacher un programme Tracepoint/Kprobe via perf_event_open + perf_event_set_bpf_program, déclencher l’événement, vérifier un compteur de carte ; Drop détache.
  • Introspection : *_get_next_id + *_get_fd_by_id sur un objet créé dans le test.
  • perf : ouvrir un compteur Software/PERF_COUNT_SW_TASK_CLOCK sur 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 par BPF_OBJ_GET_INFO_BY_FD accepte 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).