Skip to main content

air_sys_syscall/
mem.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 `mem` — mappings, protection, memfd, etc.
6//!
7//! Cf. `docs/specs/layer-0/family-mem.md`.
8
9#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
10compile_error!("air-sys-syscall::mem supporte uniquement x86_64 et aarch64 (ADR-014).");
11
12use air_sys_types::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
13use core::ffi::CStr;
14use core::num::NonZeroI32;
15use core::ptr::NonNull;
16use core::sync::atomic::AtomicU32;
17
18// `MmapRegion` partage sa durée de vie via `Arc` (un seul compte fort par
19// mapping). Sous `--cfg loom`, on bascule sur l'`Arc` modélisé par loom pour
20// **prouver** le protocole de refcount (clones/drops concurrents de la garde
21// de vivacité) ; sinon `std::sync::Arc`. Même schéma que le protocole d'anneau
22// io_uring (`io_uring::ring`).
23#[cfg(not(loom))]
24use alloc::sync::Arc;
25#[cfg(loom)]
26use loom::sync::Arc;
27
28use air_sys_types::mem::{
29    MadviseAdvice, MapFlags, MappingPointer, MemfdFlags, MlockFlags, MlockallFlags, MremapFlags,
30    MsyncFlags, ProtectionFlags, RemoteIoSlice,
31};
32use air_sys_types::net::{IoSlice, IoSliceMut};
33use air_sys_types::{Errno, Pid};
34
35// ─────────────────────────────────────────────────────────────────────────
36// Helper commun : conversion d'une valeur de retour syscall négative en Errno.
37// ─────────────────────────────────────────────────────────────────────────
38
39fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
40    debug_assert!(ret < 0 && ret > -4096);
41    #[allow(clippy::cast_possible_truncation)]
42    let raw = ret.wrapping_neg() as i32;
43    let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
44    Errno::from_nonzero(nz)
45}
46
47// ─────────────────────────────────────────────────────────────────────────
48// Mapping — type RAII wrappant une plage mémoire mappée.
49//
50// Note d'architecture : `Mapping` est placé dans `air-sys-syscall::mem`
51// et non dans `air-sys-types`, contrairement à ce qu'indique la spec,
52// car son `Drop` doit appeler `munmap` (un syscall). Placer un appel
53// syscall dans `air-sys-types` violerait l'invariant d'architecture
54// « les types ne font pas de syscalls ». Voir QUESTIONS-implementation.md.
55// ─────────────────────────────────────────────────────────────────────────
56
57/// Mapping mémoire RAII.
58///
59/// Crée automatiquement par [`mmap_anonymous`], [`mmap_file`] et
60/// retourné par [`mremap`]. Appelle `munmap(2)` automatiquement à la
61/// destruction pour éviter tout fuite de mémoire mappée.
62///
63/// Contrairement à un [`MappingPointer`] brut, ce type garantit que
64/// `munmap` sera appelé exactement une fois, même en cas de panique.
65///
66/// Le `Drop` automatique ignore l'erreur de `munmap` (cas extrêmement
67/// rare sur Linux moderne). Pour récupérer une éventuelle erreur,
68/// utiliser [`munmap`] qui consomme le `Mapping` explicitement.
69#[derive(Debug)]
70pub struct Mapping {
71    addr: NonNull<u8>,
72    len: usize,
73}
74
75// SAFETY: voir MappingPointer — la synchronisation est à la charge de l'appelant.
76unsafe impl Send for Mapping {}
77unsafe impl Sync for Mapping {}
78
79impl Mapping {
80    /// Retourne un pointeur constant vers le début du mapping.
81    #[must_use]
82    pub fn as_ptr(&self) -> *const u8 {
83        self.addr.as_ptr()
84    }
85
86    /// Retourne un pointeur mutable vers le début du mapping.
87    #[must_use]
88    pub fn as_mut_ptr(&mut self) -> *mut u8 {
89        self.addr.as_ptr()
90    }
91
92    /// Longueur du mapping en octets.
93    #[must_use]
94    pub fn len(&self) -> usize {
95        self.len
96    }
97
98    /// Retourne `true` si la longueur est zéro (mapping vide).
99    #[must_use]
100    pub fn is_empty(&self) -> bool {
101        self.len == 0
102    }
103
104    /// Vue en lecture du mapping sous forme de slice d'octets.
105    ///
106    /// # Safety
107    ///
108    /// Le mapping doit être lisible (au moins `ProtectionFlags::READ`) et le
109    /// contenu doit être valide pour la durée de vie de la référence
110    /// retournée.
111    #[must_use]
112    pub fn as_slice(&self) -> &[u8] {
113        // SAFETY: address est NonNull, length est la taille du mapping, et
114        // la durée de vie est liée à &self.
115        unsafe { core::slice::from_raw_parts(self.addr.as_ptr(), self.len) }
116    }
117
118    /// Vue en écriture du mapping sous forme de slice d'octets.
119    ///
120    /// # Safety
121    ///
122    /// Le mapping doit être accessible en écriture (au moins
123    /// `ProtectionFlags::WRITE`). L'appelant est responsable de ne pas créer
124    /// de références Rust aliasées sur la même plage mémoire.
125    pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] {
126        // SAFETY: préconditions documentées dans la section # Safety.
127        unsafe { core::slice::from_raw_parts_mut(self.addr.as_ptr(), self.len) }
128    }
129}
130
131impl Drop for Mapping {
132    fn drop(&mut self) {
133        // Appel munmap en ignorant l'erreur (rare sur Linux moderne).
134        // Pour récupérer l'erreur, utiliser munmap() explicite.
135        let _ = raw_munmap(self.addr.as_ptr(), self.len);
136    }
137}
138
139/// Appel syscall munmap brut, partagé entre Mapping::drop et munmap().
140fn raw_munmap(address: *mut u8, length: usize) -> Result<(), Errno> {
141    // SAFETY: munmap(2) ne lit ni n'écrit de mémoire utilisateur (il
142    // libère le mapping). address et length sont fournis tels quels au kernel.
143    let ret = unsafe { raw_syscall_munmap(address as u64, length) };
144    if ret < 0 {
145        return Err(errno_from_negative_syscall_ret(ret));
146    }
147    Ok(())
148}
149
150// ─────────────────────────────────────────────────────────────────────────
151// Helper interne : appel mmap brut commun.
152// ─────────────────────────────────────────────────────────────────────────
153
154/// Appel mmap brut partagé par mmap_anonymous, mmap_file et mmap_fixed.
155///
156/// # Safety
157///
158/// L'appelant doit fournir des arguments valides pour le syscall mmap(2).
159/// En particulier :
160/// - `fd` doit être -1 pour les mappings anonymes, ou un fd ouvert valide.
161/// - `offset` doit être aligné sur la page size.
162/// - Si `flags` contient `MAP_FIXED`, `address` peut écraser des mappings existants.
163unsafe fn raw_mmap(
164    address: u64,
165    length: usize,
166    prot: i32,
167    flags: i32,
168    fd: i64,
169    offset: u64,
170) -> Result<NonNull<u8>, Errno> {
171    // SAFETY: mmap(2) mappe de la mémoire ; l'appelant garantit les préconditions.
172    let ret = unsafe { raw_syscall_mmap(address, length, prot, flags, fd, offset) };
173    if ret < 0 {
174        return Err(errno_from_negative_syscall_ret(ret));
175    }
176    // ret est l'adresse du mapping (i64 positif, jamais 0 sur Linux).
177    #[allow(clippy::cast_sign_loss)]
178    let addr_raw = ret as u64 as *mut u8;
179    let address = NonNull::new(addr_raw).expect("mmap returned NULL (impossible)");
180    Ok(address)
181}
182
183// ─────────────────────────────────────────────────────────────────────────
184// Fonctions publiques — Mappings
185// ─────────────────────────────────────────────────────────────────────────
186
187/// Crée un mapping mémoire anonyme (sans fichier sous-jacent).
188///
189/// Wrappeur de `mmap(2)` avec `MAP_ANONYMOUS | MAP_PRIVATE` ajoutés
190/// automatiquement par le wrapper. Le flag [`MapFlags::ANONYMOUS`] est
191/// toujours positionné en interne — l'appelant n'a pas besoin de le
192/// passer dans `flags`.
193///
194/// La durée de vie du mapping est gérée par le [`Mapping`] retourné :
195/// `munmap` est appelé automatiquement à la destruction.
196///
197/// # Parameters
198///
199/// - `length` : taille souhaitée en octets. Le kernel arrondit à la page
200///   supérieure ; la taille effective est accessible via `Mapping::length()`.
201/// - `prot` : protections d'accès initiales.
202/// - `flags` : flags de comportement (sans `ANONYMOUS` ni `PRIVATE`, ajoutés
203///   automatiquement).
204///
205/// # Errors
206///
207/// - `EINVAL` : `length` est zéro, ou flags incompatibles (`SHARED | PRIVATE`).
208/// - `ENOMEM` : mémoire virtuelle ou physique insuffisante.
209/// - `EACCES` : permissions refusées par le kernel.
210///
211/// # Examples
212///
213/// ```no_run
214/// use air_sys_syscall::mem::mmap_anonymous;
215/// use air_sys_types::mem::{MapFlags, ProtectionFlags};
216///
217/// let mut m = mmap_anonymous(
218///     4096,
219///     ProtectionFlags::READ | ProtectionFlags::WRITE,
220///     MapFlags::PRIVATE,
221/// ).expect("mmap_anonymous");
222/// // munmap appelé automatiquement à la fin du scope
223/// ```
224pub fn mmap_anonymous(
225    length: usize,
226    prot: ProtectionFlags,
227    flags: MapFlags,
228) -> Result<Mapping, Errno> {
229    // MAP_ANONYMOUS | MAP_PRIVATE ajoutés systématiquement.
230    let effective_flags = (flags | MapFlags::ANONYMOUS | MapFlags::PRIVATE).bits();
231    // SAFETY: mmap anonyme — fd=-1, offset=0, address=0 (any). Pas d'accès
232    // à de la mémoire utilisateur existante.
233    let address = unsafe { raw_mmap(0, length, prot.bits(), effective_flags, -1, 0) }?;
234    Ok(Mapping {
235        addr: address,
236        len: length,
237    })
238}
239
240/// Crée un mapping de fichier.
241///
242/// Wrappeur de `mmap(2)` avec un FD fichier. `offset` doit être aligné
243/// sur la page size (vérification effectuée par le kernel).
244///
245/// # Parameters
246///
247/// - `fd` : FD du fichier à mapper.
248/// - `length` : longueur du mapping en octets.
249/// - `offset` : offset dans le fichier (doit être aligné sur la page size).
250/// - `prot` : protections d'accès.
251/// - `flags` : flags de mapping (`SHARED` ou `PRIVATE` obligatoire).
252///
253/// # Errors
254///
255/// - `EINVAL` : `offset` non aligné, `length` zéro, ou flags invalides.
256/// - `EACCES` : `prot` incompatible avec les permissions du FD.
257/// - `ENOMEM` : mémoire virtuelle insuffisante.
258/// - `ENODEV` : le FD référence un type de fichier non mappable.
259///
260/// # Examples
261///
262/// ```no_run
263/// use air_sys_syscall::mem::mmap_file;
264/// use air_sys_types::mem::{MapFlags, ProtectionFlags};
265/// use air_sys_types::fd::BorrowedFd;
266///
267/// // fd = FD d'un fichier de 4096 octets déjà ouvert
268/// # fn example(fd: BorrowedFd<'_>) {
269/// let m = mmap_file(fd, 4096, 0, ProtectionFlags::READ, MapFlags::PRIVATE)
270///     .expect("mmap_file");
271/// let _data: &[u8] = m.as_slice();
272/// # }
273/// ```
274pub fn mmap_file(
275    fd: BorrowedFd<'_>,
276    length: usize,
277    offset: u64,
278    prot: ProtectionFlags,
279    flags: MapFlags,
280) -> Result<Mapping, Errno> {
281    let raw_fd = i64::from(fd.as_raw_fd());
282    // SAFETY: fd est un BorrowedFd valide, donc le fd numérique est ouvert
283    // pour la durée de l'appel. offset est passé tel quel au kernel.
284    let address = unsafe { raw_mmap(0, length, prot.bits(), flags.bits(), raw_fd, offset) }?;
285    Ok(Mapping {
286        addr: address,
287        len: length,
288    })
289}
290
291/// Mappe à une adresse spécifique.
292///
293/// Wrappeur de `mmap(2)` avec `MAP_FIXED` ou `MAP_FIXED_NOREPLACE`.
294/// `unsafe` car peut écraser des mappings existants si `MAP_FIXED`
295/// est utilisé sans `FIXED_NOREPLACE`.
296///
297/// # Safety
298///
299/// L'appelant doit garantir que :
300/// 1. `address` est aligné sur la page size.
301/// 2. Si `MapFlags::FIXED` est dans `flags`, les mappings existants à
302///    cette adresse peuvent être détruits.
303/// 3. Préférer `MapFlags::FIXED_NOREPLACE` pour obtenir `EEXIST` en cas
304///    de collision plutôt que d'écraser silencieusement.
305///
306/// # Parameters
307///
308/// - `address` : adresse de base souhaitée.
309/// - `length` : longueur en octets.
310/// - `prot` : protections.
311/// - `flags` : flags de mapping (doit contenir `FIXED` ou `FIXED_NOREPLACE`).
312/// - `fd` : FD fichier optionnel (`None` pour mapping anonyme).
313/// - `offset` : offset dans le fichier (0 pour mapping anonyme).
314///
315/// # Errors
316///
317/// - `EINVAL` : adresse non alignée ou flags invalides.
318/// - `EEXIST` : adresse occupée et `FIXED_NOREPLACE` est utilisé.
319/// - `ENOMEM` : mémoire insuffisante.
320///
321/// # Examples
322///
323/// ```no_run
324/// use air_sys_syscall::mem::mmap_fixed;
325/// use air_sys_types::mem::{MapFlags, MappingPointer, ProtectionFlags};
326/// use core::ptr::NonNull;
327///
328/// # fn example(address: NonNull<u8>) {
329/// // unsafe : peut écraser des mappings existants si MAP_FIXED utilisé
330/// let ptr = unsafe {
331///     mmap_fixed(
332///         address,
333///         4096,
334///         ProtectionFlags::READ | ProtectionFlags::WRITE,
335///         MapFlags::FIXED_NOREPLACE | MapFlags::PRIVATE | MapFlags::ANONYMOUS,
336///         None,
337///         0,
338///     )
339/// }.expect("mmap_fixed");
340/// # }
341/// ```
342pub unsafe fn mmap_fixed(
343    address: NonNull<u8>,
344    length: usize,
345    prot: ProtectionFlags,
346    flags: MapFlags,
347    fd: Option<BorrowedFd<'_>>,
348    offset: u64,
349) -> Result<MappingPointer, Errno> {
350    let raw_fd: i64 = match fd {
351        Some(f) => i64::from(f.as_raw_fd()),
352        None => -1,
353    };
354    // Pour mapping anonyme sans fd, on ajoute MAP_ANONYMOUS.
355    let effective_flags = if fd.is_none() {
356        (flags | MapFlags::ANONYMOUS).bits()
357    } else {
358        flags.bits()
359    };
360    // SAFETY: l'appelant garantit que address est aligné sur la page size
361    // et que MAP_FIXED / MAP_FIXED_NOREPLACE est positionné.
362    let mapped_addr = unsafe {
363        raw_mmap(
364            address.as_ptr() as u64,
365            length,
366            prot.bits(),
367            effective_flags,
368            raw_fd,
369            offset,
370        )
371    }?;
372    Ok(MappingPointer {
373        address: mapped_addr,
374        length,
375    })
376}
377
378// ─────────────────────────────────────────────────────────────────────────
379// MmapRegion — mapping partageable, compatible avec l'asynchronie io_uring.
380//
381// Cf. docs/specs/layer-0/family-mem-mmap-region.md.
382//
383// `MmapRegion` (`Arc<MmapRegionInner>`) est l'**opt-in partageable** : à la
384// différence de `Mapping` (RAII unique, zéro-coût, **inchangé**), sa durée de
385// vie peut être partagée avec le slot S1 d'une opération io_uring en vol
386// (`madvise`, `futex_*`) via une `MmapRegionLiveness`. Le kernel lit la
387// mémoire de la région **de façon asynchrone**, jusqu'à la complétion ; la
388// garde garée dans le slot empêche `munmap` tant que l'op est en vol. `munmap`
389// n'a lieu qu'au **dernier** drop (région utilisateur ET toutes les gardes) →
390// **ni use-after-unmap, ni fuite**.
391//
392// **Exception ADR-021 c.4 (« pas d'alloc dans le happy path »), documentée :**
393// `MmapRegion` fait **une** allocation (l'`Arc` inner). C'est admis car (1)
394// opt-in — `Mapping` reste zéro-coût ; (2) négligeable et amorti devant le
395// syscall `mmap` qui crée la région ; (3) le partage de durée de vie est
396// **intrinsèquement nécessaire** à la sûreté de l'usage asynchrone (clause
397// « nécessité documentée » d'ADR-021 c.4).
398// ─────────────────────────────────────────────────────────────────────────
399
400/// Compteur de démappages simulés pour les preuves de refcount (Miri/loom).
401/// Sous `--cfg loom`, c'est l'`AtomicUsize` modélisé par loom.
402#[cfg(all(any(test, loom), loom))]
403type ProbeCounter = loom::sync::atomic::AtomicUsize;
404/// Compteur de démappages simulés pour les preuves de refcount (tests/Miri).
405#[cfg(all(any(test, loom), not(loom)))]
406type ProbeCounter = core::sync::atomic::AtomicUsize;
407
408/// État interne partagé d'une [`MmapRegion`], détenu par un `Arc`. Son `Drop`,
409/// au **dernier** détenteur fort uniquement, appelle `munmap` (erreur ignorée,
410/// comme [`Mapping`]).
411struct MmapRegionInner {
412    /// Adresse de base du mapping.
413    ptr: NonNull<u8>,
414    /// Longueur en octets.
415    len: usize,
416    /// Protections du mapping (détermine la lisibilité de [`MmapRegion::bytes`]
417    /// et l'accès à [`MmapRegion::futex_word`]).
418    prot: ProtectionFlags,
419    /// Présent uniquement en test/Miri/loom : si `Some`, la région est
420    /// **fictive** (créée par `new_for_test`, pointeur non déréférencé) et
421    /// `drop_unmap` incrémente ce compteur au lieu d'appeler le vrai `munmap`.
422    /// `None` pour les régions réelles (issues d'un `mmap`).
423    #[cfg(any(test, loom))]
424    unmap_probe: Option<Arc<ProbeCounter>>,
425}
426
427// SAFETY: comme `Mapping`/`MappingPointer`, le pointeur désigne un mapping dont
428// la **synchronisation des accès** est à la charge de l'appelant. Le partage de
429// propriété (compteur fort de l'`Arc`) est lui-même thread-safe.
430unsafe impl Send for MmapRegionInner {}
431// SAFETY: idem `Send` ci-dessus.
432unsafe impl Sync for MmapRegionInner {}
433
434impl MmapRegionInner {
435    /// Démappe la région (dernier drop). Sous test/Miri/loom **avec** une région
436    /// fictive, **n'appelle pas** le vrai syscall (Miri/loom ne peuvent pas
437    /// exécuter l'`asm!`, et la région n'a pas de vrai mapping) : incrémente le
438    /// compteur de preuve. Une région réelle (`unmap_probe = None`) démappe
439    /// pour de vrai hors Miri/loom.
440    fn drop_unmap(&mut self) {
441        #[cfg(any(test, loom))]
442        if let Some(counter) = &self.unmap_probe {
443            counter.fetch_add(1, core::sync::atomic::Ordering::SeqCst);
444            return;
445        }
446        #[cfg(not(any(miri, loom)))]
447        {
448            // SAFETY: `ptr`/`len` proviennent d'un `mmap` réussi (constructeurs)
449            // et la région n'a pas encore été démappée — le **dernier** drop est
450            // unique par construction (compteur fort de l'`Arc`). `munmap`
451            // ignore l'erreur, comme `Mapping`.
452            let _ = raw_munmap(self.ptr.as_ptr(), self.len);
453        }
454    }
455}
456
457impl Drop for MmapRegionInner {
458    fn drop(&mut self) {
459        self.drop_unmap();
460    }
461}
462
463/// Mapping mmap **partageable** et compatible io_uring : ses pages restent
464/// valides tant qu'une opération io_uring en vol (`madvise`/`futex_*`) y
465/// référence de la mémoire. Compté par référence — `munmap` au **dernier**
466/// drop (la région **et** toutes les gardes de vivacité d'ops en vol).
467///
468/// Distinct de [`Mapping`] **à dessein** : `Mapping` reste l'usage synchrone
469/// unique/zéro-coût ; `MmapRegion` est l'opt-in partageable (une allocation
470/// `Arc`, cf. exception ADR-021 c.4 ci-dessus). Cloner incrémente le compteur
471/// fort ; aucune copie de pages, seulement du partage de propriété.
472#[derive(Clone)]
473pub struct MmapRegion {
474    /// Inner partagé. Le `munmap` n'a lieu qu'au drop du **dernier** `Arc`.
475    inner: Arc<MmapRegionInner>,
476}
477
478impl core::fmt::Debug for MmapRegion {
479    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
480        f.debug_struct("MmapRegion")
481            .field("ptr", &self.inner.ptr)
482            .field("len", &self.inner.len)
483            .field("prot", &self.inner.prot)
484            .finish()
485    }
486}
487
488impl MmapRegion {
489    /// Mapping anonyme partageable (équivalent [`mmap_anonymous`] rendant une
490    /// `MmapRegion`).
491    ///
492    /// # Errors
493    ///
494    /// Toute erreur de `mmap(2)` (cf. [`mmap_anonymous`]) : `EINVAL`, `ENOMEM`…
495    pub fn new_anonymous(
496        len: usize,
497        prot: ProtectionFlags,
498        flags: MapFlags,
499    ) -> Result<Self, Errno> {
500        let mapping = mmap_anonymous(len, prot, flags)?;
501        Ok(Self::from_mapping_with_prot(mapping, prot))
502    }
503
504    /// Mapping de fichier partageable (équivalent [`mmap_file`] rendant une
505    /// `MmapRegion`).
506    ///
507    /// # Errors
508    ///
509    /// Toute erreur de `mmap(2)` (cf. [`mmap_file`]) : `EINVAL`, `EACCES`…
510    pub fn from_file(
511        fd: BorrowedFd<'_>,
512        len: usize,
513        prot: ProtectionFlags,
514        flags: MapFlags,
515        offset: u64,
516    ) -> Result<Self, Errno> {
517        let mapping = mmap_file(fd, len, offset, prot, flags)?;
518        Ok(Self::from_mapping_with_prot(mapping, prot))
519    }
520
521    /// Promeut un [`Mapping`] unique en région partageable : **transfère** la
522    /// responsabilité du `munmap` à l'inner partagé. Le `Mapping` est consommé
523    /// et **neutralisé** (son `Drop` ne munmap plus) → **pas de double
524    /// `munmap`**. Une seule allocation (l'inner).
525    ///
526    /// **`prot` explicite (obligatoire).** Un `Mapping` nu ne mémorise pas ses
527    /// protections ; l'appelant **doit** fournir le `prot` avec lequel le
528    /// `Mapping` a été créé, sinon [`MmapRegion::bytes`] / [`MmapRegion::futex_word`]
529    /// décideraient de l'accessibilité sur une base fausse (référence qui
530    /// **faute** à l'usage). Il n'y a **pas** de défaut `READ|WRITE` : le `prot`
531    /// passé fait foi.
532    #[must_use]
533    pub fn from_mapping(mapping: Mapping, prot: ProtectionFlags) -> Self {
534        Self::from_mapping_with_prot(mapping, prot)
535    }
536
537    /// Cœur commun : consomme le `Mapping` (sans déclencher son `Drop`) et
538    /// construit l'inner partagé avec les protections connues.
539    fn from_mapping_with_prot(mapping: Mapping, prot: ProtectionFlags) -> Self {
540        // ManuallyDrop empêche `Mapping::drop` d'appeler `munmap` : la
541        // responsabilité passe à l'inner (pas de double munmap).
542        let md = core::mem::ManuallyDrop::new(mapping);
543        let inner = MmapRegionInner {
544            ptr: md.addr,
545            len: md.len,
546            prot,
547            #[cfg(any(test, loom))]
548            unmap_probe: None,
549        };
550        Self {
551            inner: Arc::new(inner),
552        }
553    }
554
555    /// Longueur du mapping en octets.
556    #[must_use]
557    pub fn len(&self) -> usize {
558        self.inner.len
559    }
560
561    /// `true` si la longueur est zéro.
562    #[must_use]
563    pub fn is_empty(&self) -> bool {
564        self.inner.len == 0
565    }
566
567    /// Pointeur brut vers le début du mapping (consommateurs avancés ; pas de
568    /// transfert de propriété — le `munmap` reste géré par la `MmapRegion`).
569    #[must_use]
570    pub fn as_ptr(&self) -> *const u8 {
571        self.inner.ptr.as_ptr()
572    }
573
574    /// Tranche d'octets en lecture, **bornée par construction**. Renvoie une
575    /// tranche **vide** si la région n'est pas lisible (`prot` sans `READ`) —
576    /// jamais de référence vers de la mémoire inaccessible.
577    #[must_use]
578    pub fn bytes(&self) -> &[u8] {
579        if !self.inner.prot.contains(ProtectionFlags::READ) {
580            return &[];
581        }
582        // SAFETY: région lisible (prot ⊇ READ), `ptr` non-null issu d'un mmap,
583        // `len` = taille mappée ; la durée de vie de la tranche est liée à
584        // `&self`, donc à l'`Arc` qui maintient le mapping vivant.
585        unsafe { core::slice::from_raw_parts(self.inner.ptr.as_ptr(), self.inner.len) }
586    }
587
588    /// Référence à un **mot futex** ([`AtomicU32`]) à `offset` octets.
589    ///
590    /// La référence est **mutable par partage** (`AtomicU32` : `store`/`load` via
591    /// `&`) ; la rendre sur des pages non inscriptibles serait *unsound* (une
592    /// écriture fauterait). La validation amont (Principe 4) exige donc que la
593    /// région soit **inscriptible** (`prot ⊇ WRITE`), `offset` **borné**
594    /// (`offset + 4 ≤ len`) et **aligné sur 4** (un futex est un `u32` aligné) —
595    /// sinon la référence n'est **pas** rendue.
596    ///
597    /// # Errors
598    ///
599    /// [`Errno::EINVAL`] si la région n'est pas inscriptible, ou si `offset` est
600    /// hors bornes ou non aligné sur 4.
601    pub fn futex_word(&self, offset: usize) -> Result<&AtomicU32, Errno> {
602        if !self.inner.prot.contains(ProtectionFlags::WRITE) {
603            return Err(Errno::EINVAL);
604        }
605        // Alignement sur 4 sans opérateur arithmétique nu (bit-and).
606        if offset & 0b11 != 0 {
607            return Err(Errno::EINVAL);
608        }
609        let end = offset.checked_add(4).ok_or(Errno::EINVAL)?;
610        if end > self.inner.len {
611            return Err(Errno::EINVAL);
612        }
613        // SAFETY: `offset` validé (aligné 4, `offset+4 ≤ len`) ⇒ `ptr+offset`
614        // désigne un `u32` **entièrement** dans le mapping inscriptible.
615        // `AtomicU32` a le même layout (et les mêmes exigences d'alignement)
616        // qu'un `u32` ; l'accès concurrent est admis (le kernel y accède aussi).
617        // La durée de vie est liée à `&self` (donc à l'`Arc`).
618        let word = unsafe { self.inner.ptr.as_ptr().add(offset).cast::<AtomicU32>() };
619        // SAFETY: voir ci-dessus — `word` est aligné, valide et vivant.
620        Ok(unsafe { &*word })
621    }
622
623    /// Produit une garde de vivacité (clone interne de l'`Arc`). Réservé aux
624    /// façades io_uring `submit_madvise`/`submit_futex_*` : la garde est garée
625    /// dans le slot S1 et libérée à la complétion → `munmap` au dernier drop.
626    pub(crate) fn liveness_handle(&self) -> MmapRegionLiveness {
627        MmapRegionLiveness {
628            inner: Arc::clone(&self.inner),
629        }
630    }
631
632    /// Construit une région **fictive** (sans `mmap`) pour les preuves de
633    /// refcount (Miri/loom) : le pointeur est *dangling* et **ne doit jamais**
634    /// être déréférencé (pas de `bytes()`/`futex_word()`) ; `munmap` est simulé
635    /// par l'incrément de `counter` au dernier drop.
636    #[cfg(any(test, loom))]
637    pub(crate) fn new_for_test(len: usize, counter: Arc<ProbeCounter>) -> Self {
638        Self {
639            inner: Arc::new(MmapRegionInner {
640                ptr: NonNull::<u8>::dangling(),
641                len,
642                prot: ProtectionFlags::READ | ProtectionFlags::WRITE,
643                unmap_probe: Some(counter),
644            }),
645        }
646    }
647
648    /// Nombre de détenteurs forts de l'inner (région + gardes en vol). Réservé
649    /// aux tests de refcount déterministes (non utilisé par la preuve loom).
650    #[cfg(any(test, loom))]
651    #[cfg_attr(loom, allow(dead_code))]
652    pub(crate) fn strong_count(&self) -> usize {
653        Arc::strong_count(&self.inner)
654    }
655}
656
657/// Garde de vivacité retenue par le slot S1 d'une opération io_uring en vol qui
658/// référence la région. Maintient les pages **mappées** sans donner d'accès :
659/// tant qu'une garde existe, `munmap` ne peut pas se produire.
660pub struct MmapRegionLiveness {
661    /// Clone fort de l'inner : maintient le compteur ≥ 1. Jamais relu — seul son
662    /// `Drop` (compteur −1) compte ; `munmap` survient au dernier drop.
663    inner: Arc<MmapRegionInner>,
664}
665
666impl core::fmt::Debug for MmapRegionLiveness {
667    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
668        f.debug_struct("MmapRegionLiveness")
669            .field("ptr", &self.inner.ptr)
670            .field("len", &self.inner.len)
671            .finish()
672    }
673}
674
675/// Dé-mappe explicitement un mapping et récupère une éventuelle erreur.
676///
677/// Consomme le [`Mapping`] ; l'appelant récupère l'erreur de `munmap`
678/// si elle survient (rare sur Linux moderne). Le `Drop` automatique de
679/// `Mapping` appelle aussi `munmap` mais ignore l'erreur.
680///
681/// # Errors
682///
683/// - `EINVAL` : adresse ou longueur invalide (ne se produit pas si le
684///   `Mapping` a été créé par ce module).
685///
686/// # Examples
687///
688/// ```no_run
689/// use air_sys_syscall::mem::{mmap_anonymous, munmap};
690/// use air_sys_types::mem::{MapFlags, ProtectionFlags};
691///
692/// let m = mmap_anonymous(4096, ProtectionFlags::READ, MapFlags::PRIVATE).expect("mmap");
693/// munmap(m).expect("munmap");
694/// ```
695pub fn munmap(mapping: Mapping) -> Result<(), Errno> {
696    // ManuallyDrop empêche le Drop de Mapping d'appeler munmap une seconde fois.
697    let md = core::mem::ManuallyDrop::new(mapping);
698    raw_munmap(md.addr.as_ptr(), md.len)
699}
700
701/// Libère un mapping par (**adresse, longueur**) brutes (`munmap(2)`) — **descellement
702/// additif [ADR-085](../../../docs/adrs/ADR-085-descellement-couche0-cumule-libc-std-fr.md)**
703/// pour la **face libc**, qui gère la vie du mapping par `(addr, len)` (comme le C `void
704/// *mmap`/`int munmap(void *, size_t)`), et **non** par l'objet RAII [`Mapping`]. La
705/// couche 1 [`air-memory`] l'expose à la libc via son `AirMemoryManager`.
706///
707/// # Safety
708/// `address`/`length` **doivent** désigner une région (ou un sous-ensemble de pages
709/// alignées) **effectivement mappée** par l'appelant : `munmap` d'une région invalide
710/// rend au mieux `EINVAL` (adresse non alignée), mais `munmap` d'une région **en cours
711/// d'usage** déprogramme de la mémoire vive (comme le `munmap` C). C'est le contrat C.
712pub unsafe fn munmap_raw(address: NonNull<u8>, length: usize) -> Result<(), Errno> {
713    // Délègue au wrapper interne : le kernel valide `(addr, len)` (alignement page).
714    raw_munmap(address.as_ptr(), length)
715}
716
717/// Redimensionne (et éventuellement déplace) un mapping existant.
718///
719/// Wrappeur de `mremap(2)`. Consomme le [`Mapping`] existant et retourne
720/// un nouveau [`Mapping`] à la nouvelle adresse.
721///
722/// # Parameters
723///
724/// - `mapping` : mapping source (ownership transféré).
725/// - `new_len` : nouvelle longueur souhaitée.
726/// - `flags` : [`MremapFlags`]. Inclure `MAYMOVE` pour autoriser le
727///   déplacement si le redimensionnement sur place est impossible.
728///
729/// # Errors
730///
731/// - `EINVAL` : longueur nulle ou flags invalides.
732/// - `ENOMEM` : mémoire insuffisante (avec `MAYMOVE` absent, si sur place
733///   impossible).
734/// - `EAGAIN` : mapping verrouillé en mémoire et RLIMIT atteint.
735///
736/// # Examples
737///
738/// ```no_run
739/// use air_sys_syscall::mem::{mmap_anonymous, mremap};
740/// use air_sys_types::mem::{MapFlags, MremapFlags, ProtectionFlags};
741///
742/// let m = mmap_anonymous(4096, ProtectionFlags::READ | ProtectionFlags::WRITE, MapFlags::PRIVATE)
743///     .expect("mmap");
744/// let m2 = mremap(m, 8192, MremapFlags::MAYMOVE).expect("mremap");
745/// ```
746pub fn mremap(mapping: Mapping, new_len: usize, flags: MremapFlags) -> Result<Mapping, Errno> {
747    // ManuallyDrop empêche le Drop de Mapping d'appeler munmap sur l'ancienne
748    // adresse, qui n'est plus valide après mremap.
749    let md = core::mem::ManuallyDrop::new(mapping);
750    let old_addr = md.addr.as_ptr() as u64;
751    let old_len = md.len;
752    // SAFETY: mremap(2) consomme l'ancien mapping et retourne un nouveau.
753    // L'adresse new_addr (arg4) est 0 car on ne passe pas MREMAP_FIXED.
754    let ret = unsafe { raw_syscall_mremap(old_addr, old_len, new_len, flags.bits(), 0_u64) };
755    if ret < 0 {
756        // Le mapping original n'a pas été modifié par le kernel en cas d'échec ;
757        // il faut éviter la fuite — on le restitue dans un nouveau Mapping.
758        // Cependant ManuallyDrop a déjà évité le drop. Si l'erreur est retournée,
759        // l'original reste mappé mais son handle Rust est perdu : fuite intentionnelle
760        // documentée (conforme à la spécification de la famille mem). L'erreur est
761        // remontée à l'appelant.
762        return Err(errno_from_negative_syscall_ret(ret));
763    }
764    // ret est la nouvelle adresse du mapping (i64 positif).
765    #[allow(clippy::cast_sign_loss)]
766    let addr_raw = ret as u64 as *mut u8;
767    let address = NonNull::new(addr_raw).expect("mremap returned NULL (impossible)");
768    Ok(Mapping {
769        addr: address,
770        len: new_len,
771    })
772}
773
774// ─────────────────────────────────────────────────────────────────────────
775// Protection
776// ─────────────────────────────────────────────────────────────────────────
777
778/// Modifie les permissions d'une plage mémoire mappée.
779///
780/// Wrappeur de `mprotect(2)`. Typiquement utilisé pour rétrograder les
781/// permissions après initialisation d'une zone en lecture seule.
782///
783/// # Parameters
784///
785/// - `address` : adresse de début (doit être alignée sur la page size).
786/// - `length` : longueur de la plage en octets.
787/// - `prot` : nouvelles permissions.
788///
789/// # Errors
790///
791/// - `EINVAL` : `address` non aligné, ou plage invalide.
792/// - `EACCES` : tentative d'ajouter `EXEC` sur un FS monté `noexec`.
793/// - `ENOMEM` : ressources kernel insuffisantes.
794///
795/// # Examples
796///
797/// ```no_run
798/// use air_sys_syscall::mem::{mmap_anonymous, mprotect};
799/// use air_sys_types::mem::{MapFlags, ProtectionFlags};
800/// use core::ptr::NonNull;
801///
802/// let mut m = mmap_anonymous(4096, ProtectionFlags::READ | ProtectionFlags::WRITE, MapFlags::PRIVATE)
803///     .expect("mmap");
804/// // Initialisez les données, puis passez en lecture seule :
805/// mprotect(
806///     NonNull::new(m.as_mut_ptr()).expect("non-null"),
807///     4096,
808///     ProtectionFlags::READ,
809/// ).expect("mprotect");
810/// ```
811pub fn mprotect(address: NonNull<u8>, length: usize, prot: ProtectionFlags) -> Result<(), Errno> {
812    // SAFETY: mprotect(2) modifie les permissions d'une plage mémoire.
813    // L'appelant garantit que address est aligné et que la plage est mappée.
814    let ret = unsafe { raw_syscall_mprotect(address.as_ptr() as u64, length, prot.bits()) };
815    if ret < 0 {
816        return Err(errno_from_negative_syscall_ret(ret));
817    }
818    Ok(())
819}
820
821// ─────────────────────────────────────────────────────────────────────────
822// Conseils kernel
823// ─────────────────────────────────────────────────────────────────────────
824
825/// Informe le kernel sur le pattern d'accès attendu à une plage mémoire.
826///
827/// Wrappeur de `madvise(2)`. Ces conseils sont des hints : le kernel peut
828/// les ignorer. Aucun effet sur la sémantique du programme — uniquement
829/// sur les performances.
830///
831/// # Parameters
832///
833/// - `address` : adresse de début (alignée sur la page size).
834/// - `length` : longueur en octets.
835/// - `advice` : conseil à donner au kernel.
836///
837/// # Errors
838///
839/// - `EINVAL` : adresse non alignée, plage invalide, ou conseil inconnu.
840/// - `EACCES` : `MADV_REMOVE` sur un mapping non partagé.
841/// - `ENOMEM` : plage non mappée.
842///
843/// # Examples
844///
845/// ```no_run
846/// use air_sys_syscall::mem::{madvise, mmap_anonymous};
847/// use air_sys_types::mem::{MadviseAdvice, MapFlags, ProtectionFlags};
848/// use core::ptr::NonNull;
849///
850/// let m = mmap_anonymous(65536, ProtectionFlags::READ | ProtectionFlags::WRITE, MapFlags::PRIVATE)
851///     .expect("mmap");
852/// madvise(
853///     NonNull::new(m.as_ptr() as *mut u8).expect("non-null"),
854///     65536,
855///     MadviseAdvice::Sequential,
856/// ).expect("madvise");
857/// ```
858pub fn madvise(address: NonNull<u8>, length: usize, advice: MadviseAdvice) -> Result<(), Errno> {
859    // MadviseAdvice est #[repr(i32)] donc `advice as i32` est une conversion
860    // exacte et sans perte.
861    #[allow(clippy::as_conversions)]
862    let advice_i32 = advice as i32;
863    // SAFETY: madvise(2) est un hint pour le kernel ; pas d'accès mémoire
864    // utilisateur par le syscall lui-même. L'appelant garantit que la plage
865    // [address, address+length) est mappée.
866    let ret = unsafe { raw_syscall_madvise(address.as_ptr() as u64, length, advice_i32) };
867    if ret < 0 {
868        return Err(errno_from_negative_syscall_ret(ret));
869    }
870    Ok(())
871}
872
873// ─────────────────────────────────────────────────────────────────────────
874// Verrouillage mémoire
875// ─────────────────────────────────────────────────────────────────────────
876
877/// Verrouille une plage mémoire en RAM (empêche le swap).
878///
879/// Wrappeur de `mlock(2)`. Nécessite soit des privilèges root, soit
880/// une limite `RLIMIT_MEMLOCK` suffisante.
881///
882/// # Errors
883///
884/// - `ENOMEM` : la plage contient des pages non mappées, ou
885///   `RLIMIT_MEMLOCK` serait dépassé.
886/// - `EPERM` : `RLIMIT_MEMLOCK` est zéro et le processus n'est pas
887///   privilégié.
888/// - `EINVAL` : adresse ou longueur invalide.
889///
890/// # Examples
891///
892/// ```no_run
893/// use air_sys_syscall::mem::{mlock, mmap_anonymous};
894/// use air_sys_types::mem::{MapFlags, ProtectionFlags};
895/// use core::ptr::NonNull;
896///
897/// let m = mmap_anonymous(4096, ProtectionFlags::READ | ProtectionFlags::WRITE, MapFlags::PRIVATE)
898///     .expect("mmap");
899/// mlock(NonNull::new(m.as_ptr() as *mut u8).expect("non-null"), 4096)
900///     .expect("mlock");
901/// ```
902pub fn mlock(address: NonNull<u8>, length: usize) -> Result<(), Errno> {
903    // SAFETY: mlock(2) demande au kernel de verrouiller une plage en RAM.
904    // L'appelant garantit que [address, address+length) est une plage mappée valide.
905    let ret = unsafe { raw_syscall_mlock(address.as_ptr() as u64, length) };
906    if ret < 0 {
907        return Err(errno_from_negative_syscall_ret(ret));
908    }
909    Ok(())
910}
911
912/// Verrouille une plage mémoire avec contrôle du moment du verrouillage.
913///
914/// Variante de `mlock` avec [`MlockFlags::ONFAULT`] (Linux 4.4+) pour
915/// un verrouillage différé au fur et à mesure des fautes mémoire.
916///
917/// # Errors
918///
919/// Voir [`mlock`].
920///
921/// # Examples
922///
923/// ```no_run
924/// use air_sys_syscall::mem::mlock2;
925/// use air_sys_types::mem::MlockFlags;
926/// use core::ptr::NonNull;
927///
928/// # fn example(address: NonNull<u8>) {
929/// mlock2(address, 4096, MlockFlags::ONFAULT).expect("mlock2");
930/// # }
931/// ```
932pub fn mlock2(address: NonNull<u8>, length: usize, flags: MlockFlags) -> Result<(), Errno> {
933    // SAFETY: mlock2(2) est identique à mlock avec un argument flags supplémentaire.
934    // L'appelant garantit que [address, address+length) est mappée.
935    let ret = unsafe { raw_syscall_mlock2(address.as_ptr() as u64, length, flags.bits()) };
936    if ret < 0 {
937        return Err(errno_from_negative_syscall_ret(ret));
938    }
939    Ok(())
940}
941
942/// Déverrouille une plage mémoire précédemment verrouillée.
943///
944/// # Errors
945///
946/// - `EINVAL` : adresse ou longueur invalide.
947///
948/// # Examples
949///
950/// ```no_run
951/// use air_sys_syscall::mem::munlock;
952/// use core::ptr::NonNull;
953///
954/// # fn example(address: NonNull<u8>) {
955/// munlock(address, 4096).expect("munlock");
956/// # }
957/// ```
958pub fn munlock(address: NonNull<u8>, length: usize) -> Result<(), Errno> {
959    // SAFETY: munlock(2) déverrouille une plage mémoire.
960    let ret = unsafe { raw_syscall_munlock(address.as_ptr() as u64, length) };
961    if ret < 0 {
962        return Err(errno_from_negative_syscall_ret(ret));
963    }
964    Ok(())
965}
966
967/// Verrouille toutes les pages du processus en RAM.
968///
969/// # Errors
970///
971/// - `EPERM` : non privilégié et `RLIMIT_MEMLOCK` insuffisant.
972/// - `ENOMEM` : mémoire physique insuffisante.
973///
974/// # Examples
975///
976/// ```no_run
977/// use air_sys_syscall::mem::mlockall;
978/// use air_sys_types::mem::MlockallFlags;
979///
980/// mlockall(MlockallFlags::CURRENT | MlockallFlags::FUTURE).expect("mlockall");
981/// ```
982pub fn mlockall(flags: MlockallFlags) -> Result<(), Errno> {
983    // SAFETY: mlockall(2) prend un seul scalaire i32. Pas d'accès mémoire.
984    let ret = unsafe { raw_syscall_mlockall(flags.bits()) };
985    if ret < 0 {
986        return Err(errno_from_negative_syscall_ret(ret));
987    }
988    Ok(())
989}
990
991/// Déverrouille toutes les pages du processus.
992///
993/// # Errors
994///
995/// Normalement sans erreur si `mlockall` a été appelé préalablement.
996///
997/// # Examples
998///
999/// ```no_run
1000/// use air_sys_syscall::mem::munlockall;
1001///
1002/// munlockall().expect("munlockall");
1003/// ```
1004pub fn munlockall() -> Result<(), Errno> {
1005    // SAFETY: munlockall(2) ne prend aucun argument. Pas d'accès mémoire.
1006    let ret = unsafe { raw_syscall_munlockall() };
1007    if ret < 0 {
1008        return Err(errno_from_negative_syscall_ret(ret));
1009    }
1010    Ok(())
1011}
1012
1013// ─────────────────────────────────────────────────────────────────────────
1014// memfd
1015// ─────────────────────────────────────────────────────────────────────────
1016
1017/// Crée un fichier mémoire anonyme (memfd).
1018///
1019/// Wrappeur de `memfd_create(2)` (Linux 3.17+, numéro x86_64: 319,
1020/// aarch64: 279). Primitive centrale pour la mémoire partagée moderne
1021/// sur Linux, utilisée par le data plane de Conduit (ADR-001).
1022///
1023/// Le FD retourné peut être :
1024/// - étendu avec `ftruncate(2)` ;
1025/// - mappé avec [`mmap_file`] ;
1026/// - partagé entre processus via `SCM_RIGHTS` sur un socket Unix ;
1027/// - scellé pour interdire des modifications futures.
1028///
1029/// # Parameters
1030///
1031/// - `name` : nom descriptif (visible dans `/proc/PID/fd/`, pas unique).
1032/// - `flags` : combinaison de [`MemfdFlags`]. `CLOEXEC` recommandé.
1033///
1034/// # Errors
1035///
1036/// - `EINVAL` : flags invalides ou `name` trop long.
1037/// - `EMFILE` : quota de FD du processus atteint.
1038/// - `ENFILE` : quota de FD système atteint.
1039/// - `ENOMEM` : mémoire kernel insuffisante.
1040///
1041/// # Examples
1042///
1043/// ```no_run
1044/// use air_sys_syscall::mem::memfd_create;
1045/// use air_sys_types::mem::MemfdFlags;
1046/// use core::ffi::CStr;
1047///
1048/// let fd = memfd_create(c"shared-buffer", MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING)
1049///     .expect("memfd_create");
1050/// ```
1051pub fn memfd_create(name: &CStr, flags: MemfdFlags) -> Result<OwnedFd, Errno> {
1052    let name_ptr = name.as_ptr() as u64;
1053    // SAFETY: memfd_create(2) lit la chaîne NUL-terminée à `name_ptr`.
1054    // `name` est un &CStr valide, donc la chaîne est correctement terminée.
1055    // Le syscall retourne un fd valide ou une erreur négative.
1056    let ret = unsafe { raw_syscall_memfd_create(name_ptr, flags.bits()) };
1057    if ret < 0 {
1058        return Err(errno_from_negative_syscall_ret(ret));
1059    }
1060    // Linux borne les fd à i32::MAX ; la troncature est exacte.
1061    #[allow(clippy::cast_possible_truncation)]
1062    let raw_fd = ret as i32;
1063    // SAFETY: le kernel a alloué un fd valide ; Air en prend la propriété.
1064    let owned = unsafe { OwnedFd::from_raw_fd(raw_fd) };
1065    Ok(owned)
1066}
1067
1068/// Crée un fichier mémoire secret non accessible au kernel.
1069///
1070/// Wrappeur de `memfd_secret(2)` (Linux 5.14+, numéro 447). Réservé
1071/// aux secrets cryptographiques de haute valeur.
1072///
1073/// # Parameters
1074///
1075/// - `flags` : réservé pour usage futur, doit être 0 actuellement.
1076///
1077/// # Errors
1078///
1079/// - `ENOSYS` : kernel < 5.14 ou support non compilé.
1080/// - `ENOMEM` : mémoire insuffisante.
1081/// - `EMFILE`/`ENFILE` : quotas FD atteints.
1082///
1083/// # Examples
1084///
1085/// ```no_run
1086/// use air_sys_syscall::mem::memfd_secret;
1087///
1088/// let fd = memfd_secret(0).expect("memfd_secret");
1089/// ```
1090pub fn memfd_secret(flags: u32) -> Result<OwnedFd, Errno> {
1091    // SAFETY: memfd_secret(2) prend un seul argument scalaire u32.
1092    // Pas d'accès mémoire utilisateur. Retourne un fd ou une erreur.
1093    let ret = unsafe { raw_syscall_memfd_secret(flags) };
1094    if ret < 0 {
1095        return Err(errno_from_negative_syscall_ret(ret));
1096    }
1097    // Linux borne les fd à i32::MAX ; la troncature est exacte.
1098    #[allow(clippy::cast_possible_truncation)]
1099    let raw_fd = ret as i32;
1100    // SAFETY: le kernel a alloué un fd valide ; Air en prend la propriété.
1101    let owned = unsafe { OwnedFd::from_raw_fd(raw_fd) };
1102    Ok(owned)
1103}
1104
1105// ─────────────────────────────────────────────────────────────────────────
1106// Synchronisation
1107// ─────────────────────────────────────────────────────────────────────────
1108
1109/// Force la synchronisation d'un mapping `MAP_SHARED` vers le fichier.
1110///
1111/// Wrappeur de `msync(2)`. Utile pour garantir que les modifications
1112/// d'un mapping partagé sont visibles après une panne.
1113///
1114/// # Parameters
1115///
1116/// - `address` : adresse de début (alignée sur la page size).
1117/// - `length` : longueur en octets.
1118/// - `flags` : [`MsyncFlags::SYNC`] (bloquant) ou `ASYNC` (non-bloquant).
1119///
1120/// # Errors
1121///
1122/// - `EINVAL` : adresse non alignée, flags invalides, ou `SYNC | ASYNC`
1123///   simultanés.
1124/// - `ENOMEM` : plage non entièrement mappée.
1125///
1126/// # Examples
1127///
1128/// ```no_run
1129/// use air_sys_syscall::mem::msync;
1130/// use air_sys_types::mem::MsyncFlags;
1131/// use core::ptr::NonNull;
1132///
1133/// # fn example(address: NonNull<u8>) {
1134/// msync(address, 4096, MsyncFlags::SYNC).expect("msync");
1135/// # }
1136/// ```
1137pub fn msync(address: NonNull<u8>, length: usize, flags: MsyncFlags) -> Result<(), Errno> {
1138    // SAFETY: msync(2) synchronise une plage de pages vers le fichier
1139    // sous-jacent. L'appelant garantit que [address, address+length) est mappée.
1140    let ret = unsafe { raw_syscall_msync(address.as_ptr() as u64, length, flags.bits()) };
1141    if ret < 0 {
1142        return Err(errno_from_negative_syscall_ret(ret));
1143    }
1144    Ok(())
1145}
1146
1147// ─────────────────────────────────────────────────────────────────────────
1148// Accès inter-processus
1149// ─────────────────────────────────────────────────────────────────────────
1150
1151/// Lit la mémoire d'un autre processus sans `ptrace`.
1152///
1153/// Wrappeur de `process_vm_readv(2)` (Linux 3.2+). Lit depuis les
1154/// segments `remote_iov` du processus `pid` dans `local_iov`.
1155///
1156/// # Parameters
1157///
1158/// - `pid` : PID cible (doit avoir les mêmes UID/GID ou `CAP_SYS_PTRACE`).
1159/// - `local_iov` : buffers locaux de destination.
1160/// - `remote_iov` : segments dans l'espace d'adressage du processus cible.
1161/// - `flags` : réservé, doit être 0.
1162///
1163/// # Errors
1164///
1165/// - `ESRCH` : processus cible introuvable.
1166/// - `EPERM` : permissions insuffisantes.
1167/// - `EINVAL` : `flags` non nul, ou `iov` dépasse `UIO_MAXIOV`.
1168/// - `EFAULT` : adresse locale ou distante invalide.
1169///
1170/// # Examples
1171///
1172/// ```no_run
1173/// use air_sys_syscall::mem::process_vm_readv;
1174/// use air_sys_types::{Pid, mem::RemoteIoSlice};
1175/// use air_sys_types::net::IoSliceMut;
1176///
1177/// # fn example(target: Pid, remote_addr: u64) {
1178/// let mut buffer = [0u8; 64];
1179/// let mut local = [IoSliceMut::new(&mut buffer)];
1180/// let remote = [RemoteIoSlice { addr: remote_addr, len: 64 }];
1181/// let n = process_vm_readv(target, &mut local, &remote, 0)
1182///     .expect("process_vm_readv");
1183/// # }
1184/// ```
1185pub fn process_vm_readv(
1186    pid: Pid,
1187    local_iov: &mut [IoSliceMut<'_>],
1188    remote_iov: &[RemoteIoSlice],
1189    flags: u32,
1190) -> Result<usize, Errno> {
1191    let local_ptr = local_iov.as_ptr() as u64;
1192    let local_cnt = local_iov.len() as u64;
1193    let remote_ptr = remote_iov.as_ptr() as u64;
1194    let remote_cnt = remote_iov.len() as u64;
1195    // SAFETY:
1196    // - process_vm_readv(2) lit la mémoire distante et écrit dans les
1197    //   buffers locaux décrits par local_iov.
1198    // - IoSliceMut est #[repr(C)] compatible avec `struct iovec` Linux.
1199    // - RemoteIoSlice est #[repr(C)] compatible avec `struct iovec` Linux
1200    //   (layout identique : { ptr/addr: u64, len: usize } sur LP64).
1201    // - Les slices sont valides pour la durée du syscall (références Rust).
1202    // - pid est un Pid valide (newtype sur i32).
1203    let ret = unsafe {
1204        raw_syscall_process_vm_readv(
1205            pid.as_raw(),
1206            local_ptr,
1207            local_cnt,
1208            remote_ptr,
1209            remote_cnt,
1210            u64::from(flags),
1211        )
1212    };
1213    if ret < 0 {
1214        return Err(errno_from_negative_syscall_ret(ret));
1215    }
1216    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1217    Ok(ret as usize)
1218}
1219
1220/// Écrit dans la mémoire d'un autre processus sans `ptrace`.
1221///
1222/// Wrappeur de `process_vm_writev(2)` (Linux 3.2+). Écrit depuis
1223/// `local_iov` vers les segments `remote_iov` du processus `pid`.
1224///
1225/// # Parameters
1226///
1227/// - `pid` : PID cible.
1228/// - `local_iov` : buffers locaux source.
1229/// - `remote_iov` : segments cibles dans l'espace d'adressage distant.
1230/// - `flags` : réservé, doit être 0.
1231///
1232/// # Errors
1233///
1234/// Voir [`process_vm_readv`].
1235///
1236/// # Examples
1237///
1238/// ```no_run
1239/// use air_sys_syscall::mem::process_vm_writev;
1240/// use air_sys_types::{Pid, mem::RemoteIoSlice};
1241/// use air_sys_types::net::IoSlice;
1242///
1243/// # fn example(target: Pid, remote_addr: u64) {
1244/// let data = b"hello";
1245/// let local = [IoSlice::new(data)];
1246/// let remote = [RemoteIoSlice { addr: remote_addr, len: data.len() }];
1247/// process_vm_writev(target, &local, &remote, 0).expect("process_vm_writev");
1248/// # }
1249/// ```
1250pub fn process_vm_writev(
1251    pid: Pid,
1252    local_iov: &[IoSlice<'_>],
1253    remote_iov: &[RemoteIoSlice],
1254    flags: u32,
1255) -> Result<usize, Errno> {
1256    let local_ptr = local_iov.as_ptr() as u64;
1257    let local_cnt = local_iov.len() as u64;
1258    let remote_ptr = remote_iov.as_ptr() as u64;
1259    let remote_cnt = remote_iov.len() as u64;
1260    // SAFETY:
1261    // - process_vm_writev(2) lit les buffers locaux décrits par local_iov
1262    //   et écrit dans la mémoire distante décrite par remote_iov.
1263    // - IoSlice est #[repr(C)] compatible avec `struct iovec` Linux.
1264    // - RemoteIoSlice est #[repr(C)] compatible avec `struct iovec` Linux.
1265    // - Les slices sont valides pour la durée du syscall.
1266    // - pid est un Pid valide (newtype sur i32).
1267    let ret = unsafe {
1268        raw_syscall_process_vm_writev(
1269            pid.as_raw(),
1270            local_ptr,
1271            local_cnt,
1272            remote_ptr,
1273            remote_cnt,
1274            u64::from(flags),
1275        )
1276    };
1277    if ret < 0 {
1278        return Err(errno_from_negative_syscall_ret(ret));
1279    }
1280    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1281    Ok(ret as usize)
1282}
1283
1284// ─────────────────────────────────────────────────────────────────────────
1285// Helpers syscall bruts — mmap
1286// ─────────────────────────────────────────────────────────────────────────
1287
1288#[cfg(target_arch = "x86_64")]
1289#[inline]
1290unsafe fn raw_syscall_mmap(
1291    address: u64,
1292    length: usize,
1293    prot: i32,
1294    flags: i32,
1295    fd: i64,
1296    offset: u64,
1297) -> i64 {
1298    let ret: i64;
1299    // SAFETY: SYS_mmap (x86_64 = 9). 6 arguments : rdi, rsi, rdx, r10, r8, r9.
1300    // Le kernel mappe une plage de mémoire et retourne l'adresse ou -errno.
1301    unsafe {
1302        core::arch::asm!(
1303            "syscall",
1304            in("rax") 9_i64,
1305            in("rdi") address,
1306            in("rsi") length,
1307            in("rdx") i64::from(prot),
1308            in("r10") i64::from(flags),
1309            in("r8")  fd,
1310            in("r9")  offset,
1311            lateout("rax") ret,
1312            lateout("rcx") _,
1313            lateout("r11") _,
1314            options(nostack, preserves_flags),
1315        );
1316    }
1317    ret
1318}
1319
1320#[cfg(target_arch = "aarch64")]
1321#[inline]
1322unsafe fn raw_syscall_mmap(
1323    address: u64,
1324    length: usize,
1325    prot: i32,
1326    flags: i32,
1327    fd: i64,
1328    offset: u64,
1329) -> i64 {
1330    let ret: i64;
1331    // SAFETY: SYS_mmap (aarch64 = 222). x8=nr, x0-x5=args, retour en x0.
1332    unsafe {
1333        core::arch::asm!(
1334            "svc 0",
1335            in("x8")  222_i64,
1336            inout("x0") address => ret,
1337            in("x1")  length,
1338            in("x2")  i64::from(prot),
1339            in("x3")  i64::from(flags),
1340            in("x4")  fd,
1341            in("x5")  offset,
1342            options(nostack, preserves_flags),
1343        );
1344    }
1345    ret
1346}
1347
1348// ─────────────────────────────────────────────────────────────────────────
1349// Helpers syscall bruts — munmap
1350// ─────────────────────────────────────────────────────────────────────────
1351
1352#[cfg(target_arch = "x86_64")]
1353#[inline]
1354unsafe fn raw_syscall_munmap(address: u64, length: usize) -> i64 {
1355    let ret: i64;
1356    // SAFETY: SYS_munmap (x86_64 = 11). Libère le mapping ; pas d'accès
1357    // mémoire utilisateur par le syscall lui-même.
1358    unsafe {
1359        core::arch::asm!(
1360            "syscall",
1361            in("rax") 11_i64,
1362            in("rdi") address,
1363            in("rsi") length,
1364            lateout("rax") ret,
1365            lateout("rcx") _,
1366            lateout("r11") _,
1367            options(nostack, preserves_flags),
1368        );
1369    }
1370    ret
1371}
1372
1373#[cfg(target_arch = "aarch64")]
1374#[inline]
1375unsafe fn raw_syscall_munmap(address: u64, length: usize) -> i64 {
1376    let ret: i64;
1377    // SAFETY: SYS_munmap (aarch64 = 215).
1378    unsafe {
1379        core::arch::asm!(
1380            "svc 0",
1381            in("x8")  215_i64,
1382            inout("x0") address => ret,
1383            in("x1")  length,
1384            options(nostack, preserves_flags),
1385        );
1386    }
1387    ret
1388}
1389
1390// ─────────────────────────────────────────────────────────────────────────
1391// Helpers syscall bruts — mremap
1392// ─────────────────────────────────────────────────────────────────────────
1393
1394#[cfg(target_arch = "x86_64")]
1395#[inline]
1396unsafe fn raw_syscall_mremap(
1397    old_addr: u64,
1398    old_len: usize,
1399    new_len: usize,
1400    flags: i32,
1401    new_addr: u64,
1402) -> i64 {
1403    let ret: i64;
1404    // SAFETY: SYS_mremap (x86_64 = 25). 5 arguments : rdi, rsi, rdx, r10, r8.
1405    unsafe {
1406        core::arch::asm!(
1407            "syscall",
1408            in("rax") 25_i64,
1409            in("rdi") old_addr,
1410            in("rsi") old_len,
1411            in("rdx") new_len,
1412            in("r10") i64::from(flags),
1413            in("r8")  new_addr,
1414            lateout("rax") ret,
1415            lateout("rcx") _,
1416            lateout("r11") _,
1417            options(nostack, preserves_flags),
1418        );
1419    }
1420    ret
1421}
1422
1423#[cfg(target_arch = "aarch64")]
1424#[inline]
1425unsafe fn raw_syscall_mremap(
1426    old_addr: u64,
1427    old_len: usize,
1428    new_len: usize,
1429    flags: i32,
1430    new_addr: u64,
1431) -> i64 {
1432    let ret: i64;
1433    // SAFETY: SYS_mremap (aarch64 = 216).
1434    unsafe {
1435        core::arch::asm!(
1436            "svc 0",
1437            in("x8")  216_i64,
1438            inout("x0") old_addr => ret,
1439            in("x1")  old_len,
1440            in("x2")  new_len,
1441            in("x3")  i64::from(flags),
1442            in("x4")  new_addr,
1443            options(nostack, preserves_flags),
1444        );
1445    }
1446    ret
1447}
1448
1449// ─────────────────────────────────────────────────────────────────────────
1450// Helpers syscall bruts — mprotect
1451// ─────────────────────────────────────────────────────────────────────────
1452
1453#[cfg(target_arch = "x86_64")]
1454#[inline]
1455unsafe fn raw_syscall_mprotect(address: u64, length: usize, prot: i32) -> i64 {
1456    let ret: i64;
1457    // SAFETY: SYS_mprotect (x86_64 = 10). Scalaires uniquement.
1458    unsafe {
1459        core::arch::asm!(
1460            "syscall",
1461            in("rax") 10_i64,
1462            in("rdi") address,
1463            in("rsi") length,
1464            in("rdx") i64::from(prot),
1465            lateout("rax") ret,
1466            lateout("rcx") _,
1467            lateout("r11") _,
1468            options(nostack, preserves_flags),
1469        );
1470    }
1471    ret
1472}
1473
1474#[cfg(target_arch = "aarch64")]
1475#[inline]
1476unsafe fn raw_syscall_mprotect(address: u64, length: usize, prot: i32) -> i64 {
1477    let ret: i64;
1478    // SAFETY: SYS_mprotect (aarch64 = 226).
1479    unsafe {
1480        core::arch::asm!(
1481            "svc 0",
1482            in("x8")  226_i64,
1483            inout("x0") address => ret,
1484            in("x1")  length,
1485            in("x2")  i64::from(prot),
1486            options(nostack, preserves_flags),
1487        );
1488    }
1489    ret
1490}
1491
1492// ─────────────────────────────────────────────────────────────────────────
1493// Helpers syscall bruts — madvise
1494// ─────────────────────────────────────────────────────────────────────────
1495
1496#[cfg(target_arch = "x86_64")]
1497#[inline]
1498unsafe fn raw_syscall_madvise(address: u64, length: usize, advice: i32) -> i64 {
1499    let ret: i64;
1500    // SAFETY: SYS_madvise (x86_64 = 28). Scalaires uniquement ; readonly
1501    // serait incorrect (le kernel peut modifier des métadonnées internes).
1502    unsafe {
1503        core::arch::asm!(
1504            "syscall",
1505            in("rax") 28_i64,
1506            in("rdi") address,
1507            in("rsi") length,
1508            in("rdx") i64::from(advice),
1509            lateout("rax") ret,
1510            lateout("rcx") _,
1511            lateout("r11") _,
1512            options(nostack, preserves_flags),
1513        );
1514    }
1515    ret
1516}
1517
1518#[cfg(target_arch = "aarch64")]
1519#[inline]
1520unsafe fn raw_syscall_madvise(address: u64, length: usize, advice: i32) -> i64 {
1521    let ret: i64;
1522    // SAFETY: SYS_madvise (aarch64 = 233).
1523    unsafe {
1524        core::arch::asm!(
1525            "svc 0",
1526            in("x8")  233_i64,
1527            inout("x0") address => ret,
1528            in("x1")  length,
1529            in("x2")  i64::from(advice),
1530            options(nostack, preserves_flags),
1531        );
1532    }
1533    ret
1534}
1535
1536// ─────────────────────────────────────────────────────────────────────────
1537// Helpers syscall bruts — mlock / mlock2 / munlock / mlockall / munlockall
1538// ─────────────────────────────────────────────────────────────────────────
1539
1540#[cfg(target_arch = "x86_64")]
1541#[inline]
1542unsafe fn raw_syscall_mlock(address: u64, length: usize) -> i64 {
1543    let ret: i64;
1544    // SAFETY: SYS_mlock (x86_64 = 149).
1545    unsafe {
1546        core::arch::asm!(
1547            "syscall",
1548            in("rax") 149_i64,
1549            in("rdi") address,
1550            in("rsi") length,
1551            lateout("rax") ret,
1552            lateout("rcx") _,
1553            lateout("r11") _,
1554            options(nostack, preserves_flags),
1555        );
1556    }
1557    ret
1558}
1559
1560#[cfg(target_arch = "aarch64")]
1561#[inline]
1562unsafe fn raw_syscall_mlock(address: u64, length: usize) -> i64 {
1563    let ret: i64;
1564    // SAFETY: SYS_mlock (aarch64 = 228).
1565    unsafe {
1566        core::arch::asm!(
1567            "svc 0",
1568            in("x8")  228_i64,
1569            inout("x0") address => ret,
1570            in("x1")  length,
1571            options(nostack, preserves_flags),
1572        );
1573    }
1574    ret
1575}
1576
1577#[cfg(target_arch = "x86_64")]
1578#[inline]
1579unsafe fn raw_syscall_mlock2(address: u64, length: usize, flags: u32) -> i64 {
1580    let ret: i64;
1581    // SAFETY: SYS_mlock2 (x86_64 = 325).
1582    unsafe {
1583        core::arch::asm!(
1584            "syscall",
1585            in("rax") 325_i64,
1586            in("rdi") address,
1587            in("rsi") length,
1588            in("rdx") i64::from(flags),
1589            lateout("rax") ret,
1590            lateout("rcx") _,
1591            lateout("r11") _,
1592            options(nostack, preserves_flags),
1593        );
1594    }
1595    ret
1596}
1597
1598#[cfg(target_arch = "aarch64")]
1599#[inline]
1600unsafe fn raw_syscall_mlock2(address: u64, length: usize, flags: u32) -> i64 {
1601    let ret: i64;
1602    // SAFETY: SYS_mlock2 (aarch64 = 284).
1603    unsafe {
1604        core::arch::asm!(
1605            "svc 0",
1606            in("x8")  284_i64,
1607            inout("x0") address => ret,
1608            in("x1")  length,
1609            in("x2")  i64::from(flags),
1610            options(nostack, preserves_flags),
1611        );
1612    }
1613    ret
1614}
1615
1616#[cfg(target_arch = "x86_64")]
1617#[inline]
1618unsafe fn raw_syscall_munlock(address: u64, length: usize) -> i64 {
1619    let ret: i64;
1620    // SAFETY: SYS_munlock (x86_64 = 150).
1621    unsafe {
1622        core::arch::asm!(
1623            "syscall",
1624            in("rax") 150_i64,
1625            in("rdi") address,
1626            in("rsi") length,
1627            lateout("rax") ret,
1628            lateout("rcx") _,
1629            lateout("r11") _,
1630            options(nostack, preserves_flags),
1631        );
1632    }
1633    ret
1634}
1635
1636#[cfg(target_arch = "aarch64")]
1637#[inline]
1638unsafe fn raw_syscall_munlock(address: u64, length: usize) -> i64 {
1639    let ret: i64;
1640    // SAFETY: SYS_munlock (aarch64 = 229).
1641    unsafe {
1642        core::arch::asm!(
1643            "svc 0",
1644            in("x8")  229_i64,
1645            inout("x0") address => ret,
1646            in("x1")  length,
1647            options(nostack, preserves_flags),
1648        );
1649    }
1650    ret
1651}
1652
1653#[cfg(target_arch = "x86_64")]
1654#[inline]
1655unsafe fn raw_syscall_mlockall(flags: i32) -> i64 {
1656    let ret: i64;
1657    // SAFETY: SYS_mlockall (x86_64 = 151). Un seul scalaire.
1658    unsafe {
1659        core::arch::asm!(
1660            "syscall",
1661            in("rax") 151_i64,
1662            in("rdi") i64::from(flags),
1663            lateout("rax") ret,
1664            lateout("rcx") _,
1665            lateout("r11") _,
1666            options(nostack, preserves_flags),
1667        );
1668    }
1669    ret
1670}
1671
1672#[cfg(target_arch = "aarch64")]
1673#[inline]
1674unsafe fn raw_syscall_mlockall(flags: i32) -> i64 {
1675    let ret: i64;
1676    // SAFETY: SYS_mlockall (aarch64 = 230).
1677    unsafe {
1678        core::arch::asm!(
1679            "svc 0",
1680            in("x8")  230_i64,
1681            inout("x0") i64::from(flags) => ret,
1682            options(nostack, preserves_flags),
1683        );
1684    }
1685    ret
1686}
1687
1688#[cfg(target_arch = "x86_64")]
1689#[inline]
1690unsafe fn raw_syscall_munlockall() -> i64 {
1691    let ret: i64;
1692    // SAFETY: SYS_munlockall (x86_64 = 152). Aucun argument.
1693    unsafe {
1694        core::arch::asm!(
1695            "syscall",
1696            in("rax") 152_i64,
1697            lateout("rax") ret,
1698            lateout("rcx") _,
1699            lateout("r11") _,
1700            options(nostack, preserves_flags),
1701        );
1702    }
1703    ret
1704}
1705
1706#[cfg(target_arch = "aarch64")]
1707#[inline]
1708unsafe fn raw_syscall_munlockall() -> i64 {
1709    let ret: i64;
1710    // SAFETY: SYS_munlockall (aarch64 = 231). Aucun argument.
1711    unsafe {
1712        core::arch::asm!(
1713            "svc 0",
1714            in("x8")  231_i64,
1715            inout("x0") 0_i64 => ret,
1716            options(nostack, preserves_flags),
1717        );
1718    }
1719    ret
1720}
1721
1722// ─────────────────────────────────────────────────────────────────────────
1723// Helpers syscall bruts — memfd_create / memfd_secret
1724// ─────────────────────────────────────────────────────────────────────────
1725
1726#[cfg(target_arch = "x86_64")]
1727#[inline]
1728unsafe fn raw_syscall_memfd_create(name_ptr: u64, flags: u32) -> i64 {
1729    let ret: i64;
1730    // SAFETY: SYS_memfd_create (x86_64 = 319). Lit la chaîne NUL à `name_ptr`.
1731    unsafe {
1732        core::arch::asm!(
1733            "syscall",
1734            in("rax") 319_i64,
1735            in("rdi") name_ptr,
1736            in("rsi") i64::from(flags),
1737            lateout("rax") ret,
1738            lateout("rcx") _,
1739            lateout("r11") _,
1740            options(nostack, preserves_flags),
1741        );
1742    }
1743    ret
1744}
1745
1746#[cfg(target_arch = "aarch64")]
1747#[inline]
1748unsafe fn raw_syscall_memfd_create(name_ptr: u64, flags: u32) -> i64 {
1749    let ret: i64;
1750    // SAFETY: SYS_memfd_create (aarch64 = 279).
1751    unsafe {
1752        core::arch::asm!(
1753            "svc 0",
1754            in("x8")  279_i64,
1755            inout("x0") name_ptr => ret,
1756            in("x1")  i64::from(flags),
1757            options(nostack, preserves_flags),
1758        );
1759    }
1760    ret
1761}
1762
1763#[cfg(target_arch = "x86_64")]
1764#[inline]
1765unsafe fn raw_syscall_memfd_secret(flags: u32) -> i64 {
1766    let ret: i64;
1767    // SAFETY: SYS_memfd_secret (x86_64 = 447). Un seul scalaire.
1768    unsafe {
1769        core::arch::asm!(
1770            "syscall",
1771            in("rax") 447_i64,
1772            in("rdi") i64::from(flags),
1773            lateout("rax") ret,
1774            lateout("rcx") _,
1775            lateout("r11") _,
1776            options(nostack, preserves_flags, readonly),
1777        );
1778    }
1779    ret
1780}
1781
1782#[cfg(target_arch = "aarch64")]
1783#[inline]
1784unsafe fn raw_syscall_memfd_secret(flags: u32) -> i64 {
1785    let ret: i64;
1786    // SAFETY: SYS_memfd_secret (aarch64 = 447).
1787    unsafe {
1788        core::arch::asm!(
1789            "svc 0",
1790            in("x8")  447_i64,
1791            inout("x0") i64::from(flags) => ret,
1792            options(nostack, preserves_flags, readonly),
1793        );
1794    }
1795    ret
1796}
1797
1798// ─────────────────────────────────────────────────────────────────────────
1799// Helpers syscall bruts — msync
1800// ─────────────────────────────────────────────────────────────────────────
1801
1802#[cfg(target_arch = "x86_64")]
1803#[inline]
1804unsafe fn raw_syscall_msync(address: u64, length: usize, flags: i32) -> i64 {
1805    let ret: i64;
1806    // SAFETY: SYS_msync (x86_64 = 26). Scalaires ; le kernel lit les pages
1807    // mais ne modifie pas la mémoire utilisateur via le pointeur.
1808    unsafe {
1809        core::arch::asm!(
1810            "syscall",
1811            in("rax") 26_i64,
1812            in("rdi") address,
1813            in("rsi") length,
1814            in("rdx") i64::from(flags),
1815            lateout("rax") ret,
1816            lateout("rcx") _,
1817            lateout("r11") _,
1818            options(nostack, preserves_flags),
1819        );
1820    }
1821    ret
1822}
1823
1824#[cfg(target_arch = "aarch64")]
1825#[inline]
1826unsafe fn raw_syscall_msync(address: u64, length: usize, flags: i32) -> i64 {
1827    let ret: i64;
1828    // SAFETY: SYS_msync (aarch64 = 227).
1829    unsafe {
1830        core::arch::asm!(
1831            "svc 0",
1832            in("x8")  227_i64,
1833            inout("x0") address => ret,
1834            in("x1")  length,
1835            in("x2")  i64::from(flags),
1836            options(nostack, preserves_flags),
1837        );
1838    }
1839    ret
1840}
1841
1842// ─────────────────────────────────────────────────────────────────────────
1843// Helpers syscall bruts — process_vm_readv / process_vm_writev
1844// ─────────────────────────────────────────────────────────────────────────
1845
1846#[cfg(target_arch = "x86_64")]
1847#[inline]
1848unsafe fn raw_syscall_process_vm_readv(
1849    pid: i32,
1850    local_iov: u64,
1851    liovcnt: u64,
1852    remote_iov: u64,
1853    riovcnt: u64,
1854    flags: u64,
1855) -> i64 {
1856    let ret: i64;
1857    // SAFETY: SYS_process_vm_readv (x86_64 = 310). 6 arguments.
1858    // Le kernel lit depuis l'espace d'adressage distant et écrit dans
1859    // les buffers locaux décrits par local_iov.
1860    unsafe {
1861        core::arch::asm!(
1862            "syscall",
1863            in("rax") 310_i64,
1864            in("rdi") i64::from(pid),
1865            in("rsi") local_iov,
1866            in("rdx") liovcnt,
1867            in("r10") remote_iov,
1868            in("r8")  riovcnt,
1869            in("r9")  flags,
1870            lateout("rax") ret,
1871            lateout("rcx") _,
1872            lateout("r11") _,
1873            options(nostack, preserves_flags),
1874        );
1875    }
1876    ret
1877}
1878
1879#[cfg(target_arch = "aarch64")]
1880#[inline]
1881unsafe fn raw_syscall_process_vm_readv(
1882    pid: i32,
1883    local_iov: u64,
1884    liovcnt: u64,
1885    remote_iov: u64,
1886    riovcnt: u64,
1887    flags: u64,
1888) -> i64 {
1889    let ret: i64;
1890    // SAFETY: SYS_process_vm_readv (aarch64 = 270).
1891    unsafe {
1892        core::arch::asm!(
1893            "svc 0",
1894            in("x8")  270_i64,
1895            inout("x0") i64::from(pid) => ret,
1896            in("x1")  local_iov,
1897            in("x2")  liovcnt,
1898            in("x3")  remote_iov,
1899            in("x4")  riovcnt,
1900            in("x5")  flags,
1901            options(nostack, preserves_flags),
1902        );
1903    }
1904    ret
1905}
1906
1907#[cfg(target_arch = "x86_64")]
1908#[inline]
1909unsafe fn raw_syscall_process_vm_writev(
1910    pid: i32,
1911    local_iov: u64,
1912    liovcnt: u64,
1913    remote_iov: u64,
1914    riovcnt: u64,
1915    flags: u64,
1916) -> i64 {
1917    let ret: i64;
1918    // SAFETY: SYS_process_vm_writev (x86_64 = 311). 6 arguments.
1919    // Le kernel lit depuis les buffers locaux et écrit dans l'espace
1920    // d'adressage distant.
1921    unsafe {
1922        core::arch::asm!(
1923            "syscall",
1924            in("rax") 311_i64,
1925            in("rdi") i64::from(pid),
1926            in("rsi") local_iov,
1927            in("rdx") liovcnt,
1928            in("r10") remote_iov,
1929            in("r8")  riovcnt,
1930            in("r9")  flags,
1931            lateout("rax") ret,
1932            lateout("rcx") _,
1933            lateout("r11") _,
1934            options(nostack, preserves_flags),
1935        );
1936    }
1937    ret
1938}
1939
1940#[cfg(target_arch = "aarch64")]
1941#[inline]
1942unsafe fn raw_syscall_process_vm_writev(
1943    pid: i32,
1944    local_iov: u64,
1945    liovcnt: u64,
1946    remote_iov: u64,
1947    riovcnt: u64,
1948    flags: u64,
1949) -> i64 {
1950    let ret: i64;
1951    // SAFETY: SYS_process_vm_writev (aarch64 = 271).
1952    unsafe {
1953        core::arch::asm!(
1954            "svc 0",
1955            in("x8")  271_i64,
1956            inout("x0") i64::from(pid) => ret,
1957            in("x1")  local_iov,
1958            in("x2")  liovcnt,
1959            in("x3")  remote_iov,
1960            in("x4")  riovcnt,
1961            in("x5")  flags,
1962            options(nostack, preserves_flags),
1963        );
1964    }
1965    ret
1966}
1967
1968// ─────────────────────────────────────────────────────────────────────────
1969// Tests
1970// ─────────────────────────────────────────────────────────────────────────
1971
1972// Tests fonctionnels sur `Arc`/atomiques `std` (hors loom : créer un `Arc`
1973// loom hors d'un `loom::model` paniquerait). La preuve de concurrence vit dans
1974// `mod loom_refcount` (sous `cfg(loom)`).
1975#[cfg(all(test, target_os = "linux", not(loom)))]
1976mod tests {
1977    use super::*;
1978    use air_sys_types::fd::AsFd;
1979    use air_sys_types::mem::{
1980        MadviseAdvice, MapFlags, MemfdFlags, MremapFlags, MsyncFlags, ProtectionFlags,
1981    };
1982
1983    /// Helper : étend un memfd à `length` octets via le wrapper ftruncate de la famille fs.
1984    fn extend_memfd(fd: &OwnedFd, length: usize) {
1985        use crate::fs::ftruncate;
1986        ftruncate(fd.as_fd(), length.try_into().expect("length fits i64")).expect("ftruncate");
1987    }
1988
1989    // Taille de page standard — suffisante pour tous les tests.
1990    const PAGE: usize = 4096;
1991
1992    // ─────────────────────────────────────
1993    // mmap_anonymous
1994    // ─────────────────────────────────────
1995
1996    #[test]
1997    fn mmap_anonymous_rw_basic() {
1998        let mut m = mmap_anonymous(
1999            PAGE,
2000            ProtectionFlags::READ | ProtectionFlags::WRITE,
2001            MapFlags::PRIVATE,
2002        )
2003        .expect("mmap_anonymous");
2004        assert_eq!(m.len(), PAGE);
2005        assert!(!m.is_empty());
2006        // Écriture + relecture dans le mapping.
2007        // SAFETY: mapping READ|WRITE fraîchement alloué, aucune alias.
2008        let slice = unsafe { m.as_mut_slice() };
2009        slice[0] = 0xAB;
2010        slice[PAGE - 1] = 0xCD;
2011        assert_eq!(slice[0], 0xAB);
2012        assert_eq!(slice[PAGE - 1], 0xCD);
2013        // Drop automatique appelle munmap.
2014    }
2015
2016    #[test]
2017    fn mmap_anonymous_read_only() {
2018        let m = mmap_anonymous(PAGE, ProtectionFlags::READ, MapFlags::PRIVATE)
2019            .expect("mmap_anonymous READ-only");
2020        assert_eq!(m.len(), PAGE);
2021        // Lecture depuis le mapping.
2022        let _byte = m.as_slice()[0];
2023    }
2024
2025    #[test]
2026    fn mmap_anonymous_length_zero_fails() {
2027        let err = mmap_anonymous(
2028            0,
2029            ProtectionFlags::READ | ProtectionFlags::WRITE,
2030            MapFlags::PRIVATE,
2031        )
2032        .expect_err("longueur 0 doit retourner EINVAL");
2033        assert_eq!(err, air_sys_types::Errno::EINVAL);
2034    }
2035
2036    // ─────────────────────────────────────
2037    // munmap explicite
2038    // ─────────────────────────────────────
2039
2040    #[test]
2041    fn munmap_explicit_succeeds() {
2042        let m = mmap_anonymous(
2043            PAGE,
2044            ProtectionFlags::READ | ProtectionFlags::WRITE,
2045            MapFlags::PRIVATE,
2046        )
2047        .expect("mmap_anonymous");
2048        munmap(m).expect("munmap explicite");
2049    }
2050
2051    #[test]
2052    fn munmap_raw_frees_by_address_and_rejects_unaligned() {
2053        // Additif ADR-085 : libération par (adresse, longueur) brutes (face libc).
2054        let mut m = mmap_anonymous(
2055            PAGE,
2056            ProtectionFlags::READ | ProtectionFlags::WRITE,
2057            MapFlags::PRIVATE,
2058        )
2059        .expect("mmap_anonymous");
2060        let address = NonNull::new(m.as_mut_ptr()).expect("adresse mmap non nulle");
2061        let length = m.len();
2062        // Empêche le `Drop` de `Mapping` de re-`munmap`per : on libère via `munmap_raw`.
2063        core::mem::forget(m);
2064        // SAFETY: `(address, length)` désignent le mapping fraîchement créé ci-dessus.
2065        unsafe { munmap_raw(address, length) }.expect("munmap_raw d'une région mappée");
2066
2067        // Adresse **non alignée** sur la page ⇒ `EINVAL` **déterministe** (le kernel
2068        // rejette avant toute déprogrammation — aucun effet de bord).
2069        let unaligned = NonNull::new(4097_usize as *mut u8).expect("pointeur non nul");
2070        // SAFETY: adresse non alignée ⇒ `EINVAL` ; jamais de déprogrammation mémoire.
2071        assert!(unsafe { munmap_raw(unaligned, PAGE) }.is_err());
2072    }
2073
2074    // ─────────────────────────────────────
2075    // mmap_file
2076    // ─────────────────────────────────────
2077
2078    #[test]
2079    fn mmap_file_with_memfd() {
2080        // Crée un memfd, étend à PAGE octets, mappe en lecture/écriture.
2081        let fd = memfd_create(c"test-mmap-file", MemfdFlags::CLOEXEC).expect("memfd_create");
2082        extend_memfd(&fd, PAGE);
2083        // Mappe le fichier en lecture/écriture.
2084        let m = mmap_file(
2085            fd.as_fd(),
2086            PAGE,
2087            0,
2088            ProtectionFlags::READ | ProtectionFlags::WRITE,
2089            MapFlags::SHARED,
2090        )
2091        .expect("mmap_file");
2092        assert_eq!(m.len(), PAGE);
2093        // Vérifie que le contenu est accessible.
2094        let _byte = m.as_slice()[0];
2095    }
2096
2097    // ─────────────────────────────────────
2098    // mremap
2099    // ─────────────────────────────────────
2100
2101    #[test]
2102    fn mremap_grows_mapping() {
2103        let m = mmap_anonymous(
2104            PAGE,
2105            ProtectionFlags::READ | ProtectionFlags::WRITE,
2106            MapFlags::PRIVATE,
2107        )
2108        .expect("mmap_anonymous");
2109        let m2 = mremap(m, PAGE * 2, MremapFlags::MAYMOVE).expect("mremap");
2110        assert_eq!(m2.len(), PAGE * 2);
2111        // Vérifie que les nouvelles pages sont accessibles.
2112        let _byte = m2.as_slice()[PAGE * 2 - 1];
2113    }
2114
2115    #[test]
2116    fn mremap_shrinks_mapping() {
2117        let m = mmap_anonymous(
2118            PAGE * 4,
2119            ProtectionFlags::READ | ProtectionFlags::WRITE,
2120            MapFlags::PRIVATE,
2121        )
2122        .expect("mmap_anonymous 4 pages");
2123        let m2 = mremap(m, PAGE, MremapFlags::empty()).expect("mremap shrink");
2124        assert_eq!(m2.len(), PAGE);
2125    }
2126
2127    // ─────────────────────────────────────
2128    // mprotect
2129    // ─────────────────────────────────────
2130
2131    #[test]
2132    fn mprotect_demotes_to_read_only() {
2133        let mut m = mmap_anonymous(
2134            PAGE,
2135            ProtectionFlags::READ | ProtectionFlags::WRITE,
2136            MapFlags::PRIVATE,
2137        )
2138        .expect("mmap_anonymous");
2139        let ptr = NonNull::new(m.as_mut_ptr()).expect("non-null");
2140        mprotect(ptr, PAGE, ProtectionFlags::READ).expect("mprotect READ");
2141        // Après mprotect, la lecture doit toujours fonctionner.
2142        let _byte = m.as_slice()[0];
2143    }
2144
2145    // ─────────────────────────────────────
2146    // madvise
2147    // ─────────────────────────────────────
2148
2149    #[test]
2150    fn madvise_sequential_hint() {
2151        let m = mmap_anonymous(
2152            PAGE * 4,
2153            ProtectionFlags::READ | ProtectionFlags::WRITE,
2154            MapFlags::PRIVATE,
2155        )
2156        .expect("mmap_anonymous");
2157        let ptr = NonNull::new(m.as_ptr() as *mut u8).expect("non-null");
2158        madvise(ptr, PAGE * 4, MadviseAdvice::Sequential).expect("madvise Sequential");
2159    }
2160
2161    #[test]
2162    fn madvise_dontneed_hint() {
2163        let m = mmap_anonymous(
2164            PAGE,
2165            ProtectionFlags::READ | ProtectionFlags::WRITE,
2166            MapFlags::PRIVATE,
2167        )
2168        .expect("mmap_anonymous");
2169        let ptr = NonNull::new(m.as_ptr() as *mut u8).expect("non-null");
2170        madvise(ptr, PAGE, MadviseAdvice::DontNeed).expect("madvise DontNeed");
2171    }
2172
2173    // ─────────────────────────────────────
2174    // mlock / munlock
2175    // ─────────────────────────────────────
2176
2177    #[test]
2178    fn mlock_munlock_pair() {
2179        let m = mmap_anonymous(
2180            PAGE,
2181            ProtectionFlags::READ | ProtectionFlags::WRITE,
2182            MapFlags::PRIVATE,
2183        )
2184        .expect("mmap_anonymous");
2185        let ptr = NonNull::new(m.as_ptr() as *mut u8).expect("non-null");
2186        match mlock(ptr, PAGE) {
2187            Ok(()) => {
2188                // Si mlock a réussi, munlock doit aussi réussir.
2189                munlock(ptr, PAGE).expect("munlock");
2190            }
2191            Err(e) => {
2192                // EPERM / ENOMEM attendu dans les environnements sans privilèges.
2193                assert!(
2194                    e == air_sys_types::Errno::EPERM || e == air_sys_types::Errno::ENOMEM,
2195                    "mlock erreur inattendue : {:?}",
2196                    e
2197                );
2198            }
2199        }
2200    }
2201
2202    #[test]
2203    fn mlock2_onfault() {
2204        use air_sys_types::mem::MlockFlags;
2205        let m = mmap_anonymous(
2206            PAGE,
2207            ProtectionFlags::READ | ProtectionFlags::WRITE,
2208            MapFlags::PRIVATE,
2209        )
2210        .expect("mmap_anonymous");
2211        let ptr = NonNull::new(m.as_ptr() as *mut u8).expect("non-null");
2212        match mlock2(ptr, PAGE, MlockFlags::ONFAULT) {
2213            Ok(()) => {
2214                munlock(ptr, PAGE).expect("munlock après mlock2");
2215            }
2216            Err(e) => {
2217                assert!(
2218                    e == air_sys_types::Errno::EPERM || e == air_sys_types::Errno::ENOMEM,
2219                    "mlock2 erreur inattendue : {:?}",
2220                    e
2221                );
2222            }
2223        }
2224    }
2225
2226    // ─────────────────────────────────────
2227    // memfd_create
2228    // ─────────────────────────────────────
2229
2230    #[test]
2231    fn memfd_create_basic() {
2232        let fd = memfd_create(c"air-test-memfd", MemfdFlags::CLOEXEC).expect("memfd_create");
2233        // Le FD est valide : on peut lire sa taille via fstat.
2234        use air_sys_types::fd::AsRawFd;
2235        let raw = fd.as_raw_fd();
2236        assert!(raw >= 0);
2237        // Drop ferme le FD automatiquement.
2238    }
2239
2240    #[test]
2241    fn memfd_create_with_sealing() {
2242        let _fd = memfd_create(
2243            c"air-test-seal",
2244            MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING,
2245        )
2246        .expect("memfd_create avec ALLOW_SEALING");
2247    }
2248
2249    // ─────────────────────────────────────
2250    // msync
2251    // ─────────────────────────────────────
2252
2253    #[test]
2254    fn msync_shared_mapping() {
2255        let fd = memfd_create(c"air-test-msync", MemfdFlags::CLOEXEC).expect("memfd_create");
2256        extend_memfd(&fd, PAGE);
2257        let mut m = mmap_file(
2258            fd.as_fd(),
2259            PAGE,
2260            0,
2261            ProtectionFlags::READ | ProtectionFlags::WRITE,
2262            MapFlags::SHARED,
2263        )
2264        .expect("mmap_file");
2265        // Écriture puis msync.
2266        // SAFETY: mapping SHARED READ|WRITE.
2267        let slice = unsafe { m.as_mut_slice() };
2268        slice[0] = 0x42;
2269        let ptr = NonNull::new(m.as_ptr() as *mut u8).expect("non-null");
2270        msync(ptr, PAGE, MsyncFlags::SYNC).expect("msync SYNC");
2271    }
2272
2273    // ─────────────────────────────────────
2274    // mmap_fixed
2275    // ─────────────────────────────────────
2276
2277    // `mmap_fixed` est couvert de façon déterministe par
2278    // `mmap_fixed_over_memfd_succeeds` (succès + bras `fd = Some`) et
2279    // `mmap_fixed_noreplace_eexist_on_occupied_addr` (chemin d'erreur `?` →
2280    // EEXIST). L'ancien test `mmap_fixed_noreplace_anonymous` (munmap puis
2281    // FIXED_NOREPLACE) était flaky — l'adresse libérée pouvait être réutilisée
2282    // par un test parallèle, rendant son bras `Ok` non couvert de façon
2283    // non déterministe.
2284
2285    // ─────────────────────────────────────
2286    // mlockall / munlockall
2287    // ─────────────────────────────────────
2288
2289    #[test]
2290    fn mlockall_current_or_eperm() {
2291        match mlockall(MlockallFlags::CURRENT) {
2292            Ok(()) => {
2293                // Si mlockall réussit, munlockall doit aussi réussir.
2294                munlockall().expect("munlockall");
2295            }
2296            Err(e) => {
2297                // EPERM ou ENOMEM attendu sans CAP_IPC_LOCK.
2298                assert!(
2299                    e == air_sys_types::Errno::EPERM || e == air_sys_types::Errno::ENOMEM,
2300                    "mlockall erreur inattendue : {:?}",
2301                    e
2302                );
2303            }
2304        }
2305    }
2306
2307    #[test]
2308    fn munlockall_succeeds_or_eperm() {
2309        // munlockall() sans verrou préalable doit réussir (no-op).
2310        match munlockall() {
2311            Ok(()) => {}
2312            Err(e) => {
2313                // Sur certains kernels très restreints, EPERM est possible.
2314                assert_eq!(
2315                    e,
2316                    air_sys_types::Errno::EPERM,
2317                    "munlockall erreur inattendue : {e:?}"
2318                );
2319            }
2320        }
2321    }
2322
2323    // ─────────────────────────────────────
2324    // memfd_secret
2325    // ─────────────────────────────────────
2326
2327    #[test]
2328    fn memfd_secret_ok_or_enosys() {
2329        match memfd_secret(0) {
2330            Ok(_fd) => {
2331                // Disponible sur kernel >= 5.14 avec CONFIG_SECRETMEM.
2332            }
2333            Err(e) => {
2334                // ENOSYS sur kernels plus anciens ou sans CONFIG_SECRETMEM.
2335                assert_eq!(
2336                    e,
2337                    air_sys_types::Errno::ENOSYS,
2338                    "memfd_secret erreur inattendue : {:?}",
2339                    e
2340                );
2341            }
2342        }
2343    }
2344
2345    // ─────────────────────────────────────
2346    // process_vm_readv / process_vm_writev
2347    // ─────────────────────────────────────
2348
2349    #[test]
2350    fn process_vm_readv_own_memory() {
2351        use air_sys_types::mem::RemoteIoSlice;
2352        use air_sys_types::net::IoSliceMut;
2353
2354        // On lit notre propre mémoire via process_vm_readv.
2355        let src_data: [u8; 8] = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04];
2356        let mut dst_data = [0u8; 8];
2357
2358        let pid = crate::process::getpid();
2359        let mut local = [IoSliceMut::new(&mut dst_data)];
2360        let remote = [RemoteIoSlice {
2361            addr: src_data.as_ptr() as u64,
2362            len: 8,
2363        }];
2364        match process_vm_readv(pid, &mut local, &remote, 0) {
2365            Ok(n) => {
2366                assert_eq!(n, 8);
2367                assert_eq!(dst_data, src_data);
2368            }
2369            Err(e) => {
2370                // EPERM si Yama ptrace_scope l'interdit pour self.
2371                assert!(
2372                    e == air_sys_types::Errno::EPERM || e == air_sys_types::Errno::EACCES,
2373                    "process_vm_readv erreur inattendue : {:?}",
2374                    e
2375                );
2376            }
2377        }
2378    }
2379
2380    #[test]
2381    fn process_vm_writev_own_memory() {
2382        use air_sys_types::mem::RemoteIoSlice;
2383        use air_sys_types::net::IoSlice;
2384
2385        let src_data: [u8; 4] = [0xCA, 0xFE, 0xBA, 0xBE];
2386        let mut dst_data = [0u8; 4];
2387
2388        let pid = crate::process::getpid();
2389        let local = [IoSlice::new(&src_data)];
2390        let remote = [RemoteIoSlice {
2391            addr: dst_data.as_mut_ptr() as u64,
2392            len: 4,
2393        }];
2394        match process_vm_writev(pid, &local, &remote, 0) {
2395            Ok(n) => {
2396                assert_eq!(n, 4);
2397                assert_eq!(dst_data, src_data);
2398            }
2399            Err(e) => {
2400                assert!(
2401                    e == air_sys_types::Errno::EPERM || e == air_sys_types::Errno::EACCES,
2402                    "process_vm_writev erreur inattendue : {:?}",
2403                    e
2404                );
2405            }
2406        }
2407    }
2408
2409    // ─────────────────────────────────────
2410    // mremap error path
2411    // ─────────────────────────────────────
2412
2413    #[test]
2414    fn mremap_length_zero_fails() {
2415        let m = mmap_anonymous(
2416            PAGE,
2417            ProtectionFlags::READ | ProtectionFlags::WRITE,
2418            MapFlags::PRIVATE,
2419        )
2420        .expect("mmap_anonymous");
2421        let err = mremap(m, 0, MremapFlags::MAYMOVE).unwrap_err();
2422        assert_eq!(err, air_sys_types::Errno::EINVAL);
2423    }
2424
2425    // ─────────────────────────────────────
2426    // madvise with more hints
2427    // ─────────────────────────────────────
2428
2429    #[test]
2430    fn madvise_random_hint() {
2431        let m = mmap_anonymous(
2432            PAGE,
2433            ProtectionFlags::READ | ProtectionFlags::WRITE,
2434            MapFlags::PRIVATE,
2435        )
2436        .expect("mmap_anonymous");
2437        let ptr = NonNull::new(m.as_ptr() as *mut u8).expect("non-null");
2438        madvise(ptr, PAGE, MadviseAdvice::Random).expect("madvise Random");
2439    }
2440
2441    #[test]
2442    fn madvise_willneed_hint() {
2443        let fd = memfd_create(c"air-madvise-wn", MemfdFlags::CLOEXEC).expect("memfd");
2444        extend_memfd(&fd, PAGE);
2445        let m = mmap_file(
2446            fd.as_fd(),
2447            PAGE,
2448            0,
2449            ProtectionFlags::READ,
2450            MapFlags::PRIVATE,
2451        )
2452        .expect("mmap_file");
2453        let ptr = NonNull::new(m.as_ptr() as *mut u8).expect("non-null");
2454        // WillNeed peut retourner ENOSYS sur certains FS (memfd) ; accepté.
2455        let _ = madvise(ptr, PAGE, MadviseAdvice::WillNeed);
2456    }
2457
2458    // ─────────────────────────────────────
2459    // Branches d'erreur (`if ret < 0`) — errno réellement atteignables
2460    // ─────────────────────────────────────
2461
2462    // Couvrent les deux opérandes du `debug_assert!(ret < 0 && ret > -4096)`
2463    // de `errno_from_negative_syscall_ret` (`ret = 0` ; `ret = -5000`).
2464    #[test]
2465    #[should_panic(expected = "ret < 0 && ret > -4096")]
2466    fn errno_from_negative_syscall_ret_panics_on_non_negative() {
2467        let _ = errno_from_negative_syscall_ret(0);
2468    }
2469
2470    #[test]
2471    #[should_panic(expected = "ret < 0 && ret > -4096")]
2472    fn errno_from_negative_syscall_ret_panics_below_errno_range() {
2473        let _ = errno_from_negative_syscall_ret(-5000);
2474    }
2475
2476    /// Adresse non page-alignée et non mappée (`0x1`). `mprotect`/`madvise`/
2477    /// `msync` exigent l'alignement → `EINVAL` ; `mlock`/`munlock` arrondissent
2478    /// à la page mais l'intervalle (page 0) est non mappé → `ENOMEM`.
2479    fn bogus_addr() -> NonNull<u8> {
2480        NonNull::new(core::ptr::without_provenance_mut::<u8>(1)).expect("non-null")
2481    }
2482
2483    #[test]
2484    fn mprotect_einval_on_unaligned_addr() {
2485        let err = mprotect(bogus_addr(), PAGE, ProtectionFlags::READ).unwrap_err();
2486        assert_eq!(err, Errno::EINVAL);
2487    }
2488
2489    #[test]
2490    fn madvise_einval_on_unaligned_addr() {
2491        let err = madvise(bogus_addr(), PAGE, MadviseAdvice::Normal).unwrap_err();
2492        assert_eq!(err, Errno::EINVAL);
2493    }
2494
2495    #[test]
2496    fn msync_einval_on_unaligned_addr() {
2497        let err = msync(bogus_addr(), PAGE, MsyncFlags::SYNC).unwrap_err();
2498        assert_eq!(err, Errno::EINVAL);
2499    }
2500
2501    #[test]
2502    fn mlock_enomem_on_unmapped_range() {
2503        let err = mlock(bogus_addr(), PAGE).unwrap_err();
2504        assert_eq!(err, Errno::ENOMEM);
2505    }
2506
2507    #[test]
2508    fn mlock2_enomem_on_unmapped_range() {
2509        let err = mlock2(bogus_addr(), PAGE, air_sys_types::mem::MlockFlags::empty()).unwrap_err();
2510        assert_eq!(err, Errno::ENOMEM);
2511    }
2512
2513    #[test]
2514    fn munlock_enomem_on_unmapped_range() {
2515        let err = munlock(bogus_addr(), PAGE).unwrap_err();
2516        assert_eq!(err, Errno::ENOMEM);
2517    }
2518
2519    #[test]
2520    fn mlockall_einval_on_empty_flags() {
2521        // `mlockall` sans `MCL_CURRENT` ni `MCL_FUTURE` → EINVAL.
2522        let err = mlockall(air_sys_types::mem::MlockallFlags::empty()).unwrap_err();
2523        assert_eq!(err, Errno::EINVAL);
2524    }
2525
2526    #[test]
2527    fn memfd_create_einval_on_too_long_name() {
2528        // Le nom (préfixé `memfd:` côté kernel) doit tenir dans `NAME_MAX` ;
2529        // 300 octets dépassent la limite → EINVAL.
2530        let long = std::ffi::CString::new(vec![b'a'; 300]).expect("cstring");
2531        let err = memfd_create(&long, MemfdFlags::empty()).unwrap_err();
2532        assert_eq!(err, Errno::EINVAL);
2533    }
2534
2535    #[test]
2536    fn process_vm_readv_esrch_on_nonexistent_pid() {
2537        use air_sys_types::mem::RemoteIoSlice;
2538        use air_sys_types::net::IoSliceMut;
2539        let nope = Pid::try_from_raw(0x7FFF_FFFE).expect("pid littéral");
2540        let mut dst = [0u8; 8];
2541        let src = [0u8; 8];
2542        let mut local = [IoSliceMut::new(&mut dst)];
2543        let remote = [RemoteIoSlice {
2544            addr: src.as_ptr() as u64,
2545            len: 8,
2546        }];
2547        let err = process_vm_readv(nope, &mut local, &remote, 0).unwrap_err();
2548        assert_eq!(err, Errno::ESRCH);
2549    }
2550
2551    #[test]
2552    fn process_vm_writev_esrch_on_nonexistent_pid() {
2553        use air_sys_types::mem::RemoteIoSlice;
2554        use air_sys_types::net::IoSlice;
2555        let nope = Pid::try_from_raw(0x7FFF_FFFE).expect("pid littéral");
2556        let src = [0u8; 8];
2557        let dst = [0u8; 8];
2558        let local = [IoSlice::new(&src)];
2559        let remote = [RemoteIoSlice {
2560            addr: dst.as_ptr() as u64,
2561            len: 8,
2562        }];
2563        let err = process_vm_writev(nope, &local, &remote, 0).unwrap_err();
2564        assert_eq!(err, Errno::ESRCH);
2565    }
2566
2567    #[test]
2568    fn mmap_fixed_noreplace_eexist_on_occupied_addr() {
2569        // `FIXED_NOREPLACE` sur une adresse DÉJÀ mappée → EEXIST (couvre le
2570        // chemin d'erreur `?` de `mmap_fixed`, déterministe car `reserve`
2571        // reste vivant pendant l'appel).
2572        let reserve =
2573            mmap_anonymous(PAGE, ProtectionFlags::READ, MapFlags::PRIVATE).expect("reserve");
2574        let address = NonNull::new(reserve.as_ptr() as *mut u8).expect("non-null");
2575        // SAFETY: `address` est page-aligné et vivant ; `FIXED_NOREPLACE` ne
2576        // remplace rien (il échoue si occupé).
2577        let err = unsafe {
2578            mmap_fixed(
2579                address,
2580                PAGE,
2581                ProtectionFlags::READ,
2582                MapFlags::FIXED_NOREPLACE | MapFlags::PRIVATE,
2583                None,
2584                0,
2585            )
2586        }
2587        .unwrap_err();
2588        assert_eq!(err, Errno::EEXIST);
2589        drop(reserve);
2590    }
2591
2592    #[test]
2593    fn mmap_fixed_over_memfd_succeeds() {
2594        // Couvre le bras `fd = Some` et le succès de `mmap_fixed` : on réserve
2595        // une page puis on re-mappe un memfd PAR-DESSUS avec `MAP_FIXED` (force
2596        // le remplacement — déterministe, contrairement à `FIXED_NOREPLACE`).
2597        let reserve =
2598            mmap_anonymous(PAGE, ProtectionFlags::READ, MapFlags::PRIVATE).expect("reserve");
2599        let address = NonNull::new(reserve.as_ptr() as *mut u8).expect("non-null");
2600        let fd = memfd_create(c"air-mmap-fixed", MemfdFlags::empty()).expect("memfd");
2601        extend_memfd(&fd, PAGE);
2602        // SAFETY: `address` provient d'un mapping vivant (`reserve`), page-aligné ;
2603        // `MAP_FIXED` autorise le remplacement de la page.
2604        let mp = unsafe {
2605            mmap_fixed(
2606                address,
2607                PAGE,
2608                ProtectionFlags::READ,
2609                MapFlags::FIXED | MapFlags::SHARED,
2610                Some(fd.as_fd()),
2611                0,
2612            )
2613        }
2614        .expect("mmap_fixed over memfd");
2615        assert_eq!(mp.address, address);
2616        // `reserve` a été écrasé par `MAP_FIXED` ; éviter un double munmap via Drop.
2617        core::mem::forget(reserve);
2618        raw_munmap(mp.address.as_ptr(), mp.length).expect("munmap");
2619    }
2620
2621    // ─────────────────────────────────────
2622    // MmapRegion — preuve de refcount (déterministe + Miri)
2623    //
2624    // Régions **fictives** (`new_for_test`) : pas de `mmap` (donc exécutables
2625    // sous Miri), `munmap` simulé par un compteur. On prouve : une garde vive ⇒
2626    // jamais de `munmap` ; dernier drop ⇒ `munmap` exactement une fois.
2627    // ─────────────────────────────────────
2628
2629    use core::sync::atomic::Ordering;
2630
2631    #[test]
2632    fn mmap_region_refcount_munmap_exactly_once() {
2633        let counter = Arc::new(ProbeCounter::new(0));
2634        let region = MmapRegion::new_for_test(PAGE, Arc::clone(&counter));
2635        assert_eq!(region.strong_count(), 1);
2636
2637        // Deux gardes « en vol » + un clone utilisateur.
2638        let g1 = region.liveness_handle();
2639        let g2 = region.liveness_handle();
2640        let region2 = region.clone();
2641        assert_eq!(region.strong_count(), 4);
2642        assert_eq!(counter.load(Ordering::SeqCst), 0);
2643
2644        // L'utilisateur lâche ses deux handles : les gardes tiennent le mapping.
2645        drop(region);
2646        drop(region2);
2647        assert_eq!(
2648            counter.load(Ordering::SeqCst),
2649            0,
2650            "munmap en avance alors que des gardes vivent"
2651        );
2652
2653        // Première garde tombe : il en reste une → toujours pas de munmap.
2654        drop(g1);
2655        assert_eq!(counter.load(Ordering::SeqCst), 0);
2656
2657        // Dernière garde tombe : munmap, exactement une fois.
2658        drop(g2);
2659        assert_eq!(counter.load(Ordering::SeqCst), 1, "munmap au dernier drop");
2660    }
2661
2662    #[test]
2663    fn mmap_region_user_drop_with_guard_alive_no_munmap() {
2664        let counter = Arc::new(ProbeCounter::new(0));
2665        let region = MmapRegion::new_for_test(PAGE, Arc::clone(&counter));
2666        let guard = region.liveness_handle();
2667        drop(region);
2668        assert_eq!(
2669            counter.load(Ordering::SeqCst),
2670            0,
2671            "garde vivante ⇒ pas de munmap"
2672        );
2673        drop(guard);
2674        assert_eq!(counter.load(Ordering::SeqCst), 1);
2675    }
2676
2677    #[test]
2678    fn mmap_region_no_guard_drops_once() {
2679        let counter = Arc::new(ProbeCounter::new(0));
2680        let region = MmapRegion::new_for_test(PAGE, Arc::clone(&counter));
2681        // Couvre l'impl `Debug` de `MmapRegion`.
2682        assert!(format!("{region:?}").contains("MmapRegion"));
2683        drop(region);
2684        assert_eq!(counter.load(Ordering::SeqCst), 1);
2685    }
2686
2687    #[test]
2688    fn mmap_region_empty_len_accessors() {
2689        let counter = Arc::new(ProbeCounter::new(0));
2690        let region = MmapRegion::new_for_test(0, Arc::clone(&counter));
2691        assert!(region.is_empty());
2692        assert_eq!(region.len(), 0);
2693        drop(region);
2694        assert_eq!(counter.load(Ordering::SeqCst), 1);
2695    }
2696
2697    #[test]
2698    fn mmap_region_futex_word_validation_errors() {
2699        let counter = Arc::new(ProbeCounter::new(0));
2700        // Région fictive lisible (prot READ|WRITE) : les erreurs de validation
2701        // (bornes/alignement) sont rendues AVANT tout déréférencement.
2702        let region = MmapRegion::new_for_test(16, Arc::clone(&counter));
2703        // Hors bornes : offset + 4 > len.
2704        assert_eq!(region.futex_word(16).unwrap_err(), Errno::EINVAL);
2705        // Non aligné sur 4.
2706        assert_eq!(region.futex_word(1).unwrap_err(), Errno::EINVAL);
2707        assert_eq!(region.futex_word(2).unwrap_err(), Errno::EINVAL);
2708        // Débordement de `offset + 4` (offset aligné proche de usize::MAX).
2709        assert_eq!(
2710            region.futex_word(usize::MAX - 3).unwrap_err(),
2711            Errno::EINVAL
2712        );
2713    }
2714
2715    // ─── MmapRegion — intégration kernel réel (indisponible sous Miri) ───
2716
2717    #[test]
2718    #[cfg_attr(miri, ignore = "mmap via asm! indisponible sous Miri")]
2719    fn mmap_region_anonymous_bytes_and_futex_word() {
2720        let region = MmapRegion::new_anonymous(
2721            PAGE,
2722            ProtectionFlags::READ | ProtectionFlags::WRITE,
2723            MapFlags::PRIVATE,
2724        )
2725        .expect("new_anonymous");
2726        assert_eq!(region.len(), PAGE);
2727        assert!(!region.is_empty());
2728        assert!(!region.as_ptr().is_null());
2729
2730        // Mot futex aligné : store/load via l'AtomicU32 rendu.
2731        let word = region.futex_word(8).expect("futex_word(8)");
2732        word.store(0xABCD, Ordering::SeqCst);
2733        assert_eq!(word.load(Ordering::SeqCst), 0xABCD);
2734
2735        // bytes() relit l'octet écrit (tranche bornée à la taille de la région).
2736        let bytes = region.bytes();
2737        assert_eq!(bytes.len(), PAGE);
2738        assert_eq!(bytes.get(8..12), Some(&0xABCD_u32.to_ne_bytes()[..]));
2739    }
2740
2741    #[test]
2742    #[cfg_attr(miri, ignore = "mmap via asm! indisponible sous Miri")]
2743    fn mmap_region_prot_none_bytes_empty_and_futex_einval() {
2744        // PROT_NONE : non lisible. `bytes()` rend une tranche VIDE (jamais de
2745        // référence vers de la mémoire inaccessible) ; `futex_word` rend EINVAL.
2746        let region = MmapRegion::new_anonymous(PAGE, ProtectionFlags::empty(), MapFlags::PRIVATE)
2747            .expect("new_anonymous PROT_NONE");
2748        assert!(region.bytes().is_empty());
2749        assert_eq!(region.futex_word(0).unwrap_err(), Errno::EINVAL);
2750    }
2751
2752    #[test]
2753    #[cfg_attr(miri, ignore = "mmap via asm! indisponible sous Miri")]
2754    fn mmap_region_from_mapping_no_double_munmap() {
2755        let mapping = mmap_anonymous(
2756            PAGE,
2757            ProtectionFlags::READ | ProtectionFlags::WRITE,
2758            MapFlags::PRIVATE,
2759        )
2760        .expect("mmap_anonymous");
2761        let ptr = mapping.as_ptr();
2762        let region =
2763            MmapRegion::from_mapping(mapping, ProtectionFlags::READ | ProtectionFlags::WRITE);
2764        // Même mapping (transfert, pas de copie).
2765        assert_eq!(region.as_ptr(), ptr);
2766        assert_eq!(region.len(), PAGE);
2767        // Le mapping est vivant : écrire/relire (si `from_mapping` avait laissé
2768        // le `Mapping` munmapper, ce store fauterait — pas de double munmap).
2769        let word = region.futex_word(0).expect("futex_word");
2770        word.store(7, Ordering::SeqCst);
2771        assert_eq!(word.load(Ordering::SeqCst), 7);
2772        drop(region);
2773    }
2774
2775    /// `from_mapping` avec un `prot` **read-only** fidèle : `bytes()` lit (READ
2776    /// présent), mais `futex_word()` rend **`EINVAL`** (pas de WRITE) **avant**
2777    /// tout accès — plus de SIGSEGV latent (le `prot` n'est plus un défaut faux).
2778    #[test]
2779    #[cfg_attr(miri, ignore = "mmap via asm! indisponible sous Miri")]
2780    fn mmap_region_from_mapping_readonly_futex_word_einval() {
2781        // Mapping réellement lisible seul (PROT_READ) : un memfd pré-rempli mappé
2782        // en lecture (un mapping anonyme PROT_READ ne serait pas inscriptible non
2783        // plus, mais resterait lisible — suffisant ici).
2784        let mapping = mmap_anonymous(PAGE, ProtectionFlags::READ, MapFlags::PRIVATE)
2785            .expect("mmap_anonymous PROT_READ");
2786        let region = MmapRegion::from_mapping(mapping, ProtectionFlags::READ);
2787        // Lecture bornée OK (READ présent).
2788        assert_eq!(region.bytes().len(), PAGE);
2789        // Mot futex refusé : la région n'est pas inscriptible (WRITE absent) ⇒
2790        // EINVAL en amont, aucune référence rendue (donc aucune écriture → aucun
2791        // fault).
2792        assert_eq!(region.futex_word(0).unwrap_err(), Errno::EINVAL);
2793    }
2794
2795    #[test]
2796    #[cfg_attr(miri, ignore = "mmap via asm! indisponible sous Miri")]
2797    fn mmap_region_from_file_maps_memfd() {
2798        let fd = memfd_create(c"mmap-region-test", MemfdFlags::CLOEXEC).expect("memfd_create");
2799        extend_memfd(&fd, PAGE);
2800        let region = MmapRegion::from_file(
2801            fd.as_fd(),
2802            PAGE,
2803            ProtectionFlags::READ | ProtectionFlags::WRITE,
2804            MapFlags::SHARED,
2805            0,
2806        )
2807        .expect("from_file");
2808        assert_eq!(region.len(), PAGE);
2809        region
2810            .futex_word(4)
2811            .expect("futex_word")
2812            .store(42, Ordering::SeqCst);
2813        assert_eq!(region.bytes().get(4..8), Some(&42_u32.to_ne_bytes()[..]));
2814    }
2815
2816    proptest::proptest! {
2817        #![proptest_config(proptest::prelude::ProptestConfig::with_cases(64))]
2818
2819        /// `futex_word` rend `Ok` **ssi** l'offset est aligné sur 4 et borné.
2820        #[test]
2821        #[cfg_attr(miri, ignore = "mmap via asm! indisponible sous Miri")]
2822        fn prop_mmap_region_futex_word_validation(offset in 0usize..(2 * PAGE)) {
2823            let region = MmapRegion::new_anonymous(
2824                PAGE,
2825                ProtectionFlags::READ | ProtectionFlags::WRITE,
2826                MapFlags::PRIVATE,
2827            )
2828            .expect("new_anonymous");
2829            let aligned = offset & 0b11 == 0;
2830            let in_bounds = offset.checked_add(4).is_some_and(|end| end <= PAGE);
2831            let got_ok = region.futex_word(offset).is_ok();
2832            proptest::prop_assert_eq!(got_ok, aligned && in_bounds);
2833        }
2834    }
2835}
2836
2837// ───────────────────────────────────────────────────────────────────────────
2838// Preuve de concurrence (loom) — le compteur fort de `MmapRegion` est correct
2839// sous réordonnancement : des drops **concurrents** des gardes de vivacité
2840// produisent `munmap` **exactement une fois**, jamais en avance.
2841//
2842// Exécuter : `RUSTFLAGS="--cfg loom" cargo test -p air-sys-syscall --lib loom`.
2843// ───────────────────────────────────────────────────────────────────────────
2844
2845#[cfg(loom)]
2846mod loom_refcount {
2847    use super::{MmapRegion, ProbeCounter};
2848    use core::sync::atomic::Ordering;
2849    use loom::sync::Arc;
2850
2851    #[test]
2852    fn munmap_exactly_once_under_concurrent_guard_drops() {
2853        loom::model(|| {
2854            let counter = Arc::new(ProbeCounter::new(0));
2855            let region = MmapRegion::new_for_test(64, Arc::clone(&counter));
2856
2857            // Deux gardes « en vol » ; l'utilisateur lâche la région.
2858            let g1 = region.liveness_handle();
2859            let g2 = region.liveness_handle();
2860            drop(region);
2861            assert_eq!(counter.load(Ordering::SeqCst), 0, "munmap en avance");
2862
2863            // Les deux gardes tombent en parallèle : le compteur fort garantit
2864            // un seul drop de l'inner → un seul `munmap`, quel que soit l'ordre.
2865            let t1 = loom::thread::spawn(move || drop(g1));
2866            let t2 = loom::thread::spawn(move || drop(g2));
2867            t1.join().expect("join t1");
2868            t2.join().expect("join t2");
2869
2870            assert_eq!(
2871                counter.load(Ordering::SeqCst),
2872                1,
2873                "munmap doit survenir exactement une fois"
2874            );
2875        });
2876    }
2877}