Skip to main content

air_sys_syscall/
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//! Wrappers de la famille `signal`.
6//!
7//! Cf. `docs/specs/layer-0/family-signal.md` et `ADR-020-strategie-signaux`.
8//!
9//! Périmètre :
10//! - Sous-section 1 (signalfd + masques) : [`signalfd_create`],
11//!   [`signalfd_create_blocking`], [`SignalFd::read`],
12//!   [`SignalFd::update_mask`], [`block_signals`], [`unblock_signals`],
13//!   [`set_signal_mask`], [`current_signal_mask`], [`wait_for_signal`].
14//! - Sous-section 2 (envoi) : [`kill`], [`tgkill`], [`rt_sigqueueinfo`].
15//! - Sous-section 3 (sigaction restreint FAUTES) : sous-module
16//!   [`synchronous_handler`] avec [`synchronous_handler::install_fatal_handler`]
17//!   et [`synchronous_handler::restore_handler`] (handler **inerte** ; le noyau
18//!   force l'action par défaut sur les 4 fautes, ADR-020).
19//! - Sous-section 4 (sigaction async NON-faute, ADR-066) : sous-module
20//!   [`async_handler`] avec [`async_handler::install`]/[`async_handler::restore`]
21//!   (handler **réellement installé** — le noyau appelle le handler C à la
22//!   délivrance d'un signal gérable ; trampoline `rt_sigreturn` fourni sur
23//!   x86_64), et [`rt_sigpending`] (ensemble des signaux pendants).
24
25use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
26use core::num::NonZeroI32;
27use core::time::Duration;
28
29use air_sys_types::{
30    AltStack, Errno, Pid, Signal, SignalFdFlags, SignalFdInfo, SignalInfo, SignalMask, Tid,
31};
32
33#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
34compile_error!("air-sys-syscall::signal supporte uniquement x86_64 et aarch64 (ADR-014).");
35
36// ─────────────────────────────────────────────────────────────────────────
37// SignalFd — wrapper RAII opaque autour d'un signalfd kernel.
38// ─────────────────────────────────────────────────────────────────────────
39
40/// FD `signalfd` (cf. `signalfd4(2)`). Drop ferme le FD via [`OwnedFd`].
41#[derive(Debug)]
42pub struct SignalFd(OwnedFd);
43
44impl SignalFd {
45    /// Construit depuis un `OwnedFd` déjà ouvert sur un signalfd.
46    #[must_use]
47    #[inline]
48    pub const fn from_owned_fd(fd: OwnedFd) -> Self {
49        Self(fd)
50    }
51
52    /// Vue empruntée du FD sous-jacent.
53    #[must_use]
54    pub fn as_fd(&self) -> BorrowedFd<'_> {
55        use air_sys_types::fd::AsFd;
56        self.0.as_fd()
57    }
58
59    /// Consomme et restitue le `OwnedFd`.
60    #[must_use]
61    pub fn into_fd(self) -> OwnedFd {
62        self.0
63    }
64
65    /// Lit le prochain événement signal disponible sur le `signalfd`.
66    ///
67    /// Bloque si le FD est en mode bloquant (par défaut) et qu'aucun
68    /// signal n'est pendant ; retourne immédiatement avec
69    /// [`Errno::EAGAIN`] si le FD est en mode `NONBLOCK` et aucun
70    /// signal n'est disponible.
71    ///
72    /// # Errors
73    ///
74    /// - [`Errno::EAGAIN`] : mode non-bloquant, aucun signal pendant.
75    /// - [`Errno::EINTR`] : interrompu par un signal (remonté tel quel
76    ///   per convention 2 ADR-021).
77    pub fn read(&self) -> Result<SignalFdInfo, Errno> {
78        let mut buffer = [0_u8; SIGNALFD_SIGINFO_SIZE];
79        let ptr: *mut u8 = buffer.as_mut_ptr();
80        let length = u64::try_from(SIGNALFD_SIGINFO_SIZE)
81            .expect("buffer fixe SIGNALFD_SIGINFO_SIZE tient en u64");
82        // SAFETY:
83        // - SYS_read sur un signalfd valide écrit exactement 128 octets
84        //   dans `buffer` (taille du `signalfd_siginfo`) ou retourne une
85        //   erreur. `buffer` est local valide pour la durée du syscall.
86        let ret = unsafe { raw_syscall_read(self.0.as_raw_fd(), ptr as u64, length) };
87        if ret < 0 {
88            return Err(errno_from_negative_syscall_ret(ret));
89        }
90        // Lecture partielle d'un signalfd impossible (atomique côté kernel) ;
91        // ret == 128 ou erreur.
92        debug_assert_eq!(ret, i64::from(SIGNALFD_SIGINFO_SIZE_I32));
93        Ok(parse_signalfd_siginfo(&buffer))
94    }
95
96    /// Met à jour le mask de signaux capturés par ce `signalfd`.
97    ///
98    /// Équivalent à un nouvel appel `signalfd4(self, &mask, ...)` qui
99    /// remplace atomiquement le mask interne du FD.
100    ///
101    /// # Errors
102    ///
103    /// - [`Errno::EINVAL`] : mask invalide.
104    pub fn update_mask(&mut self, mask: &SignalMask) -> Result<(), Errno> {
105        let bits = mask.bits();
106        let mask_ptr: *const u64 = &bits;
107        // SAFETY: signalfd4(fd_existant, &mask, sizeof(u64), 0). Le
108        // kernel lit 8 octets à mask_ptr (sigset_t kernel = 1 mot sur
109        // archs où _NSIG = 64). Flags=0 préserve les flags existants
110        // (NONBLOCK/CLOEXEC) du FD.
111        let ret = unsafe {
112            raw_syscall_signalfd4(self.0.as_raw_fd(), mask_ptr as u64, SIGSET_SIZE_U64, 0_i32)
113        };
114        if ret < 0 {
115            return Err(errno_from_negative_syscall_ret(ret));
116        }
117        Ok(())
118    }
119}
120
121// ─────────────────────────────────────────────────────────────────────────
122// signalfd_create / signalfd_create_blocking.
123// ─────────────────────────────────────────────────────────────────────────
124
125/// Crée un `signalfd` pour les signaux du `mask`.
126///
127/// Le drapeau `CLOEXEC` est **toujours** ajouté par le wrapper (cf.
128/// discipline universelle couche 0 : tous les FDs créés par Air sont
129/// `O_CLOEXEC`).
130///
131/// **Important.** Créer un signalfd ne suffit pas à empêcher la
132/// délivrance normale des signaux du `mask` (handler ou comportement
133/// par défaut). Il faut **aussi** bloquer ces signaux dans le mask du
134/// thread via [`block_signals`]. Le helper [`signalfd_create_blocking`]
135/// combine les deux étapes.
136///
137/// # Errors
138///
139/// - [`Errno::EINVAL`] : mask invalide.
140/// - `EMFILE`/`ENFILE` (stub Errno) : limites de FDs atteintes.
141pub fn signalfd_create(mask: &SignalMask, flags: SignalFdFlags) -> Result<SignalFd, Errno> {
142    let bits = mask.bits();
143    let mask_ptr: *const u64 = &bits;
144    let kernel_flags = flags.bits() | SignalFdFlags::CLOEXEC.bits();
145    // SAFETY: signalfd4(-1, &mask, sizeof(u64), flags) crée un nouveau
146    // FD ; le kernel lit 8 octets à mask_ptr. Pas d'écriture en mémoire
147    // utilisateur. -1 = créer un nouveau (vs réutiliser un existant).
148    let ret =
149        unsafe { raw_syscall_signalfd4(-1_i32, mask_ptr as u64, SIGSET_SIZE_U64, kernel_flags) };
150    if ret < 0 {
151        return Err(errno_from_negative_syscall_ret(ret));
152    }
153    #[allow(clippy::cast_possible_truncation)]
154    let new_fd = ret as i32;
155    // SAFETY: kernel vient de transférer la propriété d'un fd valide.
156    let owned = unsafe { OwnedFd::from_raw_fd(new_fd) };
157    Ok(SignalFd::from_owned_fd(owned))
158}
159
160/// Helper canonique : bloque les signaux du `mask` dans le thread
161/// appelant **puis** crée un `signalfd` pour ces mêmes signaux.
162///
163/// Pattern recommandé par `family-signal.md` et ADR-020.
164///
165/// **Note.** Le blocage est PER-THREAD ; il s'applique au thread qui
166/// appelle cette fonction. À combiner avec un `MaskGuard` RAII dans le
167/// code de test pour ne pas contaminer les threads/tests suivants.
168///
169/// # Errors
170///
171/// - Toutes les erreurs de [`block_signals`] et [`signalfd_create`].
172pub fn signalfd_create_blocking(
173    mask: &SignalMask,
174    flags: SignalFdFlags,
175) -> Result<SignalFd, Errno> {
176    let _ = block_signals(mask)?;
177    signalfd_create(mask, flags)
178}
179
180// ─────────────────────────────────────────────────────────────────────────
181// Masques de signaux : rt_sigprocmask wrappers.
182// ─────────────────────────────────────────────────────────────────────────
183
184const SIG_BLOCK: i32 = 0;
185const SIG_UNBLOCK: i32 = 1;
186const SIG_SETMASK: i32 = 2;
187const SIGSET_SIZE_U64: u64 = 8;
188
189/// Bloque les signaux du `mask` dans le mask courant du thread (union).
190/// Retourne l'ancien mask (avant modification).
191///
192/// `SIGKILL` et `SIGSTOP` sont silencieusement ignorés par le kernel
193/// (non bloquables).
194///
195/// # Errors
196///
197/// - [`Errno::EINVAL`] : sigsetsize invalide.
198pub fn block_signals(mask: &SignalMask) -> Result<SignalMask, Errno> {
199    rt_sigprocmask(SIG_BLOCK, Some(mask))
200}
201
202/// Déblocque les signaux du `mask` dans le mask courant du thread
203/// (différence). Retourne l'ancien mask.
204///
205/// # Errors
206///
207/// - [`Errno::EINVAL`] : sigsetsize invalide.
208pub fn unblock_signals(mask: &SignalMask) -> Result<SignalMask, Errno> {
209    rt_sigprocmask(SIG_UNBLOCK, Some(mask))
210}
211
212/// Remplace **totalement** le mask de signaux du thread par `mask`.
213/// Retourne l'ancien mask.
214///
215/// Usage typique : restauration du mask original via un guard RAII.
216///
217/// # Errors
218///
219/// - [`Errno::EINVAL`] : sigsetsize invalide.
220pub fn set_signal_mask(mask: &SignalMask) -> Result<SignalMask, Errno> {
221    rt_sigprocmask(SIG_SETMASK, Some(mask))
222}
223
224/// Lit le mask de signaux courant sans modification.
225///
226/// # Errors
227///
228/// - [`Errno::EINVAL`] : ne se produit pas en pratique.
229pub fn current_signal_mask() -> Result<SignalMask, Errno> {
230    rt_sigprocmask(SIG_SETMASK, None)
231}
232
233/// Helper interne : encapsule l'appel `rt_sigprocmask` quelle que soit
234/// l'opération (SIG_BLOCK/UNBLOCK/SETMASK). `set = None` signifie
235/// « lecture seule ».
236fn rt_sigprocmask(how: i32, set: Option<&SignalMask>) -> Result<SignalMask, Errno> {
237    let set_bits: u64 = set.map_or(0, |m| m.bits());
238    let set_ptr: u64 = match set {
239        Some(_) => {
240            let p: *const u64 = &set_bits;
241            p as u64
242        }
243        None => 0,
244    };
245    let mut old_bits: u64 = 0;
246    let old_ptr: *mut u64 = &mut old_bits;
247    // SAFETY: rt_sigprocmask lit `set` si non-null (8 octets), écrit
248    // `oldset` (8 octets). Les deux pointeurs sont locaux ; sigsetsize=8
249    // correspond à la taille kernel `sigset_t` sur x86_64/aarch64.
250    let ret = unsafe { raw_syscall_rt_sigprocmask(how, set_ptr, old_ptr as u64, SIGSET_SIZE_U64) };
251    if ret < 0 {
252        return Err(errno_from_negative_syscall_ret(ret));
253    }
254    Ok(SignalMask::from_bits(old_bits))
255}
256
257/// Lit l'ensemble des signaux **pendants** (`rt_sigpending(2)`).
258///
259/// Retourne le [`SignalMask`] des signaux **en attente** de délivrance pour le
260/// thread appelant (union des pendants par-thread et par-process), c.-à-d.
261/// générés mais pas encore délivrés — typiquement parce qu'ils sont **bloqués**
262/// dans le masque courant. Brique de `sigpending(3)` de la libc (via la couche 1
263/// `air-signal`).
264///
265/// **Kernel = bible** : cette fonction **expose** l'état pendant du noyau, elle
266/// n'en maintient aucun (ADR-064 §6).
267///
268/// # Errors
269///
270/// - [`Errno::EFAULT`] : inatteignable via cette API (le buffer est un local
271///   vivant). En pratique `rt_sigpending` ne peut pas échouer avec un
272///   `sigsetsize` correct (fixé à 8 par le wrapper).
273pub fn rt_sigpending() -> Result<SignalMask, Errno> {
274    let mut pending_bits: u64 = 0;
275    let set_ptr: *mut u64 = &mut pending_bits;
276    // SAFETY: rt_sigpending écrit 8 octets (sigset_t kernel = 1 mot sur
277    // x86_64/aarch64) à `set_ptr`, local vivant pour la durée de l'appel.
278    // sigsetsize=8 correspond à la taille kernel `sigset_t`.
279    let ret = unsafe { raw_syscall_rt_sigpending(set_ptr as u64, SIGSET_SIZE_U64) };
280    if ret < 0 {
281        return Err(errno_from_negative_syscall_ret(ret));
282    }
283    Ok(SignalMask::from_bits(pending_bits))
284}
285
286// ─────────────────────────────────────────────────────────────────────────
287// wait_for_signal — helper signalfd temporaire.
288// ─────────────────────────────────────────────────────────────────────────
289
290/// Attend l'arrivée d'un signal du `mask`, avec timeout optionnel.
291///
292/// Implémentation : crée un `signalfd` temporaire pour `mask`, bloque
293/// ces signaux dans le thread, lit l'événement (bloquant ou avec
294/// timeout via `ppoll`).
295///
296/// **Note périmètre.** Cette PR n'implémente que la variante
297/// `timeout = None` (lecture bloquante via `read` sur le signalfd) ;
298/// la variante `Some(duration)` nécessite `ppoll` qui sera ajouté dans
299/// la PR `family-time` (timers). En `Some(_)`, retourne actuellement
300/// [`Errno::EINVAL`] — l'API publique reste conforme spec mais la
301/// branche timeout est documentée comme TODO.
302///
303/// # Errors
304///
305/// - [`Errno::EINVAL`] : `timeout.is_some()` (non encore implémenté
306///   sans wrapper `ppoll`).
307/// - Toutes les erreurs de [`signalfd_create_blocking`] et de
308///   [`SignalFd::read`].
309pub fn wait_for_signal(
310    mask: &SignalMask,
311    timeout: Option<Duration>,
312) -> Result<SignalFdInfo, Errno> {
313    if timeout.is_some() {
314        return Err(Errno::EINVAL);
315    }
316    let sfd = signalfd_create_blocking(mask, SignalFdFlags::empty())?;
317    sfd.read()
318}
319
320// ─────────────────────────────────────────────────────────────────────────
321// Envoi de signaux : kill, tgkill, rt_sigqueueinfo.
322// ─────────────────────────────────────────────────────────────────────────
323
324/// Envoie `signal` au processus `pid`.
325///
326/// `signal = None` correspond au "signal 0" Linux : test d'existence du
327/// processus sans envoyer de signal effectif (cf. convention 1
328/// ADR-021).
329///
330/// **Recommandation Air.** Pour un processus enfant dont on possède un
331/// `PidFd`, préférer [`pidfd_send_signal`](crate::process::pidfd_send_signal)
332/// — pas de race sur le PID recyclé.
333///
334/// # Errors
335///
336/// - [`Errno::EINVAL`] : signal invalide.
337/// - [`Errno::EPERM`] : permissions insuffisantes.
338/// - [`Errno::ESRCH`] : processus inexistant.
339pub fn kill(pid: Pid, signal: Option<Signal>) -> Result<(), Errno> {
340    let sig_arg = signal.map_or(0_i32, |s| s.as_raw());
341    // SAFETY: kill(2) ne touche pas la mémoire utilisateur.
342    let ret = unsafe { raw_syscall_kill(pid.as_raw(), sig_arg) };
343    if ret < 0 {
344        return Err(errno_from_negative_syscall_ret(ret));
345    }
346    Ok(())
347}
348
349/// Envoie `signal` à **tout un groupe de processus** (`kill(2)` avec un `pid` négatif) —
350/// **descellement additif [ADR-085](../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md)**
351/// pour la face libc `killpg` (le type positif-seul [`Pid`] interdit d'exprimer le `pid`
352/// négatif « groupe » via [`kill`]). `group = None` cible le **groupe de l'appelant**
353/// (sentinelle kernel `0` typée en `Option`, ADR-021 conv. 1) ; `Some(g)` cible le groupe
354/// `g` (le kernel reçoit `-g`). `signal = None` = test d'existence.
355pub fn kill_process_group(group: Option<Pid>, signal: Option<Signal>) -> Result<(), Errno> {
356    let sig_arg = signal.map_or(0_i32, |s| s.as_raw());
357    // `None` ⇒ `0` (groupe de l'appelant) ; `Some(g)` ⇒ `-g` (g > 0 ⇒ pas de débordement).
358    let target = group.map_or(0_i32, |g| g.as_raw().wrapping_neg());
359    // SAFETY: kill(2) ne touche pas la mémoire utilisateur.
360    let ret = unsafe { raw_syscall_kill(target, sig_arg) };
361    if ret < 0 {
362        return Err(errno_from_negative_syscall_ret(ret));
363    }
364    Ok(())
365}
366
367/// Installe (`new`) et/ou interroge (`old`) la **pile de signal alternative** du thread
368/// (`sigaltstack(2)`) — **descellement additif [ADR-085](../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md)**
369/// pour la face libc. `new = None` ne change rien ; `old = None` ne rapporte rien.
370/// Requis par `std` (traitement d'un `SIGSEGV` de débordement de pile sur pile dédiée).
371///
372/// # Errors
373///
374/// - [`Errno::EINVAL`] : `new.flags` invalide, ou `new.size < MINSIGSTKSZ`.
375/// - [`Errno::EPERM`] : tentative de désactivation alors que le thread est **sur** la pile
376///   alternative (`SS_ONSTACK`).
377/// - [`Errno::EFAULT`] : `new`/`old` hors de l'espace adressable (impossible via `&`).
378pub fn sigaltstack(new: Option<&AltStack>, old: Option<&mut AltStack>) -> Result<(), Errno> {
379    let new_ptr = new.map_or(0_u64, |stack| core::ptr::from_ref(stack) as u64);
380    let old_ptr = old.map_or(0_u64, |stack| core::ptr::from_mut(stack) as u64);
381    // SAFETY: le kernel **lit** `*new` (24 octets, `stack_t`) si `new_ptr != 0`, et
382    // **écrit** `*old` (24 octets) si `old_ptr != 0` ; les deux proviennent de références
383    // Rust valides et correctement dimensionnées (`AltStack` est `#[repr(C)]` = `stack_t`).
384    let ret = unsafe { raw_syscall_sigaltstack(new_ptr, old_ptr) };
385    if ret < 0 {
386        return Err(errno_from_negative_syscall_ret(ret));
387    }
388    Ok(())
389}
390
391/// Envoie `signal` au thread `tid` du process `tgid`.
392///
393/// Plus précis que [`kill`] : cible un thread spécifique d'un processus
394/// multi-thread. Indispensable pour les patterns de signaling
395/// intra-process déterministes (notamment dans les tests qui veulent
396/// délivrer un signal à un thread précis sans contaminer les autres
397/// workers).
398///
399/// `signal = None` : test d'existence (convention 1 ADR-021).
400///
401/// # Errors
402///
403/// - Mêmes que [`kill`].
404pub fn tgkill(tgid: Pid, tid: Tid, signal: Option<Signal>) -> Result<(), Errno> {
405    let sig_arg = signal.map_or(0_i32, |s| s.as_raw());
406    // SAFETY: tgkill(2) ne touche pas la mémoire utilisateur.
407    let ret = unsafe { raw_syscall_tgkill(tgid.as_raw(), tid.as_raw(), sig_arg) };
408    if ret < 0 {
409        return Err(errno_from_negative_syscall_ret(ret));
410    }
411    Ok(())
412}
413
414/// Envoie `signal` au processus `pid` avec un payload `siginfo_t`
415/// (pattern SI_QUEUE — `sigqueue(3)` côté userspace).
416///
417/// `info` est construit côté appelant via
418/// [`SignalInfo::new_queue`](air_sys_types::SignalInfo::new_queue). Le
419/// kernel écrase `si_signo` avec `signal` à la délivrance.
420///
421/// # Errors
422///
423/// - [`Errno::EINVAL`] : signal invalide.
424/// - [`Errno::EPERM`] : tentative d'envoi cross-process avec un
425///   `si_code` non autorisé (impossible via notre constructeur qui
426///   fixe `si_code = SI_QUEUE`).
427/// - [`Errno::ESRCH`] : processus inexistant.
428pub fn rt_sigqueueinfo(pid: Pid, signal: Signal, info: &SignalInfo) -> Result<(), Errno> {
429    let info_ptr: *const u8 = info.as_bytes().as_ptr();
430    // SAFETY: rt_sigqueueinfo(2) lit 128 octets à `info_ptr` (taille
431    // siginfo_t Linux côté x86_64/aarch64). Pas d'écriture user.
432    let ret =
433        unsafe { raw_syscall_rt_sigqueueinfo(pid.as_raw(), signal.as_raw(), info_ptr as u64) };
434    if ret < 0 {
435        return Err(errno_from_negative_syscall_ret(ret));
436    }
437    Ok(())
438}
439
440// ─────────────────────────────────────────────────────────────────────────
441// signalfd_siginfo : parsing du buffer kernel vers SignalFdInfo.
442// ─────────────────────────────────────────────────────────────────────────
443
444const SIGNALFD_SIGINFO_SIZE: usize = 128;
445const SIGNALFD_SIGINFO_SIZE_I32: i32 = 128;
446
447// Offsets dans `struct signalfd_siginfo` (uapi/linux/signalfd.h).
448// Identiques x86_64/aarch64.
449const SFI_SSI_SIGNO: usize = 0;
450const SFI_SSI_ERRNO: usize = 4;
451const SFI_SSI_CODE: usize = 8;
452const SFI_SSI_PID: usize = 12;
453const SFI_SSI_UID: usize = 16;
454const SFI_SSI_FD: usize = 20;
455const SFI_SSI_TID: usize = 24;
456const SFI_SSI_BAND: usize = 28;
457const SFI_SSI_OVERRUN: usize = 32;
458const SFI_SSI_TRAPNO: usize = 36;
459const SFI_SSI_STATUS: usize = 40;
460const SFI_SSI_INT: usize = 44;
461const SFI_SSI_PTR: usize = 48;
462const SFI_SSI_UTIME: usize = 56;
463const SFI_SSI_STIME: usize = 64;
464const SFI_SSI_ADDR: usize = 72;
465
466fn read_u32(buffer: &[u8], offset: usize) -> u32 {
467    u32::from_ne_bytes(
468        buffer[offset..offset.saturating_add(4)]
469            .try_into()
470            .expect("4 octets"),
471    )
472}
473
474fn read_i32(buffer: &[u8], offset: usize) -> i32 {
475    i32::from_ne_bytes(
476        buffer[offset..offset.saturating_add(4)]
477            .try_into()
478            .expect("4 octets"),
479    )
480}
481
482fn read_u64(buffer: &[u8], offset: usize) -> u64 {
483    u64::from_ne_bytes(
484        buffer[offset..offset.saturating_add(8)]
485            .try_into()
486            .expect("8 octets"),
487    )
488}
489
490fn parse_signalfd_siginfo(buffer: &[u8; SIGNALFD_SIGINFO_SIZE]) -> SignalFdInfo {
491    let signo = read_u32(buffer, SFI_SSI_SIGNO);
492    // Le kernel garantit `signo` > 0 pour tout événement signalfd réel.
493    let signal = Signal::try_from_raw(i32::try_from(signo).expect("ssi_signo fit en i32"))
494        .expect("kernel : signalfd événement avec signo > 0");
495
496    let pid_raw = read_u32(buffer, SFI_SSI_PID);
497    // `ssi_pid == 0` quand le signal vient du kernel (SI_KERNEL) ; on
498    // applique la convention 1 ADR-021 et retourne `None` dans ce cas.
499    let pid = if pid_raw == 0 {
500        None
501    } else {
502        Some(
503            Pid::try_from_raw(i32::try_from(pid_raw).expect("ssi_pid fit en i32"))
504                .expect("ssi_pid > 0 ici"),
505        )
506    };
507
508    SignalFdInfo {
509        signal,
510        errno: read_i32(buffer, SFI_SSI_ERRNO),
511        code: read_i32(buffer, SFI_SSI_CODE),
512        pid,
513        uid: read_u32(buffer, SFI_SSI_UID),
514        fd: read_i32(buffer, SFI_SSI_FD),
515        timer_id: read_u32(buffer, SFI_SSI_TID),
516        band: read_u32(buffer, SFI_SSI_BAND),
517        overrun: read_u32(buffer, SFI_SSI_OVERRUN),
518        trap_no: read_u32(buffer, SFI_SSI_TRAPNO),
519        status: read_i32(buffer, SFI_SSI_STATUS),
520        int: read_i32(buffer, SFI_SSI_INT),
521        ptr: read_u64(buffer, SFI_SSI_PTR),
522        utime: read_u64(buffer, SFI_SSI_UTIME),
523        stime: read_u64(buffer, SFI_SSI_STIME),
524        addr: read_u64(buffer, SFI_SSI_ADDR),
525    }
526}
527
528// ─────────────────────────────────────────────────────────────────────────
529// Sous-module synchronous_handler : sigaction restreint (cf. ADR-020).
530// ─────────────────────────────────────────────────────────────────────────
531
532pub mod synchronous_handler {
533    //! Wrappers `sigaction` **restreints aux 4 signaux synchrones fatals**
534    //! (cf. ADR-020).
535    //!
536    //! L'API entière est `unsafe` : l'appelant doit garantir que le
537    //! handler installé respecte la contrainte async-signal-safe
538    //! (cf. `man 7 signal-safety`).
539
540    use air_sys_types::Errno;
541    use air_sys_types::signal::synchronous_handler::{FatalHandler, FatalSignal, PreviousHandler};
542
543    use super::{SIGSET_SIZE_U64, errno_from_negative_syscall_ret};
544
545    // SA_SIGINFO = 4 (constant ABI Linux, identique x86_64/aarch64).
546    const SA_SIGINFO: u64 = 4;
547
548    /// Installe `handler` comme handler du signal fatal `signal`.
549    /// Retourne l'ancien handler dans `PreviousHandler` pour pouvoir
550    /// restaurer via [`restore_handler`].
551    ///
552    /// # Safety
553    ///
554    /// L'appelant doit garantir que `handler` est **async-signal-safe**
555    /// (cf. `man 7 signal-safety`). Concrètement : pas de `malloc`, pas
556    /// de `printf`, pas de mutex, pas de `Mutex` Rust, pas d'allocations
557    /// indirectes (incluant les `format!`). Les appels typiques admis :
558    /// `write(STDERR_FILENO, ...)`, opérations `core::sync::atomic`,
559    /// `_exit(N)`. Voir l'ADR-020 pour les patterns de crash reporter.
560    pub unsafe fn install_fatal_handler(
561        signal: FatalSignal,
562        handler: FatalHandler,
563    ) -> Result<PreviousHandler, Errno> {
564        let mut new_sa = KernelSigaction::zeroed();
565        new_sa.sa_handler = handler as usize as u64;
566        new_sa.sa_flags = SA_SIGINFO;
567        // sa_mask = 0 (aucun signal additionnel bloqué pendant le
568        // handler). sa_restorer = 0 sur x86_64 → kernel utilise le
569        // trampoline VDSO ; absent sur aarch64.
570
571        let mut previous = PreviousHandler::zeroed();
572        let new_ptr: *const KernelSigaction = &new_sa;
573        let old_ptr: *mut [u8; 32] = previous.as_bytes_mut();
574
575        // SAFETY: rt_sigaction(2). Le kernel lit `new_sa`
576        // (KernelSigaction valide, locale) et écrit jusqu'à 32 octets à
577        // `old_ptr` (capacité de `PreviousHandler`). sigsetsize=8
578        // correspond à la taille kernel `sigset_t`.
579        let ret = unsafe {
580            raw_syscall_rt_sigaction(
581                signal.as_signal().as_raw(),
582                new_ptr as u64,
583                old_ptr as u64,
584                SIGSET_SIZE_U64,
585            )
586        };
587        if ret < 0 {
588            return Err(errno_from_negative_syscall_ret(ret));
589        }
590        Ok(previous)
591    }
592
593    /// Restaure le handler `previous` pour le signal `signal`.
594    ///
595    /// # Safety
596    ///
597    /// `previous` doit avoir été obtenu par un appel précédent à
598    /// [`install_fatal_handler`] sur le **même** `signal`. Restaurer un
599    /// `PreviousHandler` qui ne provient pas du même signal est
600    /// undefined behavior côté kernel.
601    pub unsafe fn restore_handler(
602        signal: FatalSignal,
603        previous: PreviousHandler,
604    ) -> Result<(), Errno> {
605        let prev_ptr: *const [u8; 32] = previous.as_bytes();
606        // SAFETY: rt_sigaction(2). Le kernel lit jusqu'à 32 octets à
607        // `prev_ptr` (KernelSigaction est ≤ 32 octets sur x86_64/aarch64).
608        // Pas d'écriture (oldset=NULL).
609        let ret = unsafe {
610            raw_syscall_rt_sigaction(
611                signal.as_signal().as_raw(),
612                prev_ptr as u64,
613                0_u64,
614                SIGSET_SIZE_U64,
615            )
616        };
617        if ret < 0 {
618            return Err(errno_from_negative_syscall_ret(ret));
619        }
620        Ok(())
621    }
622
623    /// Représentation interne du `struct sigaction` kernel.
624    ///
625    /// Layout :
626    /// - offset 0  : `sa_handler` (pointer, 8 octets)
627    /// - offset 8  : `sa_flags` (unsigned long, 8 octets)
628    /// - offset 16 : `sa_restorer` (x86_64 uniquement, 8 octets)
629    /// - offset 24 (x86_64) ou 16 (aarch64) : `sa_mask` (sigset_t, 8 octets)
630    ///
631    /// Sur aarch64, `sa_restorer` est absent (le kernel utilise VDSO
632    /// pour le retour). On utilise donc deux layouts compatibles avec
633    /// le syscall :
634    /// - x86_64 : 32 octets.
635    /// - aarch64 : 24 octets, paddé à 32 pour uniformiser le buffer
636    ///   `PreviousHandler` (les 8 derniers octets sont ignorés par le
637    ///   kernel sur aarch64).
638    #[repr(C)]
639    #[cfg(target_arch = "x86_64")]
640    struct KernelSigaction {
641        sa_handler: u64,
642        sa_flags: u64,
643        sa_restorer: u64,
644        sa_mask: u64,
645    }
646
647    #[cfg(target_arch = "x86_64")]
648    impl KernelSigaction {
649        const fn zeroed() -> Self {
650            Self {
651                sa_handler: 0,
652                sa_flags: 0,
653                sa_restorer: 0,
654                sa_mask: 0,
655            }
656        }
657    }
658
659    #[repr(C)]
660    #[cfg(target_arch = "aarch64")]
661    struct KernelSigaction {
662        sa_handler: u64,
663        sa_flags: u64,
664        sa_mask: u64,
665    }
666
667    #[cfg(target_arch = "aarch64")]
668    impl KernelSigaction {
669        const fn zeroed() -> Self {
670            Self {
671                sa_handler: 0,
672                sa_flags: 0,
673                sa_mask: 0,
674            }
675        }
676    }
677
678    #[cfg(target_arch = "x86_64")]
679    #[inline]
680    unsafe fn raw_syscall_rt_sigaction(
681        signum: i32,
682        new_sa: u64,
683        old_sa: u64,
684        sigsetsize: u64,
685    ) -> i64 {
686        let ret: i64;
687        // SAFETY: SYS_rt_sigaction (x86_64 = 13). Le kernel lit `new_sa`
688        // (KernelSigaction, 32 octets), écrit `old_sa` (32 octets) si
689        // non-null. ABI standard ; pas de `readonly` (écriture sur
690        // `*old_sa`).
691        unsafe {
692            core::arch::asm!(
693                "syscall",
694                in("rax") 13_i64,
695                in("rdi") i64::from(signum),
696                in("rsi") new_sa,
697                in("rdx") old_sa,
698                in("r10") sigsetsize,
699                lateout("rax") ret,
700                lateout("rcx") _,
701                lateout("r11") _,
702                options(nostack, preserves_flags),
703            );
704        }
705        ret
706    }
707
708    #[cfg(target_arch = "aarch64")]
709    #[inline]
710    unsafe fn raw_syscall_rt_sigaction(
711        signum: i32,
712        new_sa: u64,
713        old_sa: u64,
714        sigsetsize: u64,
715    ) -> i64 {
716        let ret: i64;
717        // SAFETY: SYS_rt_sigaction (aarch64 = 134).
718        unsafe {
719            core::arch::asm!(
720                "svc 0",
721                in("x8") 134_i64,
722                inout("x0") i64::from(signum) => ret,
723                in("x1") new_sa,
724                in("x2") old_sa,
725                in("x3") sigsetsize,
726                options(nostack, preserves_flags),
727            );
728        }
729        ret
730    }
731
732    /// Test : KernelSigaction tient dans le buffer 32 octets de
733    /// PreviousHandler.
734    const _: () = {
735        assert!(core::mem::size_of::<KernelSigaction>() <= 32);
736    };
737
738    /// Fonction `c_int` utilitaire pour les tests : ne fait rien (handler
739    /// vide), mais respecte la signature `FatalHandler`.
740    ///
741    /// Used by tests. Marked async-signal-safe (no allocation, no I/O,
742    /// just returns).
743    ///
744    /// # Safety
745    ///
746    /// Trivially safe : no operations.
747    #[cfg(test)]
748    pub(super) unsafe extern "C" fn noop_handler(
749        _signum: core::ffi::c_int,
750        _info: *mut air_sys_types::signal::synchronous_handler::SignalInfo,
751        _context: *mut core::ffi::c_void,
752    ) {
753        // Vide. Trivially async-signal-safe.
754    }
755}
756
757// ─────────────────────────────────────────────────────────────────────────
758// Sous-module async_handler : rt_sigaction NON-faute réel (cf. ADR-066).
759// ─────────────────────────────────────────────────────────────────────────
760
761// Trampoline `rt_sigreturn` pour x86_64. À la fin d'un handler **non-faute**, le
762// noyau ne revient PAS directement au code interrompu : il saute à l'adresse
763// `sa_restorer`, dont la seule mission est d'invoquer `rt_sigreturn` (NR 15) pour
764// restaurer le contexte pré-signal empilé par le noyau (sigframe). Sur x86_64 la
765// libc/`std` fournit ce trampoline ; sans libc (Air), on le fournit nous-mêmes.
766// Sur aarch64, le noyau route le retour par le VDSO — **aucun** `sa_restorer`.
767//
768// SAFETY: le corps asm n'est **jamais** appelé depuis Rust ; c'est le **noyau**
769// qui y saute à la fin du handler, avec `%rsp` pointant sur le sigframe. Il ne
770// fait qu'exécuter `rt_sigreturn` (`mov $15, %rax; syscall`), syscall qui ne
771// retourne pas (il restaure le contexte). Aucun prologue/épilogue (pas de cadre
772// de pile), aucun accès mémoire propre : conforme au contrat ABI de `sa_restorer`.
773#[cfg(target_arch = "x86_64")]
774core::arch::global_asm!(
775    ".p2align 4",
776    ".globl __air_rt_sigreturn_trampoline",
777    ".hidden __air_rt_sigreturn_trampoline",
778    "__air_rt_sigreturn_trampoline:",
779    "mov rax, 15", // __NR_rt_sigreturn (x86_64)
780    "syscall",
781);
782
783#[cfg(target_arch = "x86_64")]
784unsafe extern "C" {
785    /// Symbole du trampoline `rt_sigreturn` défini par le `global_asm!`
786    /// ci-dessus. Jamais appelé depuis Rust — seule son **adresse** est posée
787    /// dans `sa_restorer`.
788    fn __air_rt_sigreturn_trampoline();
789}
790
791pub mod async_handler {
792    //! Wrappers `rt_sigaction` pour les signaux **non-faute** — handler
793    //! **réellement installé** (cf. ADR-064 : délivrance async réelle ; ADR-066 :
794    //! descellement additif couche 0).
795    //!
796    //! Contrairement à [`synchronous_handler`](super::synchronous_handler)
797    //! (restreint aux 4 fautes, handler **inerte** — le noyau force l'action par
798    //! défaut), ce sous-module installe un `rt_sigaction` **effectif** : le noyau
799    //! détourne le thread vers le handler C à la délivrance d'un signal
800    //! **gérable**. C'est la **fondation dual-face** que la libc (`sigaction`) et
801    //! le PAL consomment via la couche 1 `air-signal`.
802    //!
803    //! **API entièrement `unsafe`** : l'appelant garantit que le handler est
804    //! **async-signal-safe** (`man 7 signal-safety`) — précondition invérifiable
805    //! par le compilateur, portée par l'appelant (identique à
806    //! `synchronous_handler`).
807
808    use air_sys_types::Errno;
809    use air_sys_types::signal::async_handler::{PreviousDisposition, SigActionFlags};
810    use air_sys_types::{Signal, SignalMask};
811
812    use super::{SIGSET_SIZE_U64, errno_from_negative_syscall_ret};
813
814    /// `SA_RESTORER` (`0x0400_0000`) — **x86_64 uniquement**. Signale au noyau que
815    /// `sa_restorer` porte l'adresse du trampoline de retour de handler.
816    #[cfg(target_arch = "x86_64")]
817    const SA_RESTORER: u64 = 0x0400_0000;
818
819    /// Valeur `sa_handler` = `SIG_DFL` (action par défaut du noyau).
820    pub const SIG_DFL: usize = 0;
821    /// Valeur `sa_handler` = `SIG_IGN` (signal ignoré). **Sûr** (constante noyau,
822    /// aucun code utilisateur) — cf. ADR-064 §2.
823    pub const SIG_IGN: usize = 1;
824
825    /// Installe une disposition pour le signal **non-faute** `signal` via
826    /// `rt_sigaction(2)`, et rend la disposition **précédente** (pour
827    /// [`restore`]).
828    ///
829    /// - `handler_sa` : valeur brute de `sa_handler` — [`SIG_DFL`] (`0`),
830    ///   [`SIG_IGN`] (`1`), ou l'**adresse** d'une fonction handler
831    ///   `extern "C"` (cast `usize`). C'est la couche 1 (`air-signal`) qui
832    ///   fournit le type de handler ; la couche 0 reste agnostique (adresse
833    ///   brute).
834    /// - `flags` : [`SigActionFlags`] (`SA_SIGINFO`/`SA_RESTART`/…). Sur x86_64,
835    ///   le wrapper **ajoute** `SA_RESTORER` et pose `sa_restorer` sur le
836    ///   trampoline `rt_sigreturn` d'Air (obligatoire sans libc). Sur aarch64, le
837    ///   retour passe par le VDSO — aucun `sa_restorer`.
838    /// - `mask` : signaux **additionnels** bloqués pendant l'exécution du handler.
839    ///
840    /// # Safety
841    ///
842    /// Si `handler_sa` désigne une fonction (≠ [`SIG_DFL`]/[`SIG_IGN`]), l'appelant
843    /// doit garantir qu'elle est **async-signal-safe** (`man 7 signal-safety`) :
844    /// pas de `malloc`, pas de verrou, pas d'I/O bufferisée, etc. Le noyau peut
845    /// l'invoquer à un point **arbitraire** du programme. Poser [`SIG_DFL`] ou
846    /// [`SIG_IGN`] est en soi sûr (aucun code utilisateur), mais l'API reste
847    /// `unsafe` pour une frontière uniforme.
848    ///
849    /// Ce wrapper est réservé aux signaux **non-faute** : installer un handler
850    /// pour `SIGSEGV`/`SIGBUS`/`SIGFPE`/`SIGILL` est le domaine (inerte) de
851    /// [`synchronous_handler`](super::synchronous_handler) (ADR-020/ADR-064).
852    ///
853    /// # Errors
854    ///
855    /// - [`Errno::EINVAL`] : `signal` non-catchable (`SIGKILL`/`SIGSTOP`).
856    pub unsafe fn install(
857        signal: Signal,
858        handler_sa: usize,
859        flags: SigActionFlags,
860        mask: SignalMask,
861    ) -> Result<PreviousDisposition, Errno> {
862        let mut new_sa = KernelSigaction::zeroed();
863        // `usize → u64` : Air ne cible que des arches 64 bits (ADR-014) — même
864        // idiome que `synchronous_handler::install_fatal_handler`
865        // (`handler as usize as u64`), lossless, sans conversion faillible.
866        new_sa.sa_handler = handler_sa as u64;
867        new_sa.sa_mask = mask.bits();
868
869        // Drapeaux : ceux de l'appelant, plus `SA_RESTORER` sur x86_64 (le noyau
870        // a besoin d'un trampoline de retour ; sans libc, Air le fournit).
871        let kernel_flags = flags.bits();
872        #[cfg(target_arch = "x86_64")]
873        {
874            new_sa.sa_flags = kernel_flags | SA_RESTORER;
875            // Adresse du trampoline `rt_sigreturn` (fn item → usize → u64,
876            // 64 bits : lossless).
877            new_sa.sa_restorer = super::__air_rt_sigreturn_trampoline as *const () as usize as u64;
878        }
879        #[cfg(target_arch = "aarch64")]
880        {
881            new_sa.sa_flags = kernel_flags;
882        }
883
884        let mut previous = PreviousDisposition::zeroed();
885        let new_ptr: *const KernelSigaction = &new_sa;
886        let old_ptr: *mut u8 = previous.as_bytes_mut().as_mut_ptr();
887
888        // SAFETY: rt_sigaction(2). Le kernel lit `new_sa` (KernelSigaction valide,
889        // locale) et écrit jusqu'à 32 octets à `old_ptr` (capacité de
890        // `PreviousDisposition`). sigsetsize=8 = taille kernel `sigset_t`. Le
891        // handler éventuel respecte l'async-signal-safety par contrat `unsafe`.
892        let ret = unsafe {
893            raw_syscall_rt_sigaction(
894                signal.as_raw(),
895                new_ptr as u64,
896                old_ptr as u64,
897                SIGSET_SIZE_U64,
898            )
899        };
900        if ret < 0 {
901            return Err(errno_from_negative_syscall_ret(ret));
902        }
903        Ok(previous)
904    }
905
906    /// Restaure la disposition `previous` pour le signal `signal`.
907    ///
908    /// # Safety
909    ///
910    /// `previous` doit provenir d'un [`install`] précédent sur le **même**
911    /// `signal`. Restaurer une disposition d'un autre signal est un comportement
912    /// indéfini côté noyau.
913    ///
914    /// # Errors
915    ///
916    /// - [`Errno::EINVAL`] : `signal` non-catchable (ne se produit pas si
917    ///   `previous` vient d'un `install` réussi sur ce signal).
918    pub unsafe fn restore(signal: Signal, previous: PreviousDisposition) -> Result<(), Errno> {
919        let prev_ptr: *const u8 = previous.as_bytes().as_ptr();
920        // SAFETY: rt_sigaction(2). Le kernel lit jusqu'à 32 octets à `prev_ptr`
921        // (KernelSigaction ≤ 32 octets). Pas d'écriture (oldset=NULL).
922        let ret = unsafe {
923            raw_syscall_rt_sigaction(signal.as_raw(), prev_ptr as u64, 0_u64, SIGSET_SIZE_U64)
924        };
925        if ret < 0 {
926            return Err(errno_from_negative_syscall_ret(ret));
927        }
928        Ok(())
929    }
930
931    /// Représentation interne du `struct sigaction` kernel (cf.
932    /// [`synchronous_handler`](super::synchronous_handler) — même layout ABI,
933    /// dupliqué pour ne PAS toucher au sous-module des fautes).
934    ///
935    /// - offset 0  : `sa_handler` (8) ; offset 8 : `sa_flags` (8) ;
936    /// - x86_64 : offset 16 `sa_restorer` (8), offset 24 `sa_mask` (8) — 32 octets ;
937    /// - aarch64 : offset 16 `sa_mask` (8), **pas** de `sa_restorer` — 24 octets.
938    #[repr(C)]
939    #[cfg(target_arch = "x86_64")]
940    struct KernelSigaction {
941        sa_handler: u64,
942        sa_flags: u64,
943        sa_restorer: u64,
944        sa_mask: u64,
945    }
946
947    #[cfg(target_arch = "x86_64")]
948    impl KernelSigaction {
949        const fn zeroed() -> Self {
950            Self {
951                sa_handler: 0,
952                sa_flags: 0,
953                sa_restorer: 0,
954                sa_mask: 0,
955            }
956        }
957    }
958
959    #[repr(C)]
960    #[cfg(target_arch = "aarch64")]
961    struct KernelSigaction {
962        sa_handler: u64,
963        sa_flags: u64,
964        sa_mask: u64,
965    }
966
967    #[cfg(target_arch = "aarch64")]
968    impl KernelSigaction {
969        const fn zeroed() -> Self {
970            Self {
971                sa_handler: 0,
972                sa_flags: 0,
973                sa_mask: 0,
974            }
975        }
976    }
977
978    /// `KernelSigaction` tient dans le buffer 32 octets de `PreviousDisposition`.
979    const _: () = {
980        assert!(core::mem::size_of::<KernelSigaction>() <= 32);
981    };
982
983    #[cfg(target_arch = "x86_64")]
984    #[inline]
985    unsafe fn raw_syscall_rt_sigaction(
986        signum: i32,
987        new_sa: u64,
988        old_sa: u64,
989        sigsetsize: u64,
990    ) -> i64 {
991        let ret: i64;
992        // SAFETY: SYS_rt_sigaction (x86_64 = 13). Le kernel lit `new_sa`
993        // (KernelSigaction, 32 octets), écrit `old_sa` (32 octets) si non-null.
994        unsafe {
995            core::arch::asm!(
996                "syscall",
997                in("rax") 13_i64,
998                in("rdi") i64::from(signum),
999                in("rsi") new_sa,
1000                in("rdx") old_sa,
1001                in("r10") sigsetsize,
1002                lateout("rax") ret,
1003                lateout("rcx") _,
1004                lateout("r11") _,
1005                options(nostack, preserves_flags),
1006            );
1007        }
1008        ret
1009    }
1010
1011    #[cfg(target_arch = "aarch64")]
1012    #[inline]
1013    unsafe fn raw_syscall_rt_sigaction(
1014        signum: i32,
1015        new_sa: u64,
1016        old_sa: u64,
1017        sigsetsize: u64,
1018    ) -> i64 {
1019        let ret: i64;
1020        // SAFETY: SYS_rt_sigaction (aarch64 = 134).
1021        unsafe {
1022            core::arch::asm!(
1023                "svc 0",
1024                in("x8") 134_i64,
1025                inout("x0") i64::from(signum) => ret,
1026                in("x1") new_sa,
1027                in("x2") old_sa,
1028                in("x3") sigsetsize,
1029                options(nostack, preserves_flags),
1030            );
1031        }
1032        ret
1033    }
1034}
1035
1036// ─────────────────────────────────────────────────────────────────────────
1037// asm! wrappers x86_64 / aarch64.
1038// ─────────────────────────────────────────────────────────────────────────
1039
1040#[cfg(target_arch = "x86_64")]
1041#[inline]
1042unsafe fn raw_syscall_signalfd4(fd: i32, mask: u64, sigsetsize: u64, flags: i32) -> i64 {
1043    let ret: i64;
1044    // SAFETY: SYS_signalfd4 (x86_64 = 289). Le kernel lit `*mask` (8
1045    // octets) ; pas d'écriture en mémoire utilisateur (`readonly` OK).
1046    unsafe {
1047        core::arch::asm!(
1048            "syscall",
1049            in("rax") 289_i64,
1050            in("rdi") i64::from(fd),
1051            in("rsi") mask,
1052            in("rdx") sigsetsize,
1053            in("r10") i64::from(flags),
1054            lateout("rax") ret,
1055            lateout("rcx") _,
1056            lateout("r11") _,
1057            options(nostack, preserves_flags, readonly),
1058        );
1059    }
1060    ret
1061}
1062
1063#[cfg(target_arch = "aarch64")]
1064#[inline]
1065unsafe fn raw_syscall_signalfd4(fd: i32, mask: u64, sigsetsize: u64, flags: i32) -> i64 {
1066    let ret: i64;
1067    // SAFETY: SYS_signalfd4 (aarch64 = 74).
1068    unsafe {
1069        core::arch::asm!(
1070            "svc 0",
1071            in("x8") 74_i64,
1072            inout("x0") i64::from(fd) => ret,
1073            in("x1") mask,
1074            in("x2") sigsetsize,
1075            in("x3") i64::from(flags),
1076            options(nostack, preserves_flags, readonly),
1077        );
1078    }
1079    ret
1080}
1081
1082#[cfg(target_arch = "x86_64")]
1083#[inline]
1084unsafe fn raw_syscall_rt_sigprocmask(how: i32, set: u64, oldset: u64, sigsetsize: u64) -> i64 {
1085    let ret: i64;
1086    // SAFETY: SYS_rt_sigprocmask (x86_64 = 14). Le kernel lit `*set` si
1087    // non-null (8 octets) ; écrit `*oldset` si non-null (8 octets). Pas
1088    // de `readonly` (écriture sur oldset).
1089    unsafe {
1090        core::arch::asm!(
1091            "syscall",
1092            in("rax") 14_i64,
1093            in("rdi") i64::from(how),
1094            in("rsi") set,
1095            in("rdx") oldset,
1096            in("r10") sigsetsize,
1097            lateout("rax") ret,
1098            lateout("rcx") _,
1099            lateout("r11") _,
1100            options(nostack, preserves_flags),
1101        );
1102    }
1103    ret
1104}
1105
1106#[cfg(target_arch = "aarch64")]
1107#[inline]
1108unsafe fn raw_syscall_rt_sigprocmask(how: i32, set: u64, oldset: u64, sigsetsize: u64) -> i64 {
1109    let ret: i64;
1110    // SAFETY: SYS_rt_sigprocmask (aarch64 = 135).
1111    unsafe {
1112        core::arch::asm!(
1113            "svc 0",
1114            in("x8") 135_i64,
1115            inout("x0") i64::from(how) => ret,
1116            in("x1") set,
1117            in("x2") oldset,
1118            in("x3") sigsetsize,
1119            options(nostack, preserves_flags),
1120        );
1121    }
1122    ret
1123}
1124
1125#[cfg(target_arch = "x86_64")]
1126#[inline]
1127unsafe fn raw_syscall_sigaltstack(new: u64, old: u64) -> i64 {
1128    let ret: i64;
1129    // SAFETY: SYS_sigaltstack (x86_64 = 131). Le kernel **lit** `*new` (24 octets) si
1130    // `new != 0` et **écrit** `*old` (24 octets) si `old != 0` — pas de `readonly`.
1131    unsafe {
1132        core::arch::asm!(
1133            "syscall",
1134            in("rax") 131_i64,
1135            in("rdi") new,
1136            in("rsi") old,
1137            lateout("rax") ret,
1138            lateout("rcx") _,
1139            lateout("r11") _,
1140            options(nostack, preserves_flags),
1141        );
1142    }
1143    ret
1144}
1145
1146#[cfg(target_arch = "aarch64")]
1147#[inline]
1148unsafe fn raw_syscall_sigaltstack(new: u64, old: u64) -> i64 {
1149    let ret: i64;
1150    // SAFETY: SYS_sigaltstack (aarch64 = 132). Mêmes contrats mémoire que x86_64.
1151    unsafe {
1152        core::arch::asm!(
1153            "svc 0",
1154            in("x8") 132_i64,
1155            inout("x0") new => ret,
1156            in("x1") old,
1157            options(nostack, preserves_flags),
1158        );
1159    }
1160    ret
1161}
1162
1163#[cfg(target_arch = "x86_64")]
1164#[inline]
1165unsafe fn raw_syscall_rt_sigpending(set: u64, sigsetsize: u64) -> i64 {
1166    let ret: i64;
1167    // SAFETY: SYS_rt_sigpending (x86_64 = 127). Le kernel **écrit** 8 octets à
1168    // `set` (sigset_t) ; pas de `readonly`.
1169    unsafe {
1170        core::arch::asm!(
1171            "syscall",
1172            in("rax") 127_i64,
1173            in("rdi") set,
1174            in("rsi") sigsetsize,
1175            lateout("rax") ret,
1176            lateout("rcx") _,
1177            lateout("r11") _,
1178            options(nostack, preserves_flags),
1179        );
1180    }
1181    ret
1182}
1183
1184#[cfg(target_arch = "aarch64")]
1185#[inline]
1186unsafe fn raw_syscall_rt_sigpending(set: u64, sigsetsize: u64) -> i64 {
1187    let ret: i64;
1188    // SAFETY: SYS_rt_sigpending (aarch64 = 136).
1189    unsafe {
1190        core::arch::asm!(
1191            "svc 0",
1192            in("x8") 136_i64,
1193            inout("x0") set => ret,
1194            in("x1") sigsetsize,
1195            options(nostack, preserves_flags),
1196        );
1197    }
1198    ret
1199}
1200
1201#[cfg(target_arch = "x86_64")]
1202#[inline]
1203unsafe fn raw_syscall_kill(pid: i32, sig: i32) -> i64 {
1204    let ret: i64;
1205    // SAFETY: SYS_kill (x86_64 = 62). Pas d'accès mémoire utilisateur.
1206    unsafe {
1207        core::arch::asm!(
1208            "syscall",
1209            in("rax") 62_i64,
1210            in("rdi") i64::from(pid),
1211            in("rsi") i64::from(sig),
1212            lateout("rax") ret,
1213            lateout("rcx") _,
1214            lateout("r11") _,
1215            options(nostack, preserves_flags, readonly),
1216        );
1217    }
1218    ret
1219}
1220
1221#[cfg(target_arch = "aarch64")]
1222#[inline]
1223unsafe fn raw_syscall_kill(pid: i32, sig: i32) -> i64 {
1224    let ret: i64;
1225    // SAFETY: SYS_kill (aarch64 = 129).
1226    unsafe {
1227        core::arch::asm!(
1228            "svc 0",
1229            in("x8") 129_i64,
1230            inout("x0") i64::from(pid) => ret,
1231            in("x1") i64::from(sig),
1232            options(nostack, preserves_flags, readonly),
1233        );
1234    }
1235    ret
1236}
1237
1238#[cfg(target_arch = "x86_64")]
1239#[inline]
1240unsafe fn raw_syscall_tgkill(tgid: i32, tid: i32, sig: i32) -> i64 {
1241    let ret: i64;
1242    // SAFETY: SYS_tgkill (x86_64 = 234). Pas d'accès mémoire utilisateur.
1243    unsafe {
1244        core::arch::asm!(
1245            "syscall",
1246            in("rax") 234_i64,
1247            in("rdi") i64::from(tgid),
1248            in("rsi") i64::from(tid),
1249            in("rdx") i64::from(sig),
1250            lateout("rax") ret,
1251            lateout("rcx") _,
1252            lateout("r11") _,
1253            options(nostack, preserves_flags, readonly),
1254        );
1255    }
1256    ret
1257}
1258
1259#[cfg(target_arch = "aarch64")]
1260#[inline]
1261unsafe fn raw_syscall_tgkill(tgid: i32, tid: i32, sig: i32) -> i64 {
1262    let ret: i64;
1263    // SAFETY: SYS_tgkill (aarch64 = 131).
1264    unsafe {
1265        core::arch::asm!(
1266            "svc 0",
1267            in("x8") 131_i64,
1268            inout("x0") i64::from(tgid) => ret,
1269            in("x1") i64::from(tid),
1270            in("x2") i64::from(sig),
1271            options(nostack, preserves_flags, readonly),
1272        );
1273    }
1274    ret
1275}
1276
1277#[cfg(target_arch = "x86_64")]
1278#[inline]
1279unsafe fn raw_syscall_rt_sigqueueinfo(pid: i32, sig: i32, info: u64) -> i64 {
1280    let ret: i64;
1281    // SAFETY: SYS_rt_sigqueueinfo (x86_64 = 129). Le kernel lit 128
1282    // octets à `info` (siginfo_t). Pas d'écriture user (`readonly`).
1283    unsafe {
1284        core::arch::asm!(
1285            "syscall",
1286            in("rax") 129_i64,
1287            in("rdi") i64::from(pid),
1288            in("rsi") i64::from(sig),
1289            in("rdx") info,
1290            lateout("rax") ret,
1291            lateout("rcx") _,
1292            lateout("r11") _,
1293            options(nostack, preserves_flags, readonly),
1294        );
1295    }
1296    ret
1297}
1298
1299#[cfg(target_arch = "aarch64")]
1300#[inline]
1301unsafe fn raw_syscall_rt_sigqueueinfo(pid: i32, sig: i32, info: u64) -> i64 {
1302    let ret: i64;
1303    // SAFETY: SYS_rt_sigqueueinfo (aarch64 = 138).
1304    unsafe {
1305        core::arch::asm!(
1306            "svc 0",
1307            in("x8") 138_i64,
1308            inout("x0") i64::from(pid) => ret,
1309            in("x1") i64::from(sig),
1310            in("x2") info,
1311            options(nostack, preserves_flags, readonly),
1312        );
1313    }
1314    ret
1315}
1316
1317#[cfg(target_arch = "x86_64")]
1318#[inline]
1319unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: u64) -> i64 {
1320    let ret: i64;
1321    // SAFETY: SYS_read (x86_64 = 0). Le kernel écrit jusqu'à `count`
1322    // octets à `buffer`. Pour signalfd, exactement 128 octets pour un
1323    // événement complet.
1324    unsafe {
1325        core::arch::asm!(
1326            "syscall",
1327            in("rax") 0_i64,
1328            in("rdi") i64::from(fd),
1329            in("rsi") buffer,
1330            in("rdx") count,
1331            lateout("rax") ret,
1332            lateout("rcx") _,
1333            lateout("r11") _,
1334            options(nostack, preserves_flags),
1335        );
1336    }
1337    ret
1338}
1339
1340#[cfg(target_arch = "aarch64")]
1341#[inline]
1342unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: u64) -> i64 {
1343    let ret: i64;
1344    // SAFETY: SYS_read (aarch64 = 63).
1345    unsafe {
1346        core::arch::asm!(
1347            "svc 0",
1348            in("x8") 63_i64,
1349            inout("x0") i64::from(fd) => ret,
1350            in("x1") buffer,
1351            in("x2") count,
1352            options(nostack, preserves_flags),
1353        );
1354    }
1355    ret
1356}
1357
1358// ─────────────────────────────────────────────────────────────────────────
1359// Helper de conversion errno (réplique de celui de process.rs ; un
1360// futur refactor pourra centraliser).
1361// ─────────────────────────────────────────────────────────────────────────
1362
1363fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
1364    debug_assert!(ret < 0 && ret > -4096);
1365    #[allow(clippy::cast_possible_truncation)]
1366    let raw = ret.wrapping_neg() as i32;
1367    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
1368    Errno::from_nonzero(nz)
1369}
1370
1371// ─────────────────────────────────────────────────────────────────────────
1372// Tests.
1373// ─────────────────────────────────────────────────────────────────────────
1374
1375#[cfg(test)]
1376mod tests {
1377    use super::*;
1378
1379    use crate::process::{clone3, getpid, gettid, waitid};
1380    use air_sys_types::signal::async_handler::SigActionFlags;
1381    use air_sys_types::{
1382        CloneArgs, CloneFlags, CloneResult, SignalValue, WaitEvent, WaitOptions, WaitTarget,
1383    };
1384    use core::ffi::{c_int, c_void};
1385    use core::sync::atomic::{AtomicBool, Ordering};
1386    use std::sync::Mutex;
1387
1388    /// Sérialise les tests qui installent un handler **process-wide** : sans ce
1389    /// verrou, deux tests parallèles s'écraseraient mutuellement la disposition
1390    /// du signal (le handler est un état par-process). Empoisonnement toléré (on
1391    /// récupère l'inner) : un panic d'un test précédent ne doit pas bloquer les
1392    /// suivants.
1393    static ASYNC_INSTALL_LOCK: Mutex<()> = Mutex::new(());
1394
1395    /// Drapeau armé par [`async_test_handler`] : prouve que le noyau a **appelé**
1396    /// le handler async (délivrance réelle, ADR-066).
1397    static ASYNC_HANDLER_FIRED: AtomicBool = AtomicBool::new(false);
1398
1399    /// Handler async de test — **async-signal-safe** (un unique store atomique,
1400    /// aucune allocation, aucune I/O). Installé pour `SIGUSR1` et déclenché par
1401    /// auto-`tgkill` (chemin déterministe).
1402    extern "C" fn async_test_handler(_signum: c_int, _info: *mut c_void, _context: *mut c_void) {
1403        ASYNC_HANDLER_FIRED.store(true, Ordering::SeqCst);
1404    }
1405
1406    /// Flush du profil de couverture LLVM de l'enfant forké **avant**
1407    /// `exit_group` (qui court-circuite l'atexit LLVM). Sans ce flush, le
1408    /// corps des wrappers exercés dans l'enfant (install/restore handler,
1409    /// signalfd, kill, rt_sigqueueinfo…) apparaît non couvert.
1410    /// `LLVM_PROFILE_FILE` contient `%p` → un `.profraw` par enfant, fusionné.
1411    /// Aucun verrou I/O dans cette famille → le flush en fin d'enfant suffit.
1412    // Intentionnellement VIDE — **async-signal-safety**. Un enfant forké via
1413    // `clone3` hérite d'un espace d'adressage COPIÉ : si un thread frère tenait le
1414    // lock interne de `malloc` au moment du `clone3`, ce lock est copié VERROUILLÉ
1415    // (son détenteur n'existe pas dans l'enfant) ; `__llvm_profile_write_file`
1416    // **alloue** → le premier `malloc` de l'enfant deadlocke sur ce futex. On NE
1417    // flushe donc PAS dans l'enfant : ses lignes sont des exceptions **CHILD-EXIT**
1418    // (ADR-035), prouvées par le code de sortie observé via `waitid`. NE PAS
1419    // réintroduire de flush ici (flaky deadlock couche 0).
1420    fn flush_child_coverage() {}
1421
1422    /// Sortie d'un enfant forké de test : termine via `exit_group`. NE FLUSHE PLUS
1423    /// le profil (cf. [`flush_child_coverage`], désormais vide pour rester
1424    /// async-signal-safe). Les lignes de l'enfant sont des exceptions CHILD-EXIT.
1425    fn child_exit(status: i32) -> ! {
1426        flush_child_coverage();
1427        crate::process::exit_group(status)
1428    }
1429
1430    /// Guard RAII : capture le mask courant du thread à la construction,
1431    /// le restaure intégralement à la destruction via `SIG_SETMASK`.
1432    ///
1433    /// **Utilisation systématique en TOUTE PREMIÈRE LIGNE** de chaque
1434    /// test qui touche au mask de signaux (sinon contamination du
1435    /// worker thread cargo pour les tests suivants).
1436    struct MaskGuard {
1437        original: SignalMask,
1438    }
1439    impl MaskGuard {
1440        fn new() -> Self {
1441            Self {
1442                original: current_signal_mask().expect("current_signal_mask doit réussir"),
1443            }
1444        }
1445    }
1446    impl Drop for MaskGuard {
1447        fn drop(&mut self) {
1448            // SIG_SETMASK = remplacement TOTAL (jamais SIG_BLOCK/UNBLOCK
1449            // relatif). C'est le seul moyen de restaurer fidèlement le
1450            // mask initial.
1451            let _ = set_signal_mask(&self.original);
1452        }
1453    }
1454
1455    // ── current_signal_mask / set_signal_mask ─────────────────────────
1456
1457    #[test]
1458    fn current_signal_mask_returns_ok() {
1459        let _guard = MaskGuard::new();
1460        let _mask = current_signal_mask().expect("current_signal_mask doit réussir");
1461    }
1462
1463    #[test]
1464    fn mask_block_unblock_round_trip() {
1465        let _guard = MaskGuard::new();
1466        let target = SignalMask::from_signals(&[Signal::SIGUSR1]);
1467
1468        let before = current_signal_mask().expect("read");
1469        assert!(
1470            !before.contains(Signal::SIGUSR1),
1471            "SIGUSR1 ne doit pas être bloqué initialement"
1472        );
1473
1474        let _previous = block_signals(&target).expect("block");
1475        let after_block = current_signal_mask().expect("read");
1476        assert!(
1477            after_block.contains(Signal::SIGUSR1),
1478            "SIGUSR1 doit être bloqué après block_signals"
1479        );
1480
1481        let _previous = unblock_signals(&target).expect("unblock");
1482        let after_unblock = current_signal_mask().expect("read");
1483        assert!(!after_unblock.contains(Signal::SIGUSR1));
1484    }
1485
1486    #[test]
1487    fn set_signal_mask_replaces_total() {
1488        let _guard = MaskGuard::new();
1489        let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1490        let _old = set_signal_mask(&mask).expect("set");
1491        let read_back = current_signal_mask().expect("read");
1492        assert_eq!(read_back.bits(), mask.bits());
1493    }
1494
1495    // ── signalfd + delivery via tgkill (per-thread isolation) ─────────
1496
1497    #[test]
1498    fn signalfd_blocking_create_and_read_via_tgkill_self_tid() {
1499        let _guard = MaskGuard::new();
1500        let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1501        let sfd = signalfd_create_blocking(&mask, SignalFdFlags::empty())
1502            .expect("signalfd_create_blocking");
1503
1504        // tgkill cible CE thread précisément ; pas de routage à un autre
1505        // thread = déterminisme + isolation des workers cargo.
1506        tgkill(getpid(), gettid(), Some(Signal::SIGUSR1)).expect("tgkill");
1507        let info = sfd.read().expect("read signalfd");
1508        assert_eq!(info.signal, Signal::SIGUSR1);
1509    }
1510
1511    #[test]
1512    fn signalfd_update_mask_after_creation() {
1513        let _guard = MaskGuard::new();
1514        let initial = SignalMask::from_signals(&[Signal::SIGUSR1]);
1515        let mut sfd = signalfd_create_blocking(&initial, SignalFdFlags::empty()).expect("create");
1516
1517        let updated = SignalMask::from_signals(&[Signal::SIGUSR1, Signal::SIGCHLD]);
1518        block_signals(&updated).expect("block SIGCHLD too");
1519        sfd.update_mask(&updated).expect("update_mask");
1520        // Pas de delivery test ici — on vérifie juste que le syscall
1521        // update_mask réussit sans erreur. Le read suivant exercerait
1522        // une délivrance, déjà couvert par le test précédent.
1523    }
1524
1525    #[test]
1526    fn signalfd_into_fd_returns_owned_fd() {
1527        let _guard = MaskGuard::new();
1528        let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1529        let sfd = signalfd_create(&mask, SignalFdFlags::empty()).expect("create");
1530        let _owned: OwnedFd = sfd.into_fd();
1531    }
1532
1533    #[test]
1534    fn signalfd_as_fd_borrows() {
1535        let _guard = MaskGuard::new();
1536        let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1537        let sfd = signalfd_create(&mask, SignalFdFlags::empty()).expect("create");
1538        let _borrowed = sfd.as_fd();
1539    }
1540
1541    // ── wait_for_signal ───────────────────────────────────────────────
1542
1543    #[test]
1544    fn wait_for_signal_bloquant_recoit_sigusr1() {
1545        let _guard = MaskGuard::new();
1546        let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1547        block_signals(&mask).expect("block");
1548        tgkill(getpid(), gettid(), Some(Signal::SIGUSR1)).expect("tgkill");
1549        let info = wait_for_signal(&mask, None).expect("wait_for_signal");
1550        assert_eq!(info.signal, Signal::SIGUSR1);
1551    }
1552
1553    #[test]
1554    fn wait_for_signal_with_timeout_returns_einval_not_yet_implemented() {
1555        let _guard = MaskGuard::new();
1556        let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1557        let err = wait_for_signal(&mask, Some(Duration::from_millis(10)))
1558            .expect_err("timeout pas encore supporté");
1559        assert_eq!(err, Errno::EINVAL);
1560    }
1561
1562    // ── kill / tgkill / signal 0 (Option<Signal> None) ────────────────
1563
1564    #[test]
1565    fn kill_signal_none_tests_process_existence() {
1566        // signal=None ⇔ "signal 0" = test d'existence sans envoi.
1567        // Sur self, doit réussir.
1568        kill(getpid(), None).expect("kill self avec signal 0 doit réussir");
1569    }
1570
1571    #[test]
1572    fn kill_on_nonexistent_pid_returns_esrch() {
1573        let nope = Pid::try_from_raw(0x7FFF_FFFE).expect("pid littéral");
1574        let err = kill(nope, None).expect_err("kill PID inexistant");
1575        assert_eq!(err, Errno::ESRCH);
1576    }
1577
1578    #[test]
1579    fn kill_process_group_self_and_nonexistent() {
1580        // `group = None` + `signal = None` : test d'existence du **groupe de l'appelant**
1581        // (aucun signal envoyé) ⇒ succès.
1582        kill_process_group(None, None).expect("groupe de l'appelant existe");
1583        // Groupe **inexistant** (id positif jamais alloué) ⇒ `ESRCH`.
1584        let nope = Pid::try_from_raw(0x7FFF_FFFE).expect("pgid littéral");
1585        assert_eq!(
1586            kill_process_group(Some(nope), None).expect_err("groupe inexistant"),
1587            Errno::ESRCH
1588        );
1589    }
1590
1591    #[test]
1592    #[cfg_attr(miri, ignore = "syscalls non supportés par Miri")]
1593    fn sigaltstack_query_install_disable_and_reject_small() {
1594        // Interroge la pile alternative **actuelle** (`old` seul) — succès (chemin `old`).
1595        let mut current = AltStack::disabled();
1596        sigaltstack(None, Some(&mut current)).expect("interroge la pile alternative");
1597
1598        // Installe une pile alternative de **16 Kio** (> `MINSIGSTKSZ`) et récupère l'ancienne.
1599        let mut buffer = vec![0_u8; 16384];
1600        let stack = AltStack::new(buffer.as_mut_ptr(), buffer.len());
1601        let mut previous = AltStack::disabled();
1602        sigaltstack(Some(&stack), Some(&mut previous)).expect("installe la pile alternative");
1603
1604        // **Désactive** (restaure l'état du thread) AVANT que `buffer` ne soit libéré.
1605        sigaltstack(Some(&AltStack::disabled()), None).expect("désactive la pile alternative");
1606
1607        // Taille **trop petite** (8 octets < `MINSIGSTKSZ`) ⇒ erreur (chemin `Err`) :
1608        // `ENOMEM` sur les kernels récents, `EINVAL` sur les anciens.
1609        let mut tiny = [0_u8; 8];
1610        let small = AltStack::new(tiny.as_mut_ptr(), tiny.len());
1611        let err = sigaltstack(Some(&small), None).expect_err("pile trop petite");
1612        assert!(
1613            err == Errno::ENOMEM || err == Errno::EINVAL,
1614            "attendu ENOMEM/EINVAL, obtenu : {err:?}"
1615        );
1616    }
1617
1618    #[test]
1619    fn tgkill_signal_none_to_self_tid_succeeds() {
1620        // signal=None sur self_tid = test d'existence.
1621        tgkill(getpid(), gettid(), None).expect("tgkill self avec signal 0");
1622    }
1623
1624    #[test]
1625    fn tgkill_on_nonexistent_tid_returns_esrch() {
1626        // Couvre la branche d'erreur (`ret < 0`) de `tgkill` : un tid
1627        // au-delà de `PID_MAX_LIMIT` (~2^22) n'existe pas → ESRCH.
1628        let nope = Tid::try_from_raw(0x7FFF_FFFE).expect("tid littéral");
1629        let err = tgkill(getpid(), nope, None).expect_err("tgkill TID inexistant");
1630        assert_eq!(err, Errno::ESRCH);
1631    }
1632
1633    #[test]
1634    fn signalfd_nonblocking_read_with_no_pending_signal_returns_eagain() {
1635        // Couvre la branche d'erreur (`ret < 0`) de `SignalFd::read` : un
1636        // signalfd NONBLOCK sans signal pendant retourne EAGAIN
1637        // immédiatement (aucun signal n'est bloqué/envoyé ici).
1638        let _guard = MaskGuard::new();
1639        let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1640        let sfd = signalfd_create(&mask, SignalFdFlags::NONBLOCK).expect("create nonblock");
1641        let err = sfd.read().expect_err("aucun signal pendant → EAGAIN");
1642        assert_eq!(err, Errno::EAGAIN);
1643    }
1644
1645    #[test]
1646    fn tgkill_delivers_sigusr1_to_self_tid() {
1647        let _guard = MaskGuard::new();
1648        let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1649        let sfd = signalfd_create_blocking(&mask, SignalFdFlags::empty()).expect("create");
1650        tgkill(getpid(), gettid(), Some(Signal::SIGUSR1)).expect("tgkill");
1651        let info = sfd.read().expect("read");
1652        assert_eq!(info.signal, Signal::SIGUSR1);
1653    }
1654
1655    #[test]
1656    fn kill_in_forked_child_delivers_signal_to_self() {
1657        // `kill(getpid(), sig)` est dangereux en multi-thread (routing
1658        // non contrôlé). En enfant fork'é mono-thread, c'est sûr.
1659        let args = CloneArgs {
1660            flags: CloneFlags::empty(),
1661            exit_signal: Some(Signal::SIGCHLD),
1662            stack: None,
1663            child_tid: None,
1664            parent_tid: None,
1665            tls: None,
1666        };
1667        // SAFETY: fork classique.
1668        let result = unsafe { clone3(&args) }.expect("clone3");
1669        match result {
1670            CloneResult::Child => {
1671                // Enfant : block SIGUSR1, create signalfd, kill(self),
1672                // read, exit avec code 0 si OK.
1673                let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1674                if block_signals(&mask).is_err() {
1675                    child_exit(50);
1676                }
1677                let sfd = match signalfd_create(&mask, SignalFdFlags::empty()) {
1678                    Ok(s) => s,
1679                    Err(_) => child_exit(51),
1680                };
1681                if kill(getpid(), Some(Signal::SIGUSR1)).is_err() {
1682                    child_exit(52);
1683                }
1684                match sfd.read() {
1685                    Ok(info) if info.signal == Signal::SIGUSR1 => child_exit(0),
1686                    Ok(_) => child_exit(53),
1687                    Err(_) => child_exit(54),
1688                }
1689            }
1690            CloneResult::Parent { child_pid, .. } => {
1691                let status =
1692                    waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED).expect("waitid");
1693                let s = status.expect("event");
1694                assert!(
1695                    matches!(s.event, WaitEvent::Exited { code: 0 }),
1696                    "kill in forked child should succeed, event={:?}",
1697                    s.event
1698                );
1699            }
1700        }
1701    }
1702
1703    // ── rt_sigqueueinfo ────────────────────────────────────────────────
1704
1705    #[test]
1706    fn rt_sigqueueinfo_with_integer_value_in_forked_child() {
1707        // `rt_sigqueueinfo` cible un PROCESS (`pid`), pas un thread.
1708        // En multi-thread (cargo test workers parallèles), le kernel
1709        // route le signal vers un thread qui ne le bloque pas → un
1710        // worker collatéral pourrait recevoir SIGUSR1 et terminer le
1711        // process de test. Isolation par fork-enfant mono-thread.
1712        let args = CloneArgs {
1713            flags: CloneFlags::empty(),
1714            exit_signal: Some(Signal::SIGCHLD),
1715            stack: None,
1716            child_tid: None,
1717            parent_tid: None,
1718            tls: None,
1719        };
1720        // SAFETY: fork classique.
1721        let result = unsafe { clone3(&args) }.expect("clone3");
1722        match result {
1723            CloneResult::Child => {
1724                let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1725                if block_signals(&mask).is_err() {
1726                    child_exit(70);
1727                }
1728                let sfd = match signalfd_create(&mask, SignalFdFlags::empty()) {
1729                    Ok(s) => s,
1730                    Err(_) => child_exit(71),
1731                };
1732                let info = SignalInfo::new_queue(SignalValue::Integer(0xABCD));
1733                if rt_sigqueueinfo(getpid(), Signal::SIGUSR1, &info).is_err() {
1734                    child_exit(72);
1735                }
1736                match sfd.read() {
1737                    Ok(e) if e.signal == Signal::SIGUSR1 && e.int == 0xABCD && e.code == -1 => {
1738                        child_exit(0);
1739                    }
1740                    Ok(_) => child_exit(73),
1741                    Err(_) => child_exit(74),
1742                }
1743            }
1744            CloneResult::Parent { child_pid, .. } => {
1745                let status =
1746                    waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED).expect("waitid");
1747                let s = status.expect("event");
1748                assert!(
1749                    matches!(s.event, WaitEvent::Exited { code: 0 }),
1750                    "rt_sigqueueinfo Integer doit aboutir, event={:?}",
1751                    s.event
1752                );
1753            }
1754        }
1755    }
1756
1757    #[test]
1758    fn rt_sigqueueinfo_with_pointer_value_in_forked_child() {
1759        let args = CloneArgs {
1760            flags: CloneFlags::empty(),
1761            exit_signal: Some(Signal::SIGCHLD),
1762            stack: None,
1763            child_tid: None,
1764            parent_tid: None,
1765            tls: None,
1766        };
1767        // SAFETY: fork classique.
1768        let result = unsafe { clone3(&args) }.expect("clone3");
1769        match result {
1770            CloneResult::Child => {
1771                let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
1772                if block_signals(&mask).is_err() {
1773                    child_exit(80);
1774                }
1775                let sfd = match signalfd_create(&mask, SignalFdFlags::empty()) {
1776                    Ok(s) => s,
1777                    Err(_) => child_exit(81),
1778                };
1779                let payload: u64 = 0xCAFE_BABE_DEAD_BEEF;
1780                let info = SignalInfo::new_queue(SignalValue::Pointer(payload));
1781                if rt_sigqueueinfo(getpid(), Signal::SIGUSR1, &info).is_err() {
1782                    child_exit(82);
1783                }
1784                match sfd.read() {
1785                    Ok(e) if e.signal == Signal::SIGUSR1 && e.ptr == payload && e.code == -1 => {
1786                        child_exit(0);
1787                    }
1788                    Ok(_) => child_exit(83),
1789                    Err(_) => child_exit(84),
1790                }
1791            }
1792            CloneResult::Parent { child_pid, .. } => {
1793                let status =
1794                    waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED).expect("waitid");
1795                let s = status.expect("event");
1796                assert!(
1797                    matches!(s.event, WaitEvent::Exited { code: 0 }),
1798                    "rt_sigqueueinfo Pointer doit aboutir, event={:?}",
1799                    s.event
1800                );
1801            }
1802        }
1803    }
1804
1805    #[test]
1806    fn rt_sigqueueinfo_on_nonexistent_pid_returns_esrch() {
1807        let nope = Pid::try_from_raw(0x7FFF_FFFE).expect("pid littéral");
1808        let info = SignalInfo::new_queue(SignalValue::Integer(0));
1809        let err = rt_sigqueueinfo(nope, Signal::SIGUSR1, &info)
1810            .expect_err("rt_sigqueueinfo PID inexistant");
1811        assert_eq!(err, Errno::ESRCH);
1812    }
1813
1814    // ── synchronous_handler ────────────────────────────────────────────
1815
1816    #[test]
1817    fn install_and_restore_fatal_handler_in_forked_child() {
1818        // Les handlers sont PROCESS-WIDE. Pour ne pas contaminer le
1819        // process de test, on installe/restaure dans un enfant fork'é.
1820        use air_sys_types::signal::synchronous_handler::FatalSignal;
1821        use synchronous_handler::noop_handler;
1822
1823        let args = CloneArgs {
1824            flags: CloneFlags::empty(),
1825            exit_signal: Some(Signal::SIGCHLD),
1826            stack: None,
1827            child_tid: None,
1828            parent_tid: None,
1829            tls: None,
1830        };
1831        // SAFETY: fork classique.
1832        let result = unsafe { clone3(&args) }.expect("clone3");
1833        match result {
1834            CloneResult::Child => {
1835                // SAFETY: noop_handler est trivialement async-signal-safe
1836                // (ne fait rien).
1837                let previous = unsafe {
1838                    synchronous_handler::install_fatal_handler(FatalSignal::Segv, noop_handler)
1839                };
1840                let Ok(previous) = previous else {
1841                    child_exit(60);
1842                };
1843                // Restaurer immédiatement (pas de signal déclenché, on
1844                // valide juste la roundtrip d'install/restore).
1845                // SAFETY: previous a été obtenu par install_fatal_handler
1846                // sur le même FatalSignal::Segv.
1847                if unsafe { synchronous_handler::restore_handler(FatalSignal::Segv, previous) }
1848                    .is_err()
1849                {
1850                    child_exit(61);
1851                }
1852                child_exit(0);
1853            }
1854            CloneResult::Parent { child_pid, .. } => {
1855                let status =
1856                    waitid(WaitTarget::Pid(child_pid), WaitOptions::EXITED).expect("waitid");
1857                let s = status.expect("event");
1858                assert!(
1859                    matches!(s.event, WaitEvent::Exited { code: 0 }),
1860                    "install + restore fatal handler doit réussir, event={:?}",
1861                    s.event
1862                );
1863            }
1864        }
1865    }
1866
1867    #[test]
1868    fn noop_handler_is_callable_directly() {
1869        // `noop_handler` n'est jamais invoqué par le kernel dans les tests
1870        // (aucun signal fatal n'est déclenché — hors périmètre). Son corps
1871        // (vide, trivialement async-signal-safe) est couvert en l'appelant
1872        // directement avec des pointeurs nuls qu'il ne déréférence pas.
1873        use synchronous_handler::noop_handler;
1874        // SAFETY: `noop_handler` a un corps vide ; il ne lit ni n'écrit
1875        // aucun des pointeurs passés. Appel trivialement sûr.
1876        unsafe {
1877            noop_handler(0, core::ptr::null_mut(), core::ptr::null_mut());
1878        }
1879    }
1880
1881    // ── errno_from_negative_syscall_ret (réplique locale) ─────────────
1882
1883    #[test]
1884    fn errno_from_negative_syscall_ret_maps_known_codes() {
1885        assert_eq!(errno_from_negative_syscall_ret(-1), Errno::EPERM);
1886        assert_eq!(errno_from_negative_syscall_ret(-3), Errno::ESRCH);
1887        assert_eq!(errno_from_negative_syscall_ret(-4), Errno::EINTR);
1888        assert_eq!(errno_from_negative_syscall_ret(-22), Errno::EINVAL);
1889    }
1890
1891    // Les deux tests suivants couvrent les deux opérandes du
1892    // `debug_assert!(ret < 0 && ret > -4096)` : chacun force un côté faux
1893    // (`ret == 0` viole `ret < 0` ; `ret == -5000` viole `ret > -4096`).
1894    #[test]
1895    #[should_panic(expected = "ret < 0 && ret > -4096")]
1896    fn errno_from_negative_syscall_ret_panics_on_non_negative() {
1897        let _ = errno_from_negative_syscall_ret(0);
1898    }
1899
1900    #[test]
1901    #[should_panic(expected = "ret < 0 && ret > -4096")]
1902    fn errno_from_negative_syscall_ret_panics_below_errno_range() {
1903        let _ = errno_from_negative_syscall_ret(-5000);
1904    }
1905
1906    // ── parse_signalfd_siginfo : unit-tests sur buffer synthétique ────
1907
1908    #[test]
1909    fn parse_signalfd_siginfo_decodes_sigusr1_event() {
1910        let mut buffer = [0_u8; 128];
1911        // ssi_signo = SIGUSR1 = 10 à offset 0.
1912        buffer[0..4].copy_from_slice(&10_u32.to_ne_bytes());
1913        // ssi_pid = 1234 à offset 12.
1914        buffer[12..16].copy_from_slice(&1234_u32.to_ne_bytes());
1915        // ssi_uid = 1000 à offset 16.
1916        buffer[16..20].copy_from_slice(&1000_u32.to_ne_bytes());
1917        // ssi_int = 0xABCD à offset 44.
1918        buffer[44..48].copy_from_slice(&0xABCD_i32.to_ne_bytes());
1919
1920        let info = parse_signalfd_siginfo(&buffer);
1921        assert_eq!(info.signal, Signal::SIGUSR1);
1922        assert_eq!(info.pid.map(|p| p.as_raw()), Some(1234));
1923        assert_eq!(info.uid, 1000);
1924        assert_eq!(info.int, 0xABCD);
1925    }
1926
1927    #[test]
1928    fn parse_signalfd_siginfo_kernel_origin_pid_is_none() {
1929        let mut buffer = [0_u8; 128];
1930        buffer[0..4].copy_from_slice(&10_u32.to_ne_bytes()); // SIGUSR1
1931        // ssi_pid = 0 (origine kernel) → pid = None (convention 1 ADR-021).
1932        let info = parse_signalfd_siginfo(&buffer);
1933        assert!(info.pid.is_none());
1934    }
1935
1936    // ── async_handler (ADR-066) : install/restore + délivrance async réelle ───
1937
1938    #[test]
1939    fn async_handler_install_fires_and_restores_in_thread() {
1940        // Sérialisé (disposition process-wide) ; chemin 100 % déterministe et
1941        // host-couvert : install → auto-`tgkill` → handler exécuté → restore.
1942        let _serialize = ASYNC_INSTALL_LOCK
1943            .lock()
1944            .unwrap_or_else(std::sync::PoisonError::into_inner);
1945        let _mask_guard = MaskGuard::new();
1946        // SIGUSR1 doit être **débloqué** dans ce thread : sinon le signal reste
1947        // pendant au lieu d'invoquer le handler.
1948        unblock_signals(&SignalMask::from_signals(&[Signal::SIGUSR1])).expect("unblock SIGUSR1");
1949        ASYNC_HANDLER_FIRED.store(false, Ordering::SeqCst);
1950
1951        let handler: extern "C" fn(c_int, *mut c_void, *mut c_void) = async_test_handler;
1952        let handler_sa = handler as usize;
1953        // SAFETY: `async_test_handler` est async-signal-safe (un seul store
1954        // atomique). SIGUSR1 est un signal non-faute gérable.
1955        let previous = unsafe {
1956            async_handler::install(
1957                Signal::SIGUSR1,
1958                handler_sa,
1959                SigActionFlags::SIGINFO,
1960                SignalMask::empty(),
1961            )
1962        }
1963        .expect("install async handler");
1964
1965        // Auto-délivrance ciblée sur CE thread (déterministe, pas de contamination
1966        // d'un worker frère). Au retour de `tgkill`, le signal débloqué a déjà été
1967        // délivré → le handler a tourné **et** est revenu (sur x86_64, via le
1968        // trampoline `rt_sigreturn` d'Air : atteindre l'assert prouve le retour).
1969        tgkill(getpid(), gettid(), Some(Signal::SIGUSR1)).expect("tgkill self");
1970        assert!(
1971            ASYNC_HANDLER_FIRED.load(Ordering::SeqCst),
1972            "le handler async non-faute doit être exécuté par le noyau"
1973        );
1974
1975        // SAFETY: `previous` provient de l'`install` ci-dessus, sur le même signal.
1976        unsafe { async_handler::restore(Signal::SIGUSR1, previous) }.expect("restore");
1977    }
1978
1979    #[test]
1980    fn async_handler_install_ign_then_dfl_round_trip() {
1981        // Couvre les valeurs `sa_handler` SIG_IGN(1)/SIG_DFL(0) et la restauration.
1982        let _serialize = ASYNC_INSTALL_LOCK
1983            .lock()
1984            .unwrap_or_else(std::sync::PoisonError::into_inner);
1985        // SAFETY: SIG_IGN/SIG_DFL sont des constantes noyau — aucun code
1986        // utilisateur, trivialement sûres.
1987        let prev_original = unsafe {
1988            async_handler::install(
1989                Signal::SIGUSR1,
1990                async_handler::SIG_IGN,
1991                SigActionFlags::empty(),
1992                SignalMask::empty(),
1993            )
1994        }
1995        .expect("install SIG_IGN");
1996        // SAFETY: idem — SIG_DFL.
1997        let prev_ign = unsafe {
1998            async_handler::install(
1999                Signal::SIGUSR1,
2000                async_handler::SIG_DFL,
2001                SigActionFlags::empty(),
2002                SignalMask::empty(),
2003            )
2004        }
2005        .expect("install SIG_DFL");
2006        // Restaure en pile inverse : SIG_IGN puis la disposition d'origine.
2007        // SAFETY: chaque `previous` vient d'un `install` sur SIGUSR1.
2008        unsafe { async_handler::restore(Signal::SIGUSR1, prev_ign) }.expect("restore SIG_IGN");
2009        unsafe { async_handler::restore(Signal::SIGUSR1, prev_original) }.expect("restore origin");
2010    }
2011
2012    #[test]
2013    fn async_handler_install_on_sigkill_returns_einval() {
2014        // Couvre le bras d'erreur (`ret < 0`) d'`install` : `SIGKILL` n'est pas
2015        // catchable → le noyau rejette `rt_sigaction` avec EINVAL, sans changer
2016        // aucune disposition (sûr sans sérialisation).
2017        // SAFETY: SIG_DFL — aucun handler utilisateur.
2018        let err = unsafe {
2019            async_handler::install(
2020                Signal::SIGKILL,
2021                async_handler::SIG_DFL,
2022                SigActionFlags::empty(),
2023                SignalMask::empty(),
2024            )
2025        }
2026        .expect_err("SIGKILL non catchable");
2027        assert_eq!(err, Errno::EINVAL);
2028    }
2029
2030    // ── rt_sigpending (ADR-066) ────────────────────────────────────────
2031
2032    #[test]
2033    fn rt_sigpending_reports_blocked_pending_signal() {
2034        let _mask_guard = MaskGuard::new();
2035        let mask = SignalMask::from_signals(&[Signal::SIGUSR1]);
2036        // Bloque SIGUSR1 + crée un signalfd (pour drainer proprement à la fin).
2037        let sfd = signalfd_create_blocking(&mask, SignalFdFlags::empty()).expect("signalfd");
2038
2039        let before = rt_sigpending().expect("rt_sigpending initial");
2040        assert!(
2041            !before.contains(Signal::SIGUSR1),
2042            "aucun SIGUSR1 pendant au départ"
2043        );
2044
2045        tgkill(getpid(), gettid(), Some(Signal::SIGUSR1)).expect("tgkill self");
2046        let after = rt_sigpending().expect("rt_sigpending après tgkill");
2047        assert!(
2048            after.contains(Signal::SIGUSR1),
2049            "SIGUSR1 bloqué et généré doit apparaître pendant"
2050        );
2051
2052        // Draine le signal pendant AVANT que `MaskGuard` ne restaure le masque :
2053        // sinon débloquer SIGUSR1 le délivrerait avec l'action par défaut
2054        // (terminate), tuant le process de test.
2055        let info = sfd.read().expect("drain signalfd");
2056        assert_eq!(info.signal, Signal::SIGUSR1);
2057    }
2058}