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