Spec couche 0 — Famille security
Spécification technique — Version 1.0
Vue d’ensemble de la famille
Le module air-sys-syscall::security expose les primitives de sandboxing kernel : seccomp-BPF (filtrage de syscalls) et Landlock (filtrage d’accès filesystem). Ces deux mécanismes sont au cœur du modèle de sécurité Air (ADR-010 : entitlements et sandbox capability-based).
Périmètre de la famille.
Trois sous-systèmes distincts :
- seccomp-BPF : filtrage des syscalls qu’un thread peut exécuter.
- Landlock : filtrage d’accès au filesystem.
- Capabilities : déjà couvertes dans la famille
process(capget,capset).
Position dans le modèle de sécurité Air.
Le launcher Air (couche 5, ADR-010) applique au démarrage de chaque application :
- Les capabilities selon les entitlements.
- Les filtres Landlock pour restreindre l’accès filesystem.
- Les filtres seccomp pour restreindre les syscalls.
- L’isolation par namespaces.
La couche 0 fournit les primitives ; la couche 5 les orchestre.
Caractéristiques transverses.
-
Opérations privilégiées ou contraintes. Beaucoup demandent
CAP_SYS_ADMINou l’activation préalable deno_new_privs. -
Irréversibilité. Les restrictions de sécurité sont strictement croissantes : une fois appliquées, elles ne peuvent pas être affaiblies.
-
Couche 0 = primitives seules. Pour seccomp-BPF, la couche 0 n’expose pas d’API déclarative ni de compilateur de filtres : elle charge un programme BPF déjà formé (cf. Q4, sous-section 1). L’API déclarative
SeccompFilteret sa compilation vivent en couche 1. De même, les helpers Landlock de haut niveau (combinaison create/add/restrict, masquage d’ABI) relèvent de la couche 1 ; la couche 0 n’expose que les primitives Landlock. -
Pas de wrapper pour AppArmor / SELinux. Ces mécanismes sont gérés au niveau distribution.
Sous-section 1 : seccomp
Périmètre couche 0 : la primitive seulement, pas le compilateur de filtres
Décision d’architecture (Q4, validée le 2026-05-31). La rédaction initiale de cette spec plaçait en couche 0 une API déclarative (
SeccompFilter,SeccompRule,SeccompAction…) compilée en BPF en interne. C’est trop pour la couche 0 : compiler un filtre déclaratif en bytecode BPF est de la logique (encodage d’instructionssock_filter, calcul des offsets de saut, prise en compte de l’architecture), pas un wrapper de syscall. Conformément à la raison d’être de la couche 0 (« abstraire sans cacher », façade fine sur le kernel), la couche 0 n’expose que la primitive : charger dans le kernel un programme BPF déjà compilé, fourni par l’appelant. L’API déclarative et son compilateur (SeccompFilter,SeccompRule,SeccompAction,SyscallArgumentCondition,ConditionOp,SyscallNumber) sont spécifiés et implémentés en couche 1, et sortent du périmètre de la couche 0.
Frontière (Q4, actée — confirmée par le code livré, 2026-06-14). La couche 0 expose uniquement le primitif
seccomp_set_mode_filter(&SockFprog, SeccompFilterFlags)— oùSockFprogemprunte une tranche desock_filterdéjà compilée par l’appelant. La compilation déclarative (règles de filtre → bytecode cBPFsock_filter) est de la logique → couche 1 (cohérent ADR-021 : la couche 0 ne fait pas de logique). Aucunbpf_compiler/type déclaratifSeccompFilteren couche 0. (Clôture de la question Q4 deQUESTIONS-implementation.md: le code mergé implémente exactement ce primitif.)
Types : un programme BPF déjà formé
Ces types sont purs (aucun syscall) et vivent dans air-sys-types::security.
#![allow(unused)]
fn main() {
/// Une instruction de filtre BPF classique — layout exact de `struct sock_filter`.
#[repr(C)]
pub struct SockFilter {
pub code: u16,
pub jt: u8,
pub jf: u8,
pub k: u32,
}
/// Programme BPF complet à charger — équivalent de `struct sock_fprog`.
/// Emprunte une tranche d'instructions appartenant à l'appelant (zéro alloc).
pub struct SockFprog<'a> { /* len: u16, filter: *const SockFilter, marqueur 'a */ }
impl<'a> SockFprog<'a> {
/// Construit un programme à partir d'une tranche d'instructions.
///
/// # Errors
/// `EINVAL` si la tranche est vide ou dépasse `BPF_MAXINSNS` (4096).
pub fn new(instructions: &'a [SockFilter]) -> Result<Self, Errno>;
}
}
Le programme BPF lui-même est produit ailleurs (compilateur déclaratif de couche 1, ou tout autre moyen) et passé ici sous forme de tranche. La couche 0 ne l’inspecte pas et ne le transforme pas : elle le charge tel quel.
seccomp_set_mode_filter
#![allow(unused)]
fn main() {
pub unsafe fn seccomp_set_mode_filter(
prog: &SockFprog<'_>,
flags: SeccompFilterFlags,
) -> Result<(), Errno>;
bitflags! {
pub struct SeccompFilterFlags: u32 {
const TSYNC = 1;
const LOG = 2;
const SPEC_ALLOW = 4;
const NEW_LISTENER = 8;
const TSYNC_ESRCH = 16;
const WAIT_KILLABLE_RECV = 32;
}
}
}
Charge le programme BPF prog dans le kernel pour le thread courant
(seccomp(SECCOMP_SET_MODE_FILTER, flags, prog)).
unsafe — préconditions (# Safety) :
- l’appelant doit avoir appelé
set_no_new_privs()(familleprocess) ou détenirCAP_SYS_ADMIN; - le programme BPF doit autoriser tous les syscalls réellement utilisés par le runtime Rust et par le code exécuté après le chargement, faute de quoi le thread/processus sera tué ;
- l’opération est irréversible et strictement croissante (on ne peut que durcir).
TSYNC applique le filtre à tous les threads du processus. NEW_LISTENER retourne
un descripteur de notification (seccomp_unotify) — exploité en couche 1.
EINTR remonté tel quel, jamais de retry automatique (ADR-021 conv. 2).
seccomp_set_mode_strict
#![allow(unused)]
fn main() {
pub fn seccomp_set_mode_strict() -> Result<(), Errno>;
}
Active le mode strict (SECCOMP_SET_MODE_STRICT) : seuls read, write,
_exit/exit_group et sigreturn restent autorisés. Aucun programme à fournir,
aucune compilation — d’où l’absence d’unsafe. Irréversible.
Performance. Chargement d’un filtre : ~50-200 µs. Coût par syscall ensuite : ~50-100 ns pour un filtre simple.
Tests délicats. Une violation seccomp tue typiquement le thread/processus ; les
tests s’exécutent dans des sous-processus isolés (fork + observation du statut via
waitid), avec skip explicite si l’environnement n’autorise pas no_new_privs.
Prérequis prctl_no_new_privs
Couvert par la famille process (prctl), rappelé ici car prérequis à seccomp
sans privilège. Irréversible.
Sous-section 2 : Landlock
Landlock est un mécanisme Linux moderne pour restreindre les accès filesystem au niveau de chemins, sans privilèges. Disponible depuis Linux 5.13.
L’API est conçue autour de “rulesets” : des ensembles de règles qui définissent quels chemins sont accessibles avec quelles permissions.
landlock_create_ruleset
#![allow(unused)]
fn main() {
pub fn landlock_create_ruleset(
handled_access: LandlockAccessFs,
) -> Result<LandlockRuleset, Errno>;
pub struct LandlockRuleset { /* possède OwnedFd interne */ }
impl LandlockRuleset {
pub fn add_rule_path_beneath(
&mut self,
path: BorrowedFd<'_>,
allowed_access: LandlockAccessFs,
) -> Result<(), Errno>;
pub fn restrict_self(&self) -> Result<(), Errno>;
pub fn as_fd(&self) -> BorrowedFd<'_>;
}
bitflags! {
pub struct LandlockAccessFs: u64 {
const EXECUTE = 1 << 0;
const WRITE_FILE = 1 << 1;
const READ_FILE = 1 << 2;
const READ_DIR = 1 << 3;
const REMOVE_DIR = 1 << 4;
const REMOVE_FILE = 1 << 5;
const MAKE_CHAR = 1 << 6;
const MAKE_DIR = 1 << 7;
const MAKE_REG = 1 << 8;
const MAKE_SOCK = 1 << 9;
const MAKE_FIFO = 1 << 10;
const MAKE_BLOCK = 1 << 11;
const MAKE_SYM = 1 << 12;
const REFER = 1 << 13; // Landlock v2 (Linux 5.19)
const TRUNCATE = 1 << 14; // Landlock v3 (Linux 6.2)
const IOCTL_DEV = 1 << 15; // Landlock v5 (Linux 6.10)
}
}
}
Placement (Q1, validée le 2026-05-31).
LandlockRulesetest un type RAII dont les méthodes (add_rule_path_beneath,restrict_self) et leDropappellent des syscalls Landlock. Il réside donc dansair-sys-syscall::security, et non dansair-sys-types— même règle queMappingcôté famillemem: un type qui appelle un syscall vit dans la crate des wrappers, jamais dans la crate des types purs. Seuls les types sans syscall (LandlockAccessFs) restent dansair-sys-types.
Préconditions.
handled_access indique quelles permissions ce ruleset va gérer.
Les permissions disponibles dépendent de la version d’ABI Landlock du kernel.
Décision (Q6, validée le 2026-05-31) : pas de masquage automatique en couche 0.
landlock_supported_abi()se contente de rapporter la version d’ABI supportée par le kernel.landlock_create_ruleset/add_rule_path_beneath/restrict_selfne masquent pas silencieusement les bits d’accès non supportés : si l’appelant passe un bit que le kernel ne connaît pas, l’erreur kernel (EINVAL) est remontée telle quelle. Masquer en douce serait un comportement « magique », contraire au principe « abstraire sans cacher ». C’est à l’appelant — ou à un helper de couche 1 (landlock_restrict_to_paths) — d’interrogerlandlock_supported_abi()puis de masquer les bits selon l’ABI cible.
Pattern d’usage typique.
#![allow(unused)]
fn main() {
let mut ruleset = landlock_create_ruleset(
LandlockAccessFs::READ_FILE | LandlockAccessFs::READ_DIR | LandlockAccessFs::EXECUTE,
)?;
let usr_fd = openat(DirFd::Cwd, c"/usr", OpenFlags::PATH | OpenFlags::DIRECTORY, Mode::empty())?;
ruleset.add_rule_path_beneath(
usr_fd.as_fd(),
LandlockAccessFs::READ_FILE | LandlockAccessFs::EXECUTE,
)?;
let home_fd = openat(DirFd::Cwd, c"/home/user", OpenFlags::PATH | OpenFlags::DIRECTORY, Mode::empty())?;
ruleset.add_rule_path_beneath(
home_fd.as_fd(),
LandlockAccessFs::READ_FILE | LandlockAccessFs::READ_DIR,
)?;
set_no_new_privs()?;
ruleset.restrict_self()?;
}
Particularités.
- Irréversibilité.
restrict_selfest définitif. - Cumul de rulesets. Plusieurs rulesets successifs ajoutent des restrictions.
- NO_NEW_PRIVS prérequis sauf si
CAP_SYS_ADMIN. - Sémantique “path_beneath”. Une règle s’applique à un chemin et tous ses descendants.
- Versions ABI. Détecter via
landlock_supported_abi().
Performance.
Création : ~10-50 µs. Ajout de règle : ~5-10 µs. restrict_self : ~50-100 µs. Coût par accès filesystem ensuite : ~100-500 ns.
Helpers haut niveau
En couche 0, seule la requête de version d’ABI est exposée — une primitive pure de lecture, sans masquage (cf. Q6) :
#![allow(unused)]
fn main() {
/// Rapporte la version d'ABI Landlock supportée par le kernel courant.
pub fn landlock_supported_abi() -> Result<u32, Errno>;
}
Déplacé en couche 1 (Q4/Q6). Le helper
landlock_restrict_to_paths(paths)— qui combinecreate_ruleset+ plusieursadd_rule_path_beneath+restrict_selfen un appel, et qui peut masquer les bits selon l’ABI — est de la logique de commodité, pas une primitive syscall. Il est donc spécifié en couche 1, aux côtés de l’API seccomp déclarative. La couche 0 n’expose que les briques (landlock_create_ruleset,add_rule_path_beneath,restrict_self,landlock_supported_abi) ; c’est typiquement le launcher Air, via ce helper de couche 1, qui appliquera les entitlements filesystem d’une application.
Récapitulatif de la famille security
Fonctions exposées :
| Catégorie | Fonctions principales |
|---|---|
| seccomp | seccomp_set_mode_filter, seccomp_set_mode_strict |
| Landlock | landlock_create_ruleset, LandlockRuleset::add_rule_path_beneath, LandlockRuleset::restrict_self, LandlockRuleset::as_fd, landlock_supported_abi |
Total : ~6 fonctions publiques principales en couche 0. Reportés en couche 1 (hors de ce périmètre) : l’API seccomp déclarative (SeccompFilter / SeccompRule / SeccompAction / … + compilateur BPF) et le helper landlock_restrict_to_paths.
Syscalls listés mais non-wrappés en phase 0 :
bpf: API générique pour les programmes BPF. Avancé.perf_event_open: profiling et monitoring. Hors phase 0.keyctl,add_key,request_key: keyring kernel. À évaluer.pkey_alloc,pkey_free,pkey_mprotect: memory protection keys. Marginal.
Répartition des types entre les deux crates
Dans air-sys-types (purs, sans syscall)
SockFilter,SockFprog— programme BPF déjà formé.SeccompFilterFlags— drapeaux de chargement seccomp.LandlockAccessFs— bits d’accès filesystem.
Dans air-sys-syscall::security (RAII appelant des syscalls)
LandlockRuleset— possède unOwnedFd; ses méthodes et sonDropappellent des syscalls. Ne vit donc pas dansair-sys-types(décision Q1 ci-dessus ; même règle queMappingcôtémem).
Reportés en couche 1 (hors couche 0, décision Q4)
- API seccomp déclarative :
SeccompFilter,SeccompRule,SeccompAction,SyscallArgumentCondition,ConditionOp,SyscallNumber, et le compilateur déclaratif → BPF.
Décisions de fond émergées dans la famille security
1. Couche 0 = primitive seccomp seulement (Q4).
La couche 0 n’expose ni la construction ni la compilation de filtres : elle charge un programme BPF déjà formé (seccomp_set_mode_filter) ou active le mode strict (seccomp_set_mode_strict). L’API déclarative confortable (qui « évite les bugs courants ») est réelle et utile — mais c’est de la logique, donc spécifiée en couche 1, au-dessus de cette primitive.
2. Helpers haut niveau → couche 1 (Q4/Q6).
landlock_restrict_to_paths (combinaison create/add/restrict + masquage d’ABI éventuel) couvre ~90 % des usages, mais reste de la commodité construite sur les primitives : il vit en couche 1. La couche 0 ne fournit que les briques Landlock et la requête landlock_supported_abi (sans masquage, Q6).
2 bis. Types RAII à syscall hors air-sys-types (Q1).
LandlockRuleset (comme Mapping côté mem) appelle des syscalls dans ses méthodes et son Drop ; il réside dans air-sys-syscall::security, jamais dans la crate des types purs.
3. Pas d’API AppArmor / SELinux.
Ces frameworks sont configurés au niveau distribution.
4. Détection runtime de la version Landlock ABI.
Permet aux applications de s’adapter sans recompilation.
Licence du document : MPL 2.0
Statut : Spécification technique du module air-sys-syscall::security (couche 0).