Skip to main content

air_sys_syscall/ebpf/
ring_buffer.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! **Le consommateur d'un tampon circulaire BPF** (`BPF_MAP_TYPE_RINGBUF`).
6//!
7//! Un programme eBPF écrit des enregistrements (`bpf_ringbuf_output`) ; l'espace utilisateur
8//! les lit en **projetant** la carte, sans appel système par enregistrement. Le protocole est
9//! celui de `Documentation/bpf/ringbuf.rst` et de `kernel/bpf/ringbuf.c` :
10//!
11//! ```text
12//! offset 0          une page      position CONSOMMÉE (u64) — écrite par nous
13//! offset page       une page      position PRODUITE (u64)  — écrite par le noyau
14//! offset 2 × page   2 × taille    les données, projetées DEUX FOIS à la suite
15//! ```
16//!
17//! La double projection fait qu'un enregistrement à cheval sur la fin de la zone reste
18//! **contigu** : on le lit d'un seul tenant. Chaque enregistrement commence par un en-tête de
19//! 8 octets — sa longueur sur 32 bits, dont deux bits d'état (`BUSY` : en cours d'écriture ;
20//! `DISCARD` : jeté), puis 32 bits propres au noyau —, et sa taille totale est arrondie à 8.
21//!
22//! # Pourquoi en couche 0
23//!
24//! Lire une mémoire que le noyau écrit **en même temps** demande des accès atomiques sur des
25//! adresses brutes : c'est intrinsèquement `unsafe`, et l'`unsafe` d'Air vit en couche 0, nulle
26//! part ailleurs. Au-dessus, [`BpfRingBufferConsumer::consume`] ne rend que des tranches
27//! d'octets déjà publiées.
28//!
29//! # L'ordre des accès
30//!
31//! - la position produite se lit en **acquérir** : ce qui la précède est alors visible ;
32//! - la longueur d'un en-tête se lit en **acquérir** : le noyau l'écrit en **libérer** quand il
33//!   publie l'enregistrement, et c'est ce qui rend ses données visibles ;
34//! - la position consommée s'écrit en **libérer** : le noyau ne réutilise la place qu'après.
35
36use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
37
38use air_sys_types::Errno;
39use air_sys_types::fd::BorrowedFd;
40use air_sys_types::mem::{MapFlags, ProtectionFlags};
41
42use crate::mem::{Mapping, mmap_file};
43
44/// L'enregistrement est en cours d'écriture : on s'arrête avant lui.
45const BUSY: u32 = 1 << 31;
46/// L'enregistrement a été jeté : on le saute sans le rendre.
47const DISCARD: u32 = 1 << 30;
48/// Taille de l'en-tête d'un enregistrement.
49const HEADER_SIZE: u64 = 8;
50/// La plus petite page que le noyau connaisse.
51const MIN_PAGE_SIZE: usize = 4096;
52
53/// **Le consommateur d'un tampon circulaire BPF**, projeté en mémoire.
54#[derive(Debug)]
55pub struct BpfRingBufferConsumer {
56    /// La page de la position consommée, en lecture et écriture.
57    consumer: Mapping,
58    /// La page de la position produite, puis les données projetées deux fois, en lecture seule.
59    producer: Mapping,
60    /// Taille d'une page : le décalage des données dans `producer`.
61    page_size: usize,
62    /// `taille des données - 1` : la taille est une puissance de deux.
63    mask: u64,
64}
65
66impl BpfRingBufferConsumer {
67    /// Projette le tampon `map`, de `data_size` octets de données, sur une machine dont la page
68    /// fait `page_size` octets.
69    ///
70    /// # Errors
71    ///
72    /// `EINVAL` si `page_size` n'est pas une puissance de deux d'au moins 4 Kio, ou si
73    /// `data_size` n'est pas une puissance de deux multiple de la page — ce que le noyau exige
74    /// d'un tampon circulaire ; l'erreur de `mmap(2)` sinon (`map` n'est pas un tampon circulaire,
75    /// taille fausse…).
76    pub fn new(map: BorrowedFd<'_>, data_size: usize, page_size: usize) -> Result<Self, Errno> {
77        let valid = page_size.is_power_of_two()
78            && page_size >= MIN_PAGE_SIZE
79            && data_size.is_power_of_two()
80            && data_size >= page_size;
81        let producer_length = data_size
82            .checked_mul(2)
83            .and_then(|data| data.checked_add(page_size))
84            .filter(|_| valid)
85            .ok_or(Errno::EINVAL)?;
86        // `usize` → `u64` ne perd rien sur les cibles d'Air (64 bits) : le repli n'est jamais pris.
87        let producer_offset = u64::try_from(page_size).unwrap_or(u64::MAX);
88        let mask = u64::try_from(data_size)
89            .unwrap_or(u64::MAX)
90            .saturating_sub(1);
91        let consumer = mmap_file(
92            map,
93            page_size,
94            0,
95            ProtectionFlags::READ | ProtectionFlags::WRITE,
96            MapFlags::SHARED,
97        )?;
98        let producer = mmap_file(
99            map,
100            producer_length,
101            producer_offset,
102            ProtectionFlags::READ,
103            MapFlags::SHARED,
104        )?;
105        Ok(Self {
106            consumer,
107            producer,
108            page_size,
109            mask,
110        })
111    }
112
113    /// **Vide** le tampon : rend chaque enregistrement publié à `handler`, dans l'ordre, puis
114    /// avance la position consommée. S'arrête sur un enregistrement encore en cours d'écriture.
115    /// Rend le nombre d'enregistrements rendus (les jetés ne comptent pas).
116    pub fn consume(&mut self, mut handler: impl FnMut(&[u8])) -> usize {
117        let consumer = self.consumer.as_ptr().cast::<u64>();
118        let producer = self.producer.as_ptr().cast::<u64>();
119        // SAFETY: `consumer` et `producer` sont les débuts de deux projections vivantes, d'au
120        // moins une page chacune, alignées sur la page — donc sur 8 octets ; le noyau y tient un
121        // `u64` qu'il ne lit et n'écrit qu'atomiquement. `data` est le début de la zone de
122        // données, projetée deux fois, soit `2 × (mask + 1)` octets lisibles après lui.
123        unsafe {
124            let data = self.producer.as_ptr().add(self.page_size);
125            drain(
126                AtomicU64::from_ptr(consumer.cast_mut()),
127                AtomicU64::from_ptr(producer.cast_mut()),
128                data,
129                self.mask,
130                &mut handler,
131            )
132        }
133    }
134}
135
136/// Le cœur du protocole, sur des pointeurs : c'est ce qui permet de l'éprouver sur une mémoire
137/// fabriquée, avec des en-têtes que le noyau ne produirait qu'en course (`BUSY`).
138///
139/// Le gestionnaire est un `dyn` : il n'existe qu'**une** copie de ce protocole, quelle que soit
140/// la fermeture de l'appelant — et ce sont ses branches, toutes, que les tests éprouvent.
141///
142/// # Safety
143///
144/// `data` doit désigner `2 × (mask + 1)` octets lisibles, aligné sur 8 ; `mask + 1` doit être
145/// une puissance de deux ; les enregistrements entre `consumer` et `producer` doivent suivre le
146/// protocole du noyau (en-tête aligné sur 8, longueur ne dépassant pas `mask + 1`).
147pub(crate) unsafe fn drain(
148    consumer: &AtomicU64,
149    producer: &AtomicU64,
150    data: *const u8,
151    mask: u64,
152    handler: &mut dyn FnMut(&[u8]),
153) -> usize {
154    let mut delivered = 0_usize;
155    let mut position = consumer.load(Ordering::Acquire);
156    loop {
157        let published = producer.load(Ordering::Acquire);
158        if position >= published {
159            return delivered;
160        }
161        // `position & mask` est au plus `mask`, qui vient d'un `usize` : la conversion tient.
162        let offset = usize::try_from(position & mask).unwrap_or(0);
163        // SAFETY: `offset <= mask`, et la zone fait `2 × (mask + 1)` octets : l'en-tête, aligné
164        // sur 8, y est entier ; le noyau n'y écrit qu'atomiquement.
165        let header = unsafe { AtomicU32::from_ptr(data.add(offset).cast::<u32>().cast_mut()) };
166        let length = header.load(Ordering::Acquire);
167        if length & BUSY != 0 {
168            return delivered;
169        }
170        let size = length & !(BUSY | DISCARD);
171        // Une longueur plus grande que la zone : le protocole est rompu, on n'avance plus.
172        if u64::from(size) > mask {
173            return delivered;
174        }
175        if length & DISCARD == 0 {
176            // `size <= mask`, qui vient d'un `usize` : la conversion tient.
177            let size = usize::try_from(size).unwrap_or(0);
178            // SAFETY: l'enregistrement est publié (ni `BUSY` ni jeté) : le noyau ne l'écrira plus
179            // avant que la position consommée ne l'ait dépassé. Il commence 8 octets après son
180            // en-tête et tient dans la zone grâce à la double projection (`offset + 8 + size
181            // <= 2 × (mask + 1)`).
182            let record = unsafe { core::slice::from_raw_parts(data.add(offset).add(8), size) };
183            handler(record);
184            delivered = delivered.saturating_add(1);
185        }
186        let advance = (u64::from(size)
187            .saturating_add(HEADER_SIZE)
188            .saturating_add(7))
189            & !7;
190        position = position.saturating_add(advance);
191        consumer.store(position, Ordering::Release);
192    }
193}
194
195#[cfg(test)]
196mod tests;