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}