air_sys_types/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//! Types de la famille `security` — seccomp-BPF (primitif) et Landlock.
6//!
7//! Cf. `docs/specs/layer-0/family-security.md`.
8//!
9//! **API couche 0 :** cette crate expose uniquement les types primitifs.
10//! L'API déclarative (compilation de règles en BPF) appartient à la couche 1.
11
12use bitflags::bitflags;
13
14use crate::errno::Errno;
15
16// ─────────────────────────────────────────────────────────────────────────
17// seccomp-BPF — types primitifs pour charger un programme BPF précompilé.
18// ─────────────────────────────────────────────────────────────────────────
19
20/// Une instruction de filtre BPF classique — layout exact de `struct sock_filter`.
21///
22/// Opaque du point de vue de la couche 0 : la couche 0 ne compile pas ces
23/// instructions, elle les charge telles quelles dans le kernel via
24/// `seccomp(SECCOMP_SET_MODE_FILTER, …)`.
25///
26/// Cf. `linux/filter.h` et RFC 4972.
27#[repr(C)]
28#[derive(Debug, Clone, Copy)]
29pub struct SockFilter {
30 /// Opcode BPF (instruction code).
31 pub code: u16,
32 /// Jump true offset.
33 pub jt: u8,
34 /// Jump false offset.
35 pub jf: u8,
36 /// Generic multiuse field.
37 pub k: u32,
38}
39
40/// Programme BPF complet prêt à charger dans le kernel.
41///
42/// Emprunte une tranche d'instructions [`SockFilter`] appartenant à l'appelant
43/// (zéro allocation heap). Correspond à `struct sock_fprog` du kernel
44/// (`linux/filter.h`).
45///
46/// Construit via [`SockFprog::new`] ; invalide si la tranche est vide ou
47/// dépasse `BPF_MAXINSNS` (4096 instructions).
48#[derive(Debug)]
49pub struct SockFprog<'a> {
50 len: u16,
51 filter: *const SockFilter,
52 _marker: core::marker::PhantomData<&'a [SockFilter]>,
53}
54
55impl<'a> SockFprog<'a> {
56 /// Construit un programme BPF à partir d'une tranche d'instructions.
57 ///
58 /// # Errors
59 ///
60 /// Retourne `Err(Errno::EINVAL)` si la tranche est vide ou dépasse
61 /// `BPF_MAXINSNS` (4096 instructions).
62 ///
63 /// # Examples
64 ///
65 /// ```
66 /// use air_sys_types::security::{SockFilter, SockFprog};
67 ///
68 /// // Instruction BPF minimale : RET K avec valeur SECCOMP_RET_ALLOW (0x7fff0000).
69 /// let allow = SockFilter { code: 0x0006, jt: 0, jf: 0, k: 0x7fff_0000 };
70 /// let instructions = [allow];
71 /// let prog = SockFprog::new(&instructions).expect("programme valide");
72 /// assert_eq!(prog.len(), 1);
73 /// ```
74 pub fn new(instructions: &'a [SockFilter]) -> Result<Self, Errno> {
75 const BPF_MAXINSNS: usize = 4096;
76 if instructions.is_empty() || instructions.len() > BPF_MAXINSNS {
77 return Err(Errno::EINVAL);
78 }
79 // SAFETY: length <= 4096 < 65535 ; la troncature u16 est sans perte.
80 #[allow(clippy::cast_possible_truncation)]
81 Ok(Self {
82 len: instructions.len() as u16,
83 filter: instructions.as_ptr(),
84 _marker: core::marker::PhantomData,
85 })
86 }
87
88 /// Longueur du programme (nombre d'instructions).
89 #[must_use]
90 pub fn len(&self) -> u16 {
91 self.len
92 }
93
94 /// Retourne `true` si le programme est vide.
95 ///
96 /// En pratique, [`SockFprog::new`] refuse les tranches vides, donc un
97 /// `SockFprog` construit correctement a toujours `is_empty() == false`.
98 #[must_use]
99 pub fn is_empty(&self) -> bool {
100 self.len == 0
101 }
102
103 /// Pointeur brut vers le tableau d'instructions (pour appel syscall).
104 #[must_use]
105 pub fn as_ptr(&self) -> *const SockFilter {
106 self.filter
107 }
108}
109
110// SAFETY: SockFprog emprunte des données immuables derrière une référence
111// partagée ; Send + Sync sont corrects dès lors que les données référencées
112// sont elles-mêmes Send + Sync. Le raw pointer n'introduit pas de non-Send :
113// il est uniquement dérivé d'une référence valide pour la durée de vie 'a.
114unsafe impl Send for SockFprog<'_> {}
115// SAFETY: même raison que `Send` — le pointeur dérive d'une référence partagée `&'a`
116// à des filtres immuables ; le partager entre fils ne permet que de les lire.
117unsafe impl Sync for SockFprog<'_> {}
118
119bitflags! {
120 /// Drapeaux pour `seccomp(SECCOMP_SET_MODE_FILTER, flags, …)`.
121 ///
122 /// Cf. `linux/seccomp.h`.
123 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
124 pub struct SeccompFilterFlags: u32 {
125 /// Synchronise le filtre à tous les threads du processus.
126 const TSYNC = 1;
127 /// Logue chaque syscall correspondant au filtre.
128 const LOG = 2;
129 /// Désactive la migration de spectres (speculation store bypass).
130 const SPEC_ALLOW = 4;
131 /// Crée un fd `seccomp_unotify` pour l'observation (Linux 5.0+).
132 const NEW_LISTENER = 8;
133 /// Retourne `ESRCH` si `TSYNC` et un thread résiste (Linux 5.7+).
134 const TSYNC_ESRCH = 16;
135 /// Bloque le signal kill jusqu'à réception de la réponse unotify.
136 const WAIT_KILLABLE_RECV = 32;
137 }
138}
139
140// ─────────────────────────────────────────────────────────────────────────
141// Landlock — filtrage d'accès filesystem sans privilège.
142// ─────────────────────────────────────────────────────────────────────────
143
144bitflags! {
145 /// Permissions filesystem Landlock (cf. `linux/landlock.h`).
146 ///
147 /// La disponibilité de certains bits dépend de la version ABI Landlock
148 /// du kernel. Utilisez `landlock_supported_abi()` pour connaître la
149 /// version disponible et masquer les bits non supportés.
150 ///
151 /// | Bit | Depuis | Signification |
152 /// |-----|--------|---------------|
153 /// | EXECUTE … MAKE_SYM | Landlock v1 (Linux 5.13) | permissions de base |
154 /// | REFER | Landlock v2 (Linux 5.19) | déplacement inter-répertoires |
155 /// | TRUNCATE | Landlock v3 (Linux 6.2) | troncature de fichiers |
156 /// | IOCTL_DEV | Landlock v5 (Linux 6.10) | ioctl sur devices |
157 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
158 pub struct LandlockAccessFs: u64 {
159 /// Exécution de fichiers.
160 const EXECUTE = 1 << 0;
161 /// Écriture dans les fichiers (pas troncature : voir `TRUNCATE`).
162 const WRITE_FILE = 1 << 1;
163 /// Lecture de fichiers.
164 const READ_FILE = 1 << 2;
165 /// Listage de répertoires.
166 const READ_DIR = 1 << 3;
167 /// Suppression de répertoires.
168 const REMOVE_DIR = 1 << 4;
169 /// Suppression de fichiers.
170 const REMOVE_FILE = 1 << 5;
171 /// Création de fichiers caractère.
172 const MAKE_CHAR = 1 << 6;
173 /// Création de répertoires.
174 const MAKE_DIR = 1 << 7;
175 /// Création de fichiers réguliers.
176 const MAKE_REG = 1 << 8;
177 /// Création de sockets Unix.
178 const MAKE_SOCK = 1 << 9;
179 /// Création de FIFOs.
180 const MAKE_FIFO = 1 << 10;
181 /// Création de fichiers bloc.
182 const MAKE_BLOCK = 1 << 11;
183 /// Création de liens symboliques.
184 const MAKE_SYM = 1 << 12;
185 /// Déplacement de fichiers entre répertoires (Landlock v2+).
186 const REFER = 1 << 13;
187 /// Troncature de fichiers (Landlock v3+).
188 const TRUNCATE = 1 << 14;
189 /// Opérations ioctl sur devices (Landlock v5+).
190 const IOCTL_DEV = 1 << 15;
191 }
192}
193
194bitflags! {
195 /// Permissions **réseau** Landlock (cf. `linux/landlock.h`), ABI **v4+**
196 /// (Linux 6.7).
197 ///
198 /// Landlock ne borne le réseau que sur **TCP**, et seulement pour ces deux
199 /// verbes. Ce n'est pas un pare-feu : il ne filtre ni les adresses, ni UDP,
200 /// ni les paquets. Il répond à une question plus étroite — *ce processus
201 /// a-t-il le droit d'écouter, ou de se connecter, sur ce port ?* — et il y
202 /// répond dans le noyau, sans démon ni règle globale.
203 ///
204 /// Le confondre avec un pare-feu serait une erreur de conception : une
205 /// application qui reçoit `CONNECT_TCP` sur le port 443 peut joindre
206 /// **n'importe quelle** machine sur ce port.
207 ///
208 /// | Bit | Depuis | Signification |
209 /// |-----|--------|---------------|
210 /// | BIND_TCP | Landlock v4 (Linux 6.7) | se lier à un port en écoute |
211 /// | CONNECT_TCP | Landlock v4 (Linux 6.7) | se connecter à un port |
212 ///
213 /// Comme pour [`LandlockAccessFs`], demander un bit que le noyau ne connaît
214 /// pas fait échouer la création du ruleset (`EINVAL`) : interroger
215 /// `landlock_supported_abi()` avant de composer.
216 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
217 pub struct LandlockAccessNet: u64 {
218 /// Se lier à un port TCP (`bind`) — Landlock v4+.
219 const BIND_TCP = 1 << 0;
220 /// Se connecter à un port TCP (`connect`) — Landlock v4+.
221 const CONNECT_TCP = 1 << 1;
222 }
223}
224
225bitflags! {
226 /// **Portées** Landlock (cf. `linux/landlock.h`), ABI **v6+** (Linux 6.12).
227 ///
228 /// # Ce que le *scoping* borne, et que rien d'autre ne bornait
229 ///
230 /// Ce ne sont **pas** des droits par chemin. Ils ne disent pas *ce qu'on a le droit de
231 /// faire sur telle ressource*, mais *jusqu'où un domaine peut atteindre en dehors de
232 /// lui-même*. Deux voies latérales, précisément celles qu'aucune règle de fichier ne
233 /// pouvait voir :
234 ///
235 /// - un socket UNIX **abstrait** n'a **pas de chemin** — il vit dans un espace de noms
236 /// à part, sans entrée de système de fichiers. Aucune règle Landlock de fichier ne
237 /// pouvait donc l'atteindre : deux processus confinés séparément pouvaient dialoguer
238 /// sans que leur cage en sache rien ;
239 /// - un **signal** ne traverse aucun descripteur et ne touche aucun chemin.
240 ///
241 /// | Bit | Depuis | Ce qui devient interdit |
242 /// |-----|--------|-------------------------|
243 /// | `ABSTRACT_UNIX_SOCKET` | Landlock v6 (Linux 6.12) | se connecter à un socket UNIX abstrait créé **hors** du domaine |
244 /// | `SIGNAL` | Landlock v6 (Linux 6.12) | envoyer un signal à un processus **hors** du domaine |
245 ///
246 /// # La direction compte
247 ///
248 /// La borne est **sortante** : elle empêche le processus confiné d'atteindre l'extérieur,
249 /// et non l'extérieur de l'atteindre. Un parent non confiné garde donc la main sur son
250 /// enfant — ce qui est nécessaire, sans quoi un lanceur ne pourrait plus arrêter ce qu'il
251 /// a lancé.
252 ///
253 /// # À l'intérieur du domaine, rien ne change
254 ///
255 /// Deux processus du **même** domaine continuent de se joindre et de se signaler. Le
256 /// *scoping* isole des domaines les uns des autres, il ne fragmente pas un domaine.
257 ///
258 /// Comme pour [`LandlockAccessFs`] et [`LandlockAccessNet`], demander un bit que le noyau
259 /// ne connaît pas fait échouer la création du ruleset : interroger
260 /// `landlock_supported_abi()` avant de composer.
261 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
262 pub struct LandlockScope: u64 {
263 /// Se connecter à un socket UNIX **abstrait** créé hors du domaine — Landlock v6+.
264 const ABSTRACT_UNIX_SOCKET = 1 << 0;
265 /// Envoyer un signal à un processus hors du domaine — Landlock v6+.
266 const SIGNAL = 1 << 1;
267 }
268}
269
270bitflags! {
271 /// Drapeaux de `landlock_restrict_self(2)` (cf. `linux/landlock.h`).
272 ///
273 /// # Deux familles, deux natures
274 ///
275 /// - les trois premiers ne changent **aucune** décision d'accès : ils disent au noyau ce
276 /// qu'il doit **tracer** d'un refus (ABI v7). Utiles au diagnostic, ils appartiennent à
277 /// l'appelant ([ADR-156] D5) ;
278 /// - le quatrième, [`TSYNC`](Self::TSYNC), change ce qui est **borné** — et c'est le seul
279 /// qui ferme quelque chose.
280 ///
281 /// # Ce que `TSYNC` répare
282 ///
283 /// Sans lui, `landlock_restrict_self` ne borne que le **fil appelant**. Dans un processus
284 /// multifil qui se met lui-même en cage, les fils frères restent **hors** du domaine : la
285 /// cage se contourne en changeant de fil. Avec lui, la configuration s'applique
286 /// **atomiquement à tous les fils** du processus.
287 ///
288 /// Le noyau propage aussi `no_new_privs` aux fils frères quand le fil appelant le porte —
289 /// ce qui évite l'incohérence d'un fil confiné et d'un fil qui peut encore élever ses
290 /// privilèges.
291 ///
292 /// **Le noyau dit qu'il « écrase » la configuration des fils frères, quels que soient les
293 /// domaines déjà établis sur eux.** Ce que cela fait exactement d'un fil frère **plus
294 /// strictement** confiné est vérifié par un test dédié plutôt que supposé — c'est la
295 /// différence entre borner et croire borner.
296 ///
297 /// [ADR-156]: ../../../docs/adrs/ADR-156-landlock-abi-v7-fr.md
298 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
299 pub struct LandlockRestrictFlags: u32 {
300 /// Ne journalise pas les refus venant du fil qui crée le domaine, ni de ses enfants
301 /// tant qu'ils exécutent le même code (ABI v7).
302 const LOG_SAME_EXEC_OFF = 1 << 0;
303 /// Journalise les refus **après** un `execve` dans le domaine créé (ABI v7).
304 const LOG_NEW_EXEC_ON = 1 << 1;
305 /// Ne journalise pas les refus venant des **sous-domaines** créés par l'appelant ou
306 /// ses descendants (ABI v7).
307 const LOG_SUBDOMAINS_OFF = 1 << 2;
308 /// Applique la configuration **à tous les fils** du processus, atomiquement.
309 const TSYNC = 1 << 3;
310 }
311}
312
313#[cfg(test)]
314mod tests;