Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec couche 0 — Famille device

Spécification technique — Version 1.0

Vue d’ensemble de la famille

Le module air-sys-syscall::device expose les primitives d’interaction avec les périphériques matériels exposés par le kernel : réception des notifications uevent (apparition/disparition de matériel, via un socket netlink), accès aux périphériques d’entrée evdev (/dev/input/eventX : claviers, souris, manettes, écrans tactiles), et l’articulation avec sysfs (/sys/...) pour la lecture/écriture des attributs de périphériques.

Cette famille est le socle système de tout ce qui, dans les couches supérieures, construit un modèle de périphériques : énumération du matériel, hotplug, gestion des entrées du compositeur Wayland (ADR-003), services système couche 5.

Périmètre de la famille.

Trois sous-systèmes distincts :

  1. uevent (netlink) : un socket NETLINK_KOBJECT_UEVENT reçoit du kernel les messages d’apparition/disparition de périphériques. La couche 0 ouvre le socket, lit les messages, et décode le format de fil (entête action@devpath + paires CLÉ=VALEUR séparées par \0) via un itérateur emprunté, zéro allocation.

  2. evdev (entrée) : les périphériques d’entrée sont des char devices ouverts via la famille fs (openat). La couche 0 fournit la lecture typée des enregistrements struct input_event et les ioctls dédiés (EVIOC*) sous forme de fonctions typées (jamais un ioctl générique, cf. ADR-021 convention 3).

  3. sysfs : pseudo-système de fichiers ; ses attributs se lisent et s’écrivent avec la famille fs. La couche 0 n’ajoute aucun wrapper spécifique (cf. sous-section 3) — la construction de chemins et le parsing d’attributs sont de la logique de couche 1.

Caractéristiques transverses de la famille.

  1. CLOEXEC universel. Tout FD créé par la famille (socket uevent) porte CLOEXEC par défaut. Les char devices evdev sont ouverts par fs::openat, qui applique déjà la même discipline.

  2. Décodage = miroir de format kernel, pas logique. Le décodage des messages uevent (paires clé=valeur) et des input_event reflète des formats de fil ABI kernel stables. C’est la même catégorie que SignalFdInfo (famille signal), que la couche 0 décode déjà. À l’inverse, construire un modèle riche de périphérique (enums de sous-système typés, arbre de devices, corrélation uevent ↔ sysfs) est de la logique : couche 1.

  3. Parsers empruntés, zéro allocation. Les décodeurs uevent/input_event écrivent dans (ou empruntent à) un buffer fourni par l’appelant. Aucune allocation heap dans le happy path (ADR-021 convention 4).

  4. Pas d’ioctl générique. Chaque opération EVIOC* est une fonction dédiée et typée (ADR-021 convention 3). Aucune fonction ioctl(fd, request, ...) n’est exposée.

  5. Sentinelles kernel → Option<T> / enums typés (ADR-021 convention 1). EVIOCGRAB (argument 1 vs pointeur nul) devient deux fonctions distinctes evdev_grab / evdev_release ; le clockid de timestamp devient un enum.


Le kernel diffuse, sur un socket netlink du protocole NETLINK_KOBJECT_UEVENT, un message à chaque événement de cycle de vie d’un périphérique (add, remove, change, bind, unbind, move, online, offline). C’est le mécanisme de hotplug moderne, en remplacement de l’ancien /sbin/hotplug.

Le socket : UEventSocket

