Skip to main content

air_sys_types/
signal.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//! Signal Linux et types associés.
6//!
7//! Couvre la famille `signal` de la couche 0 (cf.
8//! `docs/specs/layer-0/family-signal.md`). Périmètre :
9//!
10//! - Constantes [`Signal`] : 10 / ~31 signaux POSIX standards
11//!   référencés par le code et les tests Air. **Reste stub partiel** —
12//!   les ~21 restants (et les RT signals 32-64) seront ajoutés au fil
13//!   des PRs qui les utiliseront. Le type [`Signal`] **borne**
14//!   structurellement ses valeurs à `[1, 64]` (= `_NSIG` Linux) via
15//!   [`Signal::try_from_raw`] : aucun signal hors de cette plage ne
16//!   peut être construit publiquement.
17//! - [`SignalInfo`] : payload `siginfo_t` opaque (128 octets) construit
18//!   via des constructeurs Air (`new_queue`) pour le pattern SI_QUEUE
19//!   utilisé par `rt_sigqueueinfo` et acceptable pour `pidfd_send_signal`.
20//! - [`SignalValue`] : valeur associée à un signal-envoyé (`Integer(i32)`
21//!   ou `Pointer(u64)`).
22//! - [`SignalMask`] : ensemble de signaux comme bitmask `u64`.
23//! - [`SignalFdInfo`] : structure retournée par la lecture d'un
24//!   `signalfd`.
25//! - [`SignalFdFlags`] : drapeaux de `signalfd4`.
26//! - Sous-module [`synchronous_handler`] : `FatalSignal`, `SignalInfo`,
27//!   `PreviousHandler` (cf. ADR-020 sigaction restreint).
28
29use core::num::NonZeroI32;
30
31use bitflags::bitflags;
32
33use crate::process::Pid;
34
35// ─────────────────────────────────────────────────────────────────────────
36// Signal — newtype + constantes (stub partiel 10/~31).
37// ─────────────────────────────────────────────────────────────────────────
38
39/// Nombre maximum de signaux Linux (`_NSIG` dans `asm-generic/signal.h`).
40///
41/// Signal 1 = `SIGHUP`, signal 64 = `SIGRTMAX`. Aucun signal légitime au
42/// dessus. Toute construction publique de [`Signal`] avec une valeur
43/// hors `[1, 64]` est refusée à la borne — voir [`Signal::try_from_raw`].
44const NSIG: i32 = 64;
45
46/// Numéro de signal Unix/Linux.
47///
48/// Toujours dans `[1, 64]` par construction : le signal 0 n'existe pas
49/// (sentinelle « pas de signal » exprimée par `Option<Signal>` cf.
50/// convention 1 ADR-021), et Linux n'admet aucun signal > 64
51/// (`_NSIG = 64`). La borne est validée à la construction
52/// ([`Self::try_from_raw`]) — pas de runtime check ailleurs.
53#[repr(transparent)]
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
55pub struct Signal(NonZeroI32);
56
57impl Signal {
58    /// `SIGHUP` — Hangup (1) : la fin d'un terminal de contrôle. Envoyé au shell de
59    /// login d'`air-sshd` quand le client ferme le canal interactif (E.3).
60    pub const SIGHUP: Self = Self(unwrap_nz(1));
61
62    /// `SIGILL` — Illegal instruction (4).
63    ///
64    /// Référencé par cette PR : `FatalSignal::Ill` (sous-module
65    /// `synchronous_handler`), tests d'install_fatal_handler.
66    pub const SIGILL: Self = Self(unwrap_nz(4));
67
68    /// `SIGTRAP` — Trace/breakpoint trap (5).
69    pub const SIGTRAP: Self = Self(unwrap_nz(5));
70
71    /// `SIGABRT` — Abort (6).
72    pub const SIGABRT: Self = Self(unwrap_nz(6));
73
74    /// `SIGBUS` — Bus error (7).
75    ///
76    /// Référencé par cette PR : `FatalSignal::Bus`.
77    pub const SIGBUS: Self = Self(unwrap_nz(7));
78
79    /// `SIGFPE` — Floating-point exception (8).
80    ///
81    /// Référencé par cette PR : `FatalSignal::Fpe`.
82    pub const SIGFPE: Self = Self(unwrap_nz(8));
83
84    /// `SIGKILL` — Kill (cannot be caught or ignored, 9).
85    pub const SIGKILL: Self = Self(unwrap_nz(9));
86
87    /// `SIGUSR1` — User-defined signal 1 (10, x86_64/aarch64).
88    ///
89    /// Référencé par cette PR : signal canonique pour les tests
90    /// signalfd / block / tgkill / rt_sigqueueinfo.
91    pub const SIGUSR1: Self = Self(unwrap_nz(10));
92
93    /// `SIGPIPE` — Écriture sur un tube ou une socket dont le lecteur est parti (13).
94    ///
95    /// **Le PAL de la `std` `linux-air` le pose à `SIG_IGN` au démarrage**, comme le fait le
96    /// PAL `unix` de l'amont — sans quoi tout binaire Air meurt sur signal 13 là où un
97    /// programme Rust reçoit `Err(EPIPE)` et peut le traiter.
98    ///
99    /// Son absence de cette liste était l'anomalie : onze signaux y figuraient, et celui qui
100    /// décide du sort de toute écriture sur un pair disparu n'y était pas.
101    pub const SIGPIPE: Self = Self(unwrap_nz(13));
102
103    /// `SIGSEGV` — Segmentation fault (11).
104    ///
105    /// Référencé par cette PR : `FatalSignal::Segv`.
106    pub const SIGSEGV: Self = Self(unwrap_nz(11));
107
108    /// `SIGCHLD` — Child status changed (17, x86_64/aarch64).
109    pub const SIGCHLD: Self = Self(unwrap_nz(17));
110
111    /// `SIGSTOP` — Stop process (cannot be caught or ignored, 19,
112    /// x86_64/aarch64).
113    pub const SIGSTOP: Self = Self(unwrap_nz(19));
114
115    /// Tente de construire un `Signal` à partir d'un entier brut.
116    ///
117    /// **Borné à `[1, NSIG]` (= `[1, 64]` sur Linux).** `NSIG = 64` est
118    /// le nombre maximum de signaux Unix POSIX standard + temps-réel
119    /// supportés par Linux (cf. `_NSIG` dans `asm-generic/signal.h`).
120    ///
121    /// Retourne `None` si `raw <= 0` ou `raw > 64`. Aucune valeur hors
122    /// de cette plage ne peut désigner un signal Linux valide ; les
123    /// rejeter à la construction transforme l'invariant en garantie de
124    /// type (Principe 4 — validation amont), plutôt qu'en `EINVAL`
125    /// kernel observé au runtime sur `kill`/`tgkill`/etc.
126    #[must_use]
127    #[inline]
128    pub const fn try_from_raw(raw: i32) -> Option<Self> {
129        match NonZeroI32::new(raw) {
130            Some(nz) if nz.get() > 0 && nz.get() <= NSIG => Some(Self(nz)),
131            _ => None,
132        }
133    }
134
135    /// Retourne la représentation brute (positive) du numéro de signal.
136    #[must_use]
137    #[inline]
138    pub const fn as_raw(self) -> i32 {
139        self.0.get()
140    }
141}
142
143/// Helper const : construit un `NonZeroI32` dans la plage `[1, NSIG]`
144/// depuis un littéral. Panique au compile-time si l'invariant est
145/// violé — défense en profondeur pour que tout ajout futur de constante
146/// `Signal::SIGXXX` avec une valeur hors plage soit rejeté à la
147/// compilation, en plus de la borne runtime de [`Signal::try_from_raw`].
148const fn unwrap_nz(n: i32) -> NonZeroI32 {
149    if n <= 0 || n > NSIG {
150        panic!("Signal constants must be in [1, 64]");
151    }
152    match NonZeroI32::new(n) {
153        Some(v) => v,
154        // Inatteignable : la garde au-dessus exige `n > 0`. Présent
155        // pour satisfaire le contrôle exhaustif const.
156        None => panic!("unreachable: n > 0 guaranteed by the check above"),
157    }
158}
159
160// ─────────────────────────────────────────────────────────────────────────
161// SignalValue — valeur attachée à un signal envoyé via SI_QUEUE.
162// ─────────────────────────────────────────────────────────────────────────
163
164/// Valeur attachée à un signal envoyé via le pattern SI_QUEUE
165/// (`rt_sigqueueinfo`, ou `pidfd_send_signal` avec `info` non-null).
166///
167/// Côté kernel `siginfo_t`, c'est l'union `sigval_t { int si_int;
168/// void *si_ptr; }` — discrimination via `si_code` (mais ici la
169/// discrimination est portée par la variante Rust).
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
171pub enum SignalValue {
172    /// Variante `si_int`.
173    Integer(i32),
174    /// Variante `si_ptr`.
175    Pointer(u64),
176}
177
178// ─────────────────────────────────────────────────────────────────────────
179// SignalInfo — payload siginfo_t opaque.
180// ─────────────────────────────────────────────────────────────────────────
181
182// Constantes `si_code` extraites de `include/uapi/asm-generic/siginfo.h`.
183// Identiques x86_64 / aarch64.
184const SI_QUEUE: i32 = -1;
185
186// Offsets dans la struct `siginfo_t` Linux générique (vérifiés sur
187// `arch/x86/include/uapi/asm/siginfo.h` et `asm-generic/siginfo.h`).
188//
189// Layout des 16 premiers octets (toutes archs supportées) :
190//   offset  0 : si_signo (i32)
191//   offset  4 : si_errno (i32)
192//   offset  8 : si_code (i32)
193//   offset 12 : padding (4 octets, requis sur 64-bit pour aligner l'union)
194//
195// La variante union `_sigqueue` commence à l'offset 16 :
196//   offset 16 : si_pid (i32)
197//   offset 20 : si_uid (u32)
198//   offset 24 : sigval_t (union { si_int, si_ptr }) — 8 octets
199//
200// La variante union `_sigchld` partage le début (si_pid, si_uid) mais
201// utilise les octets suivants pour si_status / si_utime / si_stime.
202const SIGINFO_OFFSET_SI_CODE: usize = 8;
203const SIGINFO_OFFSET_SI_VALUE: usize = 24;
204const SIGINFO_SIZE: usize = 128;
205
206/// Payload `siginfo_t` opaque associé à un syscall d'envoi de signal
207/// (`pidfd_send_signal`, `rt_sigqueueinfo`).
208///
209/// **Représentation interne.** Buffer brut de 128 octets, alignement 4,
210/// layout `siginfo_t` Linux côté x86_64 et aarch64 (identique sur ces
211/// deux architectures). Opaque côté API publique : on n'expose pas les
212/// champs individuels — le kernel valide ses propres invariants
213/// (notamment `si_code`) et un usage à champs ouverts serait un footgun
214/// de sécurité (cf. la note dans `kernel/signal.c::do_rt_sigqueueinfo` :
215/// « Not even root can pretend to send signals from the kernel »).
216///
217/// **Construction.** Pas de champ public. Seuls des constructeurs Air
218/// disciplinés ([`Self::new_queue`]) fabriquent des `SignalInfo`
219/// dont le kernel accepte le `si_code`. Cela ferme la dette d'ambiguïté
220/// avec `SignalQueueInfo` consignée à la PR pidfd_* (le type
221/// `SignalQueueInfo` a été retiré ; le concept est porté par
222/// [`Self::new_queue`]).
223#[repr(C)]
224#[derive(Debug)]
225pub struct SignalInfo {
226    raw: [u8; SIGINFO_SIZE],
227}
228
229impl SignalInfo {
230    /// Construit un `SignalInfo` pour le pattern **SI_QUEUE**
231    /// (`rt_sigqueueinfo`, équivalent userspace de `sigqueue(3)`).
232    ///
233    /// Le `si_code` est fixé à `SI_QUEUE` (-1), seule valeur que le
234    /// kernel accepte pour un envoi cross-process userspace
235    /// (cf. `do_rt_sigqueueinfo` dans `kernel/signal.c`). Les champs
236    /// `si_signo`, `si_pid`, `si_uid` sont laissés à zéro — le kernel
237    /// les **écrase** avec les valeurs correctes au moment de la
238    /// délivrance (`sig` du syscall, `pid`/`uid` de l'appelant).
239    ///
240    /// Le champ `si_value` (offset 24, 8 octets) reçoit la valeur
241    /// fournie : variante `Integer(i32)` écrit les 4 premiers octets,
242    /// variante `Pointer(u64)` écrit les 8 octets.
243    #[must_use]
244    pub fn new_queue(value: SignalValue) -> Self {
245        let mut raw = [0_u8; SIGINFO_SIZE];
246        // si_code = SI_QUEUE à l'offset 8.
247        raw[SIGINFO_OFFSET_SI_CODE..SIGINFO_OFFSET_SI_CODE.saturating_add(4)]
248            .copy_from_slice(&SI_QUEUE.to_ne_bytes());
249        match value {
250            SignalValue::Integer(i) => {
251                raw[SIGINFO_OFFSET_SI_VALUE..SIGINFO_OFFSET_SI_VALUE.saturating_add(4)]
252                    .copy_from_slice(&i.to_ne_bytes());
253            }
254            SignalValue::Pointer(p) => {
255                raw[SIGINFO_OFFSET_SI_VALUE..SIGINFO_OFFSET_SI_VALUE.saturating_add(8)]
256                    .copy_from_slice(&p.to_ne_bytes());
257            }
258        }
259        Self { raw }
260    }
261
262    /// Accès lecture seule à la représentation brute, **uniquement** pour
263    /// les wrappers de syscalls qui doivent passer un pointeur au kernel.
264    /// Pas destiné à l'inspection des champs côté utilisateur (les
265    /// constructeurs Air fixent les champs corrects ; le kernel les
266    /// écrase à la délivrance).
267    #[doc(hidden)]
268    #[must_use]
269    #[inline]
270    pub fn as_bytes(&self) -> &[u8; SIGINFO_SIZE] {
271        &self.raw
272    }
273
274    /// `siginfo_t` **zéro-initialisé** — tampon de sortie destiné à être
275    /// **rempli par le kernel** (ex. `IORING_OP_WAITID`, où le kernel y écrit
276    /// l'état du processus). Lire ensuite via [`Self::as_bytes`].
277    #[must_use]
278    #[inline]
279    pub const fn zeroed() -> Self {
280        Self {
281            raw: [0_u8; SIGINFO_SIZE],
282        }
283    }
284}
285
286// ─────────────────────────────────────────────────────────────────────────
287// AltStack — pile de signal alternative (`stack_t`).
288// ─────────────────────────────────────────────────────────────────────────
289
290/// Pile de signal **alternative** (`stack_t` du kernel) — descellement additif ADR-085
291/// pour la face libc `sigaltstack`. Disposition `#[repr(C)]` **exacte** de `stack_t`
292/// (LP64 : `void *ss_sp` @0, `int ss_flags` @8, `size_t ss_size` @16 ; 24 octets), pour
293/// être passée telle quelle au syscall.
294///
295/// Une pile alternative permet à un gestionnaire de signal de s'exécuter sur une pile
296/// **dédiée** (indispensable pour traiter un `SIGSEGV` de **débordement de pile** — c'est
297/// l'usage de `std`).
298#[repr(C)]
299#[derive(Debug, Clone, Copy)]
300pub struct AltStack {
301    /// `ss_sp` — base de la pile alternative (ignoré si `SS_DISABLE`).
302    stack_pointer: *mut u8,
303    /// `ss_flags` — `SS_DISABLE`/`SS_ONSTACK`/`SS_AUTODISARM`.
304    flags: i32,
305    /// `ss_size` — taille en octets de la pile alternative.
306    size: usize,
307}
308
309/// `SS_ONSTACK` (1) — le thread s'exécute **actuellement** sur la pile alternative
310/// (lecture seule, rendu par la requête).
311pub const SS_ONSTACK: i32 = 1;
312/// `SS_DISABLE` (2) — désactive la pile alternative.
313pub const SS_DISABLE: i32 = 2;
314
315impl AltStack {
316    /// Construit une pile alternative **active** de base `stack_pointer` et `size` octets.
317    #[must_use]
318    pub fn new(stack_pointer: *mut u8, size: usize) -> Self {
319        Self {
320            stack_pointer,
321            flags: 0,
322            size,
323        }
324    }
325
326    /// Construit une pile **désactivée** (`SS_DISABLE`) — pour retirer une pile alternative.
327    #[must_use]
328    pub fn disabled() -> Self {
329        Self {
330            stack_pointer: core::ptr::null_mut(),
331            flags: SS_DISABLE,
332            size: 0,
333        }
334    }
335
336    /// Base de la pile alternative (`ss_sp`).
337    #[must_use]
338    pub fn stack_pointer(&self) -> *mut u8 {
339        self.stack_pointer
340    }
341
342    /// Taille de la pile alternative (`ss_size`).
343    #[must_use]
344    pub fn size(&self) -> usize {
345        self.size
346    }
347
348    /// Drapeaux bruts (`ss_flags`).
349    #[must_use]
350    pub fn flags(&self) -> i32 {
351        self.flags
352    }
353
354    /// `true` si la pile est **désactivée** (`SS_DISABLE`).
355    #[must_use]
356    pub fn is_disabled(&self) -> bool {
357        self.flags & SS_DISABLE != 0
358    }
359
360    /// `true` si le thread s'exécute **actuellement** sur la pile alternative
361    /// (`SS_ONSTACK`) — pertinent sur la valeur **rendue** par une requête.
362    #[must_use]
363    pub fn is_on_stack(&self) -> bool {
364        self.flags & SS_ONSTACK != 0
365    }
366}
367
368// ─────────────────────────────────────────────────────────────────────────
369// SignalMask — bitmask u64 (signaux 1-64).
370// ─────────────────────────────────────────────────────────────────────────
371
372/// Ensemble de signaux représenté comme bitmask `u64`.
373///
374/// Le bit `N` du mask correspond au signal `N + 1` (convention Linux
375/// `sigset_t` : signal 1 = bit 0, signal 64 = bit 63). Les signaux
376/// au-delà de 64 (RT signals étendus, rares) ne sont pas représentables
377/// par ce stub — ils seront supportés via une représentation plus large
378/// si nécessaire dans une PR ultérieure.
379#[repr(transparent)]
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
381pub struct SignalMask(u64);
382
383impl SignalMask {
384    /// Masque vide (aucun signal).
385    #[must_use]
386    #[inline]
387    pub const fn empty() -> Self {
388        Self(0)
389    }
390
391    /// Construit depuis sa représentation brute.
392    #[must_use]
393    #[inline]
394    pub const fn from_bits(bits: u64) -> Self {
395        Self(bits)
396    }
397
398    /// Représentation brute (bits 0-63 = signaux 1-64).
399    #[must_use]
400    #[inline]
401    pub const fn bits(self) -> u64 {
402        self.0
403    }
404
405    /// Construit un mask à partir d'une liste de signaux. Les signaux
406    /// au-delà de 64 (rares) sont silencieusement ignorés.
407    #[must_use]
408    pub fn from_signals(signals: &[Signal]) -> Self {
409        let mut bits = 0_u64;
410        for sig in signals {
411            // signal N → bit N-1 ; signal positif par construction.
412            let bit_index = u32::try_from(sig.as_raw())
413                .expect("Signal positive")
414                .saturating_sub(1);
415            // COVERAGE-UNREACHABLE: le `None` de `checked_shl` demanderait `bit_index >= 64`,
416            // donc un signal hors de [1, 64] — que `Signal::try_from_raw` refuse déjà à la
417            // construction. La branche est structurellement inatteignable ; elle est
418            // conservée comme garde no-op au titre du Principe 2 (jamais l'opération nue),
419            // et pour qu'un futur constructeur de `Signal` plus permissif ne devienne pas
420            // un débordement silencieux. Elle tombera le jour où ce constructeur existera.
421            if let Some(bit) = 1_u64.checked_shl(bit_index) {
422                bits |= bit;
423            }
424        }
425        Self(bits)
426    }
427
428    /// Indique si `sig` est dans le mask.
429    ///
430    /// **Défense en profondeur.** Le type `Signal` borne déjà ses valeurs
431    /// à `[1, 64]` (cf. [`Signal::try_from_raw`]), donc `bit_index < 64`
432    /// par construction et la branche `None` de `checked_shl` est
433    /// structurellement inatteignable. Elle est conservée comme garde
434    /// no-op (retourne `false`) au cas où un futur constructeur de
435    /// `Signal` oublierait d'appliquer la borne — Principe 5 :
436    /// sur-sécuriser, ne jamais corrompre.
437    #[must_use]
438    pub fn contains(self, sig: Signal) -> bool {
439        let bit_index = u32::try_from(sig.as_raw())
440            .expect("Signal positive")
441            .saturating_sub(1);
442        match 1_u64.checked_shl(bit_index) {
443            Some(bit) => (self.0 & bit) != 0,
444            None => false,
445        }
446    }
447
448    /// Retourne un nouveau mask avec `sig` ajouté.
449    ///
450    /// Même garde no-op défensive que [`Self::contains`] : si jamais
451    /// `bit_index >= 64`, retourne le mask inchangé (sans paniquer ni
452    /// corrompre l'état).
453    #[must_use]
454    pub fn with(self, sig: Signal) -> Self {
455        let bit_index = u32::try_from(sig.as_raw())
456            .expect("Signal positive")
457            .saturating_sub(1);
458        match 1_u64.checked_shl(bit_index) {
459            Some(bit) => Self(self.0 | bit),
460            None => self,
461        }
462    }
463
464    /// Retourne un nouveau mask avec `sig` retiré.
465    ///
466    /// Même garde no-op défensive que [`Self::contains`].
467    #[must_use]
468    pub fn without(self, sig: Signal) -> Self {
469        let bit_index = u32::try_from(sig.as_raw())
470            .expect("Signal positive")
471            .saturating_sub(1);
472        match 1_u64.checked_shl(bit_index) {
473            Some(bit) => Self(self.0 & !bit),
474            None => self,
475        }
476    }
477}
478
479// ─────────────────────────────────────────────────────────────────────────
480// SignalFdFlags + SignalFdInfo.
481// ─────────────────────────────────────────────────────────────────────────
482
483bitflags! {
484    /// Drapeaux pour [`signalfd_create`] (`signalfd4(2)`).
485    ///
486    /// **Note.** Le wrapper Air ajoute toujours `CLOEXEC` (cf.
487    /// `family-signal.md` : « Tous les FDs créés sont CLOEXEC par
488    /// défaut »). Les autres bits sont à la discrétion de l'appelant.
489    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
490    pub struct SignalFdFlags: i32 {
491        /// `SFD_NONBLOCK` : le FD est créé en mode non-bloquant.
492        const NONBLOCK = 0o4000;
493        /// `SFD_CLOEXEC` : automatiquement fermé à `execve`.
494        const CLOEXEC = 0o2_000_000;
495    }
496}
497
498/// Information lue depuis un `signalfd` (un `signalfd_siginfo`).
499///
500/// Layout C parsé par le wrapper depuis le buffer kernel ; les champs
501/// non-pertinents pour le signal reçu sont à 0/None côté kernel.
502#[derive(Debug, Clone, Copy)]
503pub struct SignalFdInfo {
504    /// Signal reçu.
505    pub signal: Signal,
506    /// `si_errno` (rarement utilisé).
507    pub errno: i32,
508    /// `si_code` (origine du signal : SI_USER, SI_KERNEL, CLD_EXITED, …).
509    pub code: i32,
510    /// `si_pid` : PID expéditeur. `None` si l'origine est le kernel (0).
511    pub pid: Option<Pid>,
512    /// `si_uid` : UID expéditeur.
513    pub uid: u32,
514    /// `ssi_fd` : FD source pour les signaux liés à un I/O (`SIGIO`).
515    pub fd: i32,
516    /// `ssi_tid` : ID du timer (spec Air : `timer_id`).
517    pub timer_id: u32,
518    /// `ssi_band` : pour `SIGIO`.
519    pub band: u32,
520    /// `ssi_overrun` : nombre d'overruns timer.
521    pub overrun: u32,
522    /// `ssi_trapno` : pour `SIGTRAP`.
523    pub trap_no: u32,
524    /// `ssi_status` : pour `SIGCHLD` (code de sortie ou signal fatal).
525    pub status: i32,
526    /// `ssi_int` : entier attaché pour SI_QUEUE-style.
527    pub int: i32,
528    /// `ssi_ptr` : pointeur attaché pour SI_QUEUE-style.
529    pub ptr: u64,
530    /// `ssi_utime` : pour `SIGCHLD`.
531    pub utime: u64,
532    /// `ssi_stime` : pour `SIGCHLD`.
533    pub stime: u64,
534    /// `ssi_addr` : pour `SIGSEGV`/`SIGBUS`/`SIGFPE`/`SIGILL`.
535    pub addr: u64,
536}
537
538// ─────────────────────────────────────────────────────────────────────────
539// Sous-module synchronous_handler (cf. ADR-020).
540// ─────────────────────────────────────────────────────────────────────────
541
542/// Sous-module restreint aux 4 signaux synchrones fatals
543/// (cf. ADR-020). Les seuls types et fonctions Air permettant
544/// d'installer un handler `sigaction` vivent ici ; la barrière est par
545/// construction.
546pub mod synchronous_handler {
547    use super::Signal;
548
549    /// Les quatre signaux synchrones fatals couverts par l'ADR-020.
550    /// L'enum est restreint à ces 4 variantes ; aucune API Air n'expose
551    /// d'autre signal pour `sigaction`.
552    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
553    pub enum FatalSignal {
554        /// `SIGSEGV` — segmentation fault.
555        Segv,
556        /// `SIGBUS` — bus error.
557        Bus,
558        /// `SIGFPE` — floating-point exception.
559        Fpe,
560        /// `SIGILL` — illegal instruction.
561        Ill,
562    }
563
564    impl FatalSignal {
565        /// Convertit en [`Signal`] correspondant.
566        #[must_use]
567        pub const fn as_signal(self) -> Signal {
568            match self {
569                Self::Segv => Signal::SIGSEGV,
570                Self::Bus => Signal::SIGBUS,
571                Self::Fpe => Signal::SIGFPE,
572                Self::Ill => Signal::SIGILL,
573            }
574        }
575    }
576
577    /// `siginfo_t` parsé pour un handler fatal. **Layout C** miroir du
578    /// `siginfo_t` Linux côté receiver — différent de
579    /// [`super::SignalInfo`] qui est le builder côté sender.
580    ///
581    /// Champs exposés : les plus utiles à un crash reporter. Le reste
582    /// du `siginfo_t` est dans `_padding`.
583    #[repr(C)]
584    pub struct SignalInfo {
585        /// `si_signo` : signal reçu (devrait correspondre à
586        /// [`FatalSignal::as_signal`] sur le handler installé).
587        pub signal: core::ffi::c_int,
588        /// `si_errno`.
589        pub errno: core::ffi::c_int,
590        /// `si_code` : type de fault (SEGV_MAPERR, SEGV_ACCERR, …).
591        pub code: core::ffi::c_int,
592        /// Padding à l'alignement union (4 octets sur 64-bit).
593        pub _pad0: u32,
594        /// `si_addr` : adresse mémoire fautive (pour SIGSEGV/SIGBUS).
595        pub addr: u64,
596        /// Reste de la struct (~104 octets restants pour atteindre 128).
597        pub _trailing: [u8; 104],
598    }
599
600    /// Handler installé via `install_fatal_handler`
601    /// (`air-sys-syscall::signal::synchronous_handler`). **Contrainte
602    /// async-signal-safe** : le handler ne doit appeler que des fonctions
603    /// listées dans `man 7 signal-safety` (typiquement `write`,
604    /// `_exit`, opérations sur des `core::sync::atomic`).
605    pub type FatalHandler = unsafe extern "C" fn(
606        signum: core::ffi::c_int,
607        info: *mut SignalInfo,
608        context: *mut core::ffi::c_void,
609    );
610
611    /// Handler précédemment installé sur un `FatalSignal`, à passer à
612    /// [`restore_handler`](crate::process::Pid) (dans
613    /// `air-sys-syscall`). Représentation opaque : contient
614    /// l'ancien `struct sigaction` kernel.
615    #[repr(C)]
616    pub struct PreviousHandler {
617        /// Buffer brut du `struct sigaction` kernel précédent.
618        /// Taille fixée à 32 octets — borne supérieure pour x86_64
619        /// (32 octets) et aarch64 (24 octets, padded à 32 pour
620        /// uniformité de représentation).
621        pub(crate) raw: [u8; 32],
622    }
623
624    impl PreviousHandler {
625        #[doc(hidden)]
626        #[must_use]
627        pub fn zeroed() -> Self {
628            Self { raw: [0_u8; 32] }
629        }
630
631        #[doc(hidden)]
632        #[must_use]
633        pub fn as_bytes(&self) -> &[u8; 32] {
634            &self.raw
635        }
636
637        #[doc(hidden)]
638        pub fn as_bytes_mut(&mut self) -> &mut [u8; 32] {
639            &mut self.raw
640        }
641    }
642
643    /// Vérifications à la compilation des invariants ABI.
644    const _: () = {
645        assert!(core::mem::size_of::<SignalInfo>() == 128);
646        assert!(core::mem::size_of::<PreviousHandler>() == 32);
647    };
648}
649
650// ─────────────────────────────────────────────────────────────────────────
651// Sous-module async_handler (cf. ADR-066) — types du descellement additif
652// « rt_sigaction non-faute ».
653// ─────────────────────────────────────────────────────────────────────────
654
655/// Types du **handler asynchrone** des signaux **non-faute** (ADR-066).
656///
657/// Contrairement à [`synchronous_handler`] (restreint aux 4 fautes, ADR-020) qui
658/// installe un handler **inerte** (le noyau force l'action par défaut sur les
659/// fautes), ce sous-module décrit les attributs d'un `rt_sigaction` **réellement
660/// installé** : le noyau détourne le thread vers le handler C à la délivrance
661/// d'un signal **gérable** (`SIGINT`/`SIGTERM`/`SIGCHLD`/`SIGWINCH`/`SIGUSR1-2`/
662/// temps-réel…). C'est la **fondation dual-face** : la libc C-ABI y bâtit
663/// `sigaction`/`signal`, le PAL Rust y bâtit ses handlers.
664///
665/// Les **fonctions** qui consomment ces types (`install`/`restore`) vivent en
666/// couche 0 dans `air-sys-syscall::signal::async_handler` — même partage
667/// types/wrappers que `synchronous_handler`.
668pub mod async_handler {
669    use bitflags::bitflags;
670
671    bitflags! {
672        /// Drapeaux `sa_flags` d'un `rt_sigaction` (identiques x86_64/aarch64,
673        /// `asm-generic`).
674        ///
675        /// **Exposés** : les drapeaux utiles à un handler applicatif. Le drapeau
676        /// interne `SA_RESTORER` (`0x0400_0000`) n'apparaît **pas** ici : il est
677        /// posé par le wrapper couche 0 sur x86_64 (trampoline `rt_sigreturn`
678        /// fourni par Air) et n'a **pas** de sens sur aarch64 (retour via VDSO).
679        /// C'est un attribut **typé** (ADR-021 §3), jamais un entier magique côté
680        /// appelant.
681        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
682        pub struct SigActionFlags: u64 {
683            /// `SA_NOCLDSTOP` : pas de `SIGCHLD` quand un enfant s'arrête/reprend.
684            const NOCLDSTOP = 0x0000_0001;
685            /// `SA_NOCLDWAIT` : les enfants ne deviennent pas zombies (`SIGCHLD`).
686            const NOCLDWAIT = 0x0000_0002;
687            /// `SA_SIGINFO` : handler à 3 arguments (`siginfo_t`/contexte).
688            const SIGINFO = 0x0000_0004;
689            /// `SA_ONSTACK` : exécute le handler sur la pile alternative
690            /// (`sigaltstack`).
691            const ONSTACK = 0x0800_0000;
692            /// `SA_RESTART` : redémarre certains syscalls interrompus. **Air ne
693            /// le force jamais** (ADR-021 conv.2 / ADR-064 §4) — mais un appelant
694            /// C peut le demander explicitement.
695            const RESTART = 0x1000_0000;
696            /// `SA_NODEFER` : ne bloque pas le signal courant pendant le handler.
697            const NODEFER = 0x4000_0000;
698            /// `SA_RESETHAND` : restaure `SIG_DFL` après une première délivrance.
699            const RESETHAND = 0x8000_0000;
700        }
701    }
702
703    /// Disposition **précédente** d'un signal, capturée par `install` et rendue à
704    /// `restore` (ADR-066). Représentation **opaque** : contient l'ancien
705    /// `struct sigaction` kernel (borne 32 octets — x86_64 = 32, aarch64 = 24
706    /// paddé à 32), même buffer que
707    /// [`synchronous_handler::PreviousHandler`](super::synchronous_handler::PreviousHandler)
708    /// mais **type distinct** pour garder étanches les deux faces (faute vs
709    /// non-faute).
710    #[repr(C)]
711    #[derive(Debug)]
712    pub struct PreviousDisposition {
713        /// Buffer brut du `struct sigaction` kernel précédent.
714        pub(crate) raw: [u8; 32],
715    }
716
717    impl PreviousDisposition {
718        /// Buffer **zéro-initialisé** — tampon de sortie rempli par le kernel
719        /// (`oldact` de `rt_sigaction`).
720        #[doc(hidden)]
721        #[must_use]
722        pub fn zeroed() -> Self {
723            Self { raw: [0_u8; 32] }
724        }
725
726        /// Vue lecture seule du buffer (pour le wrapper couche 0 qui le repasse au
727        /// kernel lors du `restore`).
728        #[doc(hidden)]
729        #[must_use]
730        pub fn as_bytes(&self) -> &[u8; 32] {
731            &self.raw
732        }
733
734        /// Vue mutable du buffer (rempli par le kernel lors de l'`install`).
735        #[doc(hidden)]
736        pub fn as_bytes_mut(&mut self) -> &mut [u8; 32] {
737            &mut self.raw
738        }
739    }
740
741    /// Invariant ABI : la disposition précédente tient dans 32 octets.
742    const _: () = {
743        assert!(core::mem::size_of::<PreviousDisposition>() == 32);
744    };
745}
746
747#[cfg(test)]
748mod tests;