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;