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<'_> {}
115unsafe impl Sync for SockFprog<'_> {}
116
117bitflags! {
118 /// Drapeaux pour `seccomp(SECCOMP_SET_MODE_FILTER, flags, …)`.
119 ///
120 /// Cf. `linux/seccomp.h`.
121 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
122 pub struct SeccompFilterFlags: u32 {
123 /// Synchronise le filtre à tous les threads du processus.
124 const TSYNC = 1;
125 /// Logue chaque syscall correspondant au filtre.
126 const LOG = 2;
127 /// Désactive la migration de spectres (speculation store bypass).
128 const SPEC_ALLOW = 4;
129 /// Crée un fd `seccomp_unotify` pour l'observation (Linux 5.0+).
130 const NEW_LISTENER = 8;
131 /// Retourne `ESRCH` si `TSYNC` et un thread résiste (Linux 5.7+).
132 const TSYNC_ESRCH = 16;
133 /// Bloque le signal kill jusqu'à réception de la réponse unotify.
134 const WAIT_KILLABLE_RECV = 32;
135 }
136}
137
138// ─────────────────────────────────────────────────────────────────────────
139// Landlock — filtrage d'accès filesystem sans privilège.
140// ─────────────────────────────────────────────────────────────────────────
141
142bitflags! {
143 /// Permissions filesystem Landlock (cf. `linux/landlock.h`).
144 ///
145 /// La disponibilité de certains bits dépend de la version ABI Landlock
146 /// du kernel. Utilisez `landlock_supported_abi()` pour connaître la
147 /// version disponible et masquer les bits non supportés.
148 ///
149 /// | Bit | Depuis | Signification |
150 /// |-----|--------|---------------|
151 /// | EXECUTE … MAKE_SYM | Landlock v1 (Linux 5.13) | permissions de base |
152 /// | REFER | Landlock v2 (Linux 5.19) | déplacement inter-répertoires |
153 /// | TRUNCATE | Landlock v3 (Linux 6.2) | troncature de fichiers |
154 /// | IOCTL_DEV | Landlock v5 (Linux 6.10) | ioctl sur devices |
155 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
156 pub struct LandlockAccessFs: u64 {
157 /// Exécution de fichiers.
158 const EXECUTE = 1 << 0;
159 /// Écriture dans les fichiers (pas troncature : voir `TRUNCATE`).
160 const WRITE_FILE = 1 << 1;
161 /// Lecture de fichiers.
162 const READ_FILE = 1 << 2;
163 /// Listage de répertoires.
164 const READ_DIR = 1 << 3;
165 /// Suppression de répertoires.
166 const REMOVE_DIR = 1 << 4;
167 /// Suppression de fichiers.
168 const REMOVE_FILE = 1 << 5;
169 /// Création de fichiers caractère.
170 const MAKE_CHAR = 1 << 6;
171 /// Création de répertoires.
172 const MAKE_DIR = 1 << 7;
173 /// Création de fichiers réguliers.
174 const MAKE_REG = 1 << 8;
175 /// Création de sockets Unix.
176 const MAKE_SOCK = 1 << 9;
177 /// Création de FIFOs.
178 const MAKE_FIFO = 1 << 10;
179 /// Création de fichiers bloc.
180 const MAKE_BLOCK = 1 << 11;
181 /// Création de liens symboliques.
182 const MAKE_SYM = 1 << 12;
183 /// Déplacement de fichiers entre répertoires (Landlock v2+).
184 const REFER = 1 << 13;
185 /// Troncature de fichiers (Landlock v3+).
186 const TRUNCATE = 1 << 14;
187 /// Opérations ioctl sur devices (Landlock v5+).
188 const IOCTL_DEV = 1 << 15;
189 }
190}
191
192bitflags! {
193 /// Permissions **réseau** Landlock (cf. `linux/landlock.h`), ABI **v4+**
194 /// (Linux 6.7).
195 ///
196 /// Landlock ne borne le réseau que sur **TCP**, et seulement pour ces deux
197 /// verbes. Ce n'est pas un pare-feu : il ne filtre ni les adresses, ni UDP,
198 /// ni les paquets. Il répond à une question plus étroite — *ce processus
199 /// a-t-il le droit d'écouter, ou de se connecter, sur ce port ?* — et il y
200 /// répond dans le noyau, sans démon ni règle globale.
201 ///
202 /// Le confondre avec un pare-feu serait une erreur de conception : une
203 /// application qui reçoit `CONNECT_TCP` sur le port 443 peut joindre
204 /// **n'importe quelle** machine sur ce port.
205 ///
206 /// | Bit | Depuis | Signification |
207 /// |-----|--------|---------------|
208 /// | BIND_TCP | Landlock v4 (Linux 6.7) | se lier à un port en écoute |
209 /// | CONNECT_TCP | Landlock v4 (Linux 6.7) | se connecter à un port |
210 ///
211 /// Comme pour [`LandlockAccessFs`], demander un bit que le noyau ne connaît
212 /// pas fait échouer la création du ruleset (`EINVAL`) : interroger
213 /// `landlock_supported_abi()` avant de composer.
214 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
215 pub struct LandlockAccessNet: u64 {
216 /// Se lier à un port TCP (`bind`) — Landlock v4+.
217 const BIND_TCP = 1 << 0;
218 /// Se connecter à un port TCP (`connect`) — Landlock v4+.
219 const CONNECT_TCP = 1 << 1;
220 }
221}
222
223bitflags! {
224 /// **Portées** Landlock (cf. `linux/landlock.h`), ABI **v6+** (Linux 6.12).
225 ///
226 /// # Ce que le *scoping* borne, et que rien d'autre ne bornait
227 ///
228 /// Ce ne sont **pas** des droits par chemin. Ils ne disent pas *ce qu'on a le droit de
229 /// faire sur telle ressource*, mais *jusqu'où un domaine peut atteindre en dehors de
230 /// lui-même*. Deux voies latérales, précisément celles qu'aucune règle de fichier ne
231 /// pouvait voir :
232 ///
233 /// - un socket UNIX **abstrait** n'a **pas de chemin** — il vit dans un espace de noms
234 /// à part, sans entrée de système de fichiers. Aucune règle Landlock de fichier ne
235 /// pouvait donc l'atteindre : deux processus confinés séparément pouvaient dialoguer
236 /// sans que leur cage en sache rien ;
237 /// - un **signal** ne traverse aucun descripteur et ne touche aucun chemin.
238 ///
239 /// | Bit | Depuis | Ce qui devient interdit |
240 /// |-----|--------|-------------------------|
241 /// | `ABSTRACT_UNIX_SOCKET` | Landlock v6 (Linux 6.12) | se connecter à un socket UNIX abstrait créé **hors** du domaine |
242 /// | `SIGNAL` | Landlock v6 (Linux 6.12) | envoyer un signal à un processus **hors** du domaine |
243 ///
244 /// # La direction compte
245 ///
246 /// La borne est **sortante** : elle empêche le processus confiné d'atteindre l'extérieur,
247 /// et non l'extérieur de l'atteindre. Un parent non confiné garde donc la main sur son
248 /// enfant — ce qui est nécessaire, sans quoi un lanceur ne pourrait plus arrêter ce qu'il
249 /// a lancé.
250 ///
251 /// # À l'intérieur du domaine, rien ne change
252 ///
253 /// Deux processus du **même** domaine continuent de se joindre et de se signaler. Le
254 /// *scoping* isole des domaines les uns des autres, il ne fragmente pas un domaine.
255 ///
256 /// Comme pour [`LandlockAccessFs`] et [`LandlockAccessNet`], demander un bit que le noyau
257 /// ne connaît pas fait échouer la création du ruleset : interroger
258 /// `landlock_supported_abi()` avant de composer.
259 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
260 pub struct LandlockScope: u64 {
261 /// Se connecter à un socket UNIX **abstrait** créé hors du domaine — Landlock v6+.
262 const ABSTRACT_UNIX_SOCKET = 1 << 0;
263 /// Envoyer un signal à un processus hors du domaine — Landlock v6+.
264 const SIGNAL = 1 << 1;
265 }
266}
267
268bitflags! {
269 /// Drapeaux de `landlock_restrict_self(2)` (cf. `linux/landlock.h`).
270 ///
271 /// # Deux familles, deux natures
272 ///
273 /// - les trois premiers ne changent **aucune** décision d'accès : ils disent au noyau ce
274 /// qu'il doit **tracer** d'un refus (ABI v7). Utiles au diagnostic, ils appartiennent à
275 /// l'appelant ([ADR-156] D5) ;
276 /// - le quatrième, [`TSYNC`](Self::TSYNC), change ce qui est **borné** — et c'est le seul
277 /// qui ferme quelque chose.
278 ///
279 /// # Ce que `TSYNC` répare
280 ///
281 /// Sans lui, `landlock_restrict_self` ne borne que le **fil appelant**. Dans un processus
282 /// multifil qui se met lui-même en cage, les fils frères restent **hors** du domaine : la
283 /// cage se contourne en changeant de fil. Avec lui, la configuration s'applique
284 /// **atomiquement à tous les fils** du processus.
285 ///
286 /// Le noyau propage aussi `no_new_privs` aux fils frères quand le fil appelant le porte —
287 /// ce qui évite l'incohérence d'un fil confiné et d'un fil qui peut encore élever ses
288 /// privilèges.
289 ///
290 /// **Le noyau dit qu'il « écrase » la configuration des fils frères, quels que soient les
291 /// domaines déjà établis sur eux.** Ce que cela fait exactement d'un fil frère **plus
292 /// strictement** confiné est vérifié par un test dédié plutôt que supposé — c'est la
293 /// différence entre borner et croire borner.
294 ///
295 /// [ADR-156]: ../../../docs/adrs/ADR-156-landlock-abi-v7-fr.md
296 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
297 pub struct LandlockRestrictFlags: u32 {
298 /// Ne journalise pas les refus venant du fil qui crée le domaine, ni de ses enfants
299 /// tant qu'ils exécutent le même code (ABI v7).
300 const LOG_SAME_EXEC_OFF = 1 << 0;
301 /// Journalise les refus **après** un `execve` dans le domaine créé (ABI v7).
302 const LOG_NEW_EXEC_ON = 1 << 1;
303 /// Ne journalise pas les refus venant des **sous-domaines** créés par l'appelant ou
304 /// ses descendants (ABI v7).
305 const LOG_SUBDOMAINS_OFF = 1 << 2;
306 /// Applique la configuration **à tous les fils** du processus, atomiquement.
307 const TSYNC = 1 << 3;
308 }
309}
310
311#[cfg(test)]
312mod tests;