Skip to main content

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;