#![allow(unused)]
fn main() {
pub struct UEventSocket { /* opaque, possède un OwnedFd interne */ }

pub fn uevent_socket_open(
    groups: UEventGroups,
    flags: UEventSocketFlags,
) -> Result<UEventSocket, Errno>;

bitflags! {
    /// Groupes multicast netlink à écouter.
    pub struct UEventGroups: u32 {
        /// Messages bruts générés par le kernel (groupe netlink 1).
        const KERNEL = 1 << 0;
        /// Messages re-diffusés par le gestionnaire de périphériques
        /// userspace (groupe netlink 2, dit « libudev monitor »).
        const USERSPACE = 1 << 1;
    }
}

bitflags! {
    pub struct UEventSocketFlags: i32 {
        const NONBLOCK = 0x800;   // SOCK_NONBLOCK
        const CLOEXEC  = 0x80000; // SOCK_CLOEXEC (toujours activé par le wrapper)
    }
}

impl UEventSocket {
    pub fn as_fd(&self) -> BorrowedFd<'_>;
    pub fn into_fd(self) -> OwnedFd;

    /// Lit un message et le décode en place dans `buffer`.
    ///
    /// Le `UEventMessage` retourné **emprunte** `buffer` : il reste valide tant
    /// que le buffer n'est pas réutilisé.
    pub fn read<'b>(
        &self,
        buffer: &'b mut [u8],
    ) -> Result<UEventMessage<'b>, Errno>;
}
}

Syscalls sous-jacents. socket (AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT = 15) puis bind sur un struct sockaddr_nl (nl_family = AF_NETLINK, nl_pid = 0 — le kernel assigne, nl_groups selon UEventGroups). La lecture utilise recvmsg (et non recv) afin de récupérer l’adresse source et de vérifier l’authenticité (cf. ci-dessous). Numéros : socket (x86_64 n°41, ARM64 n°198), bind (x86_64 n°49, ARM64 n°200), recvmsg (x86_64 n°47, ARM64 n°212).

Préconditions.

  • groups ne peut pas être vide (EINVAL sinon — rien à écouter).
  • Écouter le groupe KERNEL ne requiert pas de privilège. Les messages du groupe KERNEL proviennent toujours du kernel (nl_pid == 0).
  • CLOEXEC est toujours activé par le wrapper.

Comportement.

Crée et lie le socket en une opération. À chaque read, le kernel délivre un message uevent complet. Si le buffer est trop petit, le message est tronqué et l’erreur EMSGSIZE/ENOBUFS peut être rapportée selon le mode — l’appelant dimensionne donc le buffer généreusement (cf. « Dimensionnement »).

Authenticité — vérification anti-usurpation.

N’importe quel processus disposant de CAP_NET_ADMIN peut émettre vers un groupe multicast netlink. Pour ne pas confondre un vrai uevent kernel avec un message forgé, le wrapper vérifie systématiquement, via recvmsg, que l’adresse source a nl_pid == 0 (kernel) et que le crédential SCM_CREDENTIALS, si le mode USERSPACE est utilisé, correspond à uid == 0. Un message qui échoue à cette vérification provoque Errno::EPERM (le message est consommé et rejeté).

Décision (couche 0 « abstraire sans cacher »). Le wrapper valide la source parce que c’est une précondition de sûreté du mécanisme lui-même (sans elle, l’API serait un piège). Il ne va pas plus loin : il ne filtre pas par sous-système, ne déduplique pas, ne corréle pas avec sysfs. Ces commodités sont de la logique de couche 1.

Dimensionnement du buffer.

Un message uevent kernel tient quasi toujours sous 2 Kio, mais peut atteindre ~16 Kio dans des cas extrêmes (longues listes de propriétés). Recommandation : buffer de 8192 octets. Une constante UEVENT_RECOMMENDED_BUFFER_SIZE = 8192 est exposée à titre indicatif.

Erreurs.

  • EINVAL : groups vide ou flags invalides.
  • EPERM : message rejeté pour cause d’authenticité (source non-kernel).
  • EMSGSIZE / ENOBUFS : buffer trop petit ou file de réception saturée.
  • EAGAIN : socket NONBLOCK sans message disponible.
  • EMFILE, ENFILE, ENOMEM : limites de ressources.

Performance.

Ouverture : ~10-20 µs. Lecture d’un message disponible : ~2-5 µs. Le débit d’événements est intrinsèquement faible (hotplug), donc non critique.

Le message décodé : UEventMessage

