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}