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 :
-
uevent(netlink) : un socketNETLINK_KOBJECT_UEVENTreç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êteaction@devpath+ pairesCLÉ=VALEURséparées par\0) via un itérateur emprunté, zéro allocation. -
evdev(entrée) : les périphériques d’entrée sont des char devices ouverts via la famillefs(openat). La couche 0 fournit la lecture typée des enregistrementsstruct input_eventet les ioctls dédiés (EVIOC*) sous forme de fonctions typées (jamais un ioctl générique, cf. ADR-021 convention 3). -
sysfs: pseudo-système de fichiers ; ses attributs se lisent et s’écrivent avec la famillefs. 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.
-
CLOEXEC universel. Tout FD créé par la famille (socket uevent) porte
CLOEXECpar défaut. Les char devicesevdevsont ouverts parfs::openat, qui applique déjà la même discipline. -
Décodage = miroir de format kernel, pas logique. Le décodage des messages
uevent(pairesclé=valeur) et desinput_eventreflète des formats de fil ABI kernel stables. C’est la même catégorie queSignalFdInfo(famillesignal), 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. -
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). -
Pas d’ioctl générique. Chaque opération
EVIOC*est une fonction dédiée et typée (ADR-021 convention 3). Aucune fonctionioctl(fd, request, ...)n’est exposée. -
Sentinelles kernel →
Option<T>/ enums typés (ADR-021 convention 1).EVIOCGRAB(argument1vs pointeur nul) devient deux fonctions distinctesevdev_grab/evdev_release; le clockid de timestamp devient un enum.
Sous-section 1 : uevent — notifications de périphériques (netlink)
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.
groupsne peut pas être vide (EINVALsinon — rien à écouter).- Écouter le groupe
KERNELne requiert pas de privilège. Les messages du groupeKERNELproviennent toujours du kernel (nl_pid == 0). CLOEXECest 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:groupsvide 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: socketNONBLOCKsans 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
\0—action@devpath. - Propriétés : suite de
CLÉ=VALEURterminé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
addsynthétique : vérifieraction,subsystem, itération complète des propriétés, absence d’allocation (via un allocateur de test qui panique suralloc). - 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/rmmodd’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 surUEventMessage::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 avecEBUSYsi un autre client détient déjà la capture.evdev_set_clockn’accepte queRealtime/Monotonic(typage), donc pas deEINVALsur clockid invalide côté Air.ENOTTYsi le FD n’est pas un evdev (mauvais type de fichier).EFAULTimpossible à 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 viauinput(/dev/uinput) dans un harnais de test, injecter des événements, les relire via evdev, vérifier round-trip (type/code/value).uinputpermet aussi de testerEVIOCGID,EVIOCGNAME,EVIOCGBITde 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 dansCOVERAGE-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
devicen’expose aucun wrappersysfs. Réexposerread/writesous 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élerDEVPATHd’un uevent avec son entrée sysfs, énumérer un sous-système) est de la logique → couche 1 (futur crateair-device). La couche 0 se contente de fournir les primitivesfsetuevent/evdevsur 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égorie | Fonctions principales |
|---|---|
| uevent | uevent_socket_open, UEventSocket::read, UEventMessage::{action,subsystem,device_path,property,properties,as_bytes} |
| evdev — lecture | evdev_read_events, InputEvent::slice_from_bytes |
| evdev — identité | evdev_driver_version, evdev_device_id, evdev_name, evdev_physical_location, evdev_unique_id |
| evdev — capacités | evdev_supported_codes, evdev_properties, evdev_abs_info |
| evdev — état | evdev_key_state, evdev_led_state, evdev_sound_state, evdev_switch_state |
| evdev — contrôle | evdev_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/hotpluget lenetlink 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 unOwnedFd;open/readappellentsocket/bind/recvmsg. Même règle queSignalFd/LandlockRuleset: un type qui appelle un syscall vit dans la crate des wrappers, jamais dansair-sys-types.- Toutes les fonctions
evdev_*(wrappersioctl/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).