Skip to main content

air_sys_syscall/
ebpf.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//! Wrappers de la famille `ebpf`.
6//!
7//! Cf. `docs/specs/layer-0/family-ebpf.md`. Couverture **exhaustive** des 37
8//! sous-commandes de `enum bpf_cmd` (cible 6.12), chacune en fonction dédiée
9//! typée (jamais de `bpf(cmd, attr, size)` générique, ADR-021 conv. 3), plus
10//! `perf_event_open` et ses 12 ioctls dédiés.
11//!
12//! On **charge** des programmes/BTF déjà assemblés ; on n'assemble ni ne
13//! compile rien (logique → couche 1). RAII pour cartes/programmes/liens/BTF/
14//! perf_event (Drop ferme le FD ; un `BpfLink` détache au Drop). EINTR remonté
15//! tel quel. La plupart des opérations exigent `CAP_BPF`/`CAP_PERFMON`.
16
17use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
18use core::ffi::CStr;
19use core::num::NonZeroI32;
20
21use air_sys_types::{
22    BpfAttachFlags, BpfAttachType, BpfBtfLoadFlags, BpfLinkCreateRequest, BpfMapBatchInput,
23    BpfMapBatchRequest, BpfMapCreateRequest, BpfMapLookupFlags, BpfMapUpdateFlags,
24    BpfObjectGetFlags, BpfProgramLoadRequest, BpfProgramQueryRequest, BpfProgramTestRunRequest,
25    BpfProgramTestRunResult, BpfStatsType, BpfTaskFdQueryRequest, Errno, PerfEventAttr,
26    PerfEventOpenFlags, PerfEventScope,
27};
28
29#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
30compile_error!("air-sys-syscall::ebpf supporte uniquement x86_64 et aarch64 (ADR-014).");
31
32// ─────────────────────────────────────────────────────────────────────────
33// Numéros de sous-commande `bpf_cmd` (6.12).
34// ─────────────────────────────────────────────────────────────────────────
35
36const BPF_MAP_CREATE: i32 = 0;
37const BPF_MAP_LOOKUP_ELEM: i32 = 1;
38const BPF_MAP_UPDATE_ELEM: i32 = 2;
39const BPF_MAP_DELETE_ELEM: i32 = 3;
40const BPF_MAP_GET_NEXT_KEY: i32 = 4;
41const BPF_PROG_LOAD: i32 = 5;
42const BPF_OBJ_PIN: i32 = 6;
43const BPF_OBJ_GET: i32 = 7;
44const BPF_PROG_ATTACH: i32 = 8;
45const BPF_PROG_DETACH: i32 = 9;
46const BPF_PROG_TEST_RUN: i32 = 10;
47const BPF_PROG_GET_NEXT_ID: i32 = 11;
48const BPF_MAP_GET_NEXT_ID: i32 = 12;
49const BPF_PROG_GET_FD_BY_ID: i32 = 13;
50const BPF_MAP_GET_FD_BY_ID: i32 = 14;
51const BPF_OBJ_GET_INFO_BY_FD: i32 = 15;
52const BPF_PROG_QUERY: i32 = 16;
53const BPF_RAW_TRACEPOINT_OPEN: i32 = 17;
54const BPF_BTF_LOAD: i32 = 18;
55const BPF_BTF_GET_FD_BY_ID: i32 = 19;
56const BPF_TASK_FD_QUERY: i32 = 20;
57const BPF_MAP_LOOKUP_AND_DELETE_ELEM: i32 = 21;
58const BPF_MAP_FREEZE: i32 = 22;
59const BPF_BTF_GET_NEXT_ID: i32 = 23;
60const BPF_MAP_LOOKUP_BATCH: i32 = 24;
61const BPF_MAP_LOOKUP_AND_DELETE_BATCH: i32 = 25;
62const BPF_MAP_UPDATE_BATCH: i32 = 26;
63const BPF_MAP_DELETE_BATCH: i32 = 27;
64const BPF_LINK_CREATE: i32 = 28;
65const BPF_LINK_UPDATE: i32 = 29;
66const BPF_LINK_GET_FD_BY_ID: i32 = 30;
67const BPF_LINK_GET_NEXT_ID: i32 = 31;
68const BPF_ENABLE_STATS: i32 = 32;
69const BPF_ITER_CREATE: i32 = 33;
70const BPF_LINK_DETACH: i32 = 34;
71const BPF_PROG_BIND_MAP: i32 = 35;
72const BPF_TOKEN_CREATE: i32 = 36;
73
74/// `BPF_F_*CLOEXEC` n'existe pas pour bpf() ; les FDs bpf sont CLOEXEC par
75/// défaut côté kernel depuis l'origine. Pour `open_flags`, on ne pose rien.
76const BPF_OBJ_NAME_LEN: usize = 16;
77
78// ─────────────────────────────────────────────────────────────────────────
79// Handles RAII.
80// ─────────────────────────────────────────────────────────────────────────
81
82macro_rules! raii_handle {
83    ($(#[$meta:meta])* $name:ident) => {
84        $(#[$meta])*
85        #[derive(Debug)]
86        pub struct $name(OwnedFd);
87
88        impl $name {
89            /// Ré-adopte un `OwnedFd` (p. ex. issu de `*_get_fd_by_id`).
90            #[must_use]
91            #[inline]
92            pub const fn from_fd(fd: OwnedFd) -> Self {
93                Self(fd)
94            }
95
96            /// Vue empruntée du FD sous-jacent.
97            #[must_use]
98            pub fn as_fd(&self) -> BorrowedFd<'_> {
99                use air_sys_types::fd::AsFd;
100                self.0.as_fd()
101            }
102
103            /// Consomme et restitue le `OwnedFd`.
104            #[must_use]
105            pub fn into_fd(self) -> OwnedFd {
106                self.0
107            }
108        }
109    };
110}
111
112raii_handle!(
113    /// Carte eBPF (`BpfMap`). Drop ferme le FD.
114    BpfMap
115);
116raii_handle!(
117    /// Programme eBPF chargé. Drop ferme le FD.
118    BpfProgram
119);
120raii_handle!(
121    /// Lien d'attache moderne (`bpf_link`). Drop **détache** (ferme le FD).
122    BpfLink
123);
124raii_handle!(
125    /// Objet BTF chargé. Drop ferme le FD.
126    Btf
127);
128raii_handle!(
129    /// Compteur/échantillonneur `perf_event`. Drop ferme le FD.
130    PerfEvent
131);
132
133impl PerfEvent {
134    /// Lit la valeur scalaire du compteur (`read_format` simple).
135    ///
136    /// # Errors
137    ///
138    /// - [`Errno::EINVAL`] : lecture incohérente avec le `read_format`.
139    /// - [`Errno::EBADF`] : FD invalide.
140    pub fn read_count(&self) -> Result<u64, Errno> {
141        let mut buf = [0_u8; 8];
142        // SAFETY: read(fd_perf, buf, 8) écrit 8 octets (compteur u64) dans
143        // `buf` local, ou retourne un errno.
144        let ret = unsafe { raw_syscall_read(self.0.as_raw_fd(), buf.as_mut_ptr() as u64, 8) };
145        if ret < 0 {
146            return Err(errno_from_negative_syscall_ret(ret));
147        }
148        Ok(u64::from_ne_bytes(buf))
149    }
150}
151
152// ─────────────────────────────────────────────────────────────────────────
153// Encodage de `bpf_attr` (tampon zéro-initialisé, écriture par offset).
154// ─────────────────────────────────────────────────────────────────────────
155
156/// Tampon `bpf_attr` : 192 octets couvrent la plus grande sous-commande
157/// utilisée. Déclarer une taille **inférieure** au syscall est toujours sûr
158/// (le kernel complète par des zéros) ; on ne déclare jamais plus que ce qu'on
159/// écrit.
160struct Attr {
161    bytes: [u8; 192],
162}
163
164impl Attr {
165    const fn new() -> Self {
166        Self { bytes: [0_u8; 192] }
167    }
168
169    fn put_u32(&mut self, offset: usize, value: u32) {
170        let end = offset.checked_add(4).expect("offset attr borné");
171        if let Some(slot) = self.bytes.get_mut(offset..end) {
172            slot.copy_from_slice(&value.to_ne_bytes());
173        }
174    }
175
176    fn put_u64(&mut self, offset: usize, value: u64) {
177        let end = offset.checked_add(8).expect("offset attr borné");
178        if let Some(slot) = self.bytes.get_mut(offset..end) {
179            slot.copy_from_slice(&value.to_ne_bytes());
180        }
181    }
182
183    fn read_u32(&self, offset: usize) -> u32 {
184        let end = offset.checked_add(4).unwrap_or(self.bytes.len());
185        match self.bytes.get(offset..end).and_then(|s| s.try_into().ok()) {
186            Some(array) => u32::from_ne_bytes(array),
187            None => 0,
188        }
189    }
190
191    /// Écrit `name` (tronqué à 15 octets + NUL) à `offset`.
192    fn put_name(&mut self, offset: usize, name: &CStr) {
193        let raw = name.to_bytes();
194        let take = raw.len().min(BPF_OBJ_NAME_LEN.saturating_sub(1));
195        let end = offset.checked_add(take).expect("offset nom borné");
196        if let (Some(dst), Some(src)) = (self.bytes.get_mut(offset..end), raw.get(..take)) {
197            dst.copy_from_slice(src);
198        }
199    }
200
201    /// Lance `bpf(cmd, &attr, size)`.
202    fn call(&mut self, cmd: i32, size: usize) -> i64 {
203        let size_u32 = u32::try_from(size).expect("taille attr ≤ 192");
204        // SAFETY: bpf(cmd, ptr, size) lit/écrit `size` octets à `ptr` (≤ 192,
205        // tampon local). Les pointeurs internes (clé/valeur/insns) pointent
206        // vers des zones fournies par l'appelant, vivantes pour l'appel.
207        unsafe { raw_syscall_bpf(cmd, self.bytes.as_mut_ptr() as u64, size_u32) }
208    }
209}
210
211/// FD `i32` (toujours ≥ 0 pour un fd ouvert) → bits `u32` du champ `bpf_attr`.
212fn fd_bits(fd: BorrowedFd<'_>) -> u32 {
213    u32::from_ne_bytes(fd.as_raw_fd().to_ne_bytes())
214}
215
216fn owned_from_ret(ret: i64) -> Result<OwnedFd, Errno> {
217    if ret < 0 {
218        return Err(errno_from_negative_syscall_ret(ret));
219    }
220    let raw = i32::try_from(ret).map_err(|_| Errno::EINVAL)?;
221    // SAFETY: le kernel vient de transférer la propriété d'un fd valide.
222    Ok(unsafe { OwnedFd::from_raw_fd(raw) })
223}
224
225// ─────────────────────────────────────────────────────────────────────────
226// Cartes — création et gel.
227// ─────────────────────────────────────────────────────────────────────────
228
229/// `BPF_MAP_CREATE` — crée une carte eBPF.
230///
231/// # Errors
232///
233/// - [`Errno::EPERM`] / [`Errno::EACCES`] : privilège manquant (`CAP_BPF`).
234/// - [`Errno::EINVAL`] : géométrie incohérente avec `map_type`.
235/// - [`Errno::E2BIG`] / [`Errno::ENOMEM`] : carte trop grande.
236pub fn bpf_map_create(request: &BpfMapCreateRequest<'_>) -> Result<BpfMap, Errno> {
237    let mut attr = Attr::new();
238    attr.put_u32(0, request.map_type.to_raw());
239    attr.put_u32(4, request.key_size);
240    attr.put_u32(8, request.value_size);
241    attr.put_u32(12, request.max_entries);
242    attr.put_u32(16, request.flags.bits());
243    if let Some(inner) = request.inner_map {
244        attr.put_u32(20, fd_bits(inner));
245    }
246    if let Some(node) = request.numa_node {
247        attr.put_u32(24, node);
248    }
249    if let Some(name) = request.name {
250        attr.put_name(28, name);
251    }
252    if let Some(btf) = request.btf {
253        attr.put_u32(48, fd_bits(btf.btf_fd));
254        attr.put_u32(52, btf.key_type_id);
255        attr.put_u32(56, btf.value_type_id);
256    }
257    owned_from_ret(attr.call(BPF_MAP_CREATE, 64)).map(BpfMap::from_fd)
258}
259
260/// `BPF_MAP_FREEZE` — rend la carte non modifiable depuis l'espace utilisateur.
261///
262/// # Errors
263///
264/// - [`Errno::EPERM`] : privilège manquant ou déjà gelée.
265pub fn bpf_map_freeze(map: BorrowedFd<'_>) -> Result<(), Errno> {
266    let mut attr = Attr::new();
267    attr.put_u32(0, fd_bits(map));
268    ret_unit(attr.call(BPF_MAP_FREEZE, 4))
269}
270
271// ─────────────────────────────────────────────────────────────────────────
272// Cartes — accès élément par élément.
273// ─────────────────────────────────────────────────────────────────────────
274
275/// `BPF_MAP_LOOKUP_ELEM` — lit la valeur associée à `key` dans `value_out`.
276///
277/// `value_out.len()` doit valoir `value_size` (ou `value_size *
278/// num_possible_cpus` pour les cartes per-CPU) ; un mismatch ⇒ [`Errno::EINVAL`]
279/// remonté du kernel.
280///
281/// # Errors
282///
283/// - [`Errno::ENOENT`] : clé absente.
284/// - [`Errno::EINVAL`] : taille de clé/valeur incohérente.
285pub fn bpf_map_lookup_element(
286    map: BorrowedFd<'_>,
287    key: &[u8],
288    value_out: &mut [u8],
289    flags: BpfMapLookupFlags,
290) -> Result<(), Errno> {
291    let mut attr = Attr::new();
292    attr.put_u32(0, fd_bits(map));
293    attr.put_u64(8, key.as_ptr() as u64);
294    attr.put_u64(16, value_out.as_mut_ptr() as u64);
295    attr.put_u64(24, flags.bits());
296    ret_unit(attr.call(BPF_MAP_LOOKUP_ELEM, 32))
297}
298
299/// `BPF_MAP_UPDATE_ELEM` — insère ou remplace la paire `(key, value)`.
300///
301/// # Errors
302///
303/// - [`Errno::E2BIG`] : carte pleine (`max_entries` atteint).
304/// - [`Errno::EEXIST`] / [`Errno::ENOENT`] : violation de `flags`.
305pub fn bpf_map_update_element(
306    map: BorrowedFd<'_>,
307    key: &[u8],
308    value: &[u8],
309    flags: BpfMapUpdateFlags,
310) -> Result<(), Errno> {
311    let mut attr = Attr::new();
312    attr.put_u32(0, fd_bits(map));
313    attr.put_u64(8, key.as_ptr() as u64);
314    attr.put_u64(16, value.as_ptr() as u64);
315    attr.put_u64(24, flags.bits());
316    ret_unit(attr.call(BPF_MAP_UPDATE_ELEM, 32))
317}
318
319/// `BPF_MAP_DELETE_ELEM` — supprime l'entrée `key`.
320///
321/// # Errors
322///
323/// - [`Errno::ENOENT`] : clé absente.
324pub fn bpf_map_delete_element(map: BorrowedFd<'_>, key: &[u8]) -> Result<(), Errno> {
325    let mut attr = Attr::new();
326    attr.put_u32(0, fd_bits(map));
327    attr.put_u64(8, key.as_ptr() as u64);
328    ret_unit(attr.call(BPF_MAP_DELETE_ELEM, 24))
329}
330
331/// `BPF_MAP_GET_NEXT_KEY` — itère les clés. `key = None` ⇒ première clé.
332///
333/// Retourne `false` à la fin de l'itération (le kernel renvoie `ENOENT`).
334///
335/// # Errors
336///
337/// - [`Errno::EINVAL`] : taille de clé incohérente.
338pub fn bpf_map_get_next_key(
339    map: BorrowedFd<'_>,
340    key: Option<&[u8]>,
341    next_key_out: &mut [u8],
342) -> Result<bool, Errno> {
343    let mut attr = Attr::new();
344    attr.put_u32(0, fd_bits(map));
345    if let Some(k) = key {
346        attr.put_u64(8, k.as_ptr() as u64);
347    }
348    attr.put_u64(16, next_key_out.as_mut_ptr() as u64);
349    let ret = attr.call(BPF_MAP_GET_NEXT_KEY, 24);
350    if ret < 0 {
351        let err = errno_from_negative_syscall_ret(ret);
352        if err == Errno::ENOENT {
353            return Ok(false);
354        }
355        return Err(err);
356    }
357    Ok(true)
358}
359
360/// `BPF_MAP_LOOKUP_AND_DELETE_ELEM` — lit puis retire (cartes Queue/Stack).
361///
362/// # Errors
363///
364/// - [`Errno::ENOENT`] : carte vide / clé absente.
365pub fn bpf_map_lookup_and_delete_element(
366    map: BorrowedFd<'_>,
367    key: Option<&[u8]>,
368    value_out: &mut [u8],
369    flags: BpfMapLookupFlags,
370) -> Result<(), Errno> {
371    let mut attr = Attr::new();
372    attr.put_u32(0, fd_bits(map));
373    if let Some(k) = key {
374        attr.put_u64(8, k.as_ptr() as u64);
375    }
376    attr.put_u64(16, value_out.as_mut_ptr() as u64);
377    attr.put_u64(24, flags.bits());
378    ret_unit(attr.call(BPF_MAP_LOOKUP_AND_DELETE_ELEM, 32))
379}
380
381// ─────────────────────────────────────────────────────────────────────────
382// Cartes — opérations par lot (batch).
383// ─────────────────────────────────────────────────────────────────────────
384
385fn batch_in(
386    cmd: i32,
387    map: BorrowedFd<'_>,
388    request: &mut BpfMapBatchRequest<'_>,
389) -> Result<u32, Errno> {
390    let mut attr = Attr::new();
391    if let Some(cursor) = request.in_batch {
392        attr.put_u64(0, cursor);
393    }
394    attr.put_u64(8, (&raw mut request.out_batch) as u64);
395    attr.put_u64(16, request.keys.as_mut_ptr() as u64);
396    attr.put_u64(24, request.values.as_mut_ptr() as u64);
397    attr.put_u32(32, request.count);
398    attr.put_u32(36, fd_bits(map));
399    attr.put_u64(40, request.elem_flags);
400    attr.put_u64(48, request.flags);
401    let ret = attr.call(cmd, 56);
402    if ret < 0 {
403        return Err(errno_from_negative_syscall_ret(ret));
404    }
405    Ok(attr.read_u32(32))
406}
407
408/// `BPF_MAP_LOOKUP_BATCH` — lit un lot d'éléments. Retourne le nombre traité.
409///
410/// # Errors
411///
412/// - [`Errno::ENOENT`] : fin d'itération.
413pub fn bpf_map_lookup_batch(
414    map: BorrowedFd<'_>,
415    request: &mut BpfMapBatchRequest<'_>,
416) -> Result<u32, Errno> {
417    batch_in(BPF_MAP_LOOKUP_BATCH, map, request)
418}
419
420/// `BPF_MAP_LOOKUP_AND_DELETE_BATCH` — lit et retire un lot.
421///
422/// # Errors
423///
424/// - [`Errno::ENOENT`] : fin d'itération.
425pub fn bpf_map_lookup_and_delete_batch(
426    map: BorrowedFd<'_>,
427    request: &mut BpfMapBatchRequest<'_>,
428) -> Result<u32, Errno> {
429    batch_in(BPF_MAP_LOOKUP_AND_DELETE_BATCH, map, request)
430}
431
432fn batch_out(cmd: i32, map: BorrowedFd<'_>, request: &BpfMapBatchInput<'_>) -> Result<u32, Errno> {
433    let mut attr = Attr::new();
434    attr.put_u64(16, request.keys.as_ptr() as u64);
435    attr.put_u64(24, request.values.as_ptr() as u64);
436    attr.put_u32(32, request.count);
437    attr.put_u32(36, fd_bits(map));
438    attr.put_u64(40, request.elem_flags);
439    attr.put_u64(48, request.flags);
440    let ret = attr.call(cmd, 56);
441    if ret < 0 {
442        return Err(errno_from_negative_syscall_ret(ret));
443    }
444    Ok(attr.read_u32(32))
445}
446
447/// `BPF_MAP_UPDATE_BATCH` — insère/remplace un lot. Retourne le nombre traité.
448///
449/// # Errors
450///
451/// - [`Errno::E2BIG`] : carte pleine.
452pub fn bpf_map_update_batch(
453    map: BorrowedFd<'_>,
454    request: &BpfMapBatchInput<'_>,
455) -> Result<u32, Errno> {
456    batch_out(BPF_MAP_UPDATE_BATCH, map, request)
457}
458
459/// `BPF_MAP_DELETE_BATCH` — supprime un lot de clés. Retourne le nombre traité.
460///
461/// # Errors
462///
463/// - [`Errno::ENOENT`] : clé absente.
464pub fn bpf_map_delete_batch(
465    map: BorrowedFd<'_>,
466    request: &BpfMapBatchInput<'_>,
467) -> Result<u32, Errno> {
468    batch_out(BPF_MAP_DELETE_BATCH, map, request)
469}
470
471// ─────────────────────────────────────────────────────────────────────────
472// Programmes.
473// ─────────────────────────────────────────────────────────────────────────
474
475/// `BPF_PROG_LOAD` — charge un programme **déjà assemblé**.
476///
477/// En cas de rejet du vérifieur, `verifier_log` (s'il est fourni) est rempli
478/// avec le diagnostic textuel — indispensable au débogage.
479///
480/// `bpf_program_load` n'est **pas** `unsafe` : le vérifieur kernel garantit la
481/// sûreté mémoire du programme chargé.
482///
483/// # Errors
484///
485/// - [`Errno::EACCES`] / [`Errno::EINVAL`] : rejet du vérifieur (voir log).
486/// - [`Errno::EPERM`] : privilège manquant.
487/// - [`Errno::ENOSPC`] : buffer de log trop court.
488pub fn bpf_program_load(
489    request: &BpfProgramLoadRequest<'_>,
490    verifier_log: Option<&mut [u8]>,
491) -> Result<BpfProgram, Errno> {
492    let mut attr = Attr::new();
493    attr.put_u32(0, request.program_type.to_raw());
494    let insn_count = u32::try_from(request.instructions.len()).map_err(|_| Errno::EINVAL)?;
495    attr.put_u32(4, insn_count);
496    attr.put_u64(8, request.instructions.as_ptr() as u64);
497    attr.put_u64(16, request.license.as_ptr() as u64);
498    attr.put_u32(24, request.log_level.to_raw());
499    if let Some(log) = verifier_log {
500        let log_size = u32::try_from(log.len()).unwrap_or(u32::MAX);
501        // log_level doit être non nul pour que le kernel écrive le log.
502        if request.log_level.to_raw() == 0 {
503            attr.put_u32(24, 1);
504        }
505        attr.put_u32(28, log_size);
506        attr.put_u64(32, log.as_mut_ptr() as u64);
507    }
508    attr.put_u32(44, request.flags.bits());
509    if let Some(name) = request.name {
510        attr.put_name(48, name);
511    }
512    if let Some(attach) = request.expected_attach_type {
513        attr.put_u32(68, attach.to_raw());
514    }
515    if let Some(btf) = request.attach_btf {
516        attr.put_u32(72, fd_bits(btf));
517    }
518    if let Some(btf_id) = request.attach_btf_id {
519        attr.put_u32(108, btf_id);
520    }
521    if let Some(prog) = request.attach_program {
522        attr.put_u32(112, fd_bits(prog));
523    }
524    owned_from_ret(attr.call(BPF_PROG_LOAD, 120)).map(BpfProgram::from_fd)
525}
526
527/// `BPF_PROG_TEST_RUN` — exécute le programme sur des données fournies.
528///
529/// # Errors
530///
531/// - [`Errno::EINVAL`] : contexte/données incohérents avec le type.
532/// - [`Errno::ENOTSUPP`] (remonté) : type non testable.
533pub fn bpf_program_test_run(
534    program: BorrowedFd<'_>,
535    request: &mut BpfProgramTestRunRequest<'_>,
536) -> Result<BpfProgramTestRunResult, Errno> {
537    // Un tampon optionnel **vide** est passé en pointeur NULL : le kernel
538    // distingue « absent » (NULL) de « présent, taille 0 » (pointeur non nul),
539    // ce dernier étant rejeté par `EINVAL` pour le contexte.
540    let ptr_or_null = |slice: &[u8]| -> u64 {
541        if slice.is_empty() {
542            0
543        } else {
544            slice.as_ptr() as u64
545        }
546    };
547    let mut attr = Attr::new();
548    attr.put_u32(0, fd_bits(program));
549    attr.put_u32(8, u32::try_from(request.data_in.len()).unwrap_or(u32::MAX));
550    attr.put_u32(
551        12,
552        u32::try_from(request.data_out.len()).unwrap_or(u32::MAX),
553    );
554    attr.put_u64(16, ptr_or_null(request.data_in));
555    attr.put_u64(24, ptr_or_null(request.data_out));
556    attr.put_u32(32, request.repeat);
557    attr.put_u32(
558        40,
559        u32::try_from(request.context_in.len()).unwrap_or(u32::MAX),
560    );
561    attr.put_u32(
562        44,
563        u32::try_from(request.context_out.len()).unwrap_or(u32::MAX),
564    );
565    attr.put_u64(48, ptr_or_null(request.context_in));
566    attr.put_u64(56, ptr_or_null(request.context_out));
567    attr.put_u32(64, request.flags);
568    let ret = attr.call(BPF_PROG_TEST_RUN, 72);
569    if ret < 0 {
570        return Err(errno_from_negative_syscall_ret(ret));
571    }
572    Ok(BpfProgramTestRunResult {
573        return_value: attr.read_u32(4),
574        duration: attr.read_u32(36),
575        data_size_out: attr.read_u32(12),
576        context_size_out: attr.read_u32(44),
577    })
578}
579
580/// `BPF_PROG_BIND_MAP` — lie une carte à un programme (durée de vie).
581///
582/// # Errors
583///
584/// - [`Errno::EBADF`] : fd invalide.
585pub fn bpf_program_bind_map(
586    program: BorrowedFd<'_>,
587    map: BorrowedFd<'_>,
588    flags: u32,
589) -> Result<(), Errno> {
590    let mut attr = Attr::new();
591    attr.put_u32(0, fd_bits(program));
592    attr.put_u32(4, fd_bits(map));
593    attr.put_u32(8, flags);
594    ret_unit(attr.call(BPF_PROG_BIND_MAP, 12))
595}
596
597// ─────────────────────────────────────────────────────────────────────────
598// Attache et liens.
599// ─────────────────────────────────────────────────────────────────────────
600
601/// `BPF_PROG_ATTACH` — attache historique (cgroup, sk_skb, flow_dissector…).
602///
603/// # Effets
604///
605/// Modifie le comportement du système (filtrage, hooks). Orchestré sous
606/// entitlements par la couche 5 (ADR-010).
607///
608/// # Errors
609///
610/// - [`Errno::EPERM`] : privilège manquant.
611/// - [`Errno::EINVAL`] : type d'attache incohérent avec la cible.
612pub fn bpf_program_attach(
613    program: BorrowedFd<'_>,
614    target: BorrowedFd<'_>,
615    attach_type: BpfAttachType,
616    flags: BpfAttachFlags,
617) -> Result<(), Errno> {
618    let mut attr = Attr::new();
619    attr.put_u32(0, fd_bits(target));
620    attr.put_u32(4, fd_bits(program));
621    attr.put_u32(8, attach_type.to_raw());
622    attr.put_u32(12, flags.bits());
623    ret_unit(attr.call(BPF_PROG_ATTACH, 16))
624}
625
626/// `BPF_PROG_DETACH` — détache un programme historique.
627///
628/// # Errors
629///
630/// - [`Errno::ENOENT`] : rien d'attaché.
631pub fn bpf_program_detach(
632    target: BorrowedFd<'_>,
633    attach_type: BpfAttachType,
634    program: Option<BorrowedFd<'_>>,
635) -> Result<(), Errno> {
636    let mut attr = Attr::new();
637    attr.put_u32(0, fd_bits(target));
638    if let Some(prog) = program {
639        attr.put_u32(4, fd_bits(prog));
640    }
641    attr.put_u32(8, attach_type.to_raw());
642    ret_unit(attr.call(BPF_PROG_DETACH, 16))
643}
644
645/// `BPF_LINK_CREATE` — crée un lien moderne (FD RAII, détache au Drop).
646///
647/// # Effets
648///
649/// Voir [`bpf_program_attach`].
650///
651/// # Errors
652///
653/// - [`Errno::EPERM`] : privilège manquant.
654/// - [`Errno::EINVAL`] : attache incohérente.
655pub fn bpf_link_create(request: &BpfLinkCreateRequest<'_>) -> Result<BpfLink, Errno> {
656    let mut attr = Attr::new();
657    attr.put_u32(0, fd_bits(request.program));
658    if let Some(target) = request.target {
659        attr.put_u32(4, fd_bits(target));
660    }
661    attr.put_u32(8, request.attach_type.to_raw());
662    attr.put_u32(12, request.flags);
663    owned_from_ret(attr.call(BPF_LINK_CREATE, 16)).map(BpfLink::from_fd)
664}
665
666/// `BPF_LINK_UPDATE` — remplace à chaud le programme d'un lien.
667///
668/// # Errors
669///
670/// - [`Errno::EINVAL`] : programme incompatible.
671pub fn bpf_link_update(
672    link: BorrowedFd<'_>,
673    new_program: BorrowedFd<'_>,
674    old_program: Option<BorrowedFd<'_>>,
675    flags: u32,
676) -> Result<(), Errno> {
677    let mut attr = Attr::new();
678    attr.put_u32(0, fd_bits(link));
679    attr.put_u32(4, fd_bits(new_program));
680    attr.put_u32(8, flags);
681    if let Some(old) = old_program {
682        attr.put_u32(12, fd_bits(old));
683    }
684    ret_unit(attr.call(BPF_LINK_UPDATE, 16))
685}
686
687/// `BPF_LINK_DETACH` — détache le programme en gardant le lien.
688///
689/// # Errors
690///
691/// - [`Errno::EBADF`] : fd invalide.
692pub fn bpf_link_detach(link: BorrowedFd<'_>) -> Result<(), Errno> {
693    let mut attr = Attr::new();
694    attr.put_u32(0, fd_bits(link));
695    ret_unit(attr.call(BPF_LINK_DETACH, 4))
696}
697
698/// `BPF_RAW_TRACEPOINT_OPEN` — attache un programme à un tracepoint brut.
699///
700/// # Errors
701///
702/// - [`Errno::ENOENT`] : tracepoint inconnu.
703/// - [`Errno::EPERM`] : privilège manquant.
704pub fn bpf_raw_tracepoint_open(name: &CStr, program: BorrowedFd<'_>) -> Result<BpfLink, Errno> {
705    let mut attr = Attr::new();
706    attr.put_u64(0, name.as_ptr() as u64);
707    attr.put_u32(8, fd_bits(program));
708    owned_from_ret(attr.call(BPF_RAW_TRACEPOINT_OPEN, 12)).map(BpfLink::from_fd)
709}
710
711/// `BPF_ITER_CREATE` — crée un itérateur depuis un lien d'itération.
712///
713/// Retourne un FD lisible produisant la sortie de l'itérateur.
714///
715/// # Errors
716///
717/// - [`Errno::EINVAL`] : le lien n'est pas un lien d'itération.
718pub fn bpf_iterator_create(link: BorrowedFd<'_>, flags: u32) -> Result<OwnedFd, Errno> {
719    let mut attr = Attr::new();
720    attr.put_u32(0, fd_bits(link));
721    attr.put_u32(4, flags);
722    owned_from_ret(attr.call(BPF_ITER_CREATE, 8))
723}
724
725// ─────────────────────────────────────────────────────────────────────────
726// Épinglage (bpffs).
727// ─────────────────────────────────────────────────────────────────────────
728
729/// `BPF_OBJ_PIN` — épingle un objet sous un chemin `bpffs`.
730///
731/// # Errors
732///
733/// - [`Errno::EPERM`] : privilège manquant.
734/// - [`Errno::EEXIST`] : chemin déjà occupé.
735pub fn bpf_object_pin(object: BorrowedFd<'_>, path: &CStr) -> Result<(), Errno> {
736    let mut attr = Attr::new();
737    attr.put_u64(0, path.as_ptr() as u64);
738    attr.put_u32(8, fd_bits(object));
739    ret_unit(attr.call(BPF_OBJ_PIN, 16))
740}
741
742/// `BPF_OBJ_GET` — récupère un FD vers un objet épinglé.
743///
744/// # Errors
745///
746/// - [`Errno::ENOENT`] : chemin inexistant.
747pub fn bpf_object_get(path: &CStr, flags: BpfObjectGetFlags) -> Result<OwnedFd, Errno> {
748    let mut attr = Attr::new();
749    attr.put_u64(0, path.as_ptr() as u64);
750    attr.put_u32(12, flags.bits());
751    owned_from_ret(attr.call(BPF_OBJ_GET, 16))
752}
753
754// ─────────────────────────────────────────────────────────────────────────
755// Introspection par identifiant.
756// ─────────────────────────────────────────────────────────────────────────
757
758fn get_next_id(cmd: i32, after: Option<u32>) -> Result<Option<u32>, Errno> {
759    let mut attr = Attr::new();
760    attr.put_u32(0, after.unwrap_or(0));
761    let ret = attr.call(cmd, 12);
762    if ret < 0 {
763        let err = errno_from_negative_syscall_ret(ret);
764        if err == Errno::ENOENT {
765            return Ok(None);
766        }
767        return Err(err);
768    }
769    Ok(Some(attr.read_u32(4)))
770}
771
772/// `BPF_PROG_GET_NEXT_ID` — id de programme suivant (`None` = fin).
773///
774/// # Errors
775///
776/// - [`Errno::EPERM`] : privilège manquant.
777pub fn bpf_program_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno> {
778    get_next_id(BPF_PROG_GET_NEXT_ID, after)
779}
780
781/// `BPF_MAP_GET_NEXT_ID` — id de carte suivant (`None` = fin).
782///
783/// # Errors
784///
785/// - [`Errno::EPERM`] : privilège manquant.
786pub fn bpf_map_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno> {
787    get_next_id(BPF_MAP_GET_NEXT_ID, after)
788}
789
790/// `BPF_BTF_GET_NEXT_ID` — id de BTF suivant (`None` = fin).
791///
792/// # Errors
793///
794/// - [`Errno::EPERM`] : privilège manquant.
795pub fn bpf_btf_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno> {
796    get_next_id(BPF_BTF_GET_NEXT_ID, after)
797}
798
799/// `BPF_LINK_GET_NEXT_ID` — id de lien suivant (`None` = fin).
800///
801/// # Errors
802///
803/// - [`Errno::EPERM`] : privilège manquant.
804pub fn bpf_link_get_next_id(after: Option<u32>) -> Result<Option<u32>, Errno> {
805    get_next_id(BPF_LINK_GET_NEXT_ID, after)
806}
807
808fn get_fd_by_id(cmd: i32, id: u32) -> Result<OwnedFd, Errno> {
809    let mut attr = Attr::new();
810    attr.put_u32(0, id);
811    owned_from_ret(attr.call(cmd, 12))
812}
813
814/// `BPF_PROG_GET_FD_BY_ID` — FD d'un programme par id (`CAP_SYS_ADMIN`).
815///
816/// # Errors
817///
818/// - [`Errno::EPERM`] : privilège manquant.
819/// - [`Errno::ENOENT`] : id inconnu.
820pub fn bpf_program_get_fd_by_id(id: u32) -> Result<BpfProgram, Errno> {
821    get_fd_by_id(BPF_PROG_GET_FD_BY_ID, id).map(BpfProgram::from_fd)
822}
823
824/// `BPF_MAP_GET_FD_BY_ID` — FD d'une carte par id.
825///
826/// # Errors
827///
828/// - [`Errno::EPERM`] : privilège manquant.
829/// - [`Errno::ENOENT`] : id inconnu.
830pub fn bpf_map_get_fd_by_id(id: u32) -> Result<BpfMap, Errno> {
831    get_fd_by_id(BPF_MAP_GET_FD_BY_ID, id).map(BpfMap::from_fd)
832}
833
834/// `BPF_BTF_GET_FD_BY_ID` — FD d'un objet BTF par id.
835///
836/// # Errors
837///
838/// - [`Errno::EPERM`] : privilège manquant.
839/// - [`Errno::ENOENT`] : id inconnu.
840pub fn bpf_btf_get_fd_by_id(id: u32) -> Result<Btf, Errno> {
841    get_fd_by_id(BPF_BTF_GET_FD_BY_ID, id).map(Btf::from_fd)
842}
843
844/// `BPF_LINK_GET_FD_BY_ID` — FD d'un lien par id.
845///
846/// # Errors
847///
848/// - [`Errno::EPERM`] : privilège manquant.
849/// - [`Errno::ENOENT`] : id inconnu.
850pub fn bpf_link_get_fd_by_id(id: u32) -> Result<BpfLink, Errno> {
851    get_fd_by_id(BPF_LINK_GET_FD_BY_ID, id).map(BpfLink::from_fd)
852}
853
854/// `BPF_OBJ_GET_INFO_BY_FD` — remplit `info_out` (octets écrits retournés).
855///
856/// # Errors
857///
858/// - [`Errno::EBADF`] : fd invalide.
859pub fn bpf_object_get_info_by_fd(
860    object: BorrowedFd<'_>,
861    info_out: &mut [u8],
862) -> Result<u32, Errno> {
863    let mut attr = Attr::new();
864    attr.put_u32(0, fd_bits(object));
865    attr.put_u32(4, u32::try_from(info_out.len()).unwrap_or(u32::MAX));
866    attr.put_u64(8, info_out.as_mut_ptr() as u64);
867    let ret = attr.call(BPF_OBJ_GET_INFO_BY_FD, 16);
868    if ret < 0 {
869        return Err(errno_from_negative_syscall_ret(ret));
870    }
871    Ok(attr.read_u32(4))
872}
873
874/// `BPF_PROG_QUERY` — liste les programmes attachés à une cible.
875///
876/// Retourne le nombre de programmes effectivement attachés.
877///
878/// # Errors
879///
880/// - [`Errno::EPERM`] : privilège manquant.
881pub fn bpf_program_query(request: &mut BpfProgramQueryRequest<'_>) -> Result<u32, Errno> {
882    // Layout `bpf_attr.query` : target_fd(0) attach_type(4) query_flags(8)
883    // attach_flags(12, out) prog_ids(16, __aligned_u64) prog_cnt(24, in=capacité
884    // / out=nombre attaché).
885    let mut attr = Attr::new();
886    attr.put_u32(0, fd_bits(request.target));
887    attr.put_u32(4, request.attach_type.to_raw());
888    attr.put_u32(8, request.query_flags);
889    attr.put_u64(16, request.program_ids.as_mut_ptr() as u64);
890    attr.put_u32(
891        24,
892        u32::try_from(request.program_ids.len()).unwrap_or(u32::MAX),
893    );
894    let ret = attr.call(BPF_PROG_QUERY, 32);
895    if ret < 0 {
896        return Err(errno_from_negative_syscall_ret(ret));
897    }
898    Ok(attr.read_u32(24))
899}
900
901/// `BPF_TASK_FD_QUERY` — interroge le programme derrière un fd perf/tracepoint.
902///
903/// # Errors
904///
905/// - [`Errno::ENOENT`] : pas de programme derrière le fd.
906pub fn bpf_task_fd_query(request: &mut BpfTaskFdQueryRequest<'_>) -> Result<(), Errno> {
907    let mut attr = Attr::new();
908    let pid = u32::from_ne_bytes(request.pid.as_raw().to_ne_bytes());
909    attr.put_u32(0, pid);
910    attr.put_u32(4, fd_bits(request.fd));
911    attr.put_u32(
912        12,
913        u32::try_from(request.name_buffer.len()).unwrap_or(u32::MAX),
914    );
915    attr.put_u64(16, request.name_buffer.as_mut_ptr() as u64);
916    ret_unit(attr.call(BPF_TASK_FD_QUERY, 48))
917}
918
919// ─────────────────────────────────────────────────────────────────────────
920// BTF.
921// ─────────────────────────────────────────────────────────────────────────
922
923/// `BPF_BTF_LOAD` — charge un blob BTF **déjà formé**.
924///
925/// # Errors
926///
927/// - [`Errno::EINVAL`] : BTF malformé (voir log).
928/// - [`Errno::ENOSPC`] : buffer de log trop court.
929pub fn bpf_btf_load(
930    btf_blob: &[u8],
931    verifier_log: Option<&mut [u8]>,
932    flags: BpfBtfLoadFlags,
933) -> Result<Btf, Errno> {
934    let mut attr = Attr::new();
935    attr.put_u64(0, btf_blob.as_ptr() as u64);
936    attr.put_u32(16, u32::try_from(btf_blob.len()).unwrap_or(u32::MAX));
937    if let Some(log) = verifier_log {
938        attr.put_u64(8, log.as_mut_ptr() as u64);
939        attr.put_u32(20, u32::try_from(log.len()).unwrap_or(u32::MAX));
940        attr.put_u32(24, 1); // btf_log_level
941    }
942    attr.put_u32(32, flags.bits());
943    owned_from_ret(attr.call(BPF_BTF_LOAD, 40)).map(Btf::from_fd)
944}
945
946// ─────────────────────────────────────────────────────────────────────────
947// Statistiques et jetons.
948// ─────────────────────────────────────────────────────────────────────────
949
950/// `BPF_ENABLE_STATS` — active un type de statistiques (RAII : actif tant que
951/// le FD retourné est ouvert).
952///
953/// # Errors
954///
955/// - [`Errno::EPERM`] : privilège manquant.
956pub fn bpf_enable_statistics(kind: BpfStatsType) -> Result<OwnedFd, Errno> {
957    let mut attr = Attr::new();
958    attr.put_u32(0, kind.to_raw());
959    owned_from_ret(attr.call(BPF_ENABLE_STATS, 4))
960}
961
962/// `BPF_TOKEN_CREATE` (Linux 6.9+) — jeton de délégation BPF depuis un `bpffs`.
963///
964/// # Errors
965///
966/// - [`Errno::EPERM`] : privilège manquant.
967/// - [`Errno::EINVAL`] : `bpffs` invalide.
968pub fn bpf_token_create(bpffs: BorrowedFd<'_>, flags: u32) -> Result<OwnedFd, Errno> {
969    let mut attr = Attr::new();
970    attr.put_u32(0, flags);
971    attr.put_u32(4, fd_bits(bpffs));
972    owned_from_ret(attr.call(BPF_TOKEN_CREATE, 8))
973}
974
975fn ret_unit(ret: i64) -> Result<(), Errno> {
976    if ret < 0 {
977        return Err(errno_from_negative_syscall_ret(ret));
978    }
979    Ok(())
980}
981
982// ─────────────────────────────────────────────────────────────────────────
983// perf_event_open et contrôle.
984// ─────────────────────────────────────────────────────────────────────────
985
986const PERF_FLAG_PID_CGROUP: u64 = 1 << 2;
987
988/// Ouvre un compteur/échantillonneur de performance.
989///
990/// `FD_CLOEXEC` est **toujours** posé. Point d'accroche canonique des
991/// programmes eBPF de tracing (via [`perf_event_set_bpf_program`]).
992///
993/// # Errors
994///
995/// - [`Errno::EACCES`] / [`Errno::EPERM`] : privilège / `perf_event_paranoid`.
996/// - [`Errno::EINVAL`] : attributs incohérents.
997/// - [`Errno::ENOENT`] : type/config inconnu.
998pub fn perf_event_open(
999    attr: &PerfEventAttr,
1000    scope: PerfEventScope<'_>,
1001    group_leader: Option<BorrowedFd<'_>>,
1002    flags: PerfEventOpenFlags,
1003) -> Result<PerfEvent, Errno> {
1004    let mut local = *attr;
1005    local.size = u32::try_from(core::mem::size_of::<PerfEventAttr>()).expect("136 tient en u32");
1006
1007    let (pid, cpu, scope_flags) = match scope {
1008        PerfEventScope::CallingProcessAnyCpu => (0_i32, -1_i32, 0_u64),
1009        PerfEventScope::CallingProcessOnCpu(cpu) => {
1010            (0, i32::try_from(cpu).map_err(|_| Errno::EINVAL)?, 0)
1011        }
1012        PerfEventScope::ProcessAnyCpu(process) => (process.as_raw(), -1, 0),
1013        PerfEventScope::ProcessOnCpu { process, cpu } => (
1014            process.as_raw(),
1015            i32::try_from(cpu).map_err(|_| Errno::EINVAL)?,
1016            0,
1017        ),
1018        PerfEventScope::AllProcessesOnCpu(cpu) => {
1019            (-1, i32::try_from(cpu).map_err(|_| Errno::EINVAL)?, 0)
1020        }
1021        PerfEventScope::Cgroup { cgroup, cpu } => (
1022            cgroup.as_raw_fd(),
1023            i32::try_from(cpu).map_err(|_| Errno::EINVAL)?,
1024            PERF_FLAG_PID_CGROUP,
1025        ),
1026    };
1027
1028    let group_fd = group_leader.map_or(-1, |fd| fd.as_raw_fd());
1029    let open_flags = flags.bits() | PerfEventOpenFlags::FD_CLOEXEC.bits() | scope_flags;
1030
1031    // SAFETY: perf_event_open(&attr, pid, cpu, group_fd, flags) lit la
1032    // structure `local` (136 octets) ; pas d'écriture en mémoire utilisateur.
1033    let ret = unsafe {
1034        raw_syscall_perf_event_open((&raw const local) as u64, pid, cpu, group_fd, open_flags)
1035    };
1036    owned_from_ret(ret).map(PerfEvent::from_fd)
1037}
1038
1039/// Encode `_IOC(dir, type, nr, size)` (asm-generic, identique aux 2 arches).
1040const fn ioc(dir: u32, ty: u8, nr: u8, size: u32) -> u32 {
1041    (dir << 30) | (size << 16) | ((ty as u32) << 8) | (nr as u32)
1042}
1043
1044/// Octet de type des ioctls perf (`'$'`).
1045const PERF_IOC_TYPE: u8 = b'$';
1046const IOC_NONE: u32 = 0;
1047const IOC_WRITE: u32 = 1;
1048const IOC_READ: u32 = 2;
1049
1050fn perf_ioctl(event: BorrowedFd<'_>, request: u32, arg: u64) -> Result<(), Errno> {
1051    // SAFETY: ioctl(fd_perf, PERF_EVENT_IOC_*, arg). `arg` est soit une valeur
1052    // (enable/refresh/set_bpf), soit un pointeur vers une zone valide
1053    // (period/filter/id), garanti par l'appelant.
1054    let ret = unsafe { raw_syscall_ioctl(event.as_raw_fd(), u64::from(request), arg) };
1055    if ret < 0 {
1056        return Err(errno_from_negative_syscall_ret(ret));
1057    }
1058    Ok(())
1059}
1060
1061/// `PERF_EVENT_IOC_ENABLE`.
1062///
1063/// # Errors
1064///
1065/// - [`Errno::EBADF`] : fd invalide.
1066pub fn perf_event_enable(event: BorrowedFd<'_>) -> Result<(), Errno> {
1067    perf_ioctl(event, ioc(IOC_NONE, PERF_IOC_TYPE, 0, 0), 0)
1068}
1069
1070/// `PERF_EVENT_IOC_DISABLE`.
1071///
1072/// # Errors
1073///
1074/// - [`Errno::EBADF`] : fd invalide.
1075pub fn perf_event_disable(event: BorrowedFd<'_>) -> Result<(), Errno> {
1076    perf_ioctl(event, ioc(IOC_NONE, PERF_IOC_TYPE, 1, 0), 0)
1077}
1078
1079/// `PERF_EVENT_IOC_RESET`.
1080///
1081/// # Errors
1082///
1083/// - [`Errno::EBADF`] : fd invalide.
1084pub fn perf_event_reset(event: BorrowedFd<'_>) -> Result<(), Errno> {
1085    perf_ioctl(event, ioc(IOC_NONE, PERF_IOC_TYPE, 3, 0), 0)
1086}
1087
1088/// `PERF_EVENT_IOC_REFRESH` — réarme pour `count` débordements.
1089///
1090/// # Errors
1091///
1092/// - [`Errno::EINVAL`] : `count` invalide.
1093pub fn perf_event_refresh(event: BorrowedFd<'_>, count: u32) -> Result<(), Errno> {
1094    perf_ioctl(event, ioc(IOC_NONE, PERF_IOC_TYPE, 2, 0), u64::from(count))
1095}
1096
1097/// `PERF_EVENT_IOC_PERIOD` — change la période d'échantillonnage.
1098///
1099/// # Errors
1100///
1101/// - [`Errno::EBADF`] : fd invalide.
1102pub fn perf_event_set_period(event: BorrowedFd<'_>, period: u64) -> Result<(), Errno> {
1103    let local = period;
1104    perf_ioctl(
1105        event,
1106        ioc(IOC_WRITE, PERF_IOC_TYPE, 4, 8),
1107        (&raw const local) as u64,
1108    )
1109}
1110
1111/// `PERF_EVENT_IOC_SET_FILTER` — pose un filtre (tracepoint).
1112///
1113/// # Errors
1114///
1115/// - [`Errno::EINVAL`] : filtre invalide.
1116pub fn perf_event_set_filter(event: BorrowedFd<'_>, filter: &CStr) -> Result<(), Errno> {
1117    perf_ioctl(
1118        event,
1119        ioc(IOC_WRITE, PERF_IOC_TYPE, 6, 8),
1120        filter.as_ptr() as u64,
1121    )
1122}
1123
1124/// `PERF_EVENT_IOC_ID` — identifiant unique de l'événement.
1125///
1126/// # Errors
1127///
1128/// - [`Errno::EBADF`] : fd invalide.
1129pub fn perf_event_id(event: BorrowedFd<'_>) -> Result<u64, Errno> {
1130    let mut id: u64 = 0;
1131    perf_ioctl(
1132        event,
1133        ioc(IOC_READ, PERF_IOC_TYPE, 7, 8),
1134        (&raw mut id) as u64,
1135    )?;
1136    Ok(id)
1137}
1138
1139/// `PERF_EVENT_IOC_SET_BPF` — attache un programme eBPF (LE PONT eBPF ↔ perf).
1140///
1141/// # Errors
1142///
1143/// - [`Errno::EINVAL`] : programme incompatible avec l'événement.
1144pub fn perf_event_set_bpf_program(
1145    event: BorrowedFd<'_>,
1146    program: BorrowedFd<'_>,
1147) -> Result<(), Errno> {
1148    perf_ioctl(
1149        event,
1150        ioc(IOC_WRITE, PERF_IOC_TYPE, 8, 4),
1151        u64::from(fd_bits(program)),
1152    )
1153}
1154
1155/// `PERF_EVENT_IOC_SET_OUTPUT` — redirige la sortie ; `None` détache.
1156///
1157/// # Errors
1158///
1159/// - [`Errno::EBADF`] : fd invalide.
1160pub fn perf_event_set_output(
1161    event: BorrowedFd<'_>,
1162    output: Option<BorrowedFd<'_>>,
1163) -> Result<(), Errno> {
1164    let output_fd = output.map_or(-1_i32, |fd| fd.as_raw_fd());
1165    let arg = u64::from(u32::from_ne_bytes(output_fd.to_ne_bytes()));
1166    perf_ioctl(event, ioc(IOC_NONE, PERF_IOC_TYPE, 5, 0), arg)
1167}
1168
1169/// `PERF_EVENT_IOC_PAUSE_OUTPUT` — met en pause/reprend l'anneau de sortie.
1170///
1171/// # Errors
1172///
1173/// - [`Errno::EBADF`] : fd invalide.
1174pub fn perf_event_pause_output(event: BorrowedFd<'_>, pause: bool) -> Result<(), Errno> {
1175    perf_ioctl(
1176        event,
1177        ioc(IOC_WRITE, PERF_IOC_TYPE, 9, 4),
1178        u64::from(u32::from(pause)),
1179    )
1180}
1181
1182/// `PERF_EVENT_IOC_QUERY_BPF` — liste les ids de programmes BPF attachés.
1183///
1184/// Retourne le nombre de programmes ; remplit `ids_out` jusqu'à sa capacité.
1185///
1186/// # Errors
1187///
1188/// - [`Errno::EBADF`] : fd invalide.
1189pub fn perf_event_query_bpf(event: BorrowedFd<'_>, ids_out: &mut [u32]) -> Result<u32, Errno> {
1190    // struct perf_event_query_bpf { __u32 ids_len; __u32 prog_cnt; __u32 ids[0]; }
1191    let capacity = u32::try_from(ids_out.len()).unwrap_or(u32::MAX);
1192    let total = ids_out.len().checked_add(2).ok_or(Errno::EINVAL)?;
1193    let mut buffer = vec![0_u32; total];
1194    buffer[0] = capacity;
1195    perf_ioctl(
1196        event,
1197        ioc(IOC_READ | IOC_WRITE, PERF_IOC_TYPE, 10, 8),
1198        buffer.as_mut_ptr() as u64,
1199    )?;
1200    let prog_cnt = buffer[1];
1201    let copy = ids_out.len().min(buffer.len().saturating_sub(2));
1202    let src_end = copy.saturating_add(2);
1203    if let (Some(dst), Some(src)) = (ids_out.get_mut(..copy), buffer.get(2..src_end)) {
1204        dst.copy_from_slice(src);
1205    }
1206    Ok(prog_cnt)
1207}
1208
1209/// `PERF_EVENT_IOC_MODIFY_ATTRIBUTES` — modifie les attributs à chaud.
1210///
1211/// # Errors
1212///
1213/// - [`Errno::EINVAL`] : attributs incohérents.
1214pub fn perf_event_modify_attributes(
1215    event: BorrowedFd<'_>,
1216    attr: &PerfEventAttr,
1217) -> Result<(), Errno> {
1218    let mut local = *attr;
1219    local.size = u32::try_from(core::mem::size_of::<PerfEventAttr>()).expect("136 tient en u32");
1220    perf_ioctl(
1221        event,
1222        ioc(IOC_WRITE, PERF_IOC_TYPE, 11, 8),
1223        (&raw const local) as u64,
1224    )
1225}
1226
1227// ─────────────────────────────────────────────────────────────────────────
1228// asm! wrappers x86_64 / aarch64.
1229// ─────────────────────────────────────────────────────────────────────────
1230
1231#[cfg(target_arch = "x86_64")]
1232#[inline]
1233unsafe fn raw_syscall_bpf(cmd: i32, attr: u64, size: u32) -> i64 {
1234    let ret: i64;
1235    // SAFETY: SYS_bpf (x86_64 = 321). Le kernel lit/écrit `size` octets à
1236    // `attr` et déréférence les pointeurs internes selon `cmd`.
1237    unsafe {
1238        core::arch::asm!(
1239            "syscall",
1240            in("rax") 321_i64,
1241            in("rdi") i64::from(cmd),
1242            in("rsi") attr,
1243            in("rdx") u64::from(size),
1244            lateout("rax") ret,
1245            lateout("rcx") _,
1246            lateout("r11") _,
1247            options(nostack, preserves_flags),
1248        );
1249    }
1250    ret
1251}
1252
1253#[cfg(target_arch = "aarch64")]
1254#[inline]
1255unsafe fn raw_syscall_bpf(cmd: i32, attr: u64, size: u32) -> i64 {
1256    let ret: i64;
1257    // SAFETY: SYS_bpf (aarch64 = 280).
1258    unsafe {
1259        core::arch::asm!(
1260            "svc 0",
1261            in("x8") 280_i64,
1262            inout("x0") i64::from(cmd) => ret,
1263            in("x1") attr,
1264            in("x2") u64::from(size),
1265            options(nostack, preserves_flags),
1266        );
1267    }
1268    ret
1269}
1270
1271#[cfg(target_arch = "x86_64")]
1272#[inline]
1273unsafe fn raw_syscall_perf_event_open(
1274    attr: u64,
1275    pid: i32,
1276    cpu: i32,
1277    group_fd: i32,
1278    flags: u64,
1279) -> i64 {
1280    let ret: i64;
1281    // SAFETY: SYS_perf_event_open (x86_64 = 298). Le kernel lit la struct attr.
1282    unsafe {
1283        core::arch::asm!(
1284            "syscall",
1285            in("rax") 298_i64,
1286            in("rdi") attr,
1287            in("rsi") i64::from(pid),
1288            in("rdx") i64::from(cpu),
1289            in("r10") i64::from(group_fd),
1290            in("r8") flags,
1291            lateout("rax") ret,
1292            lateout("rcx") _,
1293            lateout("r11") _,
1294            options(nostack, preserves_flags, readonly),
1295        );
1296    }
1297    ret
1298}
1299
1300#[cfg(target_arch = "aarch64")]
1301#[inline]
1302unsafe fn raw_syscall_perf_event_open(
1303    attr: u64,
1304    pid: i32,
1305    cpu: i32,
1306    group_fd: i32,
1307    flags: u64,
1308) -> i64 {
1309    let ret: i64;
1310    // SAFETY: SYS_perf_event_open (aarch64 = 241).
1311    unsafe {
1312        core::arch::asm!(
1313            "svc 0",
1314            in("x8") 241_i64,
1315            inout("x0") attr => ret,
1316            in("x1") i64::from(pid),
1317            in("x2") i64::from(cpu),
1318            in("x3") i64::from(group_fd),
1319            in("x4") flags,
1320            options(nostack, preserves_flags, readonly),
1321        );
1322    }
1323    ret
1324}
1325
1326#[cfg(target_arch = "x86_64")]
1327#[inline]
1328unsafe fn raw_syscall_ioctl(fd: i32, request: u64, arg: u64) -> i64 {
1329    let ret: i64;
1330    // SAFETY: SYS_ioctl (x86_64 = 16).
1331    unsafe {
1332        core::arch::asm!(
1333            "syscall",
1334            in("rax") 16_i64,
1335            in("rdi") i64::from(fd),
1336            in("rsi") request,
1337            in("rdx") arg,
1338            lateout("rax") ret,
1339            lateout("rcx") _,
1340            lateout("r11") _,
1341            options(nostack, preserves_flags),
1342        );
1343    }
1344    ret
1345}
1346
1347#[cfg(target_arch = "aarch64")]
1348#[inline]
1349unsafe fn raw_syscall_ioctl(fd: i32, request: u64, arg: u64) -> i64 {
1350    let ret: i64;
1351    // SAFETY: SYS_ioctl (aarch64 = 29).
1352    unsafe {
1353        core::arch::asm!(
1354            "svc 0",
1355            in("x8") 29_i64,
1356            inout("x0") i64::from(fd) => ret,
1357            in("x1") request,
1358            in("x2") arg,
1359            options(nostack, preserves_flags),
1360        );
1361    }
1362    ret
1363}
1364
1365#[cfg(target_arch = "x86_64")]
1366#[inline]
1367unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: u64) -> i64 {
1368    let ret: i64;
1369    // SAFETY: SYS_read (x86_64 = 0).
1370    unsafe {
1371        core::arch::asm!(
1372            "syscall",
1373            in("rax") 0_i64,
1374            in("rdi") i64::from(fd),
1375            in("rsi") buffer,
1376            in("rdx") count,
1377            lateout("rax") ret,
1378            lateout("rcx") _,
1379            lateout("r11") _,
1380            options(nostack, preserves_flags),
1381        );
1382    }
1383    ret
1384}
1385
1386#[cfg(target_arch = "aarch64")]
1387#[inline]
1388unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: u64) -> i64 {
1389    let ret: i64;
1390    // SAFETY: SYS_read (aarch64 = 63).
1391    unsafe {
1392        core::arch::asm!(
1393            "svc 0",
1394            in("x8") 63_i64,
1395            inout("x0") i64::from(fd) => ret,
1396            in("x1") buffer,
1397            in("x2") count,
1398            options(nostack, preserves_flags),
1399        );
1400    }
1401    ret
1402}
1403
1404// ─────────────────────────────────────────────────────────────────────────
1405// errno helper.
1406// ─────────────────────────────────────────────────────────────────────────
1407
1408fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
1409    debug_assert!(ret < 0 && ret > -4096);
1410    #[allow(clippy::cast_possible_truncation)]
1411    let raw = ret.wrapping_neg() as i32;
1412    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
1413    Errno::from_nonzero(nz)
1414}
1415
1416/// Surface de fuzzing (présente uniquement sous `--cfg fuzzing`, posé par
1417/// cargo-fuzz). Harnais **pur**, sans syscall.
1418#[cfg(fuzzing)]
1419pub mod fuzz_api {
1420    use super::Attr;
1421
1422    /// Décode les champs réécrits par le kernel dans un `bpf_attr` (ex.
1423    /// `next_id`, `count`, `out_batch`, octets d'info écrits par
1424    /// `BPF_OBJ_GET_INFO_BY_FD`) : pour un contenu **arbitraire** (kernel
1425    /// hostile, Principe 3), les lectures bornées ne paniquent jamais et ne
1426    /// débordent jamais — y compris à un offset hors borne (retourne 0).
1427    pub fn probe_attr_readback(bytes: &[u8]) {
1428        let mut attr = Attr::new();
1429        let n = bytes.len().min(attr.bytes.len());
1430        if let (Some(dst), Some(src)) = (attr.bytes.get_mut(..n), bytes.get(..n)) {
1431            dst.copy_from_slice(src);
1432        }
1433        // Offsets effectivement lus en production + un offset hors borne.
1434        for off in [0_usize, 4, 12, 16, 24, 32, 36, 44, 180, 188, 200, 1024] {
1435            let _ = attr.read_u32(off);
1436        }
1437    }
1438}
1439
1440#[cfg(test)]
1441mod tests;