Skip to main content

air_sys_syscall/
terminal.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//! Wrappers de la famille `terminal` (termios/tty).
6//!
7//! Cf. `docs/specs/layer-0/family-terminal.md` et
8//! [ADR-060](../../../docs/adrs/ADR-060-descellement-couche0-termios-tty-fr.md).
9//!
10//! **Tranche 2 — groupe « Attributs ».** Les deux ioctls typés du cœur termios,
11//! consommant le type [`Termios`] d'`air-sys-types` :
12//! - [`tcgetattr`] (`TCGETS`) : lit les attributs courants du terminal ;
13//! - [`tcsetattr`] (`TCSETS`/`TCSETSW`/`TCSETSF`) : les applique selon une
14//!   [`SetAction`] typée (mapping par `match` exhaustif — **jamais** l'arithmétique
15//!   `TCSETS + action`, fragile, pointée par l'audit musl §8).
16//!
17//! **Tranche 3 — groupes « Contrôle de ligne » et « Taille fenêtre ».** Les `tc*`
18//! POSIX restants et les ioctls de dimension de fenêtre :
19//! - [`tcdrain`] (`TCSBRK`, argument `1`) : attend la transmission de la sortie ;
20//! - [`tcflush`] (`TCFLSH`) : purge une [`FlushQueue`] (entrée/sortie/les deux) ;
21//! - [`tcflow`] (`TCXONC`) : suspend/reprend le flux selon une [`FlowAction`] ;
22//! - [`tcsendbreak`] (`TCSBRK`, argument `0`) : envoie un break (durée **non
23//!   paramétrée** — honnêteté de contrat, cf. audit musl §6/§8) ;
24//! - [`get_winsize`]/[`set_winsize`] (`TIOCGWINSZ`/`TIOCSWINSZ`) : lit/écrit la
25//!   [`Winsize`] du terminal.
26//!
27//! **Tranche 4 — groupe « PTY ».** Allocation d'une paire maître/esclave de
28//! pseudo-terminal :
29//! - [`open_pty_master`] : ouvre un nouveau maître via `/dev/ptmx` (dogfoode
30//!   [`crate::fs::openat`]) ;
31//! - [`pty_number`] (`TIOCGPTN`) : numéro [`PtyNumber`] de l'esclave (`/dev/pts/N`) ;
32//! - [`pty_unlock`] (`TIOCSPTLCK`) : déverrouille l'esclave (prérequis d'ouverture) ;
33//! - [`pty_peer`] (`TIOCGPTPEER`) : ouvre **directement** l'esclave (anti-TOCTOU,
34//!   Linux 4.13+) — évite la résolution du chemin `/dev/pts/N`.
35//!
36//! **Tranche 5 — groupe « Session / job-control ».** Terminal de contrôle et
37//! groupes de premier plan :
38//! - [`tcgetpgrp`] (`TIOCGPGRP`) : groupe de premier plan du terminal
39//!   (`Option<Pid>` — un `pid_t` `0` du kernel = **aucun** groupe, ADR-021 conv. 1) ;
40//! - [`tcsetpgrp`] (`TIOCSPGRP`) : installe un groupe de premier plan typé ;
41//! - [`session_id`] (`TIOCGSID`) : identifiant de session du terminal ;
42//! - [`set_ctty`] (`TIOCSCTTY`, argument `0`) : fait du terminal le terminal de
43//!   **contrôle** du leader de session appelant — **sans vol** (le vol `arg=1`
44//!   exige `CAP_SYS_ADMIN`, hors périmètre — UNSUPPORTED) ;
45//! - [`clear_ctty`] (`TIOCNOTTY`) : détache le terminal de contrôle.
46//!
47//! Ces cinq-là exigent, pour leur chemin **nominal**, un terminal de **contrôle**
48//! (que le kernel refuse hors leader de session) : ils sont donc validés dans un
49//! **enfant forké** `setsid` (tests), et les lignes de leur bras `Ok` — exécutées
50//! uniquement dans cet enfant — sont des exceptions **CHILD-EXIT** (ADR-035 ;
51//! profil LLVM non flushé côté enfant, async-signal-safe — cf. `process.rs`).
52//!
53//! Chaque opération est une **fonction dédiée typée** (ADR-021 conv. 3 : jamais
54//! d'`ioctl(fd, REQ, …)` générique). Zéro allocation (le [`Termios`]/[`Winsize`]
55//! vit sur la pile). `EINTR` est remonté tel quel à l'appelant (ADR-021 conv. 2),
56//! aucun retry automatique — [`tcdrain`] est un point d'annulation (audit §3). Le
57//! maître `/dev/ptmx` sert de tty réel dans les tests.
58
59use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
60use core::num::NonZeroI32;
61
62use air_sys_types::fs::{DirFd, OpenFlags};
63use air_sys_types::{Errno, FlowAction, FlushQueue, Pid, PtyNumber, SetAction, Termios, Winsize};
64
65#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
66compile_error!("air-sys-syscall::terminal supporte uniquement x86_64 et aarch64 (ADR-014).");
67
68// ─────────────────────────────────────────────────────────────────────────
69// Constantes kernel — requêtes ioctl termios (identiques x86_64 / aarch64).
70//
71// Valeurs de `<asm-generic/ioctls.h>` : sur `asm-generic`, la famille `TC*` est
72// numérotée en clair (pas d'encodage `_IOC`), et les deux architectures cibles
73// (x86_64, aarch64) partagent ces mêmes valeurs.
74// ─────────────────────────────────────────────────────────────────────────
75
76/// `TCGETS` — lit `struct termios` du terminal.
77const TCGETS: u64 = 0x5401;
78/// `TCSETS` — écrit `struct termios` immédiatement (`TCSANOW`).
79const TCSETS: u64 = 0x5402;
80/// `TCSETSW` — écrit après vidage de la file de sortie (`TCSADRAIN`).
81const TCSETSW: u64 = 0x5403;
82/// `TCSETSF` — écrit après vidage sortie + purge de l'entrée (`TCSAFLUSH`).
83const TCSETSF: u64 = 0x5404;
84/// `TCSBRK` — envoie un break (argument `0`) ou draine la sortie (argument non
85/// nul) ; l'argument entier distingue les deux sémantiques (cf. musl).
86const TCSBRK: u64 = 0x5409;
87/// `TCXONC` — contrôle de flux (démarrer/suspendre l'entrée ou la sortie).
88const TCXONC: u64 = 0x540A;
89/// `TCFLSH` — purge une (ou les deux) file(s) d'octets non transmis/non lus.
90const TCFLSH: u64 = 0x540B;
91/// `TIOCGWINSZ` — lit `struct winsize` (dimensions de la fenêtre du terminal).
92const TIOCGWINSZ: u64 = 0x5413;
93/// `TIOCSWINSZ` — écrit `struct winsize` (déclenche `SIGWINCH`).
94const TIOCSWINSZ: u64 = 0x5414;
95
96// Requêtes ioctl d'allocation PTY (tranche 4). Contrairement aux `TC*`
97// numérotées en clair ci-dessus, ces trois-là suivent le schéma d'encodage
98// `_IOC` arch-générique de `<asm-generic/ioctls.h>` (`_IOR`/`_IOW`/`_IO`) —
99// **identiques sur x86_64 et aarch64** (tous deux `asm-generic`).
100
101/// `TIOCGPTN` — `_IOR('T', 0x30, unsigned int)` : le kernel **écrit** le numéro
102/// (`u32`) de l'esclave associé au maître `/dev/ptmx`.
103const TIOCGPTN: u64 = 0x8004_5430;
104/// `TIOCSPTLCK` — `_IOW('T', 0x31, int)` : le kernel **lit** un `int` de
105/// (dé)verrouillage de l'esclave (`0` = déverrouille, prérequis à son ouverture).
106const TIOCSPTLCK: u64 = 0x4004_5431;
107/// `TIOCGPTPEER` — `_IO('T', 0x41)` : ouvre **directement** l'esclave du maître
108/// (Linux 4.13+, évite le TOCTOU du chemin `/dev/pts/N`). L'argument est une
109/// valeur entière de drapeaux d'ouverture ; la valeur de retour est le nouveau fd.
110const TIOCGPTPEER: u64 = 0x5441;
111
112// Requêtes ioctl de session / job-control (tranche 5). Numérotées en clair dans
113// `<asm-generic/ioctls.h>` (comme les `TC*`), identiques x86_64/aarch64.
114
115/// `TIOCSCTTY` — fait de `fd` le terminal de **contrôle** du leader de session
116/// appelant. L'argument est un entier : `0` = acquisition **sans vol**.
117const TIOCSCTTY: u64 = 0x540E;
118/// `TIOCGPGRP` — le kernel **écrit** le `pid_t` (i32) du groupe de premier plan.
119const TIOCGPGRP: u64 = 0x540F;
120/// `TIOCSPGRP` — le kernel **lit** le `pid_t` (i32) du groupe de premier plan à
121/// installer.
122const TIOCSPGRP: u64 = 0x5410;
123/// `TIOCNOTTY` — détache le terminal de **contrôle** du processus appelant.
124const TIOCNOTTY: u64 = 0x5422;
125/// `TIOCGSID` — le kernel **écrit** le `pid_t` (i32) du leader de session du
126/// terminal.
127const TIOCGSID: u64 = 0x5429;
128
129/// Argument `TIOCSCTTY` : `0` → **acquisition sans vol**. Le vol (`arg = 1`) exige
130/// `CAP_SYS_ADMIN` et n'est **pas** exposé (hors périmètre — UNSUPPORTED).
131const TIOCSCTTY_NO_STEAL: u64 = 0;
132
133// Arguments entiers (non pointeurs) des ioctls `TCSBRK`/`TCFLSH`/`TCXONC`.
134// Valeurs `<asm-generic/termbits.h>`, identiques x86_64/aarch64.
135
136/// Argument `TCSBRK` : `0` → **envoi de break** ([`tcsendbreak`]).
137const TCSBRK_SEND_BREAK: u64 = 0;
138/// Argument `TCSBRK` : `1` (non nul) → **drainage de la sortie** ([`tcdrain`]).
139const TCSBRK_DRAIN: u64 = 1;
140/// Argument `TCFLSH` : `TCIFLUSH` — purge les données reçues non lues.
141const TCIFLUSH: u64 = 0;
142/// Argument `TCFLSH` : `TCOFLUSH` — purge les données écrites non transmises.
143const TCOFLUSH: u64 = 1;
144/// Argument `TCFLSH` : `TCIOFLUSH` — purge les deux files.
145const TCIOFLUSH: u64 = 2;
146/// Argument `TCXONC` : `TCOOFF` — suspend la sortie.
147const TCOOFF: u64 = 0;
148/// Argument `TCXONC` : `TCOON` — reprend la sortie.
149const TCOON: u64 = 1;
150/// Argument `TCXONC` : `TCIOFF` — émet un STOP (suspend l'entrée distante).
151const TCIOFF: u64 = 2;
152/// Argument `TCXONC` : `TCION` — émet un START (reprend l'entrée distante).
153const TCION: u64 = 3;
154
155// ─────────────────────────────────────────────────────────────────────────
156// Attributs — tcgetattr / tcsetattr.
157// ─────────────────────────────────────────────────────────────────────────
158
159/// `tcgetattr` (`TCGETS`) — lit les attributs courants du terminal `fd`.
160///
161/// Alloue un [`Termios::zeroed`] sur la pile, le fait remplir par le kernel via
162/// l'ioctl `TCGETS`, puis le renvoie. Aucune allocation heap. Le retour est
163/// **normalisé** (comme musl) : sur échec de l'ioctl, l'`Errno` correspondant ;
164/// sinon le [`Termios`] rempli.
165///
166/// # Errors
167///
168/// - [`Errno::ENOTTY`] : `fd` n'est pas un terminal.
169/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
170/// - [`Errno::EINTR`] : interrompu par un signal (remonté tel quel, ADR-021 conv. 2).
171pub fn tcgetattr(fd: BorrowedFd<'_>) -> Result<Termios, Errno> {
172    let mut attributes = Termios::zeroed();
173    // Le kernel écrit `sizeof(struct termios)` octets à cette adresse ; on la
174    // passe en `u64` (conversion pointeur→entier, non lossy sur LP64).
175    let argument = (&raw mut attributes).cast::<u8>() as u64;
176    // SAFETY: ioctl(fd, TCGETS, &mut termios) écrit `struct termios` à `argument`
177    // — une variable de pile vivante, alignée, dimensionnée pour `Termios`
178    // (image `#[repr(C)]` exacte du kernel). Aucune autre mémoire n'est touchée.
179    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TCGETS, argument) };
180    if result < 0 {
181        return Err(errno_from_negative_syscall_ret(result));
182    }
183    Ok(attributes)
184}
185
186/// `tcsetattr` (`TCSETS`/`TCSETSW`/`TCSETSF`) — applique `attributes` au terminal
187/// `fd`, à l'instant indiqué par `action`.
188///
189/// L'`action` est mappée vers la requête ioctl par un `match` **exhaustif** (et
190/// non par l'arithmétique `TCSETS + action`, fragile car dépendante de la
191/// contiguïté des constantes — audit musl §8) : [`SetAction::Now`] → `TCSETS`,
192/// [`SetAction::Drain`] → `TCSETSW`, [`SetAction::Flush`] → `TCSETSF`.
193///
194/// # Errors
195///
196/// - [`Errno::ENOTTY`] : `fd` n'est pas un terminal.
197/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
198/// - [`Errno::EINTR`] : interrompu par un signal (remonté tel quel, ADR-021 conv. 2).
199pub fn tcsetattr(fd: BorrowedFd<'_>, action: SetAction, attributes: &Termios) -> Result<(), Errno> {
200    let request = match action {
201        SetAction::Now => TCSETS,
202        SetAction::Drain => TCSETSW,
203        SetAction::Flush => TCSETSF,
204    };
205    // Le kernel lit `sizeof(struct termios)` octets à cette adresse ; on la passe
206    // en `u64` (conversion pointeur→entier, non lossy sur LP64).
207    let argument = (attributes as *const Termios).cast::<u8>() as u64;
208    // SAFETY: ioctl(fd, TCSETS*, &termios) lit `struct termios` à `argument` —
209    // le `Termios` emprunté par `attributes`, vivant et aligné (image `#[repr(C)]`
210    // exacte du kernel). Aucune écriture de mémoire utilisateur.
211    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), request, argument) };
212    if result < 0 {
213        return Err(errno_from_negative_syscall_ret(result));
214    }
215    Ok(())
216}
217
218// ─────────────────────────────────────────────────────────────────────────
219// Contrôle de ligne — tcdrain / tcflush / tcflow / tcsendbreak (tranche 3).
220// ─────────────────────────────────────────────────────────────────────────
221
222/// `tcdrain` (`TCSBRK`, argument **1**) — bloque jusqu'à ce que tous les octets
223/// déjà écrits vers `fd` aient été transmis.
224///
225/// Sur Linux, `ioctl(fd, TCSBRK, arg)` avec un `arg` **non nul** ne fait pas un
226/// envoi de break : il **draine** la file de sortie (sémantique `tcdrain`, cf.
227/// musl). L'envoi de break correspond à `arg == 0` ([`tcsendbreak`]).
228///
229/// # Errors
230///
231/// - [`Errno::ENOTTY`] : `fd` n'est pas un terminal.
232/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
233/// - [`Errno::EINTR`] : interrompu par un signal pendant l'attente — **remonté
234///   tel quel**, aucun retry automatique (ADR-021 conv. 2 ; l'appelant qui veut
235///   retenter écrit la boucle). `tcdrain` est le point d'annulation de la famille
236///   (audit musl §3).
237pub fn tcdrain(fd: BorrowedFd<'_>) -> Result<(), Errno> {
238    // SAFETY: ioctl(fd, TCSBRK, 1) — l'argument est une valeur entière (1 =
239    // drainer la sortie), pas un pointeur : aucune mémoire utilisateur n'est lue
240    // ni écrite par le kernel.
241    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TCSBRK, TCSBRK_DRAIN) };
242    if result < 0 {
243        return Err(errno_from_negative_syscall_ret(result));
244    }
245    Ok(())
246}
247
248/// `tcflush` (`TCFLSH`) — purge (jette) les octets non traités de la (des) file(s)
249/// désignée(s) par `queue`.
250///
251/// Le mapping [`FlushQueue`] → argument entier se fait par un `match` **exhaustif**
252/// (ADR-021 conv. 3) : [`FlushQueue::Input`] → `TCIFLUSH` (`0`),
253/// [`FlushQueue::Output`] → `TCOFLUSH` (`1`), [`FlushQueue::Both`] → `TCIOFLUSH`
254/// (`2`).
255///
256/// # Errors
257///
258/// - [`Errno::ENOTTY`] : `fd` n'est pas un terminal.
259/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
260pub fn tcflush(fd: BorrowedFd<'_>, queue: FlushQueue) -> Result<(), Errno> {
261    let argument = match queue {
262        FlushQueue::Input => TCIFLUSH,
263        FlushQueue::Output => TCOFLUSH,
264        FlushQueue::Both => TCIOFLUSH,
265    };
266    // SAFETY: ioctl(fd, TCFLSH, arg) — `arg` est une valeur entière (file à
267    // purger), pas un pointeur : aucune mémoire utilisateur n'est accédée.
268    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TCFLSH, argument) };
269    if result < 0 {
270        return Err(errno_from_negative_syscall_ret(result));
271    }
272    Ok(())
273}
274
275/// `tcflow` (`TCXONC`) — suspend ou reprend le flux d'entrée ou de sortie de `fd`.
276///
277/// Le mapping [`FlowAction`] → argument entier se fait par un `match` **exhaustif**
278/// (ADR-021 conv. 3) : [`FlowAction::SuspendOutput`] → `TCOOFF` (`0`),
279/// [`FlowAction::ResumeOutput`] → `TCOON` (`1`), [`FlowAction::SuspendInput`] →
280/// `TCIOFF` (`2`, émet un STOP), [`FlowAction::ResumeInput`] → `TCION` (`3`, émet
281/// un START).
282///
283/// # Errors
284///
285/// - [`Errno::ENOTTY`] : `fd` n'est pas un terminal.
286/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
287pub fn tcflow(fd: BorrowedFd<'_>, action: FlowAction) -> Result<(), Errno> {
288    let argument = match action {
289        FlowAction::SuspendOutput => TCOOFF,
290        FlowAction::ResumeOutput => TCOON,
291        FlowAction::SuspendInput => TCIOFF,
292        FlowAction::ResumeInput => TCION,
293    };
294    // SAFETY: ioctl(fd, TCXONC, arg) — `arg` est une valeur entière (action de
295    // flux), pas un pointeur : aucune mémoire utilisateur n'est accédée.
296    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TCXONC, argument) };
297    if result < 0 {
298        return Err(errno_from_negative_syscall_ret(result));
299    }
300    Ok(())
301}
302
303/// `tcsendbreak` (`TCSBRK`, argument **0**) — transmet une séquence de bits `0`
304/// (condition BREAK) sur une ligne asynchrone.
305///
306/// **Durée non paramétrée (honnêteté de contrat).** POSIX prévoit un paramètre de
307/// durée, mais l'implémentation le rend *implementation-defined* et musl l'**ignore
308/// silencieusement** (audit musl §6/§8). Plutôt que d'exposer un paramètre qui
309/// serait avalé sans effet, Air n'en expose **aucun** : le break dure la durée par
310/// défaut du kernel. Le contrat dit exactement ce qu'il fait (doctrine « la libc
311/// fait ce qu'elle dit »).
312///
313/// # Errors
314///
315/// - [`Errno::ENOTTY`] : `fd` n'est pas un terminal.
316/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
317pub fn tcsendbreak(fd: BorrowedFd<'_>) -> Result<(), Errno> {
318    // SAFETY: ioctl(fd, TCSBRK, 0) — l'argument est une valeur entière (0 = envoi
319    // de break), pas un pointeur : aucune mémoire utilisateur n'est accédée.
320    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TCSBRK, TCSBRK_SEND_BREAK) };
321    if result < 0 {
322        return Err(errno_from_negative_syscall_ret(result));
323    }
324    Ok(())
325}
326
327// ─────────────────────────────────────────────────────────────────────────
328// Taille fenêtre — get_winsize / set_winsize (tranche 3).
329// ─────────────────────────────────────────────────────────────────────────
330
331/// `get_winsize` (`TIOCGWINSZ`) — lit les dimensions courantes de la fenêtre du
332/// terminal `fd`.
333///
334/// Alloue un [`Winsize::zeroed`] sur la pile, le fait remplir par le kernel via
335/// l'ioctl `TIOCGWINSZ`, puis le renvoie. Aucune allocation heap. Sur échec de
336/// l'ioctl, l'`Errno` correspondant.
337///
338/// # Errors
339///
340/// - [`Errno::ENOTTY`] : `fd` n'est pas un terminal.
341/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
342pub fn get_winsize(fd: BorrowedFd<'_>) -> Result<Winsize, Errno> {
343    let mut size = Winsize::zeroed();
344    // Le kernel écrit `sizeof(struct winsize)` octets à cette adresse ; on la
345    // passe en `u64` (conversion pointeur→entier, non lossy sur LP64).
346    let argument = (&raw mut size).cast::<u8>() as u64;
347    // SAFETY: ioctl(fd, TIOCGWINSZ, &mut winsize) écrit `struct winsize` à
348    // `argument` — une variable de pile vivante, alignée, dimensionnée pour
349    // `Winsize` (image `#[repr(C)]` exacte du kernel). Aucune autre mémoire touchée.
350    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TIOCGWINSZ, argument) };
351    if result < 0 {
352        return Err(errno_from_negative_syscall_ret(result));
353    }
354    Ok(size)
355}
356
357/// `set_winsize` (`TIOCSWINSZ`) — impose les dimensions `size` à la fenêtre du
358/// terminal `fd` (le kernel émet `SIGWINCH` vers le groupe de premier plan).
359///
360/// # Errors
361///
362/// - [`Errno::ENOTTY`] : `fd` n'est pas un terminal.
363/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
364pub fn set_winsize(fd: BorrowedFd<'_>, size: &Winsize) -> Result<(), Errno> {
365    // Le kernel lit `sizeof(struct winsize)` octets à cette adresse ; on la passe
366    // en `u64` (conversion pointeur→entier, non lossy sur LP64).
367    let argument = (size as *const Winsize).cast::<u8>() as u64;
368    // SAFETY: ioctl(fd, TIOCSWINSZ, &winsize) lit `struct winsize` à `argument` —
369    // le `Winsize` emprunté par `size`, vivant et aligné (image `#[repr(C)]` exacte
370    // du kernel). Aucune écriture de mémoire utilisateur.
371    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TIOCSWINSZ, argument) };
372    if result < 0 {
373        return Err(errno_from_negative_syscall_ret(result));
374    }
375    Ok(())
376}
377
378// ─────────────────────────────────────────────────────────────────────────
379// PTY — open_pty_master / pty_number / pty_unlock / pty_peer (tranche 4).
380// ─────────────────────────────────────────────────────────────────────────
381
382/// `open_pty_master` — ouvre un **nouveau** maître de pseudo-terminal via
383/// `/dev/ptmx`.
384///
385/// Chaque appel alloue une **paire maître/esclave** distincte (le kernel crée un
386/// nouvel esclave `/dev/pts/N` à chaque ouverture de `/dev/ptmx`). Le maître est
387/// rendu en [`OwnedFd`] (RAII — fermé au `Drop`, ce qui libère la paire).
388///
389/// Dogfoode l'API `fs` : délègue à [`crate::fs::openat`] (qui ajoute `O_CLOEXEC`),
390/// plutôt qu'un `open` brut. `O_RDWR` (lecture/écriture du maître) et `O_NOCTTY`
391/// (le maître ne doit **pas** devenir le terminal contrôlant du processus).
392///
393/// Le déverrouillage de l'esclave ([`pty_unlock`]) puis son ouverture
394/// ([`pty_peer`]) constituent la suite du flux d'allocation.
395///
396/// # Errors
397///
398/// - [`Errno::ENOSPC`] : plus aucun pseudo-terminal disponible (limite atteinte).
399/// - [`Errno::EACCES`]/[`Errno::ENOENT`] : `/dev/ptmx` absent ou inaccessible.
400/// - Toute erreur propagée par [`crate::fs::openat`].
401pub fn open_pty_master() -> Result<OwnedFd, Errno> {
402    crate::fs::openat(
403        DirFd::Cwd,
404        c"/dev/ptmx",
405        OpenFlags::RDWR | OpenFlags::NOCTTY,
406        0,
407    )
408}
409
410/// `pty_number` (`TIOCGPTN`) — numéro de l'esclave associé au maître `master`.
411///
412/// Le kernel écrit un `unsigned int` (`u32`) à une adresse de pile ; on le
413/// renvoie enveloppé dans un [`PtyNumber`] typé. C'est l'`N` du nœud
414/// `/dev/pts/N`.
415///
416/// # Errors
417///
418/// - [`Errno::ENOTTY`] : `master` n'est pas un maître de PTY.
419/// - [`Errno::EBADF`] : `master` n'est pas un descripteur valide.
420pub fn pty_number(master: BorrowedFd<'_>) -> Result<PtyNumber, Errno> {
421    let mut number: u32 = 0;
422    // Le kernel écrit `sizeof(unsigned int)` octets à cette adresse ; on la passe
423    // en `u64` (conversion pointeur→entier, non lossy sur LP64).
424    let argument = (&raw mut number).cast::<u8>() as u64;
425    // SAFETY: ioctl(master, TIOCGPTN, &mut number) écrit un `unsigned int` (u32) à
426    // `argument` — une variable de pile vivante, alignée, dimensionnée pour un u32.
427    // Aucune autre mémoire n'est touchée.
428    let result = unsafe { raw_syscall_ioctl(master.as_raw_fd(), TIOCGPTN, argument) };
429    if result < 0 {
430        return Err(errno_from_negative_syscall_ret(result));
431    }
432    Ok(PtyNumber::new(number))
433}
434
435/// `pty_unlock` (`TIOCSPTLCK`, valeur `0`) — déverrouille l'esclave de `master`.
436///
437/// Le déverrouillage est **obligatoire** avant d'ouvrir l'esclave (équivalent de
438/// `unlockpt`). Le kernel **lit** un `int` à l'adresse passée : on lui présente un
439/// `0` de pile. Le verrouillage (`1`) n'est **pas** exposé — seul le
440/// déverrouillage est utile en pratique (comme `unlockpt`).
441///
442/// # Errors
443///
444/// - [`Errno::ENOTTY`] : `master` n'est pas un maître de PTY.
445/// - [`Errno::EBADF`] : `master` n'est pas un descripteur valide.
446pub fn pty_unlock(master: BorrowedFd<'_>) -> Result<(), Errno> {
447    let unlock: i32 = 0;
448    // Le kernel lit `sizeof(int)` octets à cette adresse ; on la passe en `u64`
449    // (conversion pointeur→entier, non lossy sur LP64).
450    let argument = (&raw const unlock).cast::<u8>() as u64;
451    // SAFETY: ioctl(master, TIOCSPTLCK, &unlock) LIT un `int` à `argument` — une
452    // variable de pile vivante, alignée, dimensionnée pour un i32 (valeur 0 =
453    // déverrouiller). Aucune écriture de mémoire utilisateur.
454    let result = unsafe { raw_syscall_ioctl(master.as_raw_fd(), TIOCSPTLCK, argument) };
455    if result < 0 {
456        return Err(errno_from_negative_syscall_ret(result));
457    }
458    Ok(())
459}
460
461/// `pty_peer` (`TIOCGPTPEER`) — ouvre **directement** l'esclave de `master` et
462/// renvoie son descripteur.
463///
464/// Voie **moderne** (Linux 4.13+) : elle évite le TOCTOU du chemin `/dev/pts/N`
465/// (où l'esclave pourrait être substitué entre la résolution du nom et
466/// l'ouverture). L'argument ioctl est une **valeur entière** de drapeaux
467/// d'ouverture — `O_RDWR | O_NOCTTY | O_CLOEXEC`, `O_CLOEXEC` étant demandé
468/// **explicitement** (cet ioctl ne l'ajoute pas, contrairement à [`crate::fs::openat`]).
469/// La **valeur de retour** de l'ioctl est le nouveau fd de l'esclave.
470///
471/// Prérequis : l'esclave doit être déverrouillé au préalable ([`pty_unlock`]).
472///
473/// # Errors
474///
475/// - [`Errno::ENOTTY`] : `master` n'est pas un maître de PTY.
476/// - [`Errno::EBADF`] : `master` n'est pas un descripteur valide.
477/// - [`Errno::EINVAL`] : kernel antérieur à 4.13 (ioctl non pris en charge).
478pub fn pty_peer(master: BorrowedFd<'_>) -> Result<OwnedFd, Errno> {
479    // Drapeaux d'ouverture de l'esclave, passés comme ENTIER (pas un pointeur).
480    let open_flags = (OpenFlags::RDWR | OpenFlags::NOCTTY | OpenFlags::CLOEXEC).bits();
481    // SAFETY: ioctl(master, TIOCGPTPEER, flags) — `flags` est une valeur entière
482    // (drapeaux d'ouverture), pas un pointeur : aucune mémoire utilisateur n'est
483    // accédée. La valeur de retour (>= 0) est un descripteur fraîchement ouvert.
484    let result = unsafe { raw_syscall_ioctl(master.as_raw_fd(), TIOCGPTPEER, open_flags) };
485    if result < 0 {
486        return Err(errno_from_negative_syscall_ret(result));
487    }
488    // Le kernel rend un fd valide, toujours dans l'intervalle `i32` (un descripteur
489    // est < 2^31) : la troncature `as i32` est exacte, et le bras `Err` d'un
490    // `i32::try_from(result)` serait **inatteignable** (motif du repo, cf.
491    // `errno_from_negative_syscall_ret`).
492    #[allow(clippy::cast_possible_truncation)]
493    let raw_fd = result as i32;
494    // SAFETY: `raw_fd` est un esclave de PTY fraîchement ouvert par le kernel via
495    // TIOCGPTPEER (résultat >= 0, donc != -1) ; sa propriété est transférée à
496    // l'appelant, qui le fermera au `Drop` de l'`OwnedFd`.
497    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
498}
499
500// ─────────────────────────────────────────────────────────────────────────
501// Session / job-control — tcgetpgrp / tcsetpgrp / session_id / set_ctty /
502// clear_ctty (tranche 5).
503// ─────────────────────────────────────────────────────────────────────────
504
505/// `tcgetpgrp` (`TIOCGPGRP`) — groupe de premier plan du terminal `fd`.
506///
507/// Le kernel écrit un `pid_t` (i32) à une adresse de pile. Un `0` signifie
508/// **« aucun groupe de premier plan »** : la sentinelle est convertie en
509/// [`None`] (ADR-021 conv. 1). La conversion passe par [`Pid::try_from_raw`],
510/// qui replie **tout** `pid_t` non strictement positif sur `None` — donc `0`
511/// **comme** un éventuel `pid_t` négatif (aberrant : ce ioctl ne peut pas en
512/// produire) donnent tous deux `Ok(None)` : défense en profondeur (Principe 5).
513/// Un `pid_t > 0` est enveloppé dans un [`Pid`].
514///
515/// # Errors
516///
517/// - [`Errno::ENOTTY`] : `fd` n'est pas le terminal de contrôle de l'appelant.
518/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
519pub fn tcgetpgrp(fd: BorrowedFd<'_>) -> Result<Option<Pid>, Errno> {
520    let mut process_group: i32 = 0;
521    // Le kernel écrit `sizeof(pid_t)` octets à cette adresse ; on la passe en
522    // `u64` (conversion pointeur→entier, non lossy sur LP64).
523    let argument = (&raw mut process_group).cast::<u8>() as u64;
524    // SAFETY: ioctl(fd, TIOCGPGRP, &mut pgrp) écrit un `pid_t` (i32) à `argument`
525    // — une variable de pile vivante, alignée, dimensionnée pour un i32. Aucune
526    // autre mémoire n'est touchée.
527    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TIOCGPGRP, argument) };
528    if result < 0 {
529        return Err(errno_from_negative_syscall_ret(result));
530    }
531    Ok(Pid::try_from_raw(process_group))
532}
533
534/// `tcsetpgrp` (`TIOCSPGRP`) — installe `process_group` comme groupe de premier
535/// plan du terminal `fd`.
536///
537/// Le kernel **lit** un `pid_t` (i32) à l'adresse passée : on lui présente la
538/// valeur brute du [`Pid`] sur la pile. `Pid` étant non nul par typage, aucune
539/// sentinelle `0` n'est jamais présentée au kernel (ADR-021 conv. 1).
540///
541/// # Errors
542///
543/// - [`Errno::ENOTTY`] : `fd` n'est pas le terminal de contrôle de l'appelant.
544/// - [`Errno::EPERM`] : `process_group` n'appartient pas à la session de `fd`.
545/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
546pub fn tcsetpgrp(fd: BorrowedFd<'_>, process_group: Pid) -> Result<(), Errno> {
547    let raw_group: i32 = process_group.as_raw();
548    // Le kernel lit `sizeof(pid_t)` octets à cette adresse ; on la passe en `u64`
549    // (conversion pointeur→entier, non lossy sur LP64).
550    let argument = (&raw const raw_group).cast::<u8>() as u64;
551    // SAFETY: ioctl(fd, TIOCSPGRP, &pgrp) LIT un `pid_t` (i32) à `argument` — une
552    // variable de pile vivante, alignée, dimensionnée pour un i32. Aucune écriture
553    // de mémoire utilisateur.
554    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TIOCSPGRP, argument) };
555    if result < 0 {
556        return Err(errno_from_negative_syscall_ret(result));
557    }
558    Ok(())
559}
560
561/// `session_id` (`TIOCGSID`) — identifiant de session du terminal `fd`.
562///
563/// Le kernel écrit le `pid_t` du leader de session à une adresse de pile ; il est
564/// **toujours strictement positif** pour une session valide. La conversion via
565/// [`Pid::try_from_raw`] est dénotée par `.expect` (même motif que `getsid` de la
566/// famille process) : un `pid_t ≤ 0` violerait le contrat kernel — inatteignable
567/// par construction.
568///
569/// # Errors
570///
571/// - [`Errno::ENOTTY`] : `fd` n'a pas de session associée (pas un terminal de
572///   contrôle) ou n'est pas un terminal.
573/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
574pub fn session_id(fd: BorrowedFd<'_>) -> Result<Pid, Errno> {
575    let mut session: i32 = 0;
576    // Le kernel écrit `sizeof(pid_t)` octets à cette adresse ; on la passe en
577    // `u64` (conversion pointeur→entier, non lossy sur LP64).
578    let argument = (&raw mut session).cast::<u8>() as u64;
579    // SAFETY: ioctl(fd, TIOCGSID, &mut sid) écrit un `pid_t` (i32) à `argument`
580    // — une variable de pile vivante, alignée, dimensionnée pour un i32. Aucune
581    // autre mémoire n'est touchée.
582    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TIOCGSID, argument) };
583    if result < 0 {
584        return Err(errno_from_negative_syscall_ret(result));
585    }
586    Ok(Pid::try_from_raw(session).expect("kernel : SID de session strictement positif"))
587}
588
589/// `set_ctty` (`TIOCSCTTY`, argument **0**) — fait de `fd` le terminal de
590/// **contrôle** du leader de session appelant.
591///
592/// L'appelant doit être **leader de session** ([`crate::process::setsid`]) et ne
593/// pas déjà posséder de terminal de contrôle. L'argument **`0`** demande une
594/// acquisition **sans vol** : Air n'expose **pas** le vol (`arg = 1`, qui exige
595/// `CAP_SYS_ADMIN` — hors périmètre, listé en UNSUPPORTED).
596///
597/// # Errors
598///
599/// - [`Errno::ENOTTY`] : `fd` n'est pas un terminal, l'appelant n'est pas leader
600///   de session, ou le terminal est déjà le terminal de contrôle d'une **autre**
601///   session (vol refusé, `arg = 0`).
602/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
603pub fn set_ctty(fd: BorrowedFd<'_>) -> Result<(), Errno> {
604    // SAFETY: ioctl(fd, TIOCSCTTY, 0) — l'argument est une valeur entière (0 =
605    // acquisition sans vol), pas un pointeur : aucune mémoire utilisateur n'est
606    // accédée.
607    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TIOCSCTTY, TIOCSCTTY_NO_STEAL) };
608    if result < 0 {
609        return Err(errno_from_negative_syscall_ret(result));
610    }
611    Ok(())
612}
613
614/// `clear_ctty` (`TIOCNOTTY`) — détache le terminal de **contrôle** du processus
615/// appelant.
616///
617/// # Errors
618///
619/// - [`Errno::ENOTTY`] : `fd` n'est pas le terminal de contrôle de l'appelant.
620/// - [`Errno::EBADF`] : `fd` n'est pas un descripteur valide.
621pub fn clear_ctty(fd: BorrowedFd<'_>) -> Result<(), Errno> {
622    // SAFETY: ioctl(fd, TIOCNOTTY, 0) — l'argument est ignoré (valeur entière),
623    // pas un pointeur : aucune mémoire utilisateur n'est accédée.
624    let result = unsafe { raw_syscall_ioctl(fd.as_raw_fd(), TIOCNOTTY, 0) };
625    if result < 0 {
626        return Err(errno_from_negative_syscall_ret(result));
627    }
628    Ok(())
629}
630
631// ─────────────────────────────────────────────────────────────────────────
632// asm! wrapper ioctl x86_64 / aarch64 (propre au module, modèle de `device.rs`).
633// ─────────────────────────────────────────────────────────────────────────
634
635#[cfg(target_arch = "x86_64")]
636#[inline]
637unsafe fn raw_syscall_ioctl(fd: i32, request: u64, arg: u64) -> i64 {
638    let ret: i64;
639    // SAFETY: SYS_ioctl (x86_64 = 16). L'effet mémoire dépend de `request` ;
640    // chaque appelant a justifié la validité de `arg` pour sa requête : soit un
641    // pointeur de pile valide (TCGETS/TIOCGWINSZ/TIOCGPTN/TIOCGPGRP/TIOCGSID
642    // écrivent, TCSETS*/TIOCSWINSZ/TIOCSPTLCK/TIOCSPGRP lisent une valeur de
643    // pile), soit une valeur entière sans déréférencement (TCSBRK/TCFLSH/TCXONC ;
644    // TIOCSCTTY/TIOCNOTTY ; TIOCGPTPEER, dont le retour est le fd de l'esclave).
645    unsafe {
646        core::arch::asm!(
647            "syscall",
648            in("rax") 16_i64,
649            in("rdi") i64::from(fd),
650            in("rsi") request,
651            in("rdx") arg,
652            lateout("rax") ret,
653            lateout("rcx") _,
654            lateout("r11") _,
655            options(nostack, preserves_flags),
656        );
657    }
658    ret
659}
660
661#[cfg(target_arch = "aarch64")]
662#[inline]
663unsafe fn raw_syscall_ioctl(fd: i32, request: u64, arg: u64) -> i64 {
664    let ret: i64;
665    // SAFETY: SYS_ioctl (aarch64 = 29). L'effet mémoire dépend de `request` ;
666    // chaque appelant a justifié la validité de `arg` pour sa requête : soit un
667    // pointeur de pile valide (TCGETS/TIOCGWINSZ/TIOCGPTN/TIOCGPGRP/TIOCGSID
668    // écrivent, TCSETS*/TIOCSWINSZ/TIOCSPTLCK/TIOCSPGRP lisent une valeur de
669    // pile), soit une valeur entière sans déréférencement (TCSBRK/TCFLSH/TCXONC ;
670    // TIOCSCTTY/TIOCNOTTY ; TIOCGPTPEER, dont le retour est le fd de l'esclave).
671    unsafe {
672        core::arch::asm!(
673            "svc 0",
674            in("x8") 29_i64,
675            inout("x0") i64::from(fd) => ret,
676            in("x1") request,
677            in("x2") arg,
678            options(nostack, preserves_flags),
679        );
680    }
681    ret
682}
683
684// ─────────────────────────────────────────────────────────────────────────
685// errno helper (propre au module, modèle de `device.rs`).
686// ─────────────────────────────────────────────────────────────────────────
687
688fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
689    debug_assert!(ret < 0 && ret > -4096);
690    #[allow(clippy::cast_possible_truncation)]
691    let raw = ret.wrapping_neg() as i32;
692    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
693    Errno::from_nonzero(nz)
694}
695
696#[cfg(test)]
697mod tests;