#![allow(unused)]
fn main() {
pub struct UEventMessage<'b> { /* emprunte &'b [u8] */ }

impl<'b> UEventMessage<'b> {
    /// L'action (entête avant le premier `\0`), p. ex. `add`, `remove`.
    /// Sous-ensemble typé pour les actions connues, brut sinon.
    pub fn action(&self) -> UEventAction;

    /// Le `DEVPATH` relatif à `/sys` (extrait de l'entête ou des propriétés).
    pub fn device_path(&self) -> Option<&'b [u8]>;

    /// Le sous-système (`SUBSYSTEM=...`), p. ex. `usb`, `input`, `block`.
    pub fn subsystem(&self) -> Option<&'b [u8]>;

    /// La valeur d'une propriété arbitraire par clé.
    pub fn property(&self, key: &[u8]) -> Option<&'b [u8]>;

    /// Itère toutes les paires `(clé, valeur)` sans allouer.
    pub fn properties(&self) -> UEventProperties<'b>;

    /// Les octets bruts du message (pour diagnostic / passthrough).
    pub fn as_bytes(&self) -> &'b [u8];
}

pub struct UEventProperties<'b> { /* curseur sur le buffer */ }

impl<'b> Iterator for UEventProperties<'b> {
    type Item = (&'b [u8], &'b [u8]); // (clé, valeur), tranches empruntées
    fn next(&mut self) -> Option<Self::Item>;
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UEventAction {
    Add,
    Remove,
    Change,
    Move,
    Online,
    Offline,
    Bind,
    Unbind,
    /// Action non reconnue ; la tranche est l'entête brut.
    Other,
}
}

Format de fil décodé.

Un message uevent kernel a la forme :

add@/devices/pci0000:00/.../input/input12\0ACTION=add\0DEVPATH=/devices/.../input12\0SUBSYSTEM=input\0...\0
  • Entête : tout ce qui précède le premier \0action@devpath.
  • Propriétés : suite de CLÉ=VALEUR terminées par \0.

Le décodeur ne fait aucune copie : action(), subsystem(), property() et l’itérateur properties() rendent des tranches qui pointent dans le buffer de l’appelant. Les clés/valeurs sont des octets (&[u8]), pas du str : le kernel ne garantit pas l’UTF-8 (ADR — couche 0, « zéro présomption sur les encodages », Principe 3). La conversion en str est laissée à l’appelant.

Note sur le format USERSPACE (libudev).

Les messages du groupe USERSPACE sont préfixés d’un entête binaire libudev (magic 0xfeedcafe, offsets). Le décodeur le détecte et expose les propriétés de la même façon ; l’entête binaire est masqué derrière l’API. Si le magic est absent ou corrompu, read renvoie EBADMSG.

Tests.

  • Décodage d’un message add synthétique : vérifier action, subsystem, itération complète des propriétés, absence d’allocation (via un allocateur de test qui panique sur alloc).
  • Message tronqué / sans entête → erreur propre, pas de panique ni d’OOB (slicing via get, jamais d’indexation directe — Principe 3).
  • Test de bout en bout (privilégié, marqué « ignore » par défaut) : déclencher un vrai uevent (p. ex. modprobe/rmmod d’un module factice, ou écriture /sys/.../uevent) et lire le message.
  • Property-based (proptest) : pour tout buffer d’octets, le décodeur ne panique jamais et l’itérateur termine.
  • Fuzzing (cargo-fuzz) : le décodeur uevent accepte des données externes (le buffer kernel) → harnais de fuzz obligatoire sur UEventMessage::parse.

Sous-section 2 : evdev — périphériques d’entrée

Les périphériques d’entrée Linux sont exposés sous /dev/input/eventX. On les ouvre avec la famille fs (openat en lecture, éventuellement OpenFlags::NONBLOCK) — la famille device n’a pas de fonction d’ouverture dédiée, un evdev étant un char device ordinaire. Ce que la couche 0 ajoute : la lecture typée des événements et les ioctls EVIOC* d’interrogation et de contrôle.

