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 terminal (termios/tty)

Spécification technique — Version 1.0

Vue d’ensemble de la famille

La famille terminal porte l’interface POSIX termios — la discipline de ligne des terminaux (POSIX.1-1988, racines System V), terminal-agnostique et stable. Elle est introduite par le descellement additif de la couche 0 (ADR-060) pour débloquer la libc (tcgetattr/tcsetattr/PTY pour OpenSSH, isatty pour stdio) et l’objet ergonomique de couche 1 air-terminal.

Découpage en tranches.

  1. Tranche 1 — le TYPE (ce document). [Termios] et ses types associés vivent dans air-sys-types/src/terminal.rs. C’est un objet ABI pur, entièrement testable sur l’hôte (aucun syscall) : structure #[repr(C)], accesseurs typés, encodage/décodage des drapeaux et de la vitesse. Couverture 100 % lignes + branches.
  2. Tranche 2 — les WRAPPERS « Attributs ». [tcgetattr]/[tcsetattr] (cœur termios) arrivent dans air-sys-syscall/src/terminal.rs et consomment le type décrit ici. Documentées ci-dessous (section « Wrappers — Attributs (tranche 2) »). Couverture 100 % lignes + branches.
  3. Tranche 3 — les WRAPPERS « Contrôle de ligne » et « Taille fenêtre ». Les tc* POSIX restants ([tcdrain]/[tcflush]/[tcflow]/[tcsendbreak]) et les ioctls de dimension de fenêtre ([get_winsize]/[set_winsize], avec le type [Winsize]) arrivent dans air-sys-syscall/src/terminal.rs. Documentés ci-dessous (sections « Wrappers — Contrôle de ligne (tranche 3) » et « Wrappers — Taille fenêtre (tranche 3) »). Couverture 100 % lignes + branches.
  4. Tranche 4 — les WRAPPERS « PTY ». L’allocation d’une paire maître/esclave de pseudo-terminal ([open_pty_master], [pty_number] avec le type [PtyNumber], [pty_unlock], [pty_peer]) arrive dans air-sys-syscall/src/terminal.rs. Documentée ci-dessous (section « Wrappers — PTY (tranche 4) »). Couverture 100 % lignes + branches.
  5. Tranche 5 — les WRAPPERS « Session / job-control ». Le terminal de contrôle et les groupes de premier plan (tcgetpgrp/tcsetpgrp/session_id/set_ctty/clear_ctty). Documentés ci-dessous (section « Wrappers — Session / job-control (tranche 5) »). Couverture 100 % hors exceptions CHILD-EXIT documentées et justifiées (ADR-035 : cycle nominal validé dans un enfant forké setsid).

Caractéristiques transverses.

  1. Image ABI exacte du kernel. [Termios] reproduit struct termios de <asm-generic/termbits.h>, identique sur x86_64 et aarch64 (tous deux asm-generic). Aucune divergence de disposition.
  2. Doctrine « kernel = bible ». Les champs bruts (c_iflag, c_oflag, c_cflag, c_lflag) sont conservés tels quels ; les bits inconnus (d’un kernel plus récent) sont retenus, jamais silencieusement effacés.
  3. Zéro allocation. Le type est Copy, sur la pile, sans indirection ni heap — compatible no_std futur.
  4. Accès typé en lecture, champs bruts en écriture directe possible. Les accesseurs retournent des drapeaux/enum typés ; les champs restent pub pour l’échange direct avec le kernel via les ioctls (couche syscall).

Sous-section 1 : la structure Termios

#![allow(unused)]
fn main() {
pub const NCCS: usize = 19;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct Termios {
    pub c_iflag: u32,      // drapeaux d'entrée (brut)
    pub c_oflag: u32,      // drapeaux de sortie (brut)
    pub c_cflag: u32,      // drapeaux de contrôle (brut, inclut la vitesse CBAUD)
    pub c_lflag: u32,      // drapeaux locaux (brut)
    pub c_line: u8,        // discipline de ligne
    pub c_cc: [u8; NCCS],  // caractères de contrôle
}
}

Disposition. #[repr(C)] reproduit struct termios du kernel ; la même disposition vaut sur x86_64 et aarch64 (asm-generic). NCCS vaut 19 : le tableau c_cc a 19 slots, dont seuls les indices 0..=16 sont nommés (cf. [ControlChar]) — les slots 17 et 18 sont réservés (padding kernel, sans nom POSIX) et conservés à zéro.

