Skip to main content

air_sys_syscall/
ipc.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 `ipc` — eventfd, pipe, opérations zero-copy.
6//!
7//! Cf. `docs/specs/layer-0/family-ipc.md`.
8
9#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
10compile_error!("air-sys-syscall::ipc supporte uniquement x86_64 et aarch64 (ADR-014).");
11
12use air_sys_types::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
13use core::num::NonZeroI32;
14
15use air_sys_types::Errno;
16use air_sys_types::ipc::{EventFdFlags, PipeFlags, SpliceFlags};
17use air_sys_types::net::IoSlice;
18
19// ─────────────────────────────────────────────────────────────────────────
20// Helper commun : conversion d'une valeur de retour syscall négative en Errno.
21// ─────────────────────────────────────────────────────────────────────────
22
23fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
24    debug_assert!(ret < 0 && ret > -4096);
25    #[allow(clippy::cast_possible_truncation)]
26    let raw = ret.wrapping_neg() as i32;
27    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
28    Errno::from_nonzero(nz)
29}
30
31// ─────────────────────────────────────────────────────────────────────────
32// EventFd — compteur kernel exposé comme FD.
33// ─────────────────────────────────────────────────────────────────────────
34
35/// FD `eventfd` (cf. `eventfd2(2)`).
36///
37/// Encapsule un compteur kernel 64 bits exposé comme descripteur de
38/// fichier. Pattern privilégié pour les notifications légères
39/// inter-threads ou inter-processus, et pour l'intégration avec
40/// io_uring (wakeup du reactor).
41///
42/// La fermeture du FD est automatique à la destruction (RAII via
43/// [`OwnedFd`]).
44#[derive(Debug)]
45pub struct EventFd(OwnedFd);
46
47impl EventFd {
48    /// Vue empruntée du FD sous-jacent.
49    ///
50    /// Utile pour passer le FD à d'autres syscalls (poll, io_uring…)
51    /// sans transférer l'ownership.
52    #[must_use]
53    pub fn as_fd(&self) -> BorrowedFd<'_> {
54        self.0.as_fd()
55    }
56
57    /// Consomme le `EventFd` et restitue le [`OwnedFd`] sous-jacent.
58    #[must_use]
59    pub fn into_fd(self) -> OwnedFd {
60        self.0
61    }
62
63    /// Lit la valeur courante du compteur.
64    ///
65    /// **Mode normal** (sans `SEMAPHORE`) : retourne la valeur courante et
66    /// remet le compteur à zéro. Bloque si le compteur est zéro (sauf
67    /// si `NONBLOCK` est positionné).
68    ///
69    /// **Mode sémaphore** (avec `SEMAPHORE`) : retourne 1 et décrémente
70    /// de 1. Bloque si le compteur est zéro.
71    ///
72    /// # Errors
73    ///
74    /// - `EAGAIN` : compteur à zéro et le FD est en mode non-bloquant.
75    /// - `EINTR` : interrompu par un signal (ADR-021 convention 2 :
76    ///   pas de retry automatique).
77    /// - `EBADF` : FD invalide (ne se produit pas si `EventFd` bien formé).
78    ///
79    /// # Examples
80    ///
81    /// ```no_run
82    /// use air_sys_syscall::ipc::eventfd2;
83    /// use air_sys_types::EventFdFlags;
84    ///
85    /// let efd = eventfd2(0, EventFdFlags::empty()).expect("eventfd2");
86    /// efd.write(5).expect("write");
87    /// let val = efd.read().expect("read");
88    /// assert_eq!(val, 5);
89    /// ```
90    pub fn read(&self) -> Result<u64, Errno> {
91        let mut buffer: u64 = 0;
92        let buf_ptr: *mut u64 = &mut buffer;
93        // SAFETY:
94        // - SYS_read sur un eventfd lit exactement 8 octets dans `*buf_ptr`.
95        // - `buffer` est local valide pour toute la durée du syscall.
96        // - Le FD est garanti ouvert (EventFd possède un OwnedFd valide).
97        let ret = unsafe {
98            raw_syscall_read(
99                self.0.as_raw_fd(),
100                buf_ptr as u64,
101                core::mem::size_of::<u64>(),
102            )
103        };
104        if ret < 0 {
105            return Err(errno_from_negative_syscall_ret(ret));
106        }
107        Ok(buffer)
108    }
109
110    /// Incrémente le compteur de `value`.
111    ///
112    /// Bloque si le compteur atteindrait `u64::MAX - 1` (espace épuisé).
113    /// Retourne immédiatement `EAGAIN` si non-bloquant.
114    ///
115    /// # Parameters
116    ///
117    /// - `value` : incrément. La valeur maximale autorisée par incrémentation
118    ///   est `u64::MAX - 1` ; `u64::MAX` est interdit par le kernel.
119    ///
120    /// # Errors
121    ///
122    /// - `EINVAL` : `value` == `u64::MAX` (débordement interdit).
123    /// - `EAGAIN` : le FD est non-bloquant et l'incrément bloquerait.
124    /// - `EINTR` : interrompu par un signal (pas de retry automatique).
125    ///
126    /// # Examples
127    ///
128    /// ```no_run
129    /// use air_sys_syscall::ipc::eventfd2;
130    /// use air_sys_types::EventFdFlags;
131    ///
132    /// let efd = eventfd2(0, EventFdFlags::empty()).expect("eventfd2");
133    /// efd.write(1).expect("write notification");
134    /// ```
135    pub fn write(&self, value: u64) -> Result<(), Errno> {
136        let buf_ptr: *const u64 = &value;
137        // SAFETY:
138        // - SYS_write sur un eventfd lit exactement 8 octets depuis `*buf_ptr`.
139        // - `value` est local valide pour toute la durée du syscall.
140        // - Le FD est garanti ouvert (EventFd possède un OwnedFd valide).
141        let ret = unsafe {
142            raw_syscall_write(
143                self.0.as_raw_fd(),
144                buf_ptr as u64,
145                core::mem::size_of::<u64>(),
146            )
147        };
148        if ret < 0 {
149            return Err(errno_from_negative_syscall_ret(ret));
150        }
151        Ok(())
152    }
153}
154
155// ─────────────────────────────────────────────────────────────────────────
156// Fonctions publiques
157// ─────────────────────────────────────────────────────────────────────────
158
159/// Crée un `eventfd` avec compteur initial et flags.
160///
161/// Wrappeur de `eventfd2(2)` (Linux 2.6.27+, numéro x86_64: 290,
162/// aarch64: 19). `CLOEXEC` n'est **pas** ajouté automatiquement : l'appelant
163/// doit l'inclure dans `flags` si désiré (comportement recommandé).
164///
165/// # Parameters
166///
167/// - `initial` : valeur initiale du compteur (typiquement 0).
168/// - `flags` : combinaison de [`EventFdFlags`].
169///
170/// # Errors
171///
172/// - `EINVAL` : flags invalides.
173/// - `EMFILE` : quota de FD du processus atteint.
174/// - `ENFILE` : quota de FD système atteint.
175/// - `ENODEV` : impossible de monter le pseudo-périphérique anonyme.
176/// - `ENOMEM` : mémoire kernel insuffisante.
177///
178/// # Examples
179///
180/// ```no_run
181/// use air_sys_syscall::ipc::eventfd2;
182/// use air_sys_types::EventFdFlags;
183///
184/// let efd = eventfd2(0, EventFdFlags::CLOEXEC).expect("eventfd2");
185/// ```
186pub fn eventfd2(initial: u64, flags: EventFdFlags) -> Result<EventFd, Errno> {
187    // Le kernel eventfd2 prend un u32 : les bits hauts de `initial` sont
188    // ignorés par l'ABI kernel ; on passe les 32 bits bas.
189    #[allow(clippy::cast_possible_truncation)]
190    let initial_u32 = initial as u32;
191    // SAFETY: eventfd2(2) ne lit ni n'écrit aucune mémoire utilisateur ;
192    // il prend deux scalaires (u32 initial, i32 flags) et retourne un fd.
193    let ret = unsafe { raw_syscall_eventfd2(initial_u32, flags.bits()) };
194    if ret < 0 {
195        return Err(errno_from_negative_syscall_ret(ret));
196    }
197    // Linux borne les fd à i32::MAX ; la troncature est exacte.
198    #[allow(clippy::cast_possible_truncation)]
199    let raw_fd = ret as i32;
200    // SAFETY: le kernel vient d'allouer un fd valide et nous en a transféré
201    // la propriété ; Air le wrappe immédiatement dans OwnedFd. Drop fermera.
202    let owned = unsafe { OwnedFd::from_raw_fd(raw_fd) };
203    Ok(EventFd(owned))
204}
205
206/// Crée un pipe unidirectionnel et retourne `(read_end, write_end)`.
207///
208/// Wrappeur de `pipe2(2)` (Linux 2.6.27+). Retourne deux [`OwnedFd`] :
209/// le premier est le côté lecture, le second le côté écriture.
210///
211/// `CLOEXEC` est **fortement recommandé** pour éviter la fuite du pipe
212/// vers les processus fils après `exec`. Air n'impose pas `CLOEXEC`
213/// automatiquement car certains usages légitimes (héritage volontaire
214/// vers un enfant) en ont besoin.
215///
216/// # Parameters
217///
218/// - `flags` : combinaison de [`PipeFlags`].
219///
220/// # Errors
221///
222/// - `EFAULT` : pointeur interne invalide (ne se produit pas via cette API).
223/// - `EINVAL` : flags invalides.
224/// - `EMFILE` : quota de FD du processus atteint.
225/// - `ENFILE` : quota de FD système atteint.
226///
227/// # Examples
228///
229/// ```no_run
230/// use air_sys_syscall::ipc::pipe2;
231/// use air_sys_types::PipeFlags;
232///
233/// let (read_fd, write_fd) = pipe2(PipeFlags::CLOEXEC).expect("pipe2");
234/// ```
235pub fn pipe2(flags: PipeFlags) -> Result<(OwnedFd, OwnedFd), Errno> {
236    // Le kernel pipe2 écrit deux fd dans un tableau int[2] (= [i32; 2]).
237    let mut fds: [i32; 2] = [-1, -1];
238    let fds_ptr: *mut [i32; 2] = &mut fds;
239    // SAFETY:
240    // - pipe2(2) écrit exactement 2 × sizeof(int) = 8 octets à `fds_ptr`.
241    // - `fds` est local valide pour toute la durée du syscall.
242    let ret = unsafe { raw_syscall_pipe2(fds_ptr as u64, flags.bits()) };
243    if ret < 0 {
244        return Err(errno_from_negative_syscall_ret(ret));
245    }
246    // SAFETY: le kernel a écrit deux fd valides dans `fds` ; Air en prend
247    // la propriété. Drop fermera chacun.
248    let read_end = unsafe { OwnedFd::from_raw_fd(fds[0]) };
249    let write_end = unsafe { OwnedFd::from_raw_fd(fds[1]) };
250    Ok((read_end, write_end))
251}
252
253/// Transfère jusqu'à `length` octets de `fd_in` vers `fd_out` sans copie
254/// userspace (zero-copy).
255///
256/// Wrappeur de `splice(2)` (Linux 2.6.17+). **Au moins un** des deux FDs
257/// doit être un pipe ; sinon le kernel retourne `EINVAL`.
258///
259/// Si `offset_in` / `offset_out` est `None`, la position courante du FD est
260/// utilisée et avancée. Si `Some`, la position donnée est utilisée sans
261/// modifier la position courante du FD.
262///
263/// # Parameters
264///
265/// - `fd_in` : FD source.
266/// - `offset_in` : offset dans `fd_in`, ou `None` pour la position courante.
267/// - `fd_out` : FD destination.
268/// - `offset_out` : offset dans `fd_out`, ou `None` pour la position courante.
269/// - `length` : nombre maximum d'octets à transférer.
270/// - `flags` : drapeaux [`SpliceFlags`].
271///
272/// # Errors
273///
274/// - `EINVAL` : ni `fd_in` ni `fd_out` n'est un pipe, ou flags invalides.
275/// - `EAGAIN` : mode `NONBLOCK` et l'opération bloquerait.
276/// - `EINTR` : interrompu par un signal (pas de retry automatique).
277/// - `EBADF` : FD invalide ou mode d'accès incorrect.
278/// - `ESPIPE` : `offset_in` non-nul sur un pipe (les pipes ne sont pas seekables).
279///
280/// # Examples
281///
282/// ```no_run
283/// use air_sys_syscall::ipc::{pipe2, splice};
284/// use air_sys_types::{PipeFlags, SpliceFlags};
285/// use air_sys_types::fd::AsFd;
286///
287/// let (read_fd, write_fd) = pipe2(PipeFlags::CLOEXEC).expect("pipe2");
288/// // Connecter un fichier source au pipe...
289/// let _ = splice(
290///     read_fd.as_fd(), None,
291///     write_fd.as_fd(), None,
292///     4096,
293///     SpliceFlags::empty(),
294/// );
295/// ```
296pub fn splice(
297    fd_in: BorrowedFd<'_>,
298    offset_in: Option<&mut u64>,
299    fd_out: BorrowedFd<'_>,
300    offset_out: Option<&mut u64>,
301    length: usize,
302    flags: SpliceFlags,
303) -> Result<usize, Errno> {
304    // `None` → NULL pointer (0) ; `Some` → adresse du u64 mutable.
305    let off_in_ptr: u64 = match offset_in {
306        None => 0,
307        Some(p) => {
308            let raw: *mut u64 = p;
309            raw as u64
310        }
311    };
312    let off_out_ptr: u64 = match offset_out {
313        None => 0,
314        Some(p) => {
315            let raw: *mut u64 = p;
316            raw as u64
317        }
318    };
319    // SAFETY:
320    // - splice(2) lit/écrit optionnellement via `off_in_ptr`/`off_out_ptr`
321    //   si non nuls ; les pointeurs sont valides (locaux à l'appelant, via
322    //   `Option<&mut u64>`).
323    // - fd_in et fd_out sont garantis ouverts par BorrowedFd.
324    let ret = unsafe {
325        raw_syscall_splice(
326            fd_in.as_raw_fd(),
327            off_in_ptr,
328            fd_out.as_raw_fd(),
329            off_out_ptr,
330            length,
331            flags.bits(),
332        )
333    };
334    if ret < 0 {
335        return Err(errno_from_negative_syscall_ret(ret));
336    }
337    // splice retourne le nombre d'octets transférés : non négatif, ≤ length
338    // (qui est un usize). La troncature de i64 → usize est exacte sur LP64.
339    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
340    Ok(ret as usize)
341}
342
343/// Duplique le contenu d'un pipe source dans un pipe destination sans
344/// consommer les données de la source.
345///
346/// Wrappeur de `tee(2)` (Linux 2.6.17+). Les deux FDs **doivent** être
347/// des pipes. Contrairement à `splice`, les données restent disponibles
348/// dans `fd_in` après l'opération.
349///
350/// # Parameters
351///
352/// - `fd_in` : pipe source.
353/// - `fd_out` : pipe destination.
354/// - `length` : nombre maximum d'octets à dupliquer.
355/// - `flags` : drapeaux [`SpliceFlags`] (typiquement `NONBLOCK`).
356///
357/// # Errors
358///
359/// - `EINVAL` : `fd_in` ou `fd_out` n'est pas un pipe.
360/// - `EAGAIN` : mode `NONBLOCK` et l'opération bloquerait.
361/// - `EINTR` : interrompu par un signal.
362///
363/// # Examples
364///
365/// ```no_run
366/// use air_sys_syscall::ipc::{pipe2, tee};
367/// use air_sys_types::{PipeFlags, SpliceFlags};
368/// use air_sys_types::fd::AsFd;
369///
370/// let (r1, w1) = pipe2(PipeFlags::CLOEXEC).expect("pipe2");
371/// let (r2, w2) = pipe2(PipeFlags::CLOEXEC).expect("pipe2");
372/// let _ = tee(r1.as_fd(), w2.as_fd(), 4096, SpliceFlags::empty());
373/// ```
374pub fn tee(
375    fd_in: BorrowedFd<'_>,
376    fd_out: BorrowedFd<'_>,
377    length: usize,
378    flags: SpliceFlags,
379) -> Result<usize, Errno> {
380    // SAFETY: tee(2) ne touche aucune mémoire utilisateur ; il opère
381    // uniquement sur les pipe buffers internes au kernel.
382    // fd_in et fd_out sont garantis ouverts par BorrowedFd.
383    let ret =
384        unsafe { raw_syscall_tee(fd_in.as_raw_fd(), fd_out.as_raw_fd(), length, flags.bits()) };
385    if ret < 0 {
386        return Err(errno_from_negative_syscall_ret(ret));
387    }
388    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
389    Ok(ret as usize)
390}
391
392/// Transfère des buffers userspace vers un pipe sans copie intermédiaire.
393///
394/// Wrappeur de `vmsplice(2)` (Linux 2.6.17+). `fd` doit être un pipe.
395///
396/// **Mode GIFT (dangereux) :** si [`SpliceFlags::GIFT`] est positionné,
397/// les pages userspace sont données au kernel. Les régions décrites par
398/// `iov` **ne doivent plus jamais être modifiées** après l'appel —
399/// violation d'invariant = corruption mémoire silencieuse.
400///
401/// # Parameters
402///
403/// - `fd` : pipe destination.
404/// - `iov` : slices de buffers userspace à transférer.
405/// - `flags` : drapeaux [`SpliceFlags`].
406///
407/// # Safety
408///
409/// Si [`SpliceFlags::GIFT`] est utilisé, l'appelant doit garantir que
410/// les pages décrites par `iov` ne sont plus modifiées après l'appel.
411/// Modifier ces pages après un `vmsplice GIFT` produit un comportement
412/// indéfini.
413///
414/// # Errors
415///
416/// - `EINVAL` : `fd` n'est pas un pipe, ou `iov` dépasse `UIO_MAXIOV`.
417/// - `EAGAIN` : mode `NONBLOCK` et le pipe est plein.
418/// - `EINTR` : interrompu par un signal.
419///
420/// # Examples
421///
422/// ```no_run
423/// use air_sys_syscall::ipc::{pipe2, vmsplice};
424/// use air_sys_types::{PipeFlags, SpliceFlags};
425/// use air_sys_types::net::IoSlice;
426/// use air_sys_types::fd::AsFd;
427///
428/// let data = b"hello, world";
429/// let iov = [IoSlice::new(data)];
430/// let (_r, w) = pipe2(PipeFlags::CLOEXEC).expect("pipe2");
431/// let _ = vmsplice(w.as_fd(), &iov, SpliceFlags::empty());
432/// ```
433pub fn vmsplice(
434    fd: BorrowedFd<'_>,
435    iov: &[IoSlice<'_>],
436    flags: SpliceFlags,
437) -> Result<usize, Errno> {
438    let iov_ptr: *const IoSlice<'_> = iov.as_ptr();
439    // SAFETY:
440    // - vmsplice(2) lit les iovec décrits par `iov_ptr`/`iov_len` pour
441    //   transférer des données userspace dans un pipe kernel.
442    // - Chaque IoSlice est #[repr(C)] compatible avec `struct iovec` Linux.
443    // - Les buffers pointés par les IoSlice doivent rester valides et
444    //   immuables pour la durée du syscall (garantit par la durée de vie
445    //   de la référence &[IoSlice<'_>]).
446    // - fd est garanti ouvert par BorrowedFd.
447    let ret =
448        unsafe { raw_syscall_vmsplice(fd.as_raw_fd(), iov_ptr as u64, iov.len(), flags.bits()) };
449    if ret < 0 {
450        return Err(errno_from_negative_syscall_ret(ret));
451    }
452    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
453    Ok(ret as usize)
454}
455
456// ─────────────────────────────────────────────────────────────────────────
457// Helpers syscall bruts : read / write (pour eventfd)
458// ─────────────────────────────────────────────────────────────────────────
459
460#[cfg(target_arch = "x86_64")]
461#[inline]
462unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: usize) -> i64 {
463    let ret: i64;
464    // SAFETY: SYS_read (x86_64 = 0). Le kernel écrit `count` octets à
465    // l'adresse `buffer`. L'appelant garantit que `buffer` est valide pour
466    // `count` octets en écriture. Clobbers RCX + R11 (ABI syscall x86_64).
467    unsafe {
468        core::arch::asm!(
469            "syscall",
470            in("rax") 0_i64,
471            in("rdi") i64::from(fd),
472            in("rsi") buffer,
473            in("rdx") count,
474            lateout("rax") ret,
475            lateout("rcx") _,
476            lateout("r11") _,
477            options(nostack, preserves_flags),
478        );
479    }
480    ret
481}
482
483#[cfg(target_arch = "aarch64")]
484#[inline]
485unsafe fn raw_syscall_read(fd: i32, buffer: u64, count: usize) -> i64 {
486    let ret: i64;
487    // SAFETY: SYS_read (aarch64 = 63). Numéro en X8, args en X0..X2, retour en X0.
488    // Le kernel écrit `count` octets à `buffer`.
489    unsafe {
490        core::arch::asm!(
491            "svc 0",
492            in("x8") 63_i64,
493            inout("x0") i64::from(fd) => ret,
494            in("x1") buffer,
495            in("x2") count,
496            options(nostack, preserves_flags),
497        );
498    }
499    ret
500}
501
502#[cfg(target_arch = "x86_64")]
503#[inline]
504unsafe fn raw_syscall_write(fd: i32, buffer: u64, count: usize) -> i64 {
505    let ret: i64;
506    // SAFETY: SYS_write (x86_64 = 1). Le kernel lit `count` octets depuis
507    // l'adresse `buffer`. L'appelant garantit que `buffer` est valide pour
508    // `count` octets en lecture. `readonly` serait incorrect (le syscall
509    // avance la position du fd côté kernel), donc on n'utilise pas `readonly`.
510    unsafe {
511        core::arch::asm!(
512            "syscall",
513            in("rax") 1_i64,
514            in("rdi") i64::from(fd),
515            in("rsi") buffer,
516            in("rdx") count,
517            lateout("rax") ret,
518            lateout("rcx") _,
519            lateout("r11") _,
520            options(nostack, preserves_flags),
521        );
522    }
523    ret
524}
525
526#[cfg(target_arch = "aarch64")]
527#[inline]
528unsafe fn raw_syscall_write(fd: i32, buffer: u64, count: usize) -> i64 {
529    let ret: i64;
530    // SAFETY: SYS_write (aarch64 = 64). Numéro en X8, args en X0..X2, retour en X0.
531    unsafe {
532        core::arch::asm!(
533            "svc 0",
534            in("x8") 64_i64,
535            inout("x0") i64::from(fd) => ret,
536            in("x1") buffer,
537            in("x2") count,
538            options(nostack, preserves_flags),
539        );
540    }
541    ret
542}
543
544// ─────────────────────────────────────────────────────────────────────────
545// Helpers syscall bruts : eventfd2, pipe2, splice, tee, vmsplice
546// ─────────────────────────────────────────────────────────────────────────
547
548#[cfg(target_arch = "x86_64")]
549#[inline]
550unsafe fn raw_syscall_eventfd2(initial: u32, flags: i32) -> i64 {
551    let ret: i64;
552    // SAFETY: SYS_eventfd2 (x86_64 = 290). Scalaires seulement ; pas
553    // d'accès mémoire utilisateur. `readonly` correct.
554    unsafe {
555        core::arch::asm!(
556            "syscall",
557            in("rax") 290_i64,
558            in("rdi") i64::from(initial),
559            in("rsi") i64::from(flags),
560            lateout("rax") ret,
561            lateout("rcx") _,
562            lateout("r11") _,
563            options(nostack, preserves_flags, readonly),
564        );
565    }
566    ret
567}
568
569#[cfg(target_arch = "aarch64")]
570#[inline]
571unsafe fn raw_syscall_eventfd2(initial: u32, flags: i32) -> i64 {
572    let ret: i64;
573    // SAFETY: SYS_eventfd2 (aarch64 = 19). Scalaires ; `readonly` correct.
574    unsafe {
575        core::arch::asm!(
576            "svc 0",
577            in("x8") 19_i64,
578            inout("x0") i64::from(initial) => ret,
579            in("x1") i64::from(flags),
580            options(nostack, preserves_flags, readonly),
581        );
582    }
583    ret
584}
585
586#[cfg(target_arch = "x86_64")]
587#[inline]
588unsafe fn raw_syscall_pipe2(fds_ptr: u64, flags: i32) -> i64 {
589    let ret: i64;
590    // SAFETY: SYS_pipe2 (x86_64 = 293). Le kernel écrit 2 × sizeof(int)
591    // octets à `fds_ptr`. L'appelant garantit la validité du pointeur.
592    // **Pas de `readonly`** : le kernel écrit dans la mémoire utilisateur.
593    unsafe {
594        core::arch::asm!(
595            "syscall",
596            in("rax") 293_i64,
597            in("rdi") fds_ptr,
598            in("rsi") i64::from(flags),
599            lateout("rax") ret,
600            lateout("rcx") _,
601            lateout("r11") _,
602            options(nostack, preserves_flags),
603        );
604    }
605    ret
606}
607
608#[cfg(target_arch = "aarch64")]
609#[inline]
610unsafe fn raw_syscall_pipe2(fds_ptr: u64, flags: i32) -> i64 {
611    let ret: i64;
612    // SAFETY: SYS_pipe2 (aarch64 = 59). Le kernel écrit les deux fd à `fds_ptr`.
613    unsafe {
614        core::arch::asm!(
615            "svc 0",
616            in("x8") 59_i64,
617            inout("x0") fds_ptr => ret,
618            in("x1") i64::from(flags),
619            options(nostack, preserves_flags),
620        );
621    }
622    ret
623}
624
625#[cfg(target_arch = "x86_64")]
626#[inline]
627unsafe fn raw_syscall_splice(
628    fd_in: i32,
629    offset_in: u64,
630    fd_out: i32,
631    offset_out: u64,
632    length: usize,
633    flags: u32,
634) -> i64 {
635    let ret: i64;
636    // SAFETY: SYS_splice (x86_64 = 275). Le kernel peut lire/écrire
637    // optionnellement via `offset_in`/`offset_out` si non nuls.
638    // **Pas de `readonly`** : le kernel peut modifier les offsets.
639    unsafe {
640        core::arch::asm!(
641            "syscall",
642            in("rax") 275_i64,
643            in("rdi") i64::from(fd_in),
644            in("rsi") offset_in,
645            in("rdx") i64::from(fd_out),
646            in("r10") offset_out,
647            in("r8") length,
648            in("r9") i64::from(flags),
649            lateout("rax") ret,
650            lateout("rcx") _,
651            lateout("r11") _,
652            options(nostack, preserves_flags),
653        );
654    }
655    ret
656}
657
658#[cfg(target_arch = "aarch64")]
659#[inline]
660unsafe fn raw_syscall_splice(
661    fd_in: i32,
662    offset_in: u64,
663    fd_out: i32,
664    offset_out: u64,
665    length: usize,
666    flags: u32,
667) -> i64 {
668    let ret: i64;
669    // SAFETY: SYS_splice (aarch64 = 76). Args en X0..X5.
670    unsafe {
671        core::arch::asm!(
672            "svc 0",
673            in("x8") 76_i64,
674            inout("x0") i64::from(fd_in) => ret,
675            in("x1") offset_in,
676            in("x2") i64::from(fd_out),
677            in("x3") offset_out,
678            in("x4") length,
679            in("x5") i64::from(flags),
680            options(nostack, preserves_flags),
681        );
682    }
683    ret
684}
685
686#[cfg(target_arch = "x86_64")]
687#[inline]
688unsafe fn raw_syscall_tee(fd_in: i32, fd_out: i32, length: usize, flags: u32) -> i64 {
689    let ret: i64;
690    // SAFETY: SYS_tee (x86_64 = 276). Opère uniquement sur les pipe
691    // buffers kernel ; pas d'accès mémoire utilisateur.
692    // `readonly` serait inexact (modifie l'état interne des pipes).
693    unsafe {
694        core::arch::asm!(
695            "syscall",
696            in("rax") 276_i64,
697            in("rdi") i64::from(fd_in),
698            in("rsi") i64::from(fd_out),
699            in("rdx") length,
700            in("r10") i64::from(flags),
701            lateout("rax") ret,
702            lateout("rcx") _,
703            lateout("r11") _,
704            options(nostack, preserves_flags),
705        );
706    }
707    ret
708}
709
710#[cfg(target_arch = "aarch64")]
711#[inline]
712unsafe fn raw_syscall_tee(fd_in: i32, fd_out: i32, length: usize, flags: u32) -> i64 {
713    let ret: i64;
714    // SAFETY: SYS_tee (aarch64 = 77). Args en X0..X3.
715    unsafe {
716        core::arch::asm!(
717            "svc 0",
718            in("x8") 77_i64,
719            inout("x0") i64::from(fd_in) => ret,
720            in("x1") i64::from(fd_out),
721            in("x2") length,
722            in("x3") i64::from(flags),
723            options(nostack, preserves_flags),
724        );
725    }
726    ret
727}
728
729#[cfg(target_arch = "x86_64")]
730#[inline]
731unsafe fn raw_syscall_vmsplice(fd: i32, iov: u64, iov_len: usize, flags: u32) -> i64 {
732    let ret: i64;
733    // SAFETY: SYS_vmsplice (x86_64 = 278). Le kernel lit les iovec à
734    // `iov` pour transférer des données userspace dans le pipe.
735    // **Pas de `readonly`** : les données peuvent être consommées (mode
736    // GIFT donne la propriété des pages au kernel).
737    unsafe {
738        core::arch::asm!(
739            "syscall",
740            in("rax") 278_i64,
741            in("rdi") i64::from(fd),
742            in("rsi") iov,
743            in("rdx") iov_len,
744            in("r10") i64::from(flags),
745            lateout("rax") ret,
746            lateout("rcx") _,
747            lateout("r11") _,
748            options(nostack, preserves_flags),
749        );
750    }
751    ret
752}
753
754#[cfg(target_arch = "aarch64")]
755#[inline]
756unsafe fn raw_syscall_vmsplice(fd: i32, iov: u64, iov_len: usize, flags: u32) -> i64 {
757    let ret: i64;
758    // SAFETY: SYS_vmsplice (aarch64 = 75). Args en X0..X3.
759    unsafe {
760        core::arch::asm!(
761            "svc 0",
762            in("x8") 75_i64,
763            inout("x0") i64::from(fd) => ret,
764            in("x1") iov,
765            in("x2") iov_len,
766            in("x3") i64::from(flags),
767            options(nostack, preserves_flags),
768        );
769    }
770    ret
771}
772
773// ─────────────────────────────────────────────────────────────────────────
774// Tests
775// ─────────────────────────────────────────────────────────────────────────
776
777#[cfg(test)]
778#[cfg(target_os = "linux")]
779mod tests {
780    use super::*;
781
782    /// `eventfd2` crée un FD avec CLOEXEC sans paniquer.
783    #[test]
784    fn eventfd2_cloexec_creates_fd() {
785        let efd = eventfd2(0, EventFdFlags::CLOEXEC).expect("eventfd2 avec CLOEXEC");
786        // Le FD est valide si on peut en obtenir une vue empruntée.
787        let _borrowed = efd.as_fd();
788    }
789
790    /// `EventFd::write` puis `EventFd::read` retournent la même valeur.
791    #[test]
792    fn eventfd_write_read_roundtrip() {
793        let efd = eventfd2(0, EventFdFlags::CLOEXEC).expect("eventfd2");
794        efd.write(42).expect("write 42");
795        let val = efd.read().expect("read");
796        assert_eq!(val, 42, "la valeur lue doit être 42");
797    }
798
799    /// Plusieurs écritures s'accumulent : le compteur est additif.
800    #[test]
801    fn eventfd_write_accumulates() {
802        let efd = eventfd2(0, EventFdFlags::CLOEXEC).expect("eventfd2");
803        efd.write(10).expect("write 10");
804        efd.write(5).expect("write 5");
805        let val = efd.read().expect("read");
806        // En mode normal, read retourne la somme et remet à 0.
807        assert_eq!(val, 15, "compteur doit être 15 (10+5)");
808    }
809
810    /// `eventfd2` avec valeur initiale non nulle.
811    #[test]
812    fn eventfd2_initial_value() {
813        let efd = eventfd2(7, EventFdFlags::CLOEXEC).expect("eventfd2 avec initial=7");
814        let val = efd.read().expect("read");
815        assert_eq!(val, 7, "valeur initiale doit être 7");
816    }
817
818    /// `EventFd::into_fd` transfère bien l'ownership.
819    #[test]
820    fn eventfd_into_fd_transfers_ownership() {
821        let efd = eventfd2(0, EventFdFlags::CLOEXEC).expect("eventfd2");
822        let _owned: OwnedFd = efd.into_fd();
823        // `efd` est consommé ; `_owned` sera fermé à la fin du scope.
824    }
825
826    /// `pipe2` crée deux FDs valides.
827    #[test]
828    fn pipe2_creates_valid_fds() {
829        let (read_fd, write_fd) = pipe2(PipeFlags::CLOEXEC).expect("pipe2");
830        // Les FDs sont valides si les BorrowedFd sont obtenables.
831        let _r = read_fd.as_fd();
832        let _w = write_fd.as_fd();
833    }
834
835    /// `splice` transfère des données de pipe à pipe.
836    #[test]
837    fn splice_pipe_to_pipe() {
838        let (r1, w1) = pipe2(PipeFlags::CLOEXEC).expect("pipe2 source");
839        let (r2, w2) = pipe2(PipeFlags::CLOEXEC).expect("pipe2 dest");
840
841        // Écrit "hello" dans w1 via vmsplice.
842        let data = b"hello";
843        let iov = [IoSlice::new(data)];
844        let written = vmsplice(w1.as_fd(), &iov, SpliceFlags::empty()).expect("vmsplice");
845        assert_eq!(written, 5, "vmsplice doit transférer 5 octets");
846
847        // splice de r1 vers w2.
848        let transferred =
849            splice(r1.as_fd(), None, w2.as_fd(), None, 5, SpliceFlags::empty()).expect("splice");
850        assert_eq!(transferred, 5, "splice doit transférer 5 octets");
851
852        // Lit le résultat depuis r2 via read(2) direct.
853        let mut buffer = [0u8; 5];
854        let buf_ptr: *mut u8 = buffer.as_mut_ptr();
855        // SAFETY: buffer est local valide pour 5 octets.
856        let n = unsafe { raw_syscall_read(r2.as_raw_fd(), buf_ptr as u64, 5) };
857        assert_eq!(n, 5);
858        assert_eq!(&buffer, b"hello");
859    }
860
861    /// `tee` copie sans consommer les données source.
862    #[test]
863    fn tee_copies_without_consuming() {
864        let (r1, w1) = pipe2(PipeFlags::CLOEXEC).expect("pipe2 source");
865        let (r2, w2) = pipe2(PipeFlags::CLOEXEC).expect("pipe2 dest");
866
867        // Écrit "world" dans w1.
868        let data = b"world";
869        let iov = [IoSlice::new(data)];
870        let written = vmsplice(w1.as_fd(), &iov, SpliceFlags::empty()).expect("vmsplice");
871        assert_eq!(written, 5);
872
873        // tee de r1 vers w2 : copie sans consommer.
874        let copied = tee(r1.as_fd(), w2.as_fd(), 5, SpliceFlags::empty()).expect("tee");
875        assert_eq!(copied, 5, "tee doit copier 5 octets");
876
877        // Les données sont disponibles dans r2.
878        let mut buf2 = [0u8; 5];
879        // SAFETY: `buf2` est un tableau local de 5 octets ; on passe son pointeur et
880        // la taille exacte (5) — le kernel y écrit au plus 5 octets.
881        let n2 = unsafe { raw_syscall_read(r2.as_raw_fd(), buf2.as_mut_ptr() as u64, 5) };
882        assert_eq!(n2, 5);
883        assert_eq!(&buf2, b"world");
884
885        // Et encore disponibles dans r1 (tee ne consomme pas).
886        let mut buf1 = [0u8; 5];
887        // SAFETY: `buf1` est un tableau local de 5 octets ; on passe son pointeur et
888        // la taille exacte (5) — le kernel y écrit au plus 5 octets.
889        let n1 = unsafe { raw_syscall_read(r1.as_raw_fd(), buf1.as_mut_ptr() as u64, 5) };
890        assert_eq!(n1, 5);
891        assert_eq!(&buf1, b"world");
892    }
893
894    /// `vmsplice` écrit dans un pipe et les données sont lisibles.
895    #[test]
896    fn vmsplice_writes_to_pipe() {
897        let (r, w) = pipe2(PipeFlags::CLOEXEC).expect("pipe2");
898        let data = b"vmsplice-test";
899        let iov = [IoSlice::new(data)];
900
901        let written = vmsplice(w.as_fd(), &iov, SpliceFlags::empty()).expect("vmsplice");
902        assert_eq!(written, data.len(), "vmsplice doit écrire tous les octets");
903
904        let mut buffer = [0u8; 13];
905        // SAFETY: `buffer` est un tableau local de 13 octets ; on passe son pointeur
906        // et la taille exacte (13) — le kernel y écrit au plus 13 octets.
907        let n = unsafe { raw_syscall_read(r.as_raw_fd(), buffer.as_mut_ptr() as u64, 13) };
908        assert_eq!(n, 13);
909        assert_eq!(&buffer, b"vmsplice-test");
910    }
911
912    /// `vmsplice` avec plusieurs iovec.
913    #[test]
914    fn vmsplice_multiple_iov() {
915        let (r, w) = pipe2(PipeFlags::CLOEXEC).expect("pipe2");
916        let a = b"foo";
917        let b_data = b"bar";
918        let iov = [IoSlice::new(a), IoSlice::new(b_data)];
919
920        let written = vmsplice(w.as_fd(), &iov, SpliceFlags::empty()).expect("vmsplice multi");
921        assert_eq!(written, 6, "6 octets en tout");
922
923        let mut buffer = [0u8; 6];
924        // SAFETY: `buffer` est un tableau local de 6 octets ; on passe son pointeur
925        // et la taille exacte (6) — le kernel y écrit au plus 6 octets.
926        let n = unsafe { raw_syscall_read(r.as_raw_fd(), buffer.as_mut_ptr() as u64, 6) };
927        assert_eq!(n, 6);
928        assert_eq!(&buffer, b"foobar");
929    }
930
931    // Les bras `Some(p)` des offsets de `splice` sont couverts de façon
932    // déterministe par `splice_with_some_offsets_on_invalid_fds_returns_ebadf`
933    // (ci-dessus) ; le chemin succès (offset `None`) l'est par
934    // `splice_pipe_to_pipe`. Pas besoin d'un test memfd conditionnel.
935
936    /// Helper interne pour memfd_create dans les tests (distinct de raw_syscall_*
937    /// du module parent, qui n'est pas `pub` mais accessible dans ce sous-module).
938    #[cfg(target_arch = "x86_64")]
939    unsafe fn test_memfd_create(nr: i64, name: u64, flags: u64) -> i64 {
940        let ret: i64;
941        // SAFETY: memfd_create (numéro `nr`). `name` pointe une chaîne C valide et
942        // NUL-terminée (lue par le kernel) ; retourne un FD ≥ 0 ou un errno négatif.
943        unsafe {
944            core::arch::asm!(
945                "syscall",
946                in("rax") nr,
947                in("rdi") name,
948                in("rsi") flags,
949                lateout("rax") ret,
950                lateout("rcx") _,
951                lateout("r11") _,
952                options(nostack, preserves_flags),
953            );
954        }
955        ret
956    }
957
958    #[cfg(target_arch = "aarch64")]
959    unsafe fn test_memfd_create(nr: i64, name: u64, flags: u64) -> i64 {
960        let ret: i64;
961        // SAFETY: memfd_create (numéro `nr`). `name` pointe une chaîne C valide et
962        // NUL-terminée (lue par le kernel) ; retourne un FD ≥ 0 ou un errno négatif.
963        unsafe {
964            core::arch::asm!(
965                "svc 0",
966                in("x8") nr,
967                inout("x0") name => ret,
968                in("x1") flags,
969                options(nostack, preserves_flags),
970            );
971        }
972        ret
973    }
974
975    // ── eventfd error path ─────────────────────────────────────────────────
976
977    #[test]
978    fn eventfd2_read_empty_nonblocking_returns_eagain() {
979        use air_sys_types::Errno;
980        // Couvre la branche d'erreur (`ret < 0`) de `EventFd::read` sans fd
981        // invalide : un eventfd non bloquant au compteur 0 renvoie `EAGAIN`
982        // (la lecture bloquerait). FD valide → pas de double close ni de
983        // course de réutilisation de fd entre threads de test.
984        let efd = eventfd2(0, EventFdFlags::CLOEXEC | EventFdFlags::NONBLOCK).expect("eventfd2");
985        let err = efd.read().unwrap_err();
986        assert_eq!(err, Errno::EAGAIN);
987    }
988
989    #[test]
990    fn eventfd_write_u64_max_returns_einval() {
991        // Couvre la branche d'erreur (`ret < 0`) de `EventFd::write` : la
992        // valeur sentinelle `u64::MAX` (débordement) est rejetée par le
993        // kernel avec EINVAL (cf. man eventfd(2)).
994        let efd = eventfd2(0, EventFdFlags::CLOEXEC).expect("eventfd2");
995        let err = efd.write(u64::MAX).unwrap_err();
996        assert_eq!(err, Errno::EINVAL);
997    }
998
999    #[test]
1000    fn splice_with_some_offsets_on_invalid_fds_returns_ebadf() {
1001        // Couvre les bras `Some(p)` des match `offset_in`/`offset_out` de `splice`
1002        // (évalués AVANT le syscall, donc atteints même si l'appel échoue).
1003        // Deux fd au-delà de toute table de fd → EBADF déterministe, sans
1004        // dépendre de `memfd_create`.
1005        let mut offset_in: u64 = 0;
1006        let mut offset_out: u64 = 0;
1007        // SAFETY: borrow_raw ne crée que des vues ; aucun fd n'est fermé.
1008        let fin = unsafe { BorrowedFd::borrow_raw(i32::MAX) };
1009        let fout = unsafe { BorrowedFd::borrow_raw(i32::MAX) };
1010        let err = splice(
1011            fin,
1012            Some(&mut offset_in),
1013            fout,
1014            Some(&mut offset_out),
1015            4,
1016            SpliceFlags::empty(),
1017        )
1018        .unwrap_err();
1019        assert_eq!(err, Errno::EBADF);
1020    }
1021
1022    #[test]
1023    fn vmsplice_on_invalid_fd_returns_ebadf() {
1024        // Couvre la branche d'erreur (`ret < 0`) de `vmsplice` : un fd
1025        // au-delà de toute limite (`i32::MAX`) n'est pas ouvert → EBADF.
1026        let data = [b'x'];
1027        let iov = [IoSlice::new(&data)];
1028        // SAFETY: borrow_raw ne crée qu'une vue ; aucun fd n'est fermé. Le
1029        // fd i32::MAX dépasse toute table de fd → EBADF déterministe.
1030        let fd = unsafe { BorrowedFd::borrow_raw(i32::MAX) };
1031        let err = vmsplice(fd, &iov, SpliceFlags::empty()).unwrap_err();
1032        assert_eq!(err, Errno::EBADF);
1033    }
1034
1035    // Couvrent les deux opérandes du `debug_assert!(ret < 0 && ret > -4096)`
1036    // de `errno_from_negative_syscall_ret` (`ret = 0` ; `ret = -5000`).
1037    #[test]
1038    #[should_panic(expected = "ret < 0 && ret > -4096")]
1039    fn errno_from_negative_syscall_ret_panics_on_non_negative() {
1040        let _ = errno_from_negative_syscall_ret(0);
1041    }
1042
1043    #[test]
1044    #[should_panic(expected = "ret < 0 && ret > -4096")]
1045    fn errno_from_negative_syscall_ret_panics_below_errno_range() {
1046        let _ = errno_from_negative_syscall_ret(-5000);
1047    }
1048
1049    // ── tee error path ────────────────────────────────────────────────────
1050
1051    #[test]
1052    fn tee_einval_on_non_pipe() {
1053        use air_sys_types::Errno;
1054        #[cfg(target_arch = "x86_64")]
1055        let nr_memfd: i64 = 319;
1056        #[cfg(target_arch = "aarch64")]
1057        let nr_memfd: i64 = 279;
1058        let name = c"ipc-tee-test";
1059        // SAFETY: `name` est une chaîne C statique valide et NUL-terminée ; le helper
1060        // `test_memfd_create` ne lit que ce pointeur (aucune écriture mémoire).
1061        let ret = unsafe { test_memfd_create(nr_memfd, name.as_ptr() as u64, 1) };
1062        if ret < 0 {
1063            return;
1064        }
1065        #[allow(clippy::cast_possible_truncation)]
1066        // SAFETY: `ret >= 0` ici, donc c'est un FD fraîchement créé par memfd_create,
1067        // non possédé ailleurs : on en prend la propriété exclusive.
1068        let fd = unsafe { OwnedFd::from_raw_fd(ret as i32) };
1069
1070        let (_, w) = pipe2(PipeFlags::CLOEXEC).expect("pipe2");
1071        // tee avec un non-pipe comme source doit retourner EINVAL.
1072        let err = tee(fd.as_fd(), w.as_fd(), 4, SpliceFlags::empty()).unwrap_err();
1073        assert_eq!(err, Errno::EINVAL);
1074    }
1075}