Lecture des événements : InputEvent

#![allow(unused)]
fn main() {
/// Miroir `#[repr(C)]` de `struct input_event` (24 octets sur LP64).
/// Champs aux noms kernel conservés (ADR-029, nuance « type miroir »).
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct InputEvent {
    /// Secondes de l'horodatage (`struct timeval::tv_sec`, `time_t`).
    /// **Signé** : `time_t`/`suseconds_t` sont des `long` sur LP64 (nos 2 cibles).
    pub sec: i64,
    /// Microsecondes de l'horodatage (`struct timeval::tv_usec`, `suseconds_t`).
    pub usec: i64,
    /// Type d'événement (`EV_KEY`, `EV_REL`, `EV_ABS`, `EV_SYN`...).
    pub event_type: u16,
    /// Code (touche, axe, bouton) dépendant du type.
    pub code: u16,
    /// Valeur (1/0 pour une touche, delta pour `EV_REL`, absolu pour `EV_ABS`).
    pub value: i32,
}

/// Lit un lot d'événements dans `events`, sans allocation.
/// Retourne le nombre d'événements **complets** lus.
pub fn evdev_read_events(
    device: BorrowedFd<'_>,
    events: &mut [InputEvent],
) -> Result<usize, Errno>;
}

Syscall sous-jacent. read (x86_64 n°0, ARM64 n°63) sur le FD evdev. Le kernel délivre un multiple entier de size_of::<input_event>() (24 octets sur les deux cibles LP64 d’Air, x86_64 et aarch64 — aucun découpage y2038 puisque long fait 64 bits).

Comportement.

evdev_read_events lit dans la tranche events réinterprétée en octets, puis retourne le nombre d’événements complets. Si le kernel renvoie un nombre d’octets non multiple de 24 (ne devrait jamais arriver), le wrapper rapporte EPROTO plutôt que d’exposer un événement partiel. Sur un FD NONBLOCK sans donnée : EAGAIN.

Pourquoi sec/usec en u64 plutôt qu’un Instant.

L’horodatage evdev est issu d’une horloge configurable (CLOCK_REALTIME par défaut, ou CLOCK_MONOTONIC via evdev_set_clock). La couche 0 reflète les deux champs kernel tels quels ; corréler avec Instant/Duration (famille time) est de la logique de couche 1, qui sait quelle horloge a été choisie.

Décodeur pur (sans syscall)

#![allow(unused)]
fn main() {
impl InputEvent {
    /// Réinterprète un buffer d'octets en tranche d'`InputEvent` (zéro copie).
    /// `None` si la longueur n'est pas un multiple de `size_of::<InputEvent>()`
    /// ou si l'alignement n'est pas respecté.
    pub fn slice_from_bytes(bytes: &[u8]) -> Option<&[InputEvent]>;
}
}

Utile quand les octets viennent d’ailleurs (io_uring, mmap). Type pur, vit dans air-sys-types.

Identité et description du périphérique

#![allow(unused)]
fn main() {
/// `EVIOCGVERSION` — version du protocole evdev du driver.
pub fn evdev_driver_version(device: BorrowedFd<'_>) -> Result<u32, Errno>;

/// `EVIOCGID` — identifiant bus/vendor/product/version.
pub fn evdev_device_id(device: BorrowedFd<'_>) -> Result<InputId, Errno>;

/// `EVIOCGNAME(len)` — nom du périphérique, écrit dans `buffer`.
/// Retourne la tranche effectivement remplie (octets, possiblement non-UTF-8).
pub fn evdev_name<'b>(
    device: BorrowedFd<'_>,
    buffer: &'b mut [u8],
) -> Result<&'b [u8], Errno>;

/// `EVIOCGPHYS(len)` — emplacement physique (topologie), p. ex. `usb-0000:00:14.0-1/input0`.
pub fn evdev_physical_location<'b>(
    device: BorrowedFd<'_>,
    buffer: &'b mut [u8],
) -> Result<&'b [u8], Errno>;

/// `EVIOCGUNIQ(len)` — identifiant unique (souvent vide).
pub fn evdev_unique_id<'b>(
    device: BorrowedFd<'_>,
    buffer: &'b mut [u8],
) -> Result<&'b [u8], Errno>;

/// Miroir `#[repr(C)]` de `struct input_id` (noms kernel conservés, ADR-029).
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InputId {
    pub bustype: u16,
    pub vendor: u16,
    pub product: u16,
    pub version: u16,
}
}

Capacités : bits d’événements et propriétés

#![allow(unused)]
fn main() {
/// `EVIOCGBIT(event_type, len)` — bitmap des codes supportés pour un type
/// d'événement donné. `event_type = None` interroge les **types** supportés
/// (équivalent `EVIOCGBIT(0, len)`).
pub fn evdev_supported_codes<'b>(
    device: BorrowedFd<'_>,
    event_type: Option<EventType>,
    bitmap: &'b mut [u8],
) -> Result<&'b [u8], Errno>;

/// `EVIOCGPROP(len)` — propriétés du périphérique (`INPUT_PROP_*` :
/// pointeur direct, semi-mt, bouton-pad...).
pub fn evdev_properties<'b>(
    device: BorrowedFd<'_>,
    bitmap: &'b mut [u8],
) -> Result<&'b [u8], Errno>;

/// `EVIOCGABS(axis)` — plage et état d'un axe absolu (`ABS_X`, `ABS_MT_*`...).
pub fn evdev_abs_info(
    device: BorrowedFd<'_>,
    axis: AbsAxis,
) -> Result<InputAbsInfo, Errno>;

/// Miroir `#[repr(C)]` de `struct input_absinfo` (noms kernel conservés).
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InputAbsInfo {
    pub value: i32,
    pub minimum: i32,
    pub maximum: i32,
    pub fuzz: i32,
    pub flat: i32,
    pub resolution: i32,
}
}

EventType et AbsAxis sont des enums typés (les constantes EV_* et ABS_* du kernel) — la couche 0 expose des valeurs nommées plutôt que des entiers magiques (ADR-029). Une variante Raw(u16) permet de passer un code non encore nommé sans bloquer.

État courant

#![allow(unused)]
fn main() {
/// `EVIOCGKEY(len)` — état actuel (enfoncé/relâché) de toutes les touches.
pub fn evdev_key_state<'b>(d: BorrowedFd<'_>, bitmap: &'b mut [u8]) -> Result<&'b [u8], Errno>;
/// `EVIOCGLED(len)` — état des LED.
pub fn evdev_led_state<'b>(d: BorrowedFd<'_>, bitmap: &'b mut [u8]) -> Result<&'b [u8], Errno>;
/// `EVIOCGSND(len)` — état des sorties son.
pub fn evdev_sound_state<'b>(d: BorrowedFd<'_>, bitmap: &'b mut [u8]) -> Result<&'b [u8], Errno>;
/// `EVIOCGSW(len)` — état des interrupteurs (capot, jack...).
pub fn evdev_switch_state<'b>(d: BorrowedFd<'_>, bitmap: &'b mut [u8]) -> Result<&'b [u8], Errno>;
}

Contrôle exclusif et horloge

#![allow(unused)]
fn main() {
/// `EVIOCGRAB` avec argument `1` — capture exclusive du périphérique.
/// (ADR-021 conv. 1 : pas d'argument magique, deux fonctions distinctes.)
pub fn evdev_grab(device: BorrowedFd<'_>) -> Result<(), Errno>;

/// `EVIOCGRAB` avec pointeur nul — relâche la capture exclusive.
pub fn evdev_release(device: BorrowedFd<'_>) -> Result<(), Errno>;

/// `EVIOCREVOKE` — révoque définitivement l'accès à ce FD (irréversible).
/// Utilisé par les serveurs d'affichage pour neutraliser un FD cédé.
pub fn evdev_revoke(device: BorrowedFd<'_>) -> Result<(), Errno>;

/// `EVIOCSCLOCKID` — choisit l'horloge des horodatages des événements.
pub fn evdev_set_clock(
    device: BorrowedFd<'_>,
    clock: EventClock,
) -> Result<(), Errno>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventClock {
    /// `CLOCK_REALTIME` (défaut kernel).
    Realtime,
    /// `CLOCK_MONOTONIC` (recommandé pour la corrélation d'entrées).
    Monotonic,
}
}

Syscall sous-jacent (tous les EVIOC*). ioctl (x86_64 n°16, ARM64 n°29), chaque requête étant une constante EVIOC* distincte. Aucun wrapper ioctl générique n’est exposé (ADR-021 convention 3) : chaque opération est une fonction typée ci-dessus. Les valeurs de requête (_IOR('E', ...) etc.) sont calculées en interne dans air-sys-syscall::device.

Préconditions et erreurs (transverses evdev).

  • EVIOCGRAB échoue avec EBUSY si un autre client détient déjà la capture.
  • evdev_set_clock n’accepte que Realtime/Monotonic (typage), donc pas de EINVAL sur clockid invalide côté Air.
  • ENOTTY si le FD n’est pas un evdev (mauvais type de fichier).
  • EFAULT impossible à atteindre depuis l’API sûre (buffers fournis par Air).

Performance.

ioctl d’interrogation : ~1-3 µs. evdev_read_events : ~1-2 µs par lot. La capture (grab) est négligeable.

Tests délicats.

  • Les tests nominaux des EVIOC* requièrent un vrai evdev. Stratégie : créer un périphérique virtuel via uinput (/dev/uinput) dans un harnais de test, injecter des événements, les relire via evdev, vérifier round-trip (type/code/value). uinput permet aussi de tester EVIOCGID, EVIOCGNAME, EVIOCGBIT de façon déterministe.
  • À défaut de privilège uinput, les tests sont marqués « ignore » avec skip explicite, et la logique de décodage (InputEvent::slice_from_bytes) est testée en pur sur buffers synthétiques + proptest + fuzz.
  • Couverture : les branches d’erreur ioctl difficiles à provoquer (p. ex. ENOTTY) sont consignées dans COVERAGE-EXCEPTIONS.md (catégorie « feature/kernel » ou « valeur-impossible »).

Sous-section 3 : sysfs — pas de wrapper dédié

sysfs (/sys/...) est un pseudo-système de fichiers. Lire un attribut (/sys/class/input/event3/device/name), écrire dans un uevent (/sys/.../uevent pour re-déclencher un événement), parcourir l’arbre (/sys/devices/...) : tout cela se fait avec la famille fs (openat, read, write, getdents64).

Décision (couche 0, anti-duplication). La famille device n’expose aucun wrapper sysfs. Réexposer read/write sous un nom « sysfs » serait de la duplication sans valeur, et toute la valeur ajoutée (construire le chemin /sys/class/<subsystem>/<name>/<attr>, parser un attribut en entier/booléen, corréler DEVPATH d’un uevent avec son entrée sysfs, énumérer un sous-système) est de la logique → couche 1 (futur crate air-device). La couche 0 se contente de fournir les primitives fs et uevent/evdev sur lesquelles cette logique s’appuiera.

Cette sous-section existe pour lever explicitement l’ambiguïté : si un développeur cherche « la fonction sysfs de la couche 0 », la réponse est : il n’y en a pas, c’est fs + (couche 1).


Récapitulatif de la famille device

Fonctions exposées :

CatégorieFonctions principales
ueventuevent_socket_open, UEventSocket::read, UEventMessage::{action,subsystem,device_path,property,properties,as_bytes}
evdev — lectureevdev_read_events, InputEvent::slice_from_bytes
evdev — identitéevdev_driver_version, evdev_device_id, evdev_name, evdev_physical_location, evdev_unique_id
evdev — capacitésevdev_supported_codes, evdev_properties, evdev_abs_info
evdev — étatevdev_key_state, evdev_led_state, evdev_sound_state, evdev_switch_state
evdev — contrôleevdev_grab, evdev_release, evdev_revoke, evdev_set_clock
sysfs(aucune — voir sous-section 3)

Total : ~22 fonctions publiques principales.

Syscalls non-wrappés (listés dans UNSUPPORTED.md) :

  • EVIOCSABS (set abs info) : écriture rare, réservée à la calibration ; à ajouter plus tard sans casser l’API si besoin.
  • EVIOCGKEYCODE / EVIOCSKEYCODE (et _V2) : remappage de scancodes ; spécialisé, hors périmètre couche 0 initial.
  • EVIOCGMTSLOTS : état multi-touch par slot ; ajout futur possible.
  • EVIOCSFF / EVIOCRMFF / force feedback : retour de force ; chantier dédié ultérieur.
  • L’ancien hotplug /sbin/hotplug et le netlink genl : obsolètes / hors sujet.

Répartition des types entre les deux crates

Dans air-sys-types (purs, sans syscall)

  • InputEvent, InputId, InputAbsInfo — miroirs #[repr(C)] de structures kernel (champs aux noms kernel, ADR-029).
  • EventType, AbsAxis, EventClock, UEventAction — enums typés.
  • UEventGroups, UEventSocketFlags — bitflags.
  • UEventMessage<'b>, UEventProperties<'b> — vues empruntées, décodage pur (aucun syscall) ; placées avec les types car le parsing ne touche pas le kernel.
  • Constante UEVENT_RECOMMENDED_BUFFER_SIZE.

Dans air-sys-syscall::device (appelle des syscalls)

  • UEventSocket — RAII possédant un OwnedFd ; open/read appellent socket/bind/recvmsg. Même règle que SignalFd / LandlockRuleset : un type qui appelle un syscall vit dans la crate des wrappers, jamais dans air-sys-types.
  • Toutes les fonctions evdev_* (wrappers ioctl/read).

Soit ~10 types ajoutés à air-sys-types.


Décisions de fond émergées dans la famille device

1. Décodage uevent/input_event en couche 0 (parsers empruntés).

Les formats clé=valeur (uevent) et struct input_event sont des formats de fil ABI kernel stables. Les refléter sans allouer est du miroir de structure, exactement comme SignalFdInfo : la couche 0 le fait. La frontière avec la couche 1 est nette : décoder le format = couche 0 ; interpréter (enums de sous-système, modèle de device, corrélation) = couche 1.

2. UEventSocket::read vérifie l’authenticité.

Sans la vérification nl_pid == 0 / uid == 0, l’API serait un piège de sécurité (uevents forgeables). La validation est donc une précondition de sûreté de la primitive, légitime en couche 0 — distincte d’une commodité.

3. Pas d’ouverture evdev dédiée.

Un evdev est un char device : on l’ouvre avec fs::openat. La famille device n’ajoute que ce qui est spécifique (lecture typée + EVIOC*).

4. Pas de wrapper sysfs.

Anti-duplication de fs ; toute la valeur ajoutée sysfs est de la logique de couche 1. Sous-section dédiée pour lever l’ambiguïté.

5. EVIOCGRAB → deux fonctions (grab/release).

Application de l’ADR-021 conv. 1 : l’argument magique (1 vs pointeur nul) devient deux fonctions explicites, sans sentinelle.

6. Bitmaps rendus comme tranches empruntées.

EVIOCGBIT/EVIOCGPROP/EVIOCG{KEY,LED,SND,SW} écrivent dans un buffer fourni par l’appelant : zéro allocation (ADR-021 conv. 4). L’interprétation bit-à-bit (« la touche K est-elle supportée ? ») est un helper de couche 1.


Licence du document : MPL 2.0 Statut : Spécification technique du module air-sys-syscall::device (couche 0).