Constructeur.

#![allow(unused)]
fn main() {
pub const fn zeroed() -> Termios;
}

Retourne l’image toute-à-zéro, à passer à un TCGETS qui la remplira. Sur cette image : drapeaux vides, taille de caractère Bits5 (motif CSIZE = 0), vitesse B0.

Accesseurs de drapeaux (lecture typée).

#![allow(unused)]
fn main() {
pub const fn input_flags(&self)   -> InputFlags;
pub const fn output_flags(&self)  -> OutputFlags;
pub const fn control_flags(&self) -> ControlFlags;
pub const fn local_flags(&self)   -> LocalFlags;
}

Chacun décode le champ brut correspondant via from_bits_retain : les bits inconnus sont conservés (doctrine kernel = bible). control_flags() expose le c_cflag complet, y compris les bits CSIZE/CBAUD — la taille et la vitesse se lisent via leurs accesseurs dédiés (ci-dessous), pas via ControlFlags.

Mutateurs de drapeaux (écriture typée).

#![allow(unused)]
fn main() {
pub const fn set_input_flags(&mut self, flags: InputFlags);
pub const fn set_output_flags(&mut self, flags: OutputFlags);
pub const fn set_control_flags(&mut self, flags: ControlFlags);
pub const fn set_local_flags(&mut self, flags: LocalFlags);
}

set_input_flags/set_output_flags/set_local_flags remplacent intégralement le champ brut. set_control_flags est spécial : il préserve les bits CSIZE et CBAUD de c_cflag (taille de caractère et vitesse), de sorte qu’écrire les drapeaux de contrôle ne détruise ni la taille ni la vitesse déjà posées. ControlFlags ne recouvre ni CSIZE ni CBAUD : pas de chevauchement.

Taille de caractère (CSIZE).

#![allow(unused)]
fn main() {
pub const fn char_size(&self)                 -> CharSize;
pub const fn set_char_size(&mut self, size: CharSize);
}

Lecture/écriture des 2 bits CSIZE de c_cflag, indépendamment de la vitesse et des autres drapeaux de contrôle.

Vitesse (CBAUD/CBAUDEX).

#![allow(unused)]
fn main() {
pub const fn output_speed(&self)          -> BaudRate;
pub const fn input_speed(&self)           -> BaudRate; // = output_speed (termios classique Linux)
pub const fn set_speed(&mut self, baud: BaudRate);
}

En termios classique Linux, entrée et sortie partagent le champ CBAUD de c_cflag : input_speed() et output_speed() renvoient donc la même valeur. set_speed ne touche que les bits CBAUD|CBAUDEX, indépendamment de la taille.

Caractères de contrôle (c_cc).

#![allow(unused)]
fn main() {
pub const fn control_char(&self, which: ControlChar)             -> u8;
pub const fn set_control_char(&mut self, which: ControlChar, value: u8);
}

Indexation typée du tableau c_cc par [ControlChar] — jamais d’entier brut en indice.

Sous-section 2 : les types associés

ControlChar — indices des caractères de contrôle

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum ControlChar {
    Intr = 0, Quit = 1, Erase = 2, Kill = 3, Eof = 4, Time = 5, Min = 6,
    Swtc = 7, Start = 8, Stop = 9, Susp = 10, Eol = 11, Reprint = 12,
    Discard = 13, Werase = 14, Lnext = 15, Eol2 = 16,
}
}

Chaque variante est l’indice POSIX (V* de <asm-generic/termbits.h>) dans c_cc. 17 caractères nommés (0..=16) pour 19 slots (NCCS) : les slots 17/18 sont réservés.

Drapeaux — quatre bitflags typés

Quatre types bitflags exposent les bits des quatre champs de drapeaux. Chacun dérive from_bits_retain (conserve les bits inconnus) et bits() (round-trip vers l’entier brut).

  • [InputFlags] (c_iflag) : IGNBRK, BRKINT, IGNPAR, PARMRK, INPCK, ISTRIP, INLCR, IGNCR, ICRNL, IUCLC, IXON, IXANY, IXOFF, IMAXBEL, IUTF8.
  • [OutputFlags] (c_oflag) : OPOST, OLCUC, ONLCR, OCRNL, ONOCR, ONLRET, OFILL, OFDEL.
  • [ControlFlags] (c_cflag, hors taille/vitesse) : CSTOPB, CREAD, PARENB, PARODD, HUPCL, CLOCAL, CRTSCTS.
  • [LocalFlags] (c_lflag) : ISIG, ICANON, ECHO, ECHOE, ECHOK, ECHONL, NOFLSH, TOSTOP, ECHOCTL, ECHOPRT, ECHOKE, FLUSHO, PENDIN, IEXTEN.

CharSize — nombre de bits de données (CSIZE)

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum CharSize { Bits5 = 0x00, Bits6 = 0x10, Bits7 = 0x20, Bits8 = 0x30 }
}

Le masque CSIZE (0x30) n’a que 4 motifs : le décodage est total (jamais None) ; le motif 0x30 est Bits8.

BaudRate — vitesse de transmission (CBAUD/CBAUDEX)

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BaudRate(u32);

pub const fn from_code(code: u32) -> BaudRate; // masque par CBAUD (0x100f)
pub const fn code(self)          -> u32;       // réinjection dans c_cflag
}

Newtype sur le code de vitesse encodé (CBAUD|CBAUDEX). Constantes POSIX standard exposées : B0, B50, …, B38400 (0x0..0xf), puis les vitesses étendues B57600, B115200, B230400, B460800, B921600 (0x1001..). from_code/code forment un round-trip qui préserve l’octet de vitesse ; une valeur kernel inconnue est rendue telle quelle (bits hors CBAUD_MASK ignorés, le reste conservé) — pas de perte d’information sur la vitesse.

SetAction, FlushQueue, FlowAction — arguments des wrappers à venir

Ces trois enums typent les arguments des ioctls des tranches suivantes (air-sys-syscall). Ils remplacent les constantes magiques du kernel par des variantes explicites (ADR-021 conv. 3).

#![allow(unused)]
fn main() {
pub enum SetAction  { Now, Drain, Flush }                          // tcsetattr : quand appliquer (TCSANOW/TCSADRAIN/TCSAFLUSH)
pub enum FlushQueue { Input, Output, Both }                        // tcflush : quelle file purger (TCIFLUSH/TCOFLUSH/TCIOFLUSH)
pub enum FlowAction { SuspendOutput, ResumeOutput,                 // tcflow : contrôle de flux (TCOOFF/TCOON/
                      SuspendInput,  ResumeInput }                 //          TCIOFF/TCION)
}

SetAction type l’argument de [tcgetattr]/[tcsetattr] (tranche 2) ; FlushQueue et FlowAction typent respectivement [tcflush] et [tcflow] (tranche 3, ci-dessous).

Winsize — dimensions de la fenêtre du terminal

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct Winsize {
    pub ws_row: u16,     // nombre de lignes (rangées)
    pub ws_col: u16,     // nombre de colonnes
    pub ws_xpixel: u16,  // largeur en pixels (souvent 0)
    pub ws_ypixel: u16,  // hauteur en pixels (souvent 0)
}

pub const fn zeroed()    -> Winsize;
pub const fn rows(&self)     -> u16;  // = ws_row
pub const fn cols(&self)     -> u16;  // = ws_col
pub const fn x_pixels(&self) -> u16;  // = ws_xpixel
pub const fn y_pixels(&self) -> u16;  // = ws_ypixel
}

Image ABI de struct winsize (<asm-generic/termios.h>), #[repr(C)], identique x86_64/aarch64. Champs pub bruts (échange direct avec le kernel, comme [Termios]) plus des accesseurs const nommés pour la lecture. zeroed() donne l’image nulle à passer à un TIOCGWINSZ. Les champs pixels sont souvent à 0 (nombre de terminaux ne les renseignent pas). Le kernel signale un changement de taille par SIGWINCH. Échangée par [get_winsize]/[set_winsize] (tranche 3).

Wrappers — Attributs (tranche 2)

Le module air-sys-syscall/src/terminal.rs livre les deux ioctls typés du cœur termios (ADR-060, groupe « Attributs »). Ce sont des fonctions dédiées typées (ADR-021 conv. 3 : jamais d’ioctl(fd, REQ, …) générique) ; zéro allocation (le [Termios] vit sur la pile) ; EINTR est remonté tel quel à l’appelant, sans retry automatique (ADR-021 conv. 2).

#![allow(unused)]
fn main() {
pub fn tcgetattr(fd: BorrowedFd<'_>) -> Result<Termios, Errno>;
pub fn tcsetattr(fd: BorrowedFd<'_>, action: SetAction, attributes: &Termios)
    -> Result<(), Errno>;
}

tcgetattr (TCGETS). Alloue un [Termios::zeroed] sur la pile, le fait remplir par le kernel via l’ioctl TCGETS (0x5401), puis le renvoie. Le retour est normalisé (comme musl, cf. audit §7) : ioctl < 0Err(errno) ; sinon Ok(termios) — jamais une valeur ioctl arbitraire propagée.

tcsetattr (TCSETS/TCSETSW/TCSETSF). Applique attributes au terminal à l’instant indiqué par action. Le mapping action → requête se fait par un match exhaustif, jamais par l’arithmétique TCSETS + action (fragilité pointée par l’audit musl §8, dépendante de la contiguïté des constantes) :

[SetAction]Requête ioctlValeurSémantique POSIX
NowTCSETS0x5402TCSANOW — applique immédiatement
DrainTCSETSW0x5403TCSADRAIN — après vidage de la sortie
FlushTCSETSF0x5404TCSAFLUSH — après vidage sortie + purge entrée

Les constantes TC* proviennent de <asm-generic/ioctls.h> et sont identiques sur x86_64 et aarch64. Le module porte son propre helper de syscall ioctl bas niveau (inline asm syscall/svc 0, SYS_ioctl = 16/29) et son propre helper de décodage d’errno, sur le modèle de device.rs.

Erreurs. [Errno::ENOTTY] (fd n’est pas un terminal), [Errno::EBADF] (descripteur invalide), [Errno::EINTR] (interrompu, remonté tel quel). Une erreur EFAULT est exclue par construction : le Termios échangé est toujours une variable de pile vivante et alignée.

Terminal de test. Le happy path exige un vrai terminal : les tests ouvrent le maître du PTY via /dev/ptmx (O_RDWR), qui répond aux ioctls TCGETS/ TCSETS* sur Linux sans privilège (l’allocation PTY complète — TIOCGPTN/TIOCSPTLCK — relève d’une tranche ultérieure). Le round-trip bascule un drapeau (ECHO) et vérifie qu’il est bien appliqué ; les trois variantes de [SetAction] sont chacune exercées. Les chemins d’erreur (Err) s’exercent sur un fd non-tty (/dev/nullENOTTY). Couverture 100 % lignes + branches, sans exception ADR-035.

Wrappers — Contrôle de ligne (tranche 3)

Le module air-sys-syscall/src/terminal.rs livre les quatre tc* POSIX restants (ADR-060, groupe « Contrôle de ligne »). Mêmes conventions que la tranche 2 : fonctions dédiées typées (ADR-021 conv. 3), zéro allocation, EINTR remonté tel quel sans retry.

#![allow(unused)]
fn main() {
pub fn tcdrain(fd: BorrowedFd<'_>)                     -> Result<(), Errno>;
pub fn tcflush(fd: BorrowedFd<'_>, queue: FlushQueue)  -> Result<(), Errno>;
pub fn tcflow(fd: BorrowedFd<'_>, action: FlowAction)  -> Result<(), Errno>;
pub fn tcsendbreak(fd: BorrowedFd<'_>)                 -> Result<(), Errno>;
}

tcdrain (TCSBRK, argument 1). Bloque jusqu’à la transmission de tous les octets déjà écrits vers fd. Sur Linux, ioctl(fd, TCSBRK, arg) avec un arg non nul draine la sortie (sémantique tcdrain, cf. musl) ; l’envoi de break correspond à arg == 0 ([tcsendbreak]). C’est le point d’annulation de la famille (audit musl §3) : elle peut rendre [Errno::EINTR], remonté tel quel, aucun retry automatique (ADR-021 conv. 2 — l’appelant qui veut retenter écrit la boucle).

tcflush (TCFLSH). Jette les octets non traités de la (des) file(s) visée(s). Le mapping [FlushQueue] → argument entier est un match exhaustif :

[FlushQueue]ArgumentValeurSémantique POSIX
InputTCIFLUSH0purge les données reçues non lues
OutputTCOFLUSH1purge les données écrites non transmises
BothTCIOFLUSH2purge les deux files

tcflow (TCXONC). Suspend/reprend le flux d’entrée ou de sortie. Le mapping [FlowAction] → argument entier est un match exhaustif :

[FlowAction]ArgumentValeurSémantique POSIX
SuspendOutputTCOOFF0suspend la sortie
ResumeOutputTCOON1reprend la sortie
SuspendInputTCIOFF2émet un STOP (suspend l’entrée distante)
ResumeInputTCION3émet un START (reprend l’entrée distante)

tcsendbreak (TCSBRK, argument 0). Transmet une condition BREAK. Durée non paramétrée (honnêteté de contrat). POSIX prévoit un paramètre de durée, mais il est implementation-defined et musl l’ignore silencieusement (audit musl §6/§8). Plutôt qu’exposer un paramètre avalé sans effet, Air n’en expose aucun — le break dure la durée par défaut du kernel, et le contrat dit exactement ce qu’il fait (doctrine « la libc fait ce qu’elle dit »). Le partage du code d’ioctl TCSBRK avec [tcdrain] est explicite : deux constantes d’argument nommées (0 = break, 1 = drain) distinguent les sémantiques.

Wrappers — Taille fenêtre (tranche 3)

Deux ioctls typés échangent la [Winsize] du terminal (ADR-060, groupe « Taille fenêtre »).

#![allow(unused)]
fn main() {
pub fn get_winsize(fd: BorrowedFd<'_>)                 -> Result<Winsize, Errno>;
pub fn set_winsize(fd: BorrowedFd<'_>, size: &Winsize) -> Result<(), Errno>;
}

get_winsize (TIOCGWINSZ, 0x5413). Alloue un [Winsize::zeroed] sur la pile, le fait remplir par le kernel, puis le renvoie (retour normalisé comme [tcgetattr]). set_winsize (TIOCSWINSZ, 0x5414). Impose size à la fenêtre ; le kernel émet alors SIGWINCH vers le groupe de premier plan. Les deux passent un pointeur vers la struct winsize (comme les ioctls termios).

Erreurs (contrôle de ligne + taille fenêtre). [Errno::ENOTTY] (fd n’est pas un terminal), [Errno::EBADF] (descripteur invalide), et [Errno::EINTR] pour la seule [tcdrain] (opération bloquante). EFAULT est exclue par construction pour get_winsize/set_winsize : la Winsize échangée est toujours une variable de pile vivante et alignée.

Terminal de test. Comme en tranche 2, le happy path s’exerce sur le maître du PTY via /dev/ptmx (sans privilège) : tcdrain/tcsendbreak réussissent immédiatement, les trois [FlushQueue] et les quatre [FlowAction] exercent tous les bras des match, et un round-trip set_winsize(40×120)get_winsize vérifie les valeurs relues. Les chemins d’erreur (Err) de chaque fonction s’exercent sur un fd non-tty (/dev/nullENOTTY). Couverture 100 % lignes + branches, sans exception ADR-035.

Wrappers — PTY (tranche 4)

Quatre wrappers allouent une paire maître/esclave de pseudo-terminal (ADR-060, groupe « PTY ») et un newtype d’identifiant nomme l’esclave.

#![allow(unused)]
fn main() {
pub struct PtyNumber(u32);            // N de /dev/pts/N (rendu par TIOCGPTN)
impl PtyNumber {
    pub const fn new(value: u32) -> Self;
    pub const fn value(self) -> u32;
}

pub fn open_pty_master()                     -> Result<OwnedFd, Errno>;
pub fn pty_number(master: BorrowedFd<'_>)    -> Result<PtyNumber, Errno>;
pub fn pty_unlock(master: BorrowedFd<'_>)    -> Result<(), Errno>;
pub fn pty_peer(master: BorrowedFd<'_>)      -> Result<OwnedFd, Errno>;
}

PtyNumber. Newtype d’identifiant (ADR-021 : jamais un u32 brut pour désigner un esclave, comme [Pid]/[Tid]/[PidFd]). Il porte l’N du nœud /dev/pts/N.

open_pty_master. Ouvre un nouveau maître via /dev/ptmx en O_RDWR | O_NOCTTY (chaque ouverture alloue une paire distincte). Dogfoode openat — qui ajoute O_CLOEXEC — plutôt qu’un open brut. Le maître est rendu en [OwnedFd] (RAII : sa fermeture libère la paire). O_NOCTTY empêche le maître de devenir le terminal contrôlant.

pty_number (TIOCGPTN, 0x8004_5430). Le kernel écrit un unsigned int (u32) sur la pile ; renvoyé en [PtyNumber].

pty_unlock (TIOCSPTLCK, 0x4004_5431). Le kernel lit un int sur la pile ; on lui présente un 0 qui déverrouille l’esclave (prérequis avant son ouverture, équivalent d’unlockpt). Le verrouillage (1) n’est pas exposé — seul le déverrouillage est utile.

pty_peer (TIOCGPTPEER, 0x5441). Ouvre directement l’esclave du maître et renvoie son fd (en [OwnedFd]). L’argument ioctl est une valeur entière de drapeaux d’ouverture (O_RDWR | O_NOCTTY | O_CLOEXEC, O_CLOEXEC demandé explicitement car cet ioctl ne l’ajoute pas) ; la valeur de retour de l’ioctl est le nouveau descripteur. Cette voie moderne (Linux 4.13+) est préférée au chemin /dev/pts/N : elle évite le TOCTOU (l’esclave ne peut être substitué entre la résolution du nom et l’ouverture) et ne dépend pas du montage de devpts.

Encodage des requêtes. Contrairement aux TC* numérotées en clair, ces trois ioctls suivent le schéma d’encodage _IOC arch-générique de <asm-generic/ioctls.h> (_IOR/_IOW/_IO), identique x86_64/aarch64. La valeur de retour >= 0 de TIOCGPTPEER est toujours un fd dans l’intervalle i32 (un descripteur est < 2^31) : la conversion as i32 est exacte et locale (commentée), sans try_from dont le bras Err serait inatteignable.

Flux d’allocation. open_pty_masterpty_number (identifier l’esclave, si besoin) → pty_unlock (déverrouiller) → pty_peer (ouvrir l’esclave).

Erreurs. [Errno::ENOTTY] (le fd n’est pas un maître de PTY, ex. /dev/null), [Errno::EBADF] (descripteur invalide), [Errno::EINVAL] (kernel < 4.13 pour pty_peer), [Errno::ENOSPC] (plus de PTY libre pour open_pty_master). Aucun EFAULT par construction : TIOCGPTN/TIOCSPTLCK échangent une variable de pile vivante et alignée ; TIOCGPTPEER ne prend qu’un entier.

Terminal de test. Le happy path exerce le cycle complet sans privilège : open_pty_masterpty_number (valeur lisible) → pty_unlockpty_peer, puis tcgetattr(slave) prouve que l’esclave est un vrai terminal, et master != slave prouve la paire. Les chemins d’erreur (Err) de pty_number, pty_unlock et pty_peer s’exercent sur un fd non-tty (/dev/nullENOTTY). Couverture 100 % lignes + branches, sans exception ADR-035.

Wrappers — Session / job-control (tranche 5)

Cinq wrappers pilotent le terminal de contrôle et les groupes de premier plan (ADR-060, groupe « Session / job-control »).

#![allow(unused)]
fn main() {
pub fn tcgetpgrp(fd: BorrowedFd<'_>)                       -> Result<Option<Pid>, Errno>;
pub fn tcsetpgrp(fd: BorrowedFd<'_>, process_group: Pid)   -> Result<(), Errno>;
pub fn session_id(fd: BorrowedFd<'_>)                      -> Result<Pid, Errno>;
pub fn set_ctty(fd: BorrowedFd<'_>)                        -> Result<(), Errno>;
pub fn clear_ctty(fd: BorrowedFd<'_>)                      -> Result<(), Errno>;
}

tcgetpgrp (TIOCGPGRP, 0x540F). Le kernel écrit un pid_t (i32) sur la pile : le groupe de premier plan du terminal. Un pid_t 0 signifie « aucun groupe de premier plan » → converti en [None] (sentinelle → Option, ADR-021 conv. 1). La conversion passe par [Pid::try_from_raw], qui replie tout pid_t non strictement positif (donc 0 comme un négatif aberrant que ce ioctl ne peut pas produire) sur None — défense en profondeur (Principe 5).

tcsetpgrp (TIOCSPGRP, 0x5410). Le kernel lit un pid_t (i32) sur la pile : le groupe de premier plan à installer. process_group est un [Pid] typé (non nul), donc aucune sentinelle 0 n’est jamais présentée au kernel.

session_id (TIOCGSID, 0x5429). Le kernel écrit le pid_t du leader de session du terminal, toujours strictement positif ; la conversion via [Pid::try_from_raw] est dénotée par .expect (motif de getsid) — un pid_t ≤ 0 serait une violation du contrat kernel, inatteignable par construction.

set_ctty (TIOCSCTTY, 0x540E, argument 0). Fait de fd le terminal de contrôle du leader de session appelant. L’argument 0 demande une acquisition sans vol : le vol (arg = 1) exige CAP_SYS_ADMIN et n’est pas exposé (hors périmètre — listé en UNSUPPORTED.md à la tranche 6).

clear_ctty (TIOCNOTTY, 0x5422). Détache le terminal de contrôle du processus appelant. Appelé par le leader de session, le kernel envoie SIGHUP+SIGCONT au groupe de premier plan (à prendre en compte par l’appelant).

Encodage des requêtes. Ces cinq ioctls sont numérotés en clair dans <asm-generic/ioctls.h> (comme les TC*), identiques x86_64/aarch64.

Erreurs. [Errno::ENOTTY] (le fd n’est pas le terminal de contrôle de l’appelant, ou pas un terminal du tout : /dev/null, ou pty non-contrôlant), [Errno::EPERM] (set_ctty par un non-leader, ou tcsetpgrp d’un groupe hors session), [Errno::EBADF] (descripteur invalide). Aucun EFAULT par construction : TIOCGPGRP/TIOCGSID écrivent une variable de pile vivante et alignée, TIOCSPGRP en lit une, TIOCSCTTY/TIOCNOTTY ne prennent qu’un entier.

Terminal de test — validation on-target (enfant forké setsid). Le chemin nominal de ces ioctls exige un terminal de contrôle, que le kernel n’accorde qu’à un leader de session. Les chemins d’erreur (Err) sont donc couverts dans le parent sur un fd non-tty (/dev/nullENOTTY), et le cycle nominal complet est validé dans un enfant forké setsid (helper fork_run, motif de process.rs) : setsidset_ctty(slave)session_id(slave) == SIDtcsetpgrp(slave, SID)tcgetpgrp(slave) == Some(SID)clear_ctty(slave) (après avoir bloqué SIGHUP). L’enfant encode chaque étape dans son code de sortie (0 = tout le cycle a réussi). Le bras Ok(None) de tcgetpgrp est, lui, couvert dans le parent (un maître /dev/ptmx neuf n’a aucun groupe de premier plan → TIOCGPGRP rend 0). Les lignes du bras Ok de tcsetpgrp/session_id/set_ctty/clear_ctty, exécutées uniquement dans l’enfant, sont des exceptions CHILD-EXIT (ADR-035 ; profil LLVM de l’enfant non flushé, async-signal-safe — cf. docs/COVERAGE-EXCEPTIONS.md), prouvées par le code de sortie.

Périmètre hors tranches 1 à 5

Les cinq groupes d’ioctl de la famille sont désormais livrés en air-sys-syscall (« Attributs » — tcgetattr/tcsetattr — en tranche 2 ; « Contrôle de ligne » et « Taille fenêtre » en tranche 3 ; PTYopen_pty_master/pty_number/pty_unlock/pty_peer — en tranche 4 ; Session / job-controltcgetpgrp/tcsetpgrp/session_id/set_ctty/clear_ctty — en tranche 5, documentés ci-dessus ; cf. ADR-060). Restent hors périmètre : le vol de terminal de contrôle (TIOCSCTTY arg = 1, exige CAP_SYS_ADMIN) et les ioctls tty legacy/dangereux (TIOCSTI, TCGETA/TCSETA pré-POSIX) — à consigner dans UNSUPPORTED.md. isatty et ttyname restent en couche 1 (dérivés), de même que l’objet ergonomique air-terminal (ADR-061).