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;