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::{
22    LandlockAccessFs, LandlockAccessNet, LandlockRestrictFlags, LandlockScope, SeccompFilterFlags,
23    SockFprog,
24};
25
26// ─────────────────────────────────────────────────────────────────────────
27// Numéros de syscalls
28// ─────────────────────────────────────────────────────────────────────────
29
30#[cfg(target_arch = "x86_64")]
31const SYS_SECCOMP: i64 = 317;
32#[cfg(target_arch = "aarch64")]
33const SYS_SECCOMP: i64 = 277;
34
35#[cfg(target_arch = "x86_64")]
36const SYS_LANDLOCK_CREATE_RULESET: i64 = 444;
37#[cfg(target_arch = "aarch64")]
38const SYS_LANDLOCK_CREATE_RULESET: i64 = 444;
39
40#[cfg(target_arch = "x86_64")]
41const SYS_LANDLOCK_ADD_RULE: i64 = 445;
42#[cfg(target_arch = "aarch64")]
43const SYS_LANDLOCK_ADD_RULE: i64 = 445;
44
45#[cfg(target_arch = "x86_64")]
46const SYS_LANDLOCK_RESTRICT_SELF: i64 = 446;
47#[cfg(target_arch = "aarch64")]
48const SYS_LANDLOCK_RESTRICT_SELF: i64 = 446;
49
50// ─────────────────────────────────────────────────────────────────────────
51// Constantes kernel
52// ─────────────────────────────────────────────────────────────────────────
53
54/// `seccomp(2)` — mode strict (seuls read/write/_exit/sigreturn autorisés).
55const SECCOMP_SET_MODE_STRICT: u32 = 0;
56/// `seccomp(2)` — charge un filtre BPF précompilé.
57const SECCOMP_SET_MODE_FILTER: u32 = 1;
58
59/// `landlock_create_ruleset(2)` — drapeaux : requête de version ABI.
60const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1;
61
62/// `landlock_add_rule(2)` — type de règle : chemin filesystem.
63const LANDLOCK_RULE_PATH_BENEATH: i32 = 1;
64
65/// `landlock_add_rule(2)` — type de règle : **port TCP** (ABI v4+, Linux 6.7).
66const LANDLOCK_RULE_NET_PORT: i32 = 2;
67
68// ─────────────────────────────────────────────────────────────────────────
69// Structures kernel internes (repr C, non exposées)
70// ─────────────────────────────────────────────────────────────────────────
71
72/// Attributs d'un ruleset Landlock (cf. `linux/landlock.h`).
73#[repr(C)]
74struct KernelLandlockRulesetAttr {
75    handled_access_fs: u64,
76}
77
78/// Attributs d'un ruleset Landlock **ABI v4+** — deux champs (cf. `linux/landlock.h`).
79///
80/// **Une structure distincte, et non un champ ajouté à la précédente.** Le noyau lit
81/// exactement `size` octets : passer seize octets à un noyau qui n'en connaît que huit
82/// échoue `E2BIG`. Garder les deux formes permet de n'employer la seconde que lorsque
83/// l'ABI la supporte, plutôt que de dégrader silencieusement.
84#[repr(C)]
85struct KernelLandlockRulesetAttrV4 {
86    handled_access_fs: u64,
87    handled_access_net: u64,
88}
89
90/// Attributs d'un ruleset Landlock **ABI v6+** — trois champs (cf. `linux/landlock.h`).
91///
92/// Le troisième champ, `scoped`, ne porte **pas** des droits sur des ressources : il borne
93/// ce qu'un domaine peut atteindre **hors** de lui-même — sockets UNIX abstraits et signaux,
94/// deux voies qu'aucune règle par chemin ne pouvait voir.
95///
96/// **Une troisième structure**, pour la même raison qui en imposait une deuxième : le noyau
97/// lit exactement `size` octets, et présenter vingt-quatre octets à un noyau qui n'en connaît
98/// que seize échoue `E2BIG`. La pose de cage étant *fail-closed*, cet échec tuerait le
99/// processus sur un noyau parfaitement valide.
100#[repr(C)]
101struct KernelLandlockRulesetAttrV6 {
102    handled_access_fs: u64,
103    handled_access_net: u64,
104    scoped: u64,
105}
106
107/// Attributs d'une règle Landlock de type `NET_PORT` (cf. `linux/landlock.h`, ABI v4+).
108#[repr(C)]
109struct KernelLandlockNetPortAttr {
110    allowed_access: u64,
111    /// Port en **ordre hôte**, sur 64 bits — le noyau l'exige ainsi, malgré la
112    /// tentation de croire qu'un port de réseau voyage en ordre réseau.
113    port: u64,
114}
115
116/// Attributs d'une règle Landlock de type `PATH_BENEATH` (cf. `linux/landlock.h`).
117#[repr(C)]
118struct KernelLandlockPathBeneathAttr {
119    allowed_access: u64,
120    parent_fd: i32,
121}
122
123/// `struct sock_fprog` tel que vu par le kernel sur LP64 (x86_64/aarch64).
124///
125/// Sur LP64 : `{ u16 length, 6 octets de padding, u64 filter_ptr }` = 16 octets.
126/// Le padding est nécessaire pour aligner le pointeur sur 8 octets.
127#[repr(C)]
128struct KernelSockFprog {
129    len: u16,
130    _pad: [u8; 6],
131    filter: u64,
132}
133
134// ─────────────────────────────────────────────────────────────────────────
135// Helpers syscalls bruts (privés, spécifiques à l'architecture)
136// ─────────────────────────────────────────────────────────────────────────
137
138/// Effectue `seccomp(op, flags, uargs)` et retourne la valeur brute i64.
139///
140/// # Safety
141///
142/// L'appelant est responsable de la validité de `uargs` pour l'opération
143/// demandée. Pour `SECCOMP_SET_MODE_FILTER`, `uargs` doit pointer sur un
144/// `KernelSockFprog` valide dont les instructions restent en mémoire
145/// pendant la durée de l'appel.
146#[cfg(target_arch = "x86_64")]
147unsafe fn raw_syscall_seccomp(op: u32, flags: u32, uargs: u64) -> i64 {
148    let ret: i64;
149    // SAFETY (délégué à l'appelant) : voir doc de la fonction.
150    // ABI syscall x86_64 : numéro dans RAX, args dans RDI, RSI, RDX.
151    // Clobbe : RCX, R11 (préservés par l'ABI x86_64 de toute façon).
152    unsafe {
153        core::arch::asm!(
154            "syscall",
155            inlateout("rax") SYS_SECCOMP => ret,
156            in("rdi") u64::from(op),
157            in("rsi") u64::from(flags),
158            in("rdx") uargs,
159            lateout("rcx") _,
160            lateout("r11") _,
161            options(nostack),
162        );
163    }
164    ret
165}
166
167#[cfg(target_arch = "aarch64")]
168unsafe fn raw_syscall_seccomp(op: u32, flags: u32, uargs: u64) -> i64 {
169    let ret: i64;
170    // SAFETY (délégué à l'appelant) : voir doc de la fonction.
171    // ABI syscall aarch64 : numéro dans x8, args dans x0..x5.
172    unsafe {
173        core::arch::asm!(
174            "svc #0",
175            inlateout("x0") u64::from(op) => ret,
176            in("x1") u64::from(flags),
177            in("x2") uargs,
178            in("x8") SYS_SECCOMP as u64,
179            options(nostack),
180        );
181    }
182    ret
183}
184
185/// Effectue `landlock_create_ruleset(attr, size, flags)` et retourne le
186/// résultat brut i64.
187///
188/// # Safety
189///
190/// `attr` doit être soit nul (requête de version ABI) soit un pointeur
191/// valide sur un `KernelLandlockRulesetAttr` pour la durée de l'appel.
192#[cfg(target_arch = "x86_64")]
193unsafe fn raw_syscall_landlock_create_ruleset(attr: u64, size: u64, flags: u32) -> i64 {
194    let ret: i64;
195    unsafe {
196        core::arch::asm!(
197            "syscall",
198            inlateout("rax") SYS_LANDLOCK_CREATE_RULESET => ret,
199            in("rdi") attr,
200            in("rsi") size,
201            in("rdx") u64::from(flags),
202            lateout("rcx") _,
203            lateout("r11") _,
204            options(nostack),
205        );
206    }
207    ret
208}
209
210/// Variante aarch64 de `raw_syscall_landlock_create_ruleset`.
211///
212/// # Safety
213///
214/// `attr` doit être soit nul (requête de version ABI) soit un pointeur
215/// valide sur un `KernelLandlockRulesetAttr` pour la durée de l'appel.
216#[cfg(target_arch = "aarch64")]
217unsafe fn raw_syscall_landlock_create_ruleset(attr: u64, size: u64, flags: u32) -> i64 {
218    let ret: i64;
219    unsafe {
220        core::arch::asm!(
221            "svc #0",
222            inlateout("x0") attr => ret,
223            in("x1") size,
224            in("x2") u64::from(flags),
225            in("x8") SYS_LANDLOCK_CREATE_RULESET as u64,
226            options(nostack),
227        );
228    }
229    ret
230}
231
232/// Effectue `landlock_add_rule(ruleset_fd, rule_type, rule_attr, flags)`.
233///
234/// # Safety
235///
236/// `rule_attr` doit pointer sur un `KernelLandlockPathBeneathAttr` valide
237/// pour la durée de l'appel.
238#[cfg(target_arch = "x86_64")]
239unsafe fn raw_syscall_landlock_add_rule(
240    ruleset_fd: i32,
241    rule_type: i32,
242    rule_attr: u64,
243    flags: u32,
244) -> i64 {
245    let ret: i64;
246    unsafe {
247        core::arch::asm!(
248            "syscall",
249            inlateout("rax") SYS_LANDLOCK_ADD_RULE => ret,
250            in("rdi") i64::from(ruleset_fd),
251            in("rsi") i64::from(rule_type),
252            in("rdx") rule_attr,
253            in("r10") u64::from(flags),
254            lateout("rcx") _,
255            lateout("r11") _,
256            options(nostack),
257        );
258    }
259    ret
260}
261
262/// Variante aarch64 de `raw_syscall_landlock_add_rule`.
263///
264/// # Safety
265///
266/// `rule_attr` doit pointer sur un `KernelLandlockPathBeneathAttr` valide
267/// pour la durée de l'appel.
268#[cfg(target_arch = "aarch64")]
269unsafe fn raw_syscall_landlock_add_rule(
270    ruleset_fd: i32,
271    rule_type: i32,
272    rule_attr: u64,
273    flags: u32,
274) -> i64 {
275    let ret: i64;
276    unsafe {
277        core::arch::asm!(
278            "svc #0",
279            inlateout("x0") i64::from(ruleset_fd) => ret,
280            in("x1") i64::from(rule_type),
281            in("x2") rule_attr,
282            in("x3") u64::from(flags),
283            in("x8") SYS_LANDLOCK_ADD_RULE as u64,
284            options(nostack),
285        );
286    }
287    ret
288}
289
290/// Effectue `landlock_restrict_self(ruleset_fd, flags)`.
291#[cfg(target_arch = "x86_64")]
292fn raw_syscall_landlock_restrict_self(ruleset_fd: i32, flags: u32) -> i64 {
293    let ret: i64;
294    // SAFETY:
295    // - SYS_LANDLOCK_RESTRICT_SELF ne touche à aucune mémoire utilisateur.
296    // - ABI x86_64 : numéro dans RAX, args dans RDI, RSI.
297    unsafe {
298        core::arch::asm!(
299            "syscall",
300            inlateout("rax") SYS_LANDLOCK_RESTRICT_SELF => ret,
301            in("rdi") i64::from(ruleset_fd),
302            in("rsi") u64::from(flags),
303            lateout("rcx") _,
304            lateout("r11") _,
305            options(nostack),
306        );
307    }
308    ret
309}
310
311#[cfg(target_arch = "aarch64")]
312fn raw_syscall_landlock_restrict_self(ruleset_fd: i32, flags: u32) -> i64 {
313    let ret: i64;
314    // SAFETY:
315    // - SYS_LANDLOCK_RESTRICT_SELF ne touche à aucune mémoire utilisateur.
316    // - ABI aarch64 : numéro dans x8, args dans x0, x1.
317    unsafe {
318        core::arch::asm!(
319            "svc #0",
320            inlateout("x0") i64::from(ruleset_fd) => ret,
321            in("x1") u64::from(flags),
322            in("x8") SYS_LANDLOCK_RESTRICT_SELF as u64,
323            options(nostack),
324        );
325    }
326    ret
327}
328
329// ─────────────────────────────────────────────────────────────────────────
330// Helper commun : interprétation du retour i64 d'un syscall.
331// ─────────────────────────────────────────────────────────────────────────
332
333/// Convertit un retour brut de syscall en `Result<i64, Errno>`.
334///
335/// Linux indique une erreur avec un retour dans `[-4095, -1]`.
336fn syscall_result(raw: i64) -> Result<i64, Errno> {
337    if (-4095..0_i64).contains(&raw) {
338        // raw est dans [-4095, -1] : c'est un code errno négatif.
339        // On calcule l'inverse via wrapping_neg : pas de risque d'overflow
340        // car raw != i64::MIN (borné à -4095). Le résultat est dans [1, 4095],
341        // valide pour un i32 et non nul.
342        let errno_raw_i64 = raw.wrapping_neg();
343        // errno_raw_i64 est dans [1, 4095] : la troncature i64 → i32 est
344        // sans perte (4095 < i32::MAX) et le signe ne change pas.
345        #[allow(clippy::cast_possible_truncation)]
346        let errno_raw = errno_raw_i64 as i32;
347        let nz =
348            NonZeroI32::new(errno_raw).expect("errno dans [-4095,-1] est nécessairement non nul");
349        Err(Errno::from_nonzero(nz))
350    } else {
351        Ok(raw)
352    }
353}
354
355// ─────────────────────────────────────────────────────────────────────────
356// LandlockRuleset — type RAII autour du FD ruleset Landlock.
357// ─────────────────────────────────────────────────────────────────────────
358
359/// Ruleset Landlock (cf. `landlock_create_ruleset(2)`).
360///
361/// Encapsule un FD ruleset Landlock qui accumule des règles d'accès
362/// filesystem avant d'être appliqué au thread courant via
363/// [`LandlockRuleset::restrict_self`].
364///
365/// Les restrictions sont **irréversibles pour le fil qui les pose** : une fois
366/// `restrict_self` appelé, ce fil ne peut plus voir ses permissions augmenter.
367///
368/// # Ce que la monotonie ne couvre PAS : `TSYNC`
369///
370/// Cette page affirmait « Landlock est monotone », sans réserve. **C'est faux en présence
371/// de [`LandlockRestrictFlags::TSYNC`]**, et la correction est datée du 2026-08-13 après
372/// mesure sur un noyau réel : un fil qui pose un domaine **avec** `TSYNC` **écrase** celui
373/// de ses fils frères, y compris quand le leur était **plus strict**. Le noyau le dit —
374/// *« irrespective of previously established Landlock domains »* — et l'expérience le
375/// confirme : un fil frère qui ne pouvait rien ouvrir a retrouvé l'accès.
376///
377/// Ce n'est pas une élévation de privilège au sens usuel : les fils d'un processus
378/// partagent leur espace d'adressage et n'ont jamais été une frontière de sécurité entre
379/// eux. Mais l'intuition « une restriction Landlock ne se retire pas » est **fausse à
380/// l'échelle du processus**, et la croire conduirait à poser `TSYNC` d'office.
381#[derive(Debug)]
382pub struct LandlockRuleset(OwnedFd);
383
384impl LandlockRuleset {
385    /// Vue empruntée du FD ruleset.
386    #[must_use]
387    pub fn as_fd(&self) -> BorrowedFd<'_> {
388        use air_sys_types::fd::AsFd;
389        self.0.as_fd()
390    }
391
392    /// Ajoute une règle d'accès pour un chemin filesystem et ses descendants.
393    ///
394    /// Wrappeur de `landlock_add_rule(2)` avec `LANDLOCK_RULE_PATH_BENEATH`.
395    /// La règle s'applique au chemin référencé par `path` et à tous ses
396    /// descendants. `path` doit être ouvert avec `O_PATH | O_DIRECTORY`
397    /// (ou `O_PATH` seul pour un fichier).
398    ///
399    /// **Sémantique additive :** les règles ne peuvent qu'étendre les accès
400    /// autorisés au sein de ce ruleset. `restrict_self` appliquera
401    /// l'intersection de tous les rulesets cumulés.
402    ///
403    /// # Parameters
404    ///
405    /// - `path` : FD du chemin cible (typiquement ouvert avec `O_PATH`).
406    /// - `allowed_access` : permissions autorisées sur ce chemin et ses
407    ///   descendants.
408    ///
409    /// # Errors
410    ///
411    /// - `EINVAL` : `allowed_access` contient un bit non géré par ce ruleset,
412    ///   ou `path` n'est pas un FD valide.
413    /// - `ENOMEM` : mémoire kernel insuffisante.
414    /// - `EBADFD` : `path` n'est pas un FD de fichier ou répertoire.
415    /// - `EINTR` : interruption par signal (ADR-021 convention 2 — remonté
416    ///   tel quel, sans retry automatique).
417    ///
418    /// # Examples
419    ///
420    /// ```no_run
421    /// use air_sys_syscall::security::landlock_create_ruleset;
422    /// use air_sys_types::security::LandlockAccessFs;
423    /// use air_sys_types::fd::BorrowedFd;
424    ///
425    /// # fn example(path_fd: BorrowedFd<'_>) {
426    /// let mut ruleset = landlock_create_ruleset(
427    ///     LandlockAccessFs::READ_FILE | LandlockAccessFs::EXECUTE,
428    /// ).expect("create_ruleset");
429    /// ruleset.add_rule_path_beneath(path_fd, LandlockAccessFs::READ_FILE)
430    ///     .expect("add_rule");
431    /// # }
432    /// ```
433    pub fn add_rule_path_beneath(
434        &mut self,
435        path: BorrowedFd<'_>,
436        allowed_access: LandlockAccessFs,
437    ) -> Result<(), Errno> {
438        let attr = KernelLandlockPathBeneathAttr {
439            allowed_access: allowed_access.bits(),
440            parent_fd: path.as_raw_fd(),
441        };
442        let ruleset_fd = self.0.as_raw_fd();
443        // SAFETY:
444        // - `attr` est une variable locale valide pour toute la durée de l'appel.
445        // - `LANDLOCK_RULE_PATH_BENEATH` est la seule valeur de type règle
446        //   actuellement définie et acceptée par le kernel pour `attr` de
447        //   type `KernelLandlockPathBeneathAttr`.
448        // - `flags` = 0 : aucun drapeau défini pour cette opération à ce jour.
449        let ret = unsafe {
450            raw_syscall_landlock_add_rule(
451                ruleset_fd,
452                LANDLOCK_RULE_PATH_BENEATH,
453                &raw const attr as u64,
454                0,
455            )
456        };
457        syscall_result(ret).map(|_| ())
458    }
459
460    /// Autorise un **port TCP** — `landlock_add_rule(2)` avec
461    /// `LANDLOCK_RULE_NET_PORT` (ABI **v4+**, Linux 6.7).
462    ///
463    /// # Parameters
464    ///
465    /// - `port` : le port en **ordre hôte** (`443`, pas son écriture réseau) ;
466    /// - `allowed_access` : [`LandlockAccessNet::BIND_TCP`] et/ou
467    ///   [`LandlockAccessNet::CONNECT_TCP`].
468    ///
469    /// # Ce que cette règle borne, et ce qu'elle ne borne pas
470    ///
471    /// Elle borne le **port**, jamais l'**adresse**. Un processus autorisé à se
472    /// connecter au port 443 peut joindre n'importe quelle machine sur ce port.
473    /// Landlock n'est pas un pare-feu, et le prendre pour tel donnerait un faux
474    /// sentiment de confinement réseau.
475    ///
476    /// # Errors
477    ///
478    /// - `EINVAL` : le ruleset ne **gère** pas les accès réseau (il faut l'avoir créé
479    ///   avec [`landlock_create_ruleset_with_net`]), ou `allowed_access` porte un bit
480    ///   inconnu du noyau ;
481    /// - `EAFNOSUPPORT` / `ENOSYS` : ABI Landlock antérieure à v4.
482    pub fn add_rule_net_port(
483        &mut self,
484        port: u16,
485        allowed_access: LandlockAccessNet,
486    ) -> Result<(), Errno> {
487        let attr = KernelLandlockNetPortAttr {
488            allowed_access: allowed_access.bits(),
489            port: u64::from(port),
490        };
491        let ruleset_fd = self.0.as_raw_fd();
492        // SAFETY:
493        // - `attr` est une variable locale valide pour toute la durée de l'appel.
494        // - `LANDLOCK_RULE_NET_PORT` est le type de règle défini par le kernel pour
495        //   un `attr` de type `KernelLandlockNetPortAttr` (ABI v4).
496        // - `flags` = 0 : aucun drapeau défini pour cette opération à ce jour.
497        let ret = unsafe {
498            raw_syscall_landlock_add_rule(
499                ruleset_fd,
500                LANDLOCK_RULE_NET_PORT,
501                &raw const attr as u64,
502                0,
503            )
504        };
505        syscall_result(ret).map(|_| ())
506    }
507
508    /// Applique le ruleset au thread courant.
509    ///
510    /// Wrappeur de `landlock_restrict_self(2)`. **Irréversible.** Après
511    /// cet appel, le thread ne peut plus accéder aux chemins filesystem
512    /// non couverts par les règles du ruleset (pour les accès dans
513    /// `handled_access` du ruleset).
514    ///
515    /// Prérequis : avoir appelé [`crate::process::set_no_new_privs`] ou
516    /// posséder `CAP_SYS_ADMIN`.
517    ///
518    /// # Errors
519    ///
520    /// - `EPERM` : `no_new_privs` non positionné et `CAP_SYS_ADMIN` absent.
521    /// - `EINVAL` : flags invalides.
522    /// - `EINTR` : interruption par signal (ADR-021 convention 2 — remonté
523    ///   tel quel, sans retry automatique).
524    ///
525    /// # Examples
526    ///
527    /// ```no_run
528    /// use air_sys_syscall::security::landlock_create_ruleset;
529    /// use air_sys_syscall::process::set_no_new_privs;
530    /// use air_sys_types::security::LandlockAccessFs;
531    ///
532    /// let ruleset = landlock_create_ruleset(LandlockAccessFs::READ_FILE)
533    ///     .expect("create_ruleset");
534    /// set_no_new_privs().expect("no_new_privs");
535    /// ruleset.restrict_self().expect("restrict_self");
536    /// ```
537    pub fn restrict_self(&self) -> Result<(), Errno> {
538        self.restrict_self_with(LandlockRestrictFlags::empty())
539    }
540
541    /// Applique le ruleset **avec des drapeaux** (cf. [`LandlockRestrictFlags`]).
542    ///
543    /// # Ce que `TSYNC` change, et pourquoi une fonction séparée n'aurait rien réglé
544    ///
545    /// Sans [`TSYNC`](LandlockRestrictFlags::TSYNC), le domaine ne borne que le **fil
546    /// appelant** : dans un processus multifil qui se met lui-même en cage, les fils frères
547    /// restent dehors, et la cage se contourne en changeant de fil. Avec lui, la
548    /// configuration s'applique **atomiquement à tous les fils**.
549    ///
550    /// Les drapeaux ne sont pas des opérations multiplexées (ADR-021 convention 3) : ils se
551    /// combinent, et un wrapper par combinaison serait une explosion combinatoire sans
552    /// gain de typage.
553    ///
554    /// # Le noyau refuse ce qu'il ne connaît pas
555    ///
556    /// Un drapeau inconnu de l'ABI courante fait échouer l'appel (`EINVAL`) — il n'est
557    /// **pas** ignoré. La pose de cage étant *fail-closed*, demander un drapeau trop
558    /// récent **tue** le processus : interroger [`landlock_supported_abi`] d'abord.
559    ///
560    /// # Errors
561    ///
562    /// - `EPERM` : `no_new_privs` non positionné et `CAP_SYS_ADMIN` absent.
563    /// - `EINVAL` : un drapeau inconnu de l'ABI courante.
564    /// - `EINTR` : interruption par signal (ADR-021 convention 2 — remonté tel quel).
565    pub fn restrict_self_with(&self, flags: LandlockRestrictFlags) -> Result<(), Errno> {
566        let ruleset_fd = self.0.as_raw_fd();
567        let ret = raw_syscall_landlock_restrict_self(ruleset_fd, flags.bits());
568        syscall_result(ret).map(|_| ())
569    }
570}
571
572// ─────────────────────────────────────────────────────────────────────────
573// Fonctions publiques
574// ─────────────────────────────────────────────────────────────────────────
575
576/// Retourne la version ABI Landlock supportée par le kernel courant.
577///
578/// Utilise `landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION)`.
579/// Retourne `ENOSYS` si le kernel ne supporte pas Landlock (< 5.13).
580///
581/// | Valeur retournée | Version Landlock | Kernel minimum |
582/// |---|---|---|
583/// | 1 | v1 | Linux 5.13 |
584/// | 2 | v2 (REFER) | Linux 5.19 |
585/// | 3 | v3 (TRUNCATE) | Linux 6.2 |
586/// | 5 | v5 (IOCTL_DEV) | Linux 6.10 |
587///
588/// # Errors
589///
590/// - `ENOSYS` : Landlock non supporté ou non compilé dans le kernel.
591///
592/// # Examples
593///
594/// ```no_run
595/// use air_sys_syscall::security::landlock_supported_abi;
596///
597/// match landlock_supported_abi() {
598///     Ok(v) => println!("Landlock ABI v{v}"),
599///     Err(_) => println!("Landlock non disponible"),
600/// }
601/// ```
602pub fn landlock_supported_abi() -> Result<u32, Errno> {
603    // SAFETY:
604    // - attr = 0 (NULL) et size = 0 sont les valeurs attendues pour la
605    //   requête de version ABI (drapeaux LANDLOCK_CREATE_RULESET_VERSION).
606    // - Le kernel ne déréférence pas le pointeur quand flags = VERSION.
607    let ret = unsafe { raw_syscall_landlock_create_ruleset(0, 0, LANDLOCK_CREATE_RULESET_VERSION) };
608    let version = syscall_result(ret)?;
609    // La version ABI est un entier positif <= quelques dizaines ; la
610    // troncature i64 → u32 est sans perte sur les valeurs réalistes.
611    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
612    Ok(version as u32)
613}
614
615/// Crée un ruleset Landlock pour les accès filesystem spécifiés.
616///
617/// Wrappeur de `landlock_create_ruleset(2)` (Linux 5.13+). Le ruleset
618/// gérera les permissions listées dans `handled_access` — toute
619/// permission dans `handled_access` qui n'est pas couverte par une règle
620/// sera **refusée** par défaut après `restrict_self`.
621///
622/// # Parameters
623///
624/// - `handled_access` : ensemble des permissions filesystem que ce
625///   ruleset va gérer. Doit être un sous-ensemble des bits supportés
626///   par la version ABI du kernel (voir [`landlock_supported_abi`]).
627///
628/// # Errors
629///
630/// - `EINVAL` : `handled_access` contient un bit non supporté par la
631///   version ABI courante.
632/// - `ENOMEM` : mémoire kernel insuffisante.
633/// - `ENOSYS` : Landlock non disponible sur ce kernel.
634///
635/// # Examples
636///
637/// ```no_run
638/// use air_sys_syscall::security::landlock_create_ruleset;
639/// use air_sys_types::security::LandlockAccessFs;
640///
641/// let ruleset = landlock_create_ruleset(
642///     LandlockAccessFs::READ_FILE | LandlockAccessFs::READ_DIR | LandlockAccessFs::EXECUTE,
643/// ).expect("create_ruleset");
644/// ```
645pub fn landlock_create_ruleset(handled_access: LandlockAccessFs) -> Result<LandlockRuleset, Errno> {
646    let attr = KernelLandlockRulesetAttr {
647        handled_access_fs: handled_access.bits(),
648    };
649    // SAFETY:
650    // - `attr` est une variable locale valide pour toute la durée de l'appel.
651    // - `size_of::<KernelLandlockRulesetAttr>()` est la taille exacte de la
652    //   structure attendue par le kernel pour les versions ABI connues.
653    // - `flags` = 0 : création normale (pas requête de version ABI).
654    let ret = unsafe {
655        raw_syscall_landlock_create_ruleset(
656            &raw const attr as u64,
657            size_of::<KernelLandlockRulesetAttr>() as u64,
658            0,
659        )
660    };
661    let fd_raw = syscall_result(ret)?;
662    // SAFETY : le syscall a réussi (ret >= 0) ; le kernel garantit qu'un
663    // fd retourné est valide et appartient maintenant au processus.
664    // La troncature i64 → i32 est sans perte : les numéros de fd Linux
665    // tiennent dans un i32.
666    #[allow(clippy::cast_possible_truncation)]
667    let owned_fd = unsafe { OwnedFd::from_raw_fd(fd_raw as i32) };
668    Ok(LandlockRuleset(owned_fd))
669}
670
671/// Crée un ruleset Landlock gérant **les fichiers ET le réseau** (ABI **v4+**).
672///
673/// # Pourquoi une fonction distincte de [`landlock_create_ruleset`]
674///
675/// Le noyau lit exactement `size` octets d'attributs. Passer la structure à deux champs
676/// à un noyau qui n'en connaît qu'un échoue `E2BIG` — et la pose de cage étant
677/// *fail-closed*, cet échec **tuerait** le processus sur un noyau parfaitement valide.
678///
679/// Garder les deux formes permet à l'appelant de choisir en connaissance de cause, après
680/// avoir interrogé [`landlock_supported_abi`], plutôt que de dégrader en silence. C'est le
681/// même raisonnement qui a fait borner `air-sandbox` à l'ABI v1 : demander au noyau ce
682/// qu'il ne connaît pas n'est pas une dégradation, c'est une mort.
683///
684/// # Errors
685///
686/// - `EINVAL` : un bit de `handled_fs` ou `handled_net` est inconnu de l'ABI courante ;
687/// - `E2BIG` : le noyau ne connaît pas la forme à deux champs (ABI < v4) ;
688/// - `ENOMEM`, `ENOSYS` : comme [`landlock_create_ruleset`].
689///
690/// # Examples
691///
692/// ```no_run
693/// use air_sys_syscall::security::{landlock_create_ruleset_with_net, landlock_supported_abi};
694/// use air_sys_types::security::{LandlockAccessFs, LandlockAccessNet};
695///
696/// // Le réseau n'existe qu'à partir de l'ABI v4 : on demande avant de composer.
697/// if landlock_supported_abi().unwrap_or(0) >= 4 {
698///     let ruleset = landlock_create_ruleset_with_net(
699///         LandlockAccessFs::READ_FILE,
700///         LandlockAccessNet::CONNECT_TCP,
701///     )
702///     .expect("create_ruleset avec réseau");
703///     let _ = ruleset;
704/// }
705/// ```
706pub fn landlock_create_ruleset_with_net(
707    handled_fs: LandlockAccessFs,
708    handled_net: LandlockAccessNet,
709) -> Result<LandlockRuleset, Errno> {
710    let attr = KernelLandlockRulesetAttrV4 {
711        handled_access_fs: handled_fs.bits(),
712        handled_access_net: handled_net.bits(),
713    };
714    // SAFETY:
715    // - `attr` est une variable locale valide pour toute la durée de l'appel.
716    // - `size_of::<KernelLandlockRulesetAttrV4>()` est la taille exacte de la forme
717    //   à deux champs attendue par un kernel d'ABI v4 ou supérieure.
718    // - `flags` = 0 : création normale (pas requête de version ABI).
719    let ret = unsafe {
720        raw_syscall_landlock_create_ruleset(
721            &raw const attr as u64,
722            size_of::<KernelLandlockRulesetAttrV4>() as u64,
723            0,
724        )
725    };
726    let fd_raw = syscall_result(ret)?;
727    // SAFETY : le syscall a réussi (ret >= 0) ; le kernel garantit qu'un fd retourné est
728    // valide et appartient maintenant au processus. La troncature i64 → i32 est sans
729    // perte : les numéros de fd Linux tiennent dans un i32.
730    #[allow(clippy::cast_possible_truncation)]
731    let owned_fd = unsafe { OwnedFd::from_raw_fd(fd_raw as i32) };
732    Ok(LandlockRuleset(owned_fd))
733}
734
735/// Crée un ruleset Landlock gérant **fichiers, réseau ET portées** (ABI **v6+**).
736///
737/// # Ce que la portée ajoute, et que les deux autres ne pouvaient pas dire
738///
739/// `handled_fs` et `handled_net` bornent ce qu'on a le droit de faire **sur** une ressource
740/// nommée. `scoped` borne autre chose : jusqu'où le domaine peut **atteindre en dehors de
741/// lui-même**. Un socket UNIX abstrait n'a pas de chemin et un signal ne traverse aucun
742/// descripteur — ni l'un ni l'autre ne pouvait donc être borné par une règle.
743///
744/// # Pourquoi une fonction de plus
745///
746/// Même raison qu'entre [`landlock_create_ruleset`] et [`landlock_create_ruleset_with_net`] :
747/// le noyau lit exactement `size` octets. Sonder [`landlock_supported_abi`] avant de choisir
748/// la forme est le seul moyen de ne pas mourir sur un noyau plus ancien.
749///
750/// # Errors
751///
752/// - `EINVAL` : un bit de `handled_fs`, `handled_net` ou `scope` est inconnu de l'ABI
753///   courante ;
754/// - `E2BIG` : le noyau ne connaît pas la forme à trois champs (ABI < v6) ;
755/// - `ENOMEM`, `ENOSYS` : comme [`landlock_create_ruleset`].
756///
757/// # Examples
758///
759/// ```no_run
760/// use air_sys_syscall::security::{landlock_create_ruleset_with_scope, landlock_supported_abi};
761/// use air_sys_types::security::{LandlockAccessFs, LandlockAccessNet, LandlockScope};
762///
763/// // Les portées n'existent qu'à partir de l'ABI v6 : on demande avant de composer.
764/// if landlock_supported_abi().unwrap_or(0) >= 6 {
765///     let ruleset = landlock_create_ruleset_with_scope(
766///         LandlockAccessFs::READ_FILE,
767///         LandlockAccessNet::CONNECT_TCP,
768///         LandlockScope::ABSTRACT_UNIX_SOCKET | LandlockScope::SIGNAL,
769///     )
770///     .expect("create_ruleset avec portées");
771///     let _ = ruleset;
772/// }
773/// ```
774pub fn landlock_create_ruleset_with_scope(
775    handled_fs: LandlockAccessFs,
776    handled_net: LandlockAccessNet,
777    scope: LandlockScope,
778) -> Result<LandlockRuleset, Errno> {
779    let attr = KernelLandlockRulesetAttrV6 {
780        handled_access_fs: handled_fs.bits(),
781        handled_access_net: handled_net.bits(),
782        scoped: scope.bits(),
783    };
784    // SAFETY:
785    // - `attr` est une variable locale valide pour toute la durée de l'appel.
786    // - `size_of::<KernelLandlockRulesetAttrV6>()` est la taille exacte de la forme à trois
787    //   champs attendue par un kernel d'ABI v6 ou supérieure.
788    // - `flags` = 0 : création normale (pas requête de version ABI).
789    let ret = unsafe {
790        raw_syscall_landlock_create_ruleset(
791            &raw const attr as u64,
792            size_of::<KernelLandlockRulesetAttrV6>() as u64,
793            0,
794        )
795    };
796    let fd_raw = syscall_result(ret)?;
797    // SAFETY : le syscall a réussi (ret >= 0) ; le kernel garantit qu'un fd retourné est
798    // valide et appartient maintenant au processus. La troncature i64 → i32 est sans
799    // perte : les numéros de fd Linux tiennent dans un i32.
800    #[allow(clippy::cast_possible_truncation)]
801    let owned_fd = unsafe { OwnedFd::from_raw_fd(fd_raw as i32) };
802    Ok(LandlockRuleset(owned_fd))
803}
804
805/// Applique un filtre seccomp-BPF précompilé au thread courant.
806///
807/// Wrappeur de `seccomp(SECCOMP_SET_MODE_FILTER, flags, prog)`.
808/// Charge le programme BPF tel quel dans le kernel — aucune compilation
809/// BPF n'est effectuée ici (API primitive couche 0).
810///
811/// **L'opération est irréversible.** Une fois chargé, le filtre ne peut
812/// pas être retiré. Des filtres successifs peuvent être ajoutés mais
813/// seulement pour restreindre davantage (jamais pour assouplir).
814///
815/// # Piège : avec `TSYNC`, un retour POSITIF est un ÉCHEC
816///
817/// `seccomp(2)` avec [`SeccompFilterFlags::TSYNC`] rend, en cas d'échec, le **`tid` du fil
818/// fautif** — un entier **positif** — au lieu d'un code d'erreur négatif. Ce wrapper traite
819/// tout retour ≥ 0 comme un succès : posé nu, `TSYNC` rapporterait donc une cage posée
820/// alors qu'elle ne l'est pas.
821///
822/// **Toujours l'accompagner de [`SeccompFilterFlags::TSYNC_ESRCH`]**, qui existe
823/// précisément pour cela : le noyau rend alors `-ESRCH`, et l'échec redevient un échec.
824///
825/// # Safety
826///
827/// - L'appelant doit avoir appelé `set_no_new_privs()` OU posséder
828///   `CAP_SYS_ADMIN`.
829/// - Le programme BPF doit autoriser tous les syscalls nécessaires au
830///   runtime Rust (au minimum : `read`, `write`, `mmap`, `munmap`,
831///   `futex`, `exit_group`, `rt_sigreturn`).
832/// - Un programme BPF incorrect peut bloquer le processus ou le tuer.
833/// - Irréversible.
834///
835/// # Errors
836///
837/// - `EPERM` : `no_new_privs` non positionné et `CAP_SYS_ADMIN` absent.
838/// - `EINVAL` : programme BPF invalide ou flags non reconnus.
839/// - `ENOMEM` : mémoire insuffisante.
840/// - `ENOSYS` : seccomp non compilé dans ce kernel.
841///
842/// # Examples
843///
844/// ```no_run
845/// use air_sys_syscall::security::seccomp_set_mode_filter;
846/// use air_sys_types::security::{SeccompFilterFlags, SockFilter, SockFprog};
847///
848/// // Instruction BPF RET ALLOW (0x7fff0000 = SECCOMP_RET_ALLOW).
849/// let allow_all = [SockFilter { code: 0x0006, jt: 0, jf: 0, k: 0x7fff_0000 }];
850/// let prog = SockFprog::new(&allow_all).expect("programme valide");
851///
852/// // unsafe : charge un filtre irréversible dans le kernel.
853/// unsafe {
854///     seccomp_set_mode_filter(&prog, SeccompFilterFlags::empty())
855///         .expect("seccomp_set_mode_filter");
856/// }
857/// ```
858pub unsafe fn seccomp_set_mode_filter(
859    prog: &SockFprog<'_>,
860    flags: SeccompFilterFlags,
861) -> Result<(), Errno> {
862    // Construction du struct sock_fprog attendu par le kernel sur LP64.
863    // Sur x86_64 et aarch64 (les deux archs supportées, ADR-014), la ABI
864    // LP64 impose : u16 length, 6 octets de padding, puis le pointeur aligné
865    // sur 8 octets — total 16 octets.
866    let kfprog = KernelSockFprog {
867        len: prog.len(),
868        _pad: [0u8; 6],
869        filter: prog.as_ptr() as u64,
870    };
871    // SAFETY (préconditions héritées de l'appelant) :
872    // - Le programme BPF pointé par `prog` reste valide pendant l'appel.
873    // - `kfprog` est une variable locale valide pour toute la durée
874    //   de l'appel syscall.
875    // - `SECCOMP_SET_MODE_FILTER` avec un `sock_fprog` valide ne cause
876    //   pas d'UB intrinsèquement — c'est un appel kernel ordinaire.
877    let ret = unsafe {
878        raw_syscall_seccomp(
879            SECCOMP_SET_MODE_FILTER,
880            flags.bits(),
881            &raw const kfprog as u64,
882        )
883    };
884    syscall_result(ret).map(|_| ())
885}
886
887/// Active le mode seccomp strict.
888///
889/// En mode strict, seuls `read(2)`, `write(2)`, `_exit(2)` et
890/// `sigreturn(2)` sont autorisés pour le thread courant. Tout autre
891/// syscall entraîne un `SIGKILL`.
892///
893/// **L'opération est irréversible.**
894///
895/// # Errors
896///
897/// - `EPERM` : les threads du processus utilisent déjà des filtres
898///   incompatibles (cas rare).
899/// - `ENOSYS` : seccomp non compilé dans ce kernel.
900///
901/// # Examples
902///
903/// ```no_run
904/// use air_sys_syscall::security::seccomp_set_mode_strict;
905///
906/// // N'appeler que dans un sous-processus dédié ; irréversible.
907/// seccomp_set_mode_strict().expect("seccomp_set_mode_strict");
908/// ```
909pub fn seccomp_set_mode_strict() -> Result<(), Errno> {
910    // SAFETY:
911    // - `SECCOMP_SET_MODE_STRICT` avec flags=0 et uargs=0 est l'appel
912    //   canonique documenté dans seccomp(2). Aucune mémoire utilisateur
913    //   n'est déréférencée par le kernel pour cette opération.
914    let ret = unsafe { raw_syscall_seccomp(SECCOMP_SET_MODE_STRICT, 0, 0) };
915    syscall_result(ret).map(|_| ())
916}
917
918// ─────────────────────────────────────────────────────────────────────────
919// Tests
920// ─────────────────────────────────────────────────────────────────────────
921
922#[cfg(test)]
923mod tests;