Skip to main content

air_sys_syscall/
security.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 `security` — seccomp-BPF primitif et Landlock.
6//!
7//! Cf. `docs/specs/layer-0/family-security.md`.
8//!
9//! **API primitive couche 0 :** cette crate charge un programme BPF
10//! précompilé tel quel dans le kernel (pas de compilation BPF ici).
11//! La couche 1 fournira l'API déclarative (règles → BPF).
12
13#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
14compile_error!("air-sys-syscall::security supporte uniquement x86_64 et aarch64 (ADR-014).");
15
16use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
17use core::mem::size_of;
18use core::num::NonZeroI32;
19
20use air_sys_types::Errno;
21use air_sys_types::security::{LandlockAccessFs, SeccompFilterFlags, SockFprog};
22
23// ─────────────────────────────────────────────────────────────────────────
24// Numéros de syscalls
25// ─────────────────────────────────────────────────────────────────────────
26
27#[cfg(target_arch = "x86_64")]
28const SYS_SECCOMP: i64 = 317;
29#[cfg(target_arch = "aarch64")]
30const SYS_SECCOMP: i64 = 277;
31
32#[cfg(target_arch = "x86_64")]
33const SYS_LANDLOCK_CREATE_RULESET: i64 = 444;
34#[cfg(target_arch = "aarch64")]
35const SYS_LANDLOCK_CREATE_RULESET: i64 = 444;
36
37#[cfg(target_arch = "x86_64")]
38const SYS_LANDLOCK_ADD_RULE: i64 = 445;
39#[cfg(target_arch = "aarch64")]
40const SYS_LANDLOCK_ADD_RULE: i64 = 445;
41
42#[cfg(target_arch = "x86_64")]
43const SYS_LANDLOCK_RESTRICT_SELF: i64 = 446;
44#[cfg(target_arch = "aarch64")]
45const SYS_LANDLOCK_RESTRICT_SELF: i64 = 446;
46
47// ─────────────────────────────────────────────────────────────────────────
48// Constantes kernel
49// ─────────────────────────────────────────────────────────────────────────
50
51/// `seccomp(2)` — mode strict (seuls read/write/_exit/sigreturn autorisés).
52const SECCOMP_SET_MODE_STRICT: u32 = 0;
53/// `seccomp(2)` — charge un filtre BPF précompilé.
54const SECCOMP_SET_MODE_FILTER: u32 = 1;
55
56/// `landlock_create_ruleset(2)` — drapeaux : requête de version ABI.
57const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1;
58
59/// `landlock_add_rule(2)` — type de règle : chemin filesystem.
60const LANDLOCK_RULE_PATH_BENEATH: i32 = 1;
61
62// ─────────────────────────────────────────────────────────────────────────
63// Structures kernel internes (repr C, non exposées)
64// ─────────────────────────────────────────────────────────────────────────
65
66/// Attributs d'un ruleset Landlock (cf. `linux/landlock.h`).
67#[repr(C)]
68struct KernelLandlockRulesetAttr {
69    handled_access_fs: u64,
70}
71
72/// Attributs d'une règle Landlock de type `PATH_BENEATH` (cf. `linux/landlock.h`).
73#[repr(C)]
74struct KernelLandlockPathBeneathAttr {
75    allowed_access: u64,
76    parent_fd: i32,
77}
78
79/// `struct sock_fprog` tel que vu par le kernel sur LP64 (x86_64/aarch64).
80///
81/// Sur LP64 : `{ u16 length, 6 octets de padding, u64 filter_ptr }` = 16 octets.
82/// Le padding est nécessaire pour aligner le pointeur sur 8 octets.
83#[repr(C)]
84struct KernelSockFprog {
85    len: u16,
86    _pad: [u8; 6],
87    filter: u64,
88}
89
90// ─────────────────────────────────────────────────────────────────────────
91// Helpers syscalls bruts (privés, spécifiques à l'architecture)
92// ─────────────────────────────────────────────────────────────────────────
93
94/// Effectue `seccomp(op, flags, uargs)` et retourne la valeur brute i64.
95///
96/// # Safety
97///
98/// L'appelant est responsable de la validité de `uargs` pour l'opération
99/// demandée. Pour `SECCOMP_SET_MODE_FILTER`, `uargs` doit pointer sur un
100/// `KernelSockFprog` valide dont les instructions restent en mémoire
101/// pendant la durée de l'appel.
102#[cfg(target_arch = "x86_64")]
103unsafe fn raw_syscall_seccomp(op: u32, flags: u32, uargs: u64) -> i64 {
104    let ret: i64;
105    // SAFETY (délégué à l'appelant) : voir doc de la fonction.
106    // ABI syscall x86_64 : numéro dans RAX, args dans RDI, RSI, RDX.
107    // Clobbe : RCX, R11 (préservés par l'ABI x86_64 de toute façon).
108    unsafe {
109        core::arch::asm!(
110            "syscall",
111            inlateout("rax") SYS_SECCOMP => ret,
112            in("rdi") u64::from(op),
113            in("rsi") u64::from(flags),
114            in("rdx") uargs,
115            lateout("rcx") _,
116            lateout("r11") _,
117            options(nostack),
118        );
119    }
120    ret
121}
122
123#[cfg(target_arch = "aarch64")]
124unsafe fn raw_syscall_seccomp(op: u32, flags: u32, uargs: u64) -> i64 {
125    let ret: i64;
126    // SAFETY (délégué à l'appelant) : voir doc de la fonction.
127    // ABI syscall aarch64 : numéro dans x8, args dans x0..x5.
128    unsafe {
129        core::arch::asm!(
130            "svc #0",
131            inlateout("x0") u64::from(op) => ret,
132            in("x1") u64::from(flags),
133            in("x2") uargs,
134            in("x8") SYS_SECCOMP as u64,
135            options(nostack),
136        );
137    }
138    ret
139}
140
141/// Effectue `landlock_create_ruleset(attr, size, flags)` et retourne le
142/// résultat brut i64.
143///
144/// # Safety
145///
146/// `attr` doit être soit nul (requête de version ABI) soit un pointeur
147/// valide sur un `KernelLandlockRulesetAttr` pour la durée de l'appel.
148#[cfg(target_arch = "x86_64")]
149unsafe fn raw_syscall_landlock_create_ruleset(attr: u64, size: u64, flags: u32) -> i64 {
150    let ret: i64;
151    unsafe {
152        core::arch::asm!(
153            "syscall",
154            inlateout("rax") SYS_LANDLOCK_CREATE_RULESET => ret,
155            in("rdi") attr,
156            in("rsi") size,
157            in("rdx") u64::from(flags),
158            lateout("rcx") _,
159            lateout("r11") _,
160            options(nostack),
161        );
162    }
163    ret
164}
165
166/// Variante aarch64 de `raw_syscall_landlock_create_ruleset`.
167///
168/// # Safety
169///
170/// `attr` doit être soit nul (requête de version ABI) soit un pointeur
171/// valide sur un `KernelLandlockRulesetAttr` pour la durée de l'appel.
172#[cfg(target_arch = "aarch64")]
173unsafe fn raw_syscall_landlock_create_ruleset(attr: u64, size: u64, flags: u32) -> i64 {
174    let ret: i64;
175    unsafe {
176        core::arch::asm!(
177            "svc #0",
178            inlateout("x0") attr => ret,
179            in("x1") size,
180            in("x2") u64::from(flags),
181            in("x8") SYS_LANDLOCK_CREATE_RULESET as u64,
182            options(nostack),
183        );
184    }
185    ret
186}
187
188/// Effectue `landlock_add_rule(ruleset_fd, rule_type, rule_attr, flags)`.
189///
190/// # Safety
191///
192/// `rule_attr` doit pointer sur un `KernelLandlockPathBeneathAttr` valide
193/// pour la durée de l'appel.
194#[cfg(target_arch = "x86_64")]
195unsafe fn raw_syscall_landlock_add_rule(
196    ruleset_fd: i32,
197    rule_type: i32,
198    rule_attr: u64,
199    flags: u32,
200) -> i64 {
201    let ret: i64;
202    unsafe {
203        core::arch::asm!(
204            "syscall",
205            inlateout("rax") SYS_LANDLOCK_ADD_RULE => ret,
206            in("rdi") i64::from(ruleset_fd),
207            in("rsi") i64::from(rule_type),
208            in("rdx") rule_attr,
209            in("r10") u64::from(flags),
210            lateout("rcx") _,
211            lateout("r11") _,
212            options(nostack),
213        );
214    }
215    ret
216}
217
218/// Variante aarch64 de `raw_syscall_landlock_add_rule`.
219///
220/// # Safety
221///
222/// `rule_attr` doit pointer sur un `KernelLandlockPathBeneathAttr` valide
223/// pour la durée de l'appel.
224#[cfg(target_arch = "aarch64")]
225unsafe fn raw_syscall_landlock_add_rule(
226    ruleset_fd: i32,
227    rule_type: i32,
228    rule_attr: u64,
229    flags: u32,
230) -> i64 {
231    let ret: i64;
232    unsafe {
233        core::arch::asm!(
234            "svc #0",
235            inlateout("x0") i64::from(ruleset_fd) => ret,
236            in("x1") i64::from(rule_type),
237            in("x2") rule_attr,
238            in("x3") u64::from(flags),
239            in("x8") SYS_LANDLOCK_ADD_RULE as u64,
240            options(nostack),
241        );
242    }
243    ret
244}
245
246/// Effectue `landlock_restrict_self(ruleset_fd, flags)`.
247#[cfg(target_arch = "x86_64")]
248fn raw_syscall_landlock_restrict_self(ruleset_fd: i32, flags: u32) -> i64 {
249    let ret: i64;
250    // SAFETY:
251    // - SYS_LANDLOCK_RESTRICT_SELF ne touche à aucune mémoire utilisateur.
252    // - ABI x86_64 : numéro dans RAX, args dans RDI, RSI.
253    unsafe {
254        core::arch::asm!(
255            "syscall",
256            inlateout("rax") SYS_LANDLOCK_RESTRICT_SELF => ret,
257            in("rdi") i64::from(ruleset_fd),
258            in("rsi") u64::from(flags),
259            lateout("rcx") _,
260            lateout("r11") _,
261            options(nostack),
262        );
263    }
264    ret
265}
266
267#[cfg(target_arch = "aarch64")]
268fn raw_syscall_landlock_restrict_self(ruleset_fd: i32, flags: u32) -> i64 {
269    let ret: i64;
270    // SAFETY:
271    // - SYS_LANDLOCK_RESTRICT_SELF ne touche à aucune mémoire utilisateur.
272    // - ABI aarch64 : numéro dans x8, args dans x0, x1.
273    unsafe {
274        core::arch::asm!(
275            "svc #0",
276            inlateout("x0") i64::from(ruleset_fd) => ret,
277            in("x1") u64::from(flags),
278            in("x8") SYS_LANDLOCK_RESTRICT_SELF as u64,
279            options(nostack),
280        );
281    }
282    ret
283}
284
285// ─────────────────────────────────────────────────────────────────────────
286// Helper commun : interprétation du retour i64 d'un syscall.
287// ─────────────────────────────────────────────────────────────────────────
288
289/// Convertit un retour brut de syscall en `Result<i64, Errno>`.
290///
291/// Linux indique une erreur avec un retour dans `[-4095, -1]`.
292fn syscall_result(raw: i64) -> Result<i64, Errno> {
293    if (-4095..0_i64).contains(&raw) {
294        // raw est dans [-4095, -1] : c'est un code errno négatif.
295        // On calcule l'inverse via wrapping_neg : pas de risque d'overflow
296        // car raw != i64::MIN (borné à -4095). Le résultat est dans [1, 4095],
297        // valide pour un i32 et non nul.
298        let errno_raw_i64 = raw.wrapping_neg();
299        // errno_raw_i64 est dans [1, 4095] : la troncature i64 → i32 est
300        // sans perte (4095 < i32::MAX) et le signe ne change pas.
301        #[allow(clippy::cast_possible_truncation)]
302        let errno_raw = errno_raw_i64 as i32;
303        let nz =
304            NonZeroI32::new(errno_raw).expect("errno dans [-4095,-1] est nécessairement non nul");
305        Err(Errno::from_nonzero(nz))
306    } else {
307        Ok(raw)
308    }
309}
310
311// ─────────────────────────────────────────────────────────────────────────
312// LandlockRuleset — type RAII autour du FD ruleset Landlock.
313// ─────────────────────────────────────────────────────────────────────────
314
315/// Ruleset Landlock (cf. `landlock_create_ruleset(2)`).
316///
317/// Encapsule un FD ruleset Landlock qui accumule des règles d'accès
318/// filesystem avant d'être appliqué au thread courant via
319/// [`LandlockRuleset::restrict_self`].
320///
321/// Les restrictions sont **irréversibles** : une fois
322/// `restrict_self` appelé, le thread ne peut plus voir les permissions
323/// augmenter (Landlock est monotone).
324#[derive(Debug)]
325pub struct LandlockRuleset(OwnedFd);
326
327impl LandlockRuleset {
328    /// Vue empruntée du FD ruleset.
329    #[must_use]
330    pub fn as_fd(&self) -> BorrowedFd<'_> {
331        use air_sys_types::fd::AsFd;
332        self.0.as_fd()
333    }
334
335    /// Ajoute une règle d'accès pour un chemin filesystem et ses descendants.
336    ///
337    /// Wrappeur de `landlock_add_rule(2)` avec `LANDLOCK_RULE_PATH_BENEATH`.
338    /// La règle s'applique au chemin référencé par `path` et à tous ses
339    /// descendants. `path` doit être ouvert avec `O_PATH | O_DIRECTORY`
340    /// (ou `O_PATH` seul pour un fichier).
341    ///
342    /// **Sémantique additive :** les règles ne peuvent qu'étendre les accès
343    /// autorisés au sein de ce ruleset. `restrict_self` appliquera
344    /// l'intersection de tous les rulesets cumulés.
345    ///
346    /// # Parameters
347    ///
348    /// - `path` : FD du chemin cible (typiquement ouvert avec `O_PATH`).
349    /// - `allowed_access` : permissions autorisées sur ce chemin et ses
350    ///   descendants.
351    ///
352    /// # Errors
353    ///
354    /// - `EINVAL` : `allowed_access` contient un bit non géré par ce ruleset,
355    ///   ou `path` n'est pas un FD valide.
356    /// - `ENOMEM` : mémoire kernel insuffisante.
357    /// - `EBADFD` : `path` n'est pas un FD de fichier ou répertoire.
358    /// - `EINTR` : interruption par signal (ADR-021 convention 2 — remonté
359    ///   tel quel, sans retry automatique).
360    ///
361    /// # Examples
362    ///
363    /// ```no_run
364    /// use air_sys_syscall::security::landlock_create_ruleset;
365    /// use air_sys_types::security::LandlockAccessFs;
366    /// use air_sys_types::fd::BorrowedFd;
367    ///
368    /// # fn example(path_fd: BorrowedFd<'_>) {
369    /// let mut ruleset = landlock_create_ruleset(
370    ///     LandlockAccessFs::READ_FILE | LandlockAccessFs::EXECUTE,
371    /// ).expect("create_ruleset");
372    /// ruleset.add_rule_path_beneath(path_fd, LandlockAccessFs::READ_FILE)
373    ///     .expect("add_rule");
374    /// # }
375    /// ```
376    pub fn add_rule_path_beneath(
377        &mut self,
378        path: BorrowedFd<'_>,
379        allowed_access: LandlockAccessFs,
380    ) -> Result<(), Errno> {
381        let attr = KernelLandlockPathBeneathAttr {
382            allowed_access: allowed_access.bits(),
383            parent_fd: path.as_raw_fd(),
384        };
385        let ruleset_fd = self.0.as_raw_fd();
386        // SAFETY:
387        // - `attr` est une variable locale valide pour toute la durée de l'appel.
388        // - `LANDLOCK_RULE_PATH_BENEATH` est la seule valeur de type règle
389        //   actuellement définie et acceptée par le kernel pour `attr` de
390        //   type `KernelLandlockPathBeneathAttr`.
391        // - `flags` = 0 : aucun drapeau défini pour cette opération à ce jour.
392        let ret = unsafe {
393            raw_syscall_landlock_add_rule(
394                ruleset_fd,
395                LANDLOCK_RULE_PATH_BENEATH,
396                &raw const attr as u64,
397                0,
398            )
399        };
400        syscall_result(ret).map(|_| ())
401    }
402
403    /// Applique le ruleset au thread courant.
404    ///
405    /// Wrappeur de `landlock_restrict_self(2)`. **Irréversible.** Après
406    /// cet appel, le thread ne peut plus accéder aux chemins filesystem
407    /// non couverts par les règles du ruleset (pour les accès dans
408    /// `handled_access` du ruleset).
409    ///
410    /// Prérequis : avoir appelé [`crate::process::set_no_new_privs`] ou
411    /// posséder `CAP_SYS_ADMIN`.
412    ///
413    /// # Errors
414    ///
415    /// - `EPERM` : `no_new_privs` non positionné et `CAP_SYS_ADMIN` absent.
416    /// - `EINVAL` : flags invalides.
417    /// - `EINTR` : interruption par signal (ADR-021 convention 2 — remonté
418    ///   tel quel, sans retry automatique).
419    ///
420    /// # Examples
421    ///
422    /// ```no_run
423    /// use air_sys_syscall::security::landlock_create_ruleset;
424    /// use air_sys_syscall::process::set_no_new_privs;
425    /// use air_sys_types::security::LandlockAccessFs;
426    ///
427    /// let ruleset = landlock_create_ruleset(LandlockAccessFs::READ_FILE)
428    ///     .expect("create_ruleset");
429    /// set_no_new_privs().expect("no_new_privs");
430    /// ruleset.restrict_self().expect("restrict_self");
431    /// ```
432    pub fn restrict_self(&self) -> Result<(), Errno> {
433        let ruleset_fd = self.0.as_raw_fd();
434        let ret = raw_syscall_landlock_restrict_self(ruleset_fd, 0);
435        syscall_result(ret).map(|_| ())
436    }
437}
438
439// ─────────────────────────────────────────────────────────────────────────
440// Fonctions publiques
441// ─────────────────────────────────────────────────────────────────────────
442
443/// Retourne la version ABI Landlock supportée par le kernel courant.
444///
445/// Utilise `landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION)`.
446/// Retourne `ENOSYS` si le kernel ne supporte pas Landlock (< 5.13).
447///
448/// | Valeur retournée | Version Landlock | Kernel minimum |
449/// |---|---|---|
450/// | 1 | v1 | Linux 5.13 |
451/// | 2 | v2 (REFER) | Linux 5.19 |
452/// | 3 | v3 (TRUNCATE) | Linux 6.2 |
453/// | 5 | v5 (IOCTL_DEV) | Linux 6.10 |
454///
455/// # Errors
456///
457/// - `ENOSYS` : Landlock non supporté ou non compilé dans le kernel.
458///
459/// # Examples
460///
461/// ```no_run
462/// use air_sys_syscall::security::landlock_supported_abi;
463///
464/// match landlock_supported_abi() {
465///     Ok(v) => println!("Landlock ABI v{v}"),
466///     Err(_) => println!("Landlock non disponible"),
467/// }
468/// ```
469pub fn landlock_supported_abi() -> Result<u32, Errno> {
470    // SAFETY:
471    // - attr = 0 (NULL) et size = 0 sont les valeurs attendues pour la
472    //   requête de version ABI (drapeaux LANDLOCK_CREATE_RULESET_VERSION).
473    // - Le kernel ne déréférence pas le pointeur quand flags = VERSION.
474    let ret = unsafe { raw_syscall_landlock_create_ruleset(0, 0, LANDLOCK_CREATE_RULESET_VERSION) };
475    let version = syscall_result(ret)?;
476    // La version ABI est un entier positif <= quelques dizaines ; la
477    // troncature i64 → u32 est sans perte sur les valeurs réalistes.
478    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
479    Ok(version as u32)
480}
481
482/// Crée un ruleset Landlock pour les accès filesystem spécifiés.
483///
484/// Wrappeur de `landlock_create_ruleset(2)` (Linux 5.13+). Le ruleset
485/// gérera les permissions listées dans `handled_access` — toute
486/// permission dans `handled_access` qui n'est pas couverte par une règle
487/// sera **refusée** par défaut après `restrict_self`.
488///
489/// # Parameters
490///
491/// - `handled_access` : ensemble des permissions filesystem que ce
492///   ruleset va gérer. Doit être un sous-ensemble des bits supportés
493///   par la version ABI du kernel (voir [`landlock_supported_abi`]).
494///
495/// # Errors
496///
497/// - `EINVAL` : `handled_access` contient un bit non supporté par la
498///   version ABI courante.
499/// - `ENOMEM` : mémoire kernel insuffisante.
500/// - `ENOSYS` : Landlock non disponible sur ce kernel.
501///
502/// # Examples
503///
504/// ```no_run
505/// use air_sys_syscall::security::landlock_create_ruleset;
506/// use air_sys_types::security::LandlockAccessFs;
507///
508/// let ruleset = landlock_create_ruleset(
509///     LandlockAccessFs::READ_FILE | LandlockAccessFs::READ_DIR | LandlockAccessFs::EXECUTE,
510/// ).expect("create_ruleset");
511/// ```
512pub fn landlock_create_ruleset(handled_access: LandlockAccessFs) -> Result<LandlockRuleset, Errno> {
513    let attr = KernelLandlockRulesetAttr {
514        handled_access_fs: handled_access.bits(),
515    };
516    // SAFETY:
517    // - `attr` est une variable locale valide pour toute la durée de l'appel.
518    // - `size_of::<KernelLandlockRulesetAttr>()` est la taille exacte de la
519    //   structure attendue par le kernel pour les versions ABI connues.
520    // - `flags` = 0 : création normale (pas requête de version ABI).
521    let ret = unsafe {
522        raw_syscall_landlock_create_ruleset(
523            &raw const attr as u64,
524            size_of::<KernelLandlockRulesetAttr>() as u64,
525            0,
526        )
527    };
528    let fd_raw = syscall_result(ret)?;
529    // SAFETY : le syscall a réussi (ret >= 0) ; le kernel garantit qu'un
530    // fd retourné est valide et appartient maintenant au processus.
531    // La troncature i64 → i32 est sans perte : les numéros de fd Linux
532    // tiennent dans un i32.
533    #[allow(clippy::cast_possible_truncation)]
534    let owned_fd = unsafe { OwnedFd::from_raw_fd(fd_raw as i32) };
535    Ok(LandlockRuleset(owned_fd))
536}
537
538/// Applique un filtre seccomp-BPF précompilé au thread courant.
539///
540/// Wrappeur de `seccomp(SECCOMP_SET_MODE_FILTER, flags, prog)`.
541/// Charge le programme BPF tel quel dans le kernel — aucune compilation
542/// BPF n'est effectuée ici (API primitive couche 0).
543///
544/// **L'opération est irréversible.** Une fois chargé, le filtre ne peut
545/// pas être retiré. Des filtres successifs peuvent être ajoutés mais
546/// seulement pour restreindre davantage (jamais pour assouplir).
547///
548/// # Safety
549///
550/// - L'appelant doit avoir appelé `set_no_new_privs()` OU posséder
551///   `CAP_SYS_ADMIN`.
552/// - Le programme BPF doit autoriser tous les syscalls nécessaires au
553///   runtime Rust (au minimum : `read`, `write`, `mmap`, `munmap`,
554///   `futex`, `exit_group`, `rt_sigreturn`).
555/// - Un programme BPF incorrect peut bloquer le processus ou le tuer.
556/// - Irréversible.
557///
558/// # Errors
559///
560/// - `EPERM` : `no_new_privs` non positionné et `CAP_SYS_ADMIN` absent.
561/// - `EINVAL` : programme BPF invalide ou flags non reconnus.
562/// - `ENOMEM` : mémoire insuffisante.
563/// - `ENOSYS` : seccomp non compilé dans ce kernel.
564///
565/// # Examples
566///
567/// ```no_run
568/// use air_sys_syscall::security::seccomp_set_mode_filter;
569/// use air_sys_types::security::{SeccompFilterFlags, SockFilter, SockFprog};
570///
571/// // Instruction BPF RET ALLOW (0x7fff0000 = SECCOMP_RET_ALLOW).
572/// let allow_all = [SockFilter { code: 0x0006, jt: 0, jf: 0, k: 0x7fff_0000 }];
573/// let prog = SockFprog::new(&allow_all).expect("programme valide");
574///
575/// // unsafe : charge un filtre irréversible dans le kernel.
576/// unsafe {
577///     seccomp_set_mode_filter(&prog, SeccompFilterFlags::empty())
578///         .expect("seccomp_set_mode_filter");
579/// }
580/// ```
581pub unsafe fn seccomp_set_mode_filter(
582    prog: &SockFprog<'_>,
583    flags: SeccompFilterFlags,
584) -> Result<(), Errno> {
585    // Construction du struct sock_fprog attendu par le kernel sur LP64.
586    // Sur x86_64 et aarch64 (les deux archs supportées, ADR-014), la ABI
587    // LP64 impose : u16 length, 6 octets de padding, puis le pointeur aligné
588    // sur 8 octets — total 16 octets.
589    let kfprog = KernelSockFprog {
590        len: prog.len(),
591        _pad: [0u8; 6],
592        filter: prog.as_ptr() as u64,
593    };
594    // SAFETY (préconditions héritées de l'appelant) :
595    // - Le programme BPF pointé par `prog` reste valide pendant l'appel.
596    // - `kfprog` est une variable locale valide pour toute la durée
597    //   de l'appel syscall.
598    // - `SECCOMP_SET_MODE_FILTER` avec un `sock_fprog` valide ne cause
599    //   pas d'UB intrinsèquement — c'est un appel kernel ordinaire.
600    let ret = unsafe {
601        raw_syscall_seccomp(
602            SECCOMP_SET_MODE_FILTER,
603            flags.bits(),
604            &raw const kfprog as u64,
605        )
606    };
607    syscall_result(ret).map(|_| ())
608}
609
610/// Active le mode seccomp strict.
611///
612/// En mode strict, seuls `read(2)`, `write(2)`, `_exit(2)` et
613/// `sigreturn(2)` sont autorisés pour le thread courant. Tout autre
614/// syscall entraîne un `SIGKILL`.
615///
616/// **L'opération est irréversible.**
617///
618/// # Errors
619///
620/// - `EPERM` : les threads du processus utilisent déjà des filtres
621///   incompatibles (cas rare).
622/// - `ENOSYS` : seccomp non compilé dans ce kernel.
623///
624/// # Examples
625///
626/// ```no_run
627/// use air_sys_syscall::security::seccomp_set_mode_strict;
628///
629/// // N'appeler que dans un sous-processus dédié ; irréversible.
630/// seccomp_set_mode_strict().expect("seccomp_set_mode_strict");
631/// ```
632pub fn seccomp_set_mode_strict() -> Result<(), Errno> {
633    // SAFETY:
634    // - `SECCOMP_SET_MODE_STRICT` avec flags=0 et uargs=0 est l'appel
635    //   canonique documenté dans seccomp(2). Aucune mémoire utilisateur
636    //   n'est déréférencée par le kernel pour cette opération.
637    let ret = unsafe { raw_syscall_seccomp(SECCOMP_SET_MODE_STRICT, 0, 0) };
638    syscall_result(ret).map(|_| ())
639}
640
641// ─────────────────────────────────────────────────────────────────────────
642// Tests
643// ─────────────────────────────────────────────────────────────────────────
644
645#[cfg(test)]
646mod tests {
647    use super::*;
648    use air_sys_types::security::{SockFilter, SockFprog};
649
650    // ── landlock_supported_abi ────────────────────────────────────────────
651
652    #[cfg(target_os = "linux")]
653    #[test]
654    fn landlock_supported_abi_returns_version() {
655        // Air cible des noyaux Linux tier-1 avec Landlock activé (>= 5.13) : la
656        // sonde de capacité exige donc la capacité (pas de tolérance ENOSYS, qui
657        // laisserait une branche morte sur les noyaux cibles).
658        let v = landlock_supported_abi().expect("Landlock requis sur les noyaux cibles Air");
659        assert!(v >= 1, "version ABI Landlock doit être >= 1, obtenu {v}");
660    }
661
662    // ── landlock_create_ruleset ───────────────────────────────────────────
663
664    #[cfg(target_os = "linux")]
665    #[test]
666    fn landlock_create_ruleset_succeeds() {
667        use air_sys_types::fd::AsRawFd;
668        let ruleset =
669            landlock_create_ruleset(LandlockAccessFs::READ_FILE | LandlockAccessFs::READ_DIR)
670                .expect("Landlock requis sur les noyaux cibles Air");
671        assert!(
672            ruleset.0.as_raw_fd() >= 0,
673            "le FD ruleset doit être positif"
674        );
675    }
676
677    // ── seccomp_set_mode_strict ──────────────────────────────────────────
678    //
679    // AVERTISSEMENT : `seccomp_set_mode_strict` est irréversible et restreint
680    // les syscalls disponibles à read/write/_exit/sigreturn. On ne peut PAS
681    // l'appeler dans le processus de test standard (cela tuerait le test runner
682    // Rust à la première allocation mémoire post-appel). Ce test vérifie
683    // uniquement que la fonction est compilable et que le chemin non-Linux
684    // est cohérent. Un test en subprocess séparé est nécessaire pour valider
685    // le comportement réel.
686
687    #[cfg(target_os = "linux")]
688    #[test]
689    fn seccomp_set_mode_strict_returns_ok_or_enosys_in_subprocess() {
690        // On utilise un fork via std::process::Command pour isoler l'appel.
691        // L'exécutable de test re-invoqué avec une variable d'environnement
692        // spéciale active le mode strict et vérifie le code de sortie.
693        //
694        // Implémentation simplifiée : on vérifie que la fonction existe et
695        // est appelable syntaxiquement. Le test d'intégration réel (subprocess
696        // fork + seccomp) sera dans la suite d'intégration de couche 0.
697        //
698        // Pour l'instant : on vérifie que sur un kernel Linux, seccomp(2) est
699        // bien disponible (ENOSYS ne doit pas être retourné sur tout kernel
700        // Linux >= 3.5 avec CONFIG_SECCOMP=y).
701        let prog_instructions = [SockFilter {
702            code: 0x0006,
703            jt: 0,
704            jf: 0,
705            k: 0x7fff_0000,
706        }];
707        let prog = SockFprog::new(&prog_instructions).expect("programme valide");
708        // On teste seccomp_set_mode_filter dans un sous-processus pour ne
709        // pas altérer le processus de test courant.
710        // Ici on vérifie simplement que SockFprog::new et les conversions
711        // de pointeur sont correctes sans appeler le syscall effectivement.
712        assert_eq!(prog.len(), 1);
713        assert!(!prog.is_empty());
714        // Vérification que le pointeur est non-nul.
715        assert!(!prog.as_ptr().is_null());
716    }
717
718    // ── SockFprog validation ──────────────────────────────────────────────
719    //
720    // Les tests de validation de SockFprog sont dans air-sys-types::security.
721    // On ajoute ici des tests qui vérifient l'intégration avec le syscall.
722
723    #[test]
724    fn sock_fprog_new_single_instruction_pointer_stable() {
725        let instructions = [
726            SockFilter {
727                code: 0x0006,
728                jt: 0,
729                jf: 0,
730                k: 0x7fff_0000,
731            },
732            SockFilter {
733                code: 0x0006,
734                jt: 0,
735                jf: 0,
736                k: 0x0000_0000,
737            },
738        ];
739        let prog = SockFprog::new(&instructions).expect("2 instructions valides");
740        assert_eq!(prog.len(), 2);
741        // Le pointeur doit pointer sur le premier élément de la tranche.
742        assert_eq!(prog.as_ptr(), instructions.as_ptr());
743    }
744
745    // ── KernelSockFprog layout ────────────────────────────────────────────
746
747    #[test]
748    fn kernel_sock_fprog_size_is_16_bytes() {
749        // Sur LP64 (x86_64/aarch64) : u16 + 6 bytes padding + u64 = 16 bytes.
750        assert_eq!(core::mem::size_of::<KernelSockFprog>(), 16);
751    }
752
753    #[test]
754    fn kernel_sock_fprog_alignment_is_8_bytes() {
755        assert_eq!(core::mem::align_of::<KernelSockFprog>(), 8);
756    }
757
758    // ── KernelLandlockRulesetAttr layout ─────────────────────────────────
759
760    #[test]
761    fn kernel_landlock_ruleset_attr_size() {
762        // struct landlock_ruleset_attr : un seul champ u64.
763        assert_eq!(core::mem::size_of::<KernelLandlockRulesetAttr>(), 8);
764    }
765
766    // ── KernelLandlockPathBeneathAttr layout ──────────────────────────────
767
768    #[test]
769    fn kernel_landlock_path_beneath_attr_size() {
770        // struct landlock_path_beneath_attr : u64 + i32 = 12 bytes (+ padding?).
771        // Le kernel définit : __u64 allowed_access (8) + __s32 parent_fd (4) = 12.
772        // Pas de padding final car l'alignement max est 8 (u64), et 12 % 8 != 0,
773        // donc le compilateur ajoute 4 bytes de padding pour aligner = 16 bytes.
774        // Mais le kernel lui-même a exactement 12 bytes en packed? Non : le kernel
775        // C utilise le padding naturel. Sur x86_64 et aarch64, alignof(u64) = 8,
776        // donc { u64 (8 bytes), i32 (4 bytes) } → taille = 16 avec padding final.
777        // Vérifions la valeur réelle au runtime.
778        let size = core::mem::size_of::<KernelLandlockPathBeneathAttr>();
779        // Valeur attendue : 16 (padding pour aligner sur 8 bytes).
780        assert_eq!(
781            size, 16,
782            "KernelLandlockPathBeneathAttr doit faire 16 bytes sur LP64"
783        );
784    }
785
786    // ── Harnais sous-processus : opérations irréversibles ────────────────
787    //
788    // `seccomp_set_mode_filter` et `LandlockRuleset::restrict_self` sont
789    // irréversibles : on les exerce dans un enfant forké via `clone3`, le
790    // parent observant le statut de sortie via `waitid`. L'enfant sort via
791    // `exit_group` (ni le filtre allow-all ni Landlock ne le bloquent).
792    //
793    // **Couverture (cf. docs/COVERAGE-EXCEPTIONS.md) :** l'enfant sort par
794    // `exit_group`, qui bypasse l'atexit LLVM → il n'écrit pas son `.profraw`.
795    // La branche « succès » de ces fonctions reste donc une exception de
796    // couverture légitime, mais elle est **fonctionnellement prouvée** ici.
797
798    /// Écrit le profil de couverture LLVM de l'enfant forké.
799    ///
800    /// `exit_group` (et `raw_exit`) court-circuitent le handler `atexit` de LLVM :
801    /// sans ce flush explicite, les compteurs de l'enfant sont perdus et le corps
802    /// des wrappers exécutés dans l'enfant (landlock/seccomp/…) apparaît non
803    /// couvert. `LLVM_PROFILE_FILE` contient `%p` → chaque enfant écrit un
804    /// `.profraw` distinct, fusionné ensuite par `cargo-llvm-cov`.
805    ///
806    /// À appeler tant que l'I/O fichier de l'enfant est encore permise : après un
807    /// filtre seccomp allow-all ou un ruleset Landlock qui ne *gère* pas l'écriture
808    /// l'I/O reste possible ; **avant** un verrou seccomp-strict qui, lui, tuerait
809    /// le flush (il appelle `open`/`write`).
810    // Intentionnellement VIDE — **async-signal-safety** (flaky deadlock couche 0) :
811    // `__llvm_profile_write_file` alloue, et un enfant forké via `clone3` peut
812    // hériter d'un lock `malloc` copié VERROUILLÉ → deadlock. Les lignes exécutées
813    // dans l'enfant sont des exceptions CHILD-EXIT (ADR-035), prouvées par le code
814    // de sortie. NE PAS réintroduire de flush ici.
815    #[cfg(target_os = "linux")]
816    fn flush_child_coverage() {}
817
818    /// Exécute `body` dans un enfant forké ; retourne l'événement de sortie
819    /// observé par le parent. `body` renvoie le code de sortie de l'enfant.
820    #[cfg(target_os = "linux")]
821    fn run_in_forked_child(body: impl FnOnce() -> i32) -> air_sys_types::WaitEvent {
822        use crate::process::{clone3, exit_group, waitid};
823        use air_sys_types::{CloneArgs, CloneFlags, CloneResult, Signal, WaitOptions, WaitTarget};
824        let args = CloneArgs {
825            flags: CloneFlags::PIDFD,
826            exit_signal: Some(Signal::SIGCHLD),
827            stack: None,
828            child_tid: None,
829            parent_tid: None,
830            tls: None,
831        };
832        // SAFETY: fork classique + CLONE_PIDFD ; pas de mémoire partagée.
833        match unsafe { clone3(&args) }.expect("clone3 doit réussir") {
834            CloneResult::Child => {
835                let code = body();
836                // Flush du profil avant exit_group (qui bypasse l'atexit LLVM).
837                // Les corps qui verrouillent l'I/O (seccomp-strict) flushent
838                // eux-mêmes plus tôt et sortent sans repasser ici.
839                flush_child_coverage();
840                exit_group(code);
841            }
842            CloneResult::Parent { child_pidfd, .. } => {
843                let pidfd = child_pidfd.expect("CLONE_PIDFD demandé");
844                let st = waitid(WaitTarget::PidFd(pidfd.as_fd()), WaitOptions::EXITED)
845                    .expect("waitid")
846                    .expect("événement EXITED disponible");
847                st.event
848            }
849        }
850    }
851
852    /// `seccomp_set_mode_filter` avec un filtre allow-all, appliqué dans un
853    /// enfant forké. Prouve le chargement réel du programme BPF.
854    #[cfg(target_os = "linux")]
855    #[test]
856    fn seccomp_set_mode_filter_allow_all_in_forked_child() {
857        use air_sys_types::WaitEvent;
858        let event = run_in_forked_child(|| {
859            // `no_new_privs` est requis sans CAP_SYS_ADMIN.
860            if crate::process::set_no_new_privs().is_err() {
861                return 10;
862            }
863            // Instruction unique : RET SECCOMP_RET_ALLOW (0x7fff_0000).
864            let insns = [SockFilter {
865                code: 0x0006,
866                jt: 0,
867                jf: 0,
868                k: 0x7fff_0000,
869            }];
870            let prog = match SockFprog::new(&insns) {
871                Ok(p) => p,
872                Err(_) => return 11,
873            };
874            // SAFETY: no_new_privs posé ; filtre allow-all → aucun syscall
875            // ultérieur (dont exit_group) n'est bloqué.
876            match unsafe { seccomp_set_mode_filter(&prog, SeccompFilterFlags::empty()) } {
877                Ok(()) => 0,
878                Err(Errno::ENOSYS) => 42, // seccomp indisponible : skip toléré
879                Err(_) => 12,
880            }
881        });
882        match event {
883            WaitEvent::Exited { code: 0 } => {}  // filtre chargé avec succès
884            WaitEvent::Exited { code: 42 } => {} // seccomp indisponible
885            other => panic!("enfant seccomp_filter : statut inattendu {other:?}"),
886        }
887    }
888
889    /// `LandlockRuleset::restrict_self` appliqué dans un enfant forké.
890    /// Prouve le chemin create_ruleset → restrict_self réel.
891    #[cfg(target_os = "linux")]
892    #[test]
893    fn landlock_restrict_self_in_forked_child() {
894        use air_sys_types::WaitEvent;
895        let event = run_in_forked_child(|| {
896            let ruleset = match landlock_create_ruleset(LandlockAccessFs::READ_FILE) {
897                Ok(r) => r,
898                Err(Errno::ENOSYS) => return 42, // Landlock indisponible : skip
899                Err(_) => return 11,
900            };
901            if crate::process::set_no_new_privs().is_err() {
902                return 12;
903            }
904            // Flush AVANT restrict_self : ensuite l'ouverture O_RDWR du .profraw
905            // (mode merge `%m`) serait refusée par Landlock (READ_FILE géré).
906            flush_child_coverage();
907            match ruleset.restrict_self() {
908                Ok(()) => 0,
909                Err(_) => 13,
910            }
911        });
912        match event {
913            WaitEvent::Exited { code: 0 } => {}  // restriction appliquée
914            WaitEvent::Exited { code: 42 } => {} // Landlock indisponible
915            other => panic!("enfant landlock_restrict_self : statut inattendu {other:?}"),
916        }
917    }
918
919    /// `add_rule_path_beneath` + `restrict_self` dans un enfant forké.
920    /// Prouve le chemin complet : create_ruleset → add_rule → restrict_self.
921    #[cfg(target_os = "linux")]
922    #[test]
923    fn landlock_add_rule_and_restrict_self_in_forked_child() {
924        use air_sys_types::WaitEvent;
925        use air_sys_types::fd::AsFd;
926        let event = run_in_forked_child(|| {
927            let mut ruleset = match landlock_create_ruleset(
928                LandlockAccessFs::READ_FILE | LandlockAccessFs::READ_DIR,
929            ) {
930                Ok(r) => r,
931                Err(Errno::ENOSYS) => return 42,
932                Err(_) => return 11,
933            };
934            // Ouvre /tmp en O_PATH | O_DIRECTORY pour la règle.
935            // O_PATH = 0o10000000 (x86_64/aarch64), O_DIRECTORY = 0o200000.
936            let o_path: u64 = 0o10_000_000;
937            let o_rdonly: u64 = 0;
938            let o_directory: u64 = 0o200_000;
939            let o_cloexec: u64 = 0o2_000_000;
940            let path_fd_raw = {
941                #[cfg(target_arch = "x86_64")]
942                {
943                    let ret: i64;
944                    let path = c"/tmp";
945                    // SAFETY: openat (x86_64 = 257). `path` est une chaîne C statique
946                    // valide et NUL-terminée ; le kernel ne lit que ce pointeur et
947                    // retourne un FD ≥ 0 ou un errno négatif (aucune écriture mémoire).
948                    unsafe {
949                        core::arch::asm!(
950                            "syscall",
951                            in("rax") 257_i64, // openat
952                            in("rdi") (-100_i64).cast_unsigned(), // AT_FDCWD
953                            in("rsi") path.as_ptr() as u64,
954                            in("rdx") o_rdonly | o_path | o_directory | o_cloexec,
955                            in("r10") 0_u64,
956                            lateout("rax") ret,
957                            lateout("rcx") _,
958                            lateout("r11") _,
959                            options(nostack, preserves_flags),
960                        );
961                    }
962                    ret
963                }
964                #[cfg(target_arch = "aarch64")]
965                {
966                    let ret: i64;
967                    let path = c"/tmp";
968                    // SAFETY: openat (aarch64 = 56). `path` est une chaîne C statique
969                    // valide et NUL-terminée ; le kernel ne lit que ce pointeur et
970                    // retourne un FD ≥ 0 ou un errno négatif (aucune écriture mémoire).
971                    unsafe {
972                        core::arch::asm!(
973                            "svc 0",
974                            in("x8") 56_i64, // openat
975                            inout("x0") (-100_i64).cast_unsigned() => ret,
976                            in("x1") path.as_ptr() as u64,
977                            in("x2") o_rdonly | o_path | o_directory | o_cloexec,
978                            in("x3") 0_u64,
979                            options(nostack, preserves_flags),
980                        );
981                    }
982                    ret
983                }
984            };
985            if path_fd_raw < 0 {
986                return 14;
987            }
988            #[allow(clippy::cast_possible_truncation)]
989            // SAFETY: `path_fd_raw >= 0` (vérifié juste au-dessus), donc c'est un FD
990            // fraîchement ouvert par openat, non possédé ailleurs : on en prend la
991            // propriété exclusive.
992            let path_fd = unsafe { OwnedFd::from_raw_fd(path_fd_raw as i32) };
993            match ruleset.add_rule_path_beneath(path_fd.as_fd(), LandlockAccessFs::READ_FILE) {
994                Ok(()) => {}
995                Err(Errno::ENOSYS) => return 42,
996                Err(_) => return 15,
997            }
998            if crate::process::set_no_new_privs().is_err() {
999                return 12;
1000            }
1001            // Flush AVANT restrict_self (cf. landlock_restrict_self_in_forked_child) :
1002            // capture create_ruleset + add_rule pendant que l'I/O est permise.
1003            flush_child_coverage();
1004            match ruleset.restrict_self() {
1005                Ok(()) => 0,
1006                Err(_) => 13,
1007            }
1008        });
1009        match event {
1010            WaitEvent::Exited { code: 0 } => {}
1011            WaitEvent::Exited { code: 42 } => {}
1012            other => panic!("landlock add_rule+restrict_self : {other:?}"),
1013        }
1014    }
1015
1016    /// `seccomp_set_mode_strict` dans un enfant forké.
1017    /// Mode strict : seuls read/write/_exit/sigreturn autorisés.
1018    #[cfg(target_os = "linux")]
1019    #[test]
1020    fn seccomp_set_mode_strict_in_forked_child() {
1021        use air_sys_types::WaitEvent;
1022        let event = run_in_forked_child(|| {
1023            // Flush AVANT le mode strict : ensuite `open`/`write` (donc le flush
1024            // lui-même) seraient tués. Les lignes après ce point dans l'enfant
1025            // restent non couvertes (exception CHILD-EXIT résiduelle assumée).
1026            flush_child_coverage();
1027            // Après seccomp strict, seuls read/write/exit/sigreturn restent
1028            // autorisés : `exit_group` (utilisé par le harnais) déclencherait
1029            // un SIGKILL. On sort donc via le syscall `exit` (NR 60/93), seul
1030            // moyen de terminer proprement en mode strict — ce qui prouve à la
1031            // fois que le filtre est actif et qu'il laisse passer `exit`.
1032            match seccomp_set_mode_strict() {
1033                // SAFETY: `raw_exit` invoque le syscall `exit` (seul moyen de
1034                // terminer proprement en mode seccomp strict) ; il ne retourne
1035                // jamais et ne touche à aucune mémoire utilisateur.
1036                Ok(()) => unsafe { raw_exit(0) },
1037                Err(Errno::ENOSYS) => 42, // seccomp indisponible
1038                Err(_) => 11,
1039            }
1040        });
1041        match event {
1042            WaitEvent::Exited { code: 0 } => {}
1043            WaitEvent::Exited { code: 42 } => {}
1044            other => panic!("seccomp_set_mode_strict : {other:?}"),
1045        }
1046    }
1047
1048    /// Termine le processus courant via le syscall `exit` (terminaison du
1049    /// thread appelant), seul code de sortie autorisé sous `SECCOMP_MODE_STRICT`
1050    /// — contrairement à `exit_group`. Utilisé uniquement par le test strict.
1051    #[cfg(target_arch = "x86_64")]
1052    unsafe fn raw_exit(code: i32) -> ! {
1053        // SAFETY: `exit` (NR 60) ne revient jamais ; aucun argument mémoire.
1054        unsafe {
1055            core::arch::asm!(
1056                "syscall",
1057                in("rax") 60_i64, // exit
1058                in("rdi") i64::from(code),
1059                options(noreturn, nostack),
1060            );
1061        }
1062    }
1063
1064    /// Variante aarch64 (NR 93) de [`raw_exit`].
1065    #[cfg(target_arch = "aarch64")]
1066    unsafe fn raw_exit(code: i32) -> ! {
1067        // SAFETY: `exit` (NR 93) ne revient jamais ; aucun argument mémoire.
1068        unsafe {
1069            core::arch::asm!(
1070                "svc 0",
1071                in("x8") 93_i64, // exit
1072                in("x0") i64::from(code),
1073                options(noreturn, nostack),
1074            );
1075        }
1076    }
1077
1078    /// `landlock_create_ruleset` accepte le masque d'accès maximal.
1079    #[cfg(target_os = "linux")]
1080    #[test]
1081    fn landlock_create_ruleset_accepts_all_known_access_bits() {
1082        // Tous les bits de `LandlockAccessFs` sont des accès ABI connus : le
1083        // noyau cible les accepte (chemin succès avec le masque maximal). La
1084        // branche d'erreur de create_ruleset (via syscall_result) est couverte
1085        // par `add_rule_path_beneath_bad_fd_covers_syscall_result_error`.
1086        landlock_create_ruleset(LandlockAccessFs::all())
1087            .expect("tous les bits LandlockAccessFs sont des accès ABI valides");
1088    }
1089
1090    /// La branche d'erreur de `syscall_result` (chemin Landlock), couverte
1091    /// **en in-process** (sans fork) : ruleset valide mais fd de chemin invalide.
1092    #[cfg(target_os = "linux")]
1093    #[test]
1094    fn add_rule_path_beneath_bad_fd_covers_syscall_result_error() {
1095        use air_sys_types::fd::BorrowedFd;
1096        let mut ruleset =
1097            landlock_create_ruleset(LandlockAccessFs::READ_FILE).expect("Landlock requis");
1098        // SAFETY: i32::MAX n'est jamais un fd ouvert ; transmis au kernel
1099        // uniquement pour être rejeté (EBADF), couvrant la branche erreur.
1100        let bad = unsafe { BorrowedFd::borrow_raw(i32::MAX) };
1101        let err = ruleset
1102            .add_rule_path_beneath(bad, LandlockAccessFs::READ_FILE)
1103            .expect_err("un fd de chemin invalide doit échouer");
1104        assert_eq!(err, Errno::EBADF);
1105    }
1106
1107    /// `LandlockRuleset::as_fd` retourne un BorrowedFd valide.
1108    #[cfg(target_os = "linux")]
1109    #[test]
1110    fn landlock_ruleset_as_fd_valid() {
1111        let ruleset =
1112            landlock_create_ruleset(LandlockAccessFs::READ_FILE).expect("Landlock requis");
1113        let fd = ruleset.as_fd();
1114        assert!(fd.as_raw_fd() >= 0);
1115    }
1116}