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;