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// Liaison **`.weak`** (et non `.globl`) : sur la cible `*-unknown-linux-air`
774// (std sur PAL safe, `-Z build-std`), `air-sys-syscall` est compilé **deux fois**
775// dans un même exécutable — une instance tirée DANS `std` (le PAL couche 1 via
776// `rustc-dep-of-std`) et une instance de l'arbre applicatif (ex. `air-account` →
777// `air-filesystem` → `air-sys-syscall`). Un symbole à **nom fixe** (`#[no_mangle]`/
778// `global_asm`) serait alors **défini deux fois** ⇒ `rust-lld: duplicate symbol`.
779// `.weak` autorise la **coalescence** : les deux définitions étant **identiques**
780// (même corps asm), l'éditeur de liens en garde une, sans ambiguïté. Sur les cibles
781// mono-instance (`*-linux-gnu`, barrière/CI) il n'y a **qu'une** définition ⇒
782// comportement **inchangé** (un symbole faible référencé n'est jamais éliminé).
783#[cfg(target_arch = "x86_64")]
784core::arch::global_asm!(
785    ".p2align 4",
786    ".weak __air_rt_sigreturn_trampoline",
787    ".hidden __air_rt_sigreturn_trampoline",
788    "__air_rt_sigreturn_trampoline:",
789    "mov rax, 15", // __NR_rt_sigreturn (x86_64)
790    "syscall",
791);
792
793#[cfg(target_arch = "x86_64")]
794unsafe extern "C" {
795    /// Symbole du trampoline `rt_sigreturn` défini par le `global_asm!`
796    /// ci-dessus. Jamais appelé depuis Rust — seule son **adresse** est posée
797    /// dans `sa_restorer`.
798    fn __air_rt_sigreturn_trampoline();
799}
800
801pub mod async_handler {
802    //! Wrappers `rt_sigaction` pour les signaux **non-faute** — handler
803    //! **réellement installé** (cf. ADR-064 : délivrance async réelle ; ADR-066 :
804    //! descellement additif couche 0).
805    //!
806    //! Contrairement à [`synchronous_handler`](super::synchronous_handler)
807    //! (restreint aux 4 fautes, handler **inerte** — le noyau force l'action par
808    //! défaut), ce sous-module installe un `rt_sigaction` **effectif** : le noyau
809    //! détourne le thread vers le handler C à la délivrance d'un signal
810    //! **gérable**. C'est la **fondation dual-face** que la libc (`sigaction`) et
811    //! le PAL consomment via la couche 1 `air-signal`.
812    //!
813    //! **API entièrement `unsafe`** : l'appelant garantit que le handler est
814    //! **async-signal-safe** (`man 7 signal-safety`) — précondition invérifiable
815    //! par le compilateur, portée par l'appelant (identique à
816    //! `synchronous_handler`).
817
818    use air_sys_types::Errno;
819    use air_sys_types::signal::async_handler::{PreviousDisposition, SigActionFlags};
820    use air_sys_types::{Signal, SignalMask};
821
822    use super::{SIGSET_SIZE_U64, errno_from_negative_syscall_ret};
823
824    /// `SA_RESTORER` (`0x0400_0000`) — **x86_64 uniquement**. Signale au noyau que
825    /// `sa_restorer` porte l'adresse du trampoline de retour de handler.
826    #[cfg(target_arch = "x86_64")]
827    const SA_RESTORER: u64 = 0x0400_0000;
828
829    /// Valeur `sa_handler` = `SIG_DFL` (action par défaut du noyau).
830    pub const SIG_DFL: usize = 0;
831    /// Valeur `sa_handler` = `SIG_IGN` (signal ignoré). **Sûr** (constante noyau,
832    /// aucun code utilisateur) — cf. ADR-064 §2.
833    pub const SIG_IGN: usize = 1;
834
835    /// Installe une disposition pour le signal **non-faute** `signal` via
836    /// `rt_sigaction(2)`, et rend la disposition **précédente** (pour
837    /// [`restore`]).
838    ///
839    /// - `handler_sa` : valeur brute de `sa_handler` — [`SIG_DFL`] (`0`),
840    ///   [`SIG_IGN`] (`1`), ou l'**adresse** d'une fonction handler
841    ///   `extern "C"` (cast `usize`). C'est la couche 1 (`air-signal`) qui
842    ///   fournit le type de handler ; la couche 0 reste agnostique (adresse
843    ///   brute).
844    /// - `flags` : [`SigActionFlags`] (`SA_SIGINFO`/`SA_RESTART`/…). Sur x86_64,
845    ///   le wrapper **ajoute** `SA_RESTORER` et pose `sa_restorer` sur le
846    ///   trampoline `rt_sigreturn` d'Air (obligatoire sans libc). Sur aarch64, le
847    ///   retour passe par le VDSO — aucun `sa_restorer`.
848    /// - `mask` : signaux **additionnels** bloqués pendant l'exécution du handler.
849    ///
850    /// # Safety
851    ///
852    /// Si `handler_sa` désigne une fonction (≠ [`SIG_DFL`]/[`SIG_IGN`]), l'appelant
853    /// doit garantir qu'elle est **async-signal-safe** (`man 7 signal-safety`) :
854    /// pas de `malloc`, pas de verrou, pas d'I/O bufferisée, etc. Le noyau peut
855    /// l'invoquer à un point **arbitraire** du programme. Poser [`SIG_DFL`] ou
856    /// [`SIG_IGN`] est en soi sûr (aucun code utilisateur), mais l'API reste
857    /// `unsafe` pour une frontière uniforme.
858    ///
859    /// Ce wrapper est réservé aux signaux **non-faute** : installer un handler
860    /// pour `SIGSEGV`/`SIGBUS`/`SIGFPE`/`SIGILL` est le domaine (inerte) de
861    /// [`synchronous_handler`](super::synchronous_handler) (ADR-020/ADR-064).
862    ///
863    /// # Errors
864    ///
865    /// - [`Errno::EINVAL`] : `signal` non-catchable (`SIGKILL`/`SIGSTOP`).
866    pub unsafe fn install(
867        signal: Signal,
868        handler_sa: usize,
869        flags: SigActionFlags,
870        mask: SignalMask,
871    ) -> Result<PreviousDisposition, Errno> {
872        let mut new_sa = KernelSigaction::zeroed();
873        // `usize → u64` : Air ne cible que des arches 64 bits (ADR-014) — même
874        // idiome que `synchronous_handler::install_fatal_handler`
875        // (`handler as usize as u64`), lossless, sans conversion faillible.
876        new_sa.sa_handler = handler_sa as u64;
877        new_sa.sa_mask = mask.bits();
878
879        // Drapeaux : ceux de l'appelant, plus `SA_RESTORER` sur x86_64 (le noyau
880        // a besoin d'un trampoline de retour ; sans libc, Air le fournit).
881        let kernel_flags = flags.bits();
882        #[cfg(target_arch = "x86_64")]
883        {
884            new_sa.sa_flags = kernel_flags | SA_RESTORER;
885            // Adresse du trampoline `rt_sigreturn` (fn item → usize → u64,
886            // 64 bits : lossless).
887            new_sa.sa_restorer = super::__air_rt_sigreturn_trampoline as *const () as usize as u64;
888        }
889        #[cfg(target_arch = "aarch64")]
890        {
891            new_sa.sa_flags = kernel_flags;
892        }
893
894        let mut previous = PreviousDisposition::zeroed();
895        let new_ptr: *const KernelSigaction = &new_sa;
896        let old_ptr: *mut u8 = previous.as_bytes_mut().as_mut_ptr();
897
898        // SAFETY: rt_sigaction(2). Le kernel lit `new_sa` (KernelSigaction valide,
899        // locale) et écrit jusqu'à 32 octets à `old_ptr` (capacité de
900        // `PreviousDisposition`). sigsetsize=8 = taille kernel `sigset_t`. Le
901        // handler éventuel respecte l'async-signal-safety par contrat `unsafe`.
902        let ret = unsafe {
903            raw_syscall_rt_sigaction(
904                signal.as_raw(),
905                new_ptr as u64,
906                old_ptr as u64,
907                SIGSET_SIZE_U64,
908            )
909        };
910        if ret < 0 {
911            return Err(errno_from_negative_syscall_ret(ret));
912        }
913        Ok(previous)
914    }
915
916    /// Restaure la disposition `previous` pour le signal `signal`.
917    ///
918    /// # Safety
919    ///
920    /// `previous` doit provenir d'un [`install`] précédent sur le **même**
921    /// `signal`. Restaurer une disposition d'un autre signal est un comportement
922    /// indéfini côté noyau.
923    ///
924    /// # Errors
925    ///
926    /// - [`Errno::EINVAL`] : `signal` non-catchable (ne se produit pas si
927    ///   `previous` vient d'un `install` réussi sur ce signal).
928    pub unsafe fn restore(signal: Signal, previous: PreviousDisposition) -> Result<(), Errno> {
929        let prev_ptr: *const u8 = previous.as_bytes().as_ptr();
930        // SAFETY: rt_sigaction(2). Le kernel lit jusqu'à 32 octets à `prev_ptr`
931        // (KernelSigaction ≤ 32 octets). Pas d'écriture (oldset=NULL).
932        let ret = unsafe {
933            raw_syscall_rt_sigaction(signal.as_raw(), prev_ptr as u64, 0_u64, SIGSET_SIZE_U64)
934        };
935        if ret < 0 {
936            return Err(errno_from_negative_syscall_ret(ret));
937        }
938        Ok(())
939    }
940
941    /// Représentation interne du `struct sigaction` kernel (cf.
942    /// [`synchronous_handler`](super::synchronous_handler) — même layout ABI,
943    /// dupliqué pour ne PAS toucher au sous-module des fautes).
944    ///
945    /// - offset 0  : `sa_handler` (8) ; offset 8 : `sa_flags` (8) ;
946    /// - x86_64 : offset 16 `sa_restorer` (8), offset 24 `sa_mask` (8) — 32 octets ;
947    /// - aarch64 : offset 16 `sa_mask` (8), **pas** de `sa_restorer` — 24 octets.
948    #[repr(C)]
949    #[cfg(target_arch = "x86_64")]
950    struct KernelSigaction {
951        sa_handler: u64,
952        sa_flags: u64,
953        sa_restorer: u64,
954        sa_mask: u64,
955    }
956
957    #[cfg(target_arch = "x86_64")]
958    impl KernelSigaction {
959        const fn zeroed() -> Self {
960            Self {
961                sa_handler: 0,
962                sa_flags: 0,
963                sa_restorer: 0,
964                sa_mask: 0,
965            }
966        }
967    }
968
969    #[repr(C)]
970    #[cfg(target_arch = "aarch64")]
971    struct KernelSigaction {
972        sa_handler: u64,
973        sa_flags: u64,
974        sa_mask: u64,
975    }
976
977    #[cfg(target_arch = "aarch64")]
978    impl KernelSigaction {
979        const fn zeroed() -> Self {
980            Self {
981                sa_handler: 0,
982                sa_flags: 0,
983                sa_mask: 0,
984            }
985        }
986    }
987
988    /// `KernelSigaction` tient dans le buffer 32 octets de `PreviousDisposition`.
989    const _: () = {
990        assert!(core::mem::size_of::<KernelSigaction>() <= 32);
991    };
992
993    #[cfg(target_arch = "x86_64")]
994    #[inline]
995    unsafe fn raw_syscall_rt_sigaction(
996        signum: i32,
997        new_sa: u64,
998        old_sa: u64,
999        sigsetsize: u64,
1000    ) -> i64 {
1001        let ret: i64;
1002        // SAFETY: SYS_rt_sigaction (x86_64 = 13). Le kernel lit `new_sa`
1003        // (KernelSigaction, 32 octets), écrit `old_sa` (32 octets) si non-null.
1004        unsafe {
1005            core::arch::asm!(
1006                "syscall",
1007                in("rax") 13_i64,
1008                in("rdi") i64::from(signum),
1009                in("rsi") new_sa,
1010                in("rdx") old_sa,
1011                in("r10") sigsetsize,
1012                lateout("rax") ret,
1013                lateout("rcx") _,
1014                lateout("r11") _,
1015                options(nostack, preserves_flags),
1016            );
1017        }
1018        ret
1019    }
1020
1021    #[cfg(target_arch = "aarch64")]
1022    #[inline]
1023    unsafe fn raw_syscall_rt_sigaction(
1024        signum: i32,
1025        new_sa: u64,
1026        old_sa: u64,
1027        sigsetsize: u64,
1028    ) -> i64 {
1029        let ret: i64;
1030        // SAFETY: SYS_rt_sigaction (aarch64 = 134).
1031        unsafe {
1032            core::arch::asm!(
1033                "svc 0",
1034                in("x8") 134_i64,
1035                inout("x0") i64::from(signum) => ret,
1036                in("x1") new_sa,
1037                in("x2") old_sa,
1038                in("x3") sigsetsize,
1039                options(nostack, preserves_flags),
1040            );
1041        }
1042        ret
1043    }
1044}
1045
1046// ─────────────────────────────────────────────────────────────────────────
1047// asm! wrappers x86_64 / aarch64.
1048// ─────────────────────────────────────────────────────────────────────────
1049
1050#[cfg(target_arch = "x86_64")]
1051#[inline]
1052unsafe fn raw_syscall_signalfd4(fd: i32, mask: u64, sigsetsize: u64, flags: i32) -> i64 {
1053    let ret: i64;
1054    // SAFETY: SYS_signalfd4 (x86_64 = 289). Le kernel lit `*mask` (8
1055    // octets) ; pas d'écriture en mémoire utilisateur (`readonly` OK).
1056    unsafe {
1057        core::arch::asm!(
1058            "syscall",
1059            in("rax") 289_i64,
1060            in("rdi") i64::from(fd),
1061            in("rsi") mask,
1062            in("rdx") sigsetsize,
1063            in("r10") i64::from(flags),
1064            lateout("rax") ret,
1065            lateout("rcx") _,
1066            lateout("r11") _,
1067            options(nostack, preserves_flags, readonly),
1068        );
1069    }
1070    ret
1071}
1072
1073#[cfg(target_arch = "aarch64")]
1074#[inline]
1075unsafe fn raw_syscall_signalfd4(fd: i32, mask: u64, sigsetsize: u64, flags: i32) -> i64 {
1076    let ret: i64;
1077    // SAFETY: SYS_signalfd4 (aarch64 = 74).
1078    unsafe {
1079        core::arch::asm!(
1080            "svc 0",
1081            in("x8") 74_i64,
1082            inout("x0") i64::from(fd) => ret,
1083            in("x1") mask,
1084            in("x2") sigsetsize,
1085            in("x3") i64::from(flags),
1086            options(nostack, preserves_flags, readonly),
1087        );
1088    }
1089    ret
1090}
1091
1092#[cfg(target_arch = "x86_64")]
1093#[inline]
1094unsafe fn raw_syscall_rt_sigprocmask(how: i32, set: u64, oldset: u64, sigsetsize: u64) -> i64 {
1095    let ret: i64;
1096    // SAFETY: SYS_rt_sigprocmask (x86_64 = 14). Le kernel lit `*set` si
1097    // non-null (8 octets) ; écrit `*oldset` si non-null (8 octets). Pas
1098    // de `readonly` (écriture sur oldset).
1099    unsafe {
1100        core::arch::asm!(
1101            "syscall",
1102            in("rax") 14_i64,
1103            in("rdi") i64::from(how),
1104            in("rsi") set,
1105            in("rdx") oldset,
1106            in("r10") sigsetsize,
1107            lateout("rax") ret,
1108            lateout("rcx") _,
1109            lateout("r11") _,
1110            options(nostack, preserves_flags),
1111        );
1112    }
1113    ret
1114}
1115
1116#[cfg(target_arch = "aarch64")]
1117#[inline]
1118unsafe fn raw_syscall_rt_sigprocmask(how: i32, set: u64, oldset: u64, sigsetsize: u64) -> i64 {
1119    let ret: i64;
1120    // SAFETY: SYS_rt_sigprocmask (aarch64 = 135).
1121    unsafe {
1122        core::arch::asm!(
1123            "svc 0",
1124            in("x8") 135_i64,
1125            inout("x0") i64::from(how) => ret,
1126            in("x1") set,
1127            in("x2") oldset,
1128            in("x3") sigsetsize,
1129            options(nostack, preserves_flags),
1130        );
1131    }
1132    ret
1133}
1134
1135#[cfg(target_arch = "x86_64")]
1136#[inline]
1137unsafe fn raw_syscall_sigaltstack(new: u64, old: u64) -> i64 {
1138    let ret: i64;
1139    // SAFETY: SYS_sigaltstack (x86_64 = 131). Le kernel **lit** `*new` (24 octets) si
1140    // `new != 0` et **écrit** `*old` (24 octets) si `old != 0` — pas de `readonly`.
1141    unsafe {
1142        core::arch::asm!(
1143            "syscall",
1144            in("rax") 131_i64,
1145            in("rdi") new,
1146            in("rsi") old,
1147            lateout("rax") ret,
1148            lateout("rcx") _,
1149            lateout("r11") _,
1150            options(nostack, preserves_flags),
1151        );
1152    }
1153    ret
1154}
1155
1156#[cfg(target_arch = "aarch64")]
1157#[inline]
1158unsafe fn raw_syscall_sigaltstack(new: u64, old: u64) -> i64 {
1159    let ret: i64;
1160    // SAFETY: SYS_sigaltstack (aarch64 = 132). Mêmes contrats mémoire que x86_64.
1161    unsafe {
1162        core::arch::asm!(
1163            "svc 0",
1164            in("x8") 132_i64,
1165            inout("x0") new => ret,
1166            in("x1") old,
1167            options(nostack, preserves_flags),
1168        );
1169    }
1170    ret
1171}
1172
1173#[cfg(target_arch = "x86_64")]
1174#[inline]
1175unsafe fn raw_syscall_rt_sigpending(set: u64, sigsetsize: u64) -> i64 {
1176    let ret: i64;
1177    // SAFETY: SYS_rt_sigpending (x86_64 = 127). Le kernel **écrit** 8 octets à
1178    // `set` (sigset_t) ; pas de `readonly`.
1179    unsafe {
1180        core::arch::asm!(
1181            "syscall",
1182            in("rax") 127_i64,
1183            in("rdi") set,
1184            in("rsi") sigsetsize,
1185            lateout("rax") ret,
1186            lateout("rcx") _,
1187            lateout("r11") _,
1188            options(nostack, preserves_flags),
1189        );
1190    }
1191    ret
1192}
1193
1194#[cfg(target_arch = "aarch64")]
1195#[inline]
1196unsafe fn raw_syscall_rt_sigpending(set: u64, sigsetsize: u64) -> i64 {
1197    let ret: i64;
1198    // SAFETY: SYS_rt_sigpending (aarch64 = 136).
1199    unsafe {
1200        core::arch::asm!(
1201            "svc 0",
1202            in("x8") 136_i64,
1203            inout("x0") set => ret,
1204            in("x1") sigsetsize,
1205            options(nostack, preserves_flags),
1206        );
1207    }
1208    ret
1209}
1210
1211#[cfg(target_arch = "x86_64")]
1212#[inline]
1213unsafe fn raw_syscall_kill(pid: i32, sig: i32) -> i64 {
1214    let ret: i64;
1215    // SAFETY: SYS_kill (x86_64 = 62). Pas d'accès mémoire utilisateur.
1216    unsafe {
1217        core::arch::asm!(
1218            "syscall",
1219            in("rax") 62_i64,
1220            in("rdi") i64::from(pid),
1221            in("rsi") i64::from(sig),
1222            lateout("rax") ret,
1223            lateout("rcx") _,
1224            lateout("r11") _,
1225            options(nostack, preserves_flags, readonly),
1226        );
1227    }
1228    ret
1229}
1230
1231#[cfg(target_arch = "aarch64")]
1232#[inline]
1233unsafe fn raw_syscall_kill(pid: i32, sig: i32) -> i64 {
1234    let ret: i64;
1235    // SAFETY: SYS_kill (aarch64 = 129).
1236    unsafe {
1237        core::arch::asm!(
1238            "svc 0",
1239            in("x8") 129_i64,
1240            inout("x0") i64::from(pid) => ret,
1241            in("x1") i64::from(sig),
1242            options(nostack, preserves_flags, readonly),
1243        );
1244    }
1245    ret
1246}
1247
1248#[cfg(target_arch = "x86_64")]
1249#[inline]
1250unsafe fn raw_syscall_tgkill(tgid: i32, tid: i32, sig: i32) -> i64 {
1251    let ret: i64;
1252    // SAFETY: SYS_tgkill (x86_64 = 234). Pas d'accès mémoire utilisateur.
1253    unsafe {
1254        core::arch::asm!(
1255            "syscall",
1256            in("rax") 234_i64,
1257            in("rdi") i64::from(tgid),
1258            in("rsi") i64::from(tid),
1259            in("rdx") i64::from(sig),
1260            lateout("rax") ret,
1261            lateout("rcx") _,
1262            lateout("r11") _,
1263            options(nostack, preserves_flags, readonly),
1264        );
1265    }
1266    ret
1267}
1268
1269#[cfg(target_arch = "aarch64")]
1270#[inline]
1271unsafe fn raw_syscall_tgkill(tgid: i32, tid: i32, sig: i32) -> i64 {
1272    let ret: i64;
1273    // SAFETY: SYS_tgkill (aarch64 = 131).
1274    unsafe {
1275        core::arch::asm!(
1276            "svc 0",
1277            in("x8") 131_i64,
1278            inout("x0") i64::from(tgid) => ret,
1279            in("x1") i64::from(tid),
1280            in("x2") i64::from(sig),
1281            options(nostack, preserves_flags, readonly),
1282        );
1283    }
1284    ret
1285}
1286
1287#[cfg(target_arch = "x86_64")]
1288#[inline]
1289unsafe fn raw_syscall_rt_sigqueueinfo(pid: i32, sig: i32, info: u64) -> i64 {
1290    let ret: i64;
1291    // SAFETY: SYS_rt_sigqueueinfo (x86_64 = 129). Le kernel lit 128
1292    // octets à `info` (siginfo_t). Pas d'écriture user (`readonly`).
1293    unsafe {
1294        core::arch::asm!(
1295            "syscall",
1296            in("rax") 129_i64,
1297            in("rdi") i64::from(pid),
1298            in("rsi") i64::from(sig),
1299            in("rdx") info,
1300            lateout("rax") ret,
1301            lateout("rcx") _,
1302            lateout("r11") _,
1303            options(nostack, preserves_flags, readonly),
1304        );
1305    }
1306    ret
1307}
1308
1309#[cfg(target_arch = "aarch64")]
1310#[inline]
1311unsafe fn raw_syscall_rt_sigqueueinfo(pid: i32, sig: i32, info: u64) -> i64 {
1312    let ret: i64;
1313    // SAFETY: SYS_rt_sigqueueinfo (aarch64 = 138).
1314    unsafe {
1315        core::arch::asm!(
1316            "svc 0",
1317            in("x8") 138_i64,
1318            inout("x0") i64::from(pid) => ret,
1319            in("x1") i64::from(sig),
1320            in("x2") info,
1321            options(nostack, preserves_flags, readonly),
1322        );
1323    }
1324    ret
1325}
1326
1327#[cfg(target_arch = "x86_64")]
1328#[inline]
1329unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: u64) -> i64 {
1330    let ret: i64;
1331    // SAFETY: SYS_read (x86_64 = 0). Le kernel écrit jusqu'à `count`
1332    // octets à `buffer`. Pour signalfd, exactement 128 octets pour un
1333    // événement complet.
1334    unsafe {
1335        core::arch::asm!(
1336            "syscall",
1337            in("rax") 0_i64,
1338            in("rdi") i64::from(fd),
1339            in("rsi") buffer,
1340            in("rdx") count,
1341            lateout("rax") ret,
1342            lateout("rcx") _,
1343            lateout("r11") _,
1344            options(nostack, preserves_flags),
1345        );
1346    }
1347    ret
1348}
1349
1350#[cfg(target_arch = "aarch64")]
1351#[inline]
1352unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: u64) -> i64 {
1353    let ret: i64;
1354    // SAFETY: SYS_read (aarch64 = 63).
1355    unsafe {
1356        core::arch::asm!(
1357            "svc 0",
1358            in("x8") 63_i64,
1359            inout("x0") i64::from(fd) => ret,
1360            in("x1") buffer,
1361            in("x2") count,
1362            options(nostack, preserves_flags),
1363        );
1364    }
1365    ret
1366}
1367
1368// ─────────────────────────────────────────────────────────────────────────
1369// Helper de conversion errno (réplique de celui de process.rs ; un
1370// futur refactor pourra centraliser).
1371// ─────────────────────────────────────────────────────────────────────────
1372
1373fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
1374    debug_assert!(ret < 0 && ret > -4096);
1375    #[allow(clippy::cast_possible_truncation)]
1376    let raw = ret.wrapping_neg() as i32;
1377    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
1378    Errno::from_nonzero(nz)
1379}
1380
1381// ─────────────────────────────────────────────────────────────────────────
1382// Tests.
1383// ─────────────────────────────────────────────────────────────────────────
1384
1385#[cfg(test)]
1386mod tests;