air_sys_syscall/system.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 `system`.
6//!
7//! Cf. `docs/specs/layer-0/family-system.md`.
8
9#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
10compile_error!("air-sys-syscall::system supporte uniquement x86_64 et aarch64 (ADR-014).");
11
12use alloc::ffi::CString;
13use core::ffi::CStr;
14use core::num::NonZeroI32;
15
16use air_sys_types::Errno;
17use air_sys_types::system::{GetrandomFlags, SystemInfo, UtsName};
18
19// ─────────────────────────────────────────────────────────────────────────
20// Kernel structs (repr(C), utilisées uniquement localement)
21// ─────────────────────────────────────────────────────────────────────────
22
23/// Représentation locale de `struct utsname` (Linux) : 6 champs de 65 octets
24/// chacun, chaînes C terminées par NUL.
25#[repr(C)]
26struct KernelUtsname {
27 sysname: [u8; 65],
28 nodename: [u8; 65],
29 release: [u8; 65],
30 version: [u8; 65],
31 machine: [u8; 65],
32 domainname: [u8; 65],
33}
34
35impl KernelUtsname {
36 fn zeroed() -> Self {
37 Self {
38 sysname: [0u8; 65],
39 nodename: [0u8; 65],
40 release: [0u8; 65],
41 version: [0u8; 65],
42 machine: [0u8; 65],
43 domainname: [0u8; 65],
44 }
45 }
46}
47
48/// Représentation locale de `struct sysinfo` sur LP64 (x86_64 / aarch64).
49///
50/// Le padding est arch-dépendant. Sur les cibles 64 bits Linux (LP64),
51/// la structure fait exactement 112 octets.
52#[repr(C)]
53struct KernelSysinfo {
54 uptime: i64,
55 loads: [u64; 3],
56 totalram: u64,
57 freeram: u64,
58 sharedram: u64,
59 bufferram: u64,
60 totalswap: u64,
61 freeswap: u64,
62 procs: u16,
63 pad1: [u8; 6], // padding d'alignement LP64 jusqu'à l'offset 80
64 totalhigh: u64,
65 freehigh: u64,
66 mem_unit: u32,
67 pad2: [u8; 4], // padding final à 112 octets
68}
69
70const _: () = assert!(core::mem::size_of::<KernelSysinfo>() == 112);
71
72impl KernelSysinfo {
73 fn zeroed() -> Self {
74 Self {
75 uptime: 0,
76 loads: [0; 3],
77 totalram: 0,
78 freeram: 0,
79 sharedram: 0,
80 bufferram: 0,
81 totalswap: 0,
82 freeswap: 0,
83 procs: 0,
84 pad1: [0; 6],
85 totalhigh: 0,
86 freehigh: 0,
87 mem_unit: 0,
88 pad2: [0; 4],
89 }
90 }
91}
92
93// ─────────────────────────────────────────────────────────────────────────
94// Helper errno
95// ─────────────────────────────────────────────────────────────────────────
96
97fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
98 debug_assert!(ret < 0 && ret > -4096);
99 #[allow(clippy::cast_possible_truncation)]
100 let raw = ret.wrapping_neg() as i32;
101 let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
102 Errno::from_nonzero(nz)
103}
104
105// ─────────────────────────────────────────────────────────────────────────
106// Helper : extrait une CString à partir d'un buffer null-terminated
107// ─────────────────────────────────────────────────────────────────────────
108
109/// Extrait la portion avant le premier NUL d'un buffer kernel null-terminé,
110/// et la convertit en [`CString`].
111///
112/// Convention 4 ADR-021 exception explicite : la sémantique exige un type
113/// owned (le buffer kernel est local temporaire ; la `CString` doit sortir
114/// du frame). Allocation bornée à la longueur du champ (65 octets max).
115fn cstring_from_uts_field(field: &[u8; 65]) -> CString {
116 let length = field.iter().position(|&b| b == 0).unwrap_or(65);
117 // CString::new exige l'absence de NUL interne ; on passe uniquement les
118 // octets avant le premier NUL trouvé — le kernel garantit la terminaison.
119 CString::new(&field[..length]).expect("champ utsname sans NUL interne avant terminateur")
120}
121
122// ─────────────────────────────────────────────────────────────────────────
123// getrandom
124// ─────────────────────────────────────────────────────────────────────────
125
126/// Remplit `buffer` avec des octets aléatoires cryptographiquement sûrs.
127///
128/// Utilise le syscall `getrandom(2)` (Linux 3.17+), qui puise dans le
129/// CSPRNG kernel — mécanisme recommandé par Air pour toute génération
130/// d'entropie. Ne passe jamais par `/dev/urandom` ni par la libc.
131///
132/// Peut retourner **moins** d'octets que `buffer.len()` (surtout pour les
133/// grands buffers). L'appelant doit boucler si nécessaire.
134///
135/// # Parameters
136///
137/// - `buffer` : buffer de destination à remplir.
138/// - `flags` : drapeaux de comportement (`GetrandomFlags::empty()` pour
139/// le cas nominal).
140///
141/// # Errors
142///
143/// - `EINTR` : interrompu par un signal (conforme à ADR-021 convention 2 ;
144/// l'appelant doit retry explicitement si désiré).
145/// - `EAGAIN` : entropie non disponible et `NONBLOCK` était positionné
146/// (au démarrage, avant l'initialisation du pool).
147/// - `EFAULT` : pointeur invalide (ne se produit pas via cette API safe).
148///
149/// # Examples
150///
151/// ```no_run
152/// use air_sys_syscall::system::getrandom;
153/// use air_sys_types::GetrandomFlags;
154///
155/// let mut buffer = [0u8; 32];
156/// let n = getrandom(&mut buffer, GetrandomFlags::empty()).expect("getrandom");
157/// assert!(n > 0);
158/// ```
159pub fn getrandom(buffer: &mut [u8], flags: GetrandomFlags) -> Result<usize, Errno> {
160 let buf_ptr = buffer.as_mut_ptr() as u64;
161 // Longueur du buffer : sur les cibles LP64 d'Air (x86_64/aarch64),
162 // usize == u64, la conversion est exacte. `cast_lossless` non applicable
163 // ici car usize -> u64 est lossless sur 64 bits ; on le documente.
164 #[allow(clippy::cast_lossless)]
165 let buf_len = buffer.len() as u64;
166 let flags_u64 = u64::from(flags.bits());
167
168 // SAFETY:
169 // - getrandom(2) (x86_64 = 318, aarch64 = 278) écrit `count` octets au
170 // maximum dans `buffer` (pointeur valide pour toute la durée du syscall,
171 // fourni par la référence mutable `buffer`).
172 // - Pas de `readonly` : le kernel écrit dans `*buf_ptr`.
173 let ret = unsafe { raw_syscall_getrandom(buf_ptr, buf_len, flags_u64) };
174 if ret < 0 {
175 return Err(errno_from_negative_syscall_ret(ret));
176 }
177 // ret >= 0 : nombre d'octets écrits. Borné par buffer.len() <= usize::MAX.
178 // Sur LP64, i64 et usize ont la même largeur ; le cast est exact.
179 // La troncature ne peut pas se produire car ret est borné par buffer.len(),
180 // qui est lui-même un usize — donc ret < usize::MAX sur LP64.
181 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
182 Ok(ret as usize)
183}
184
185// ─────────────────────────────────────────────────────────────────────────
186// uname
187// ─────────────────────────────────────────────────────────────────────────
188
189/// Retourne les informations sur le système d'exploitation et le kernel.
190///
191/// Wrappeur de `uname(2)`. Retourne un [`UtsName`] avec les champs
192/// `sysname`, `nodename`, `release`, `version`, `machine` et
193/// `domainname` sous forme de [`CString`] propriétaires.
194///
195/// # Errors
196///
197/// `uname(2)` ne retourne jamais d'erreur en pratique sur Linux moderne
198/// (la glibc et le kernel ne documentent aucun errno). Retour d'erreur
199/// inclus par convention défensive.
200///
201/// # Examples
202///
203/// ```no_run
204/// use air_sys_syscall::system::uname;
205///
206/// let info = uname().expect("uname");
207/// // info.machine vaut "x86_64" ou "aarch64" sur les plateformes Air
208/// ```
209pub fn uname() -> Result<UtsName, Errno> {
210 let mut buffer = KernelUtsname::zeroed();
211 let buf_ptr = (&mut buffer as *mut KernelUtsname) as u64;
212
213 // SAFETY:
214 // - uname(2) (x86_64 = 63, aarch64 = 160) écrit `sizeof(struct utsname)`
215 // octets dans le pointeur fourni. `buffer` est local valide pour toute la
216 // durée du syscall.
217 // - Pas de `readonly` : le kernel écrit dans `*buf_ptr`.
218 let ret = unsafe { raw_syscall_uname(buf_ptr) };
219 if ret < 0 {
220 return Err(errno_from_negative_syscall_ret(ret));
221 }
222
223 Ok(UtsName {
224 sysname: cstring_from_uts_field(&buffer.sysname),
225 nodename: cstring_from_uts_field(&buffer.nodename),
226 release: cstring_from_uts_field(&buffer.release),
227 version: cstring_from_uts_field(&buffer.version),
228 machine: cstring_from_uts_field(&buffer.machine),
229 domainname: cstring_from_uts_field(&buffer.domainname),
230 })
231}
232
233// ─────────────────────────────────────────────────────────────────────────
234// sysinfo
235// ─────────────────────────────────────────────────────────────────────────
236
237/// Retourne des statistiques globales sur l'état du système.
238///
239/// Wrappeur de `sysinfo(2)`. Les valeurs de RAM et swap sont converties
240/// en octets (multiplication par `mem_unit` effectuée en interne).
241/// Les `load_average` sont normalisés en `f64`.
242///
243/// # Errors
244///
245/// `sysinfo(2)` ne retourne normalement pas d'erreur.
246///
247/// # Examples
248///
249/// ```no_run
250/// use air_sys_syscall::system::sysinfo;
251///
252/// let info = sysinfo().expect("sysinfo");
253/// // info.free_ram indique la RAM libre en octets
254/// ```
255pub fn sysinfo() -> Result<SystemInfo, Errno> {
256 let mut info = KernelSysinfo::zeroed();
257 let info_ptr = (&mut info as *mut KernelSysinfo) as u64;
258
259 // SAFETY:
260 // - sysinfo(2) (x86_64 = 99, aarch64 = 179) écrit exactement 112 octets
261 // dans le pointeur fourni (struct sysinfo LP64). `info` est local valide
262 // pour toute la durée du syscall.
263 // - Pas de `readonly` : le kernel écrit dans `*info_ptr`.
264 let ret = unsafe { raw_syscall_sysinfo(info_ptr) };
265 if ret < 0 {
266 return Err(errno_from_negative_syscall_ret(ret));
267 }
268
269 // mem_unit est l'unité des valeurs mémoire du kernel. Sur Linux moderne
270 // mem_unit vaut toujours 1, mais la spec impose la multiplication.
271 let unit = u64::from(info.mem_unit);
272
273 // Saturation sur overflow : mem_unit * valeur peut théoriquement déborder
274 // un u64 sur des systèmes avec mem_unit > 1 et RAM extrême. La saturation
275 // est préférable à un résultat incorrect silencieux (Principe 2).
276 let scale = |v: u64| v.saturating_mul(unit);
277
278 // uptime : i64 de secondes. Valeur toujours >= 0 après boot.
279 let uptime_secs = u64::try_from(info.uptime).unwrap_or(0);
280 let uptime = core::time::Duration::from_secs(uptime_secs);
281
282 // Load averages : divisés par 65536.0 pour obtenir une valeur en nombre
283 // de processus. Le cast f64 d'un u64 est une widening conversion (lossless
284 // pour les valeurs dans la plage de représentation IEEE 754 double).
285 #[allow(clippy::cast_precision_loss)]
286 let load_average = [
287 info.loads[0] as f64 / 65536.0,
288 info.loads[1] as f64 / 65536.0,
289 info.loads[2] as f64 / 65536.0,
290 ];
291
292 Ok(SystemInfo {
293 uptime,
294 load_average,
295 total_ram: scale(info.totalram),
296 free_ram: scale(info.freeram),
297 shared_ram: scale(info.sharedram),
298 buffer_ram: scale(info.bufferram),
299 total_swap: scale(info.totalswap),
300 free_swap: scale(info.freeswap),
301 processes: info.procs,
302 total_high: scale(info.totalhigh),
303 free_high: scale(info.freehigh),
304 mem_unit: info.mem_unit,
305 })
306}
307
308// ─────────────────────────────────────────────────────────────────────────
309// gethostname (via uname)
310// ─────────────────────────────────────────────────────────────────────────
311
312/// Retourne le nom d'hôte du système.
313///
314/// Implémenté en interne via `uname(2)` (extraction du champ `nodename`)
315/// plutôt qu'un syscall `gethostname` séparé — économise un syscall et
316/// réutilise le code déjà testé.
317///
318/// # Errors
319///
320/// Voir [`uname`].
321///
322/// # Examples
323///
324/// ```no_run
325/// use air_sys_syscall::system::gethostname;
326///
327/// let name = gethostname().expect("gethostname");
328/// ```
329pub fn gethostname() -> Result<CString, Errno> {
330 let info = uname()?;
331 Ok(info.nodename)
332}
333
334// ─────────────────────────────────────────────────────────────────────────
335// sethostname
336// ─────────────────────────────────────────────────────────────────────────
337
338/// Modifie le nom d'hôte du système.
339///
340/// Wrappeur de `sethostname(2)`. Exige `CAP_SYS_ADMIN`. Rarement
341/// utilisé par les applications Air normales — plutôt réservé au
342/// launcher dans un UTS namespace.
343///
344/// # Parameters
345///
346/// - `name` : nouveau nom d'hôte. Doit avoir une longueur ≤
347/// `HOST_NAME_MAX` (64 octets sur Linux). Les octets NUL intermédiaires
348/// sont interdits.
349///
350/// # Errors
351///
352/// - `EPERM` : `CAP_SYS_ADMIN` manquant.
353/// - `EINVAL` : nom trop long (> 64 octets).
354///
355/// # Examples
356///
357/// ```no_run
358/// use air_sys_syscall::system::sethostname;
359/// use core::ffi::CStr;
360///
361/// // Requiert CAP_SYS_ADMIN
362/// sethostname(c"air-node-01").expect("sethostname");
363/// ```
364pub fn sethostname(name: &CStr) -> Result<(), Errno> {
365 let bytes = name.to_bytes(); // octets sans le NUL terminal
366 let ptr = bytes.as_ptr() as u64;
367 // Longueur sans le NUL terminal (sémantique sethostname(2)).
368 // Sur LP64, usize == u64, cast exact.
369 #[allow(clippy::cast_lossless)]
370 let length = bytes.len() as u64;
371
372 // SAFETY:
373 // - sethostname(2) (x86_64 = 170, aarch64 = 161) lit `length` octets à
374 // l'adresse `ptr`. `name.to_bytes()` fournit une slice valide pour
375 // toute la durée du syscall (lifetime de `name` couvre l'appel).
376 // - `readonly` correct : le kernel ne modifie pas la mémoire passée.
377 let ret = unsafe { raw_syscall_sethostname(ptr, length) };
378 if ret < 0 {
379 return Err(errno_from_negative_syscall_ret(ret));
380 }
381 Ok(())
382}
383
384// ─────────────────────────────────────────────────────────────────────────
385// Raw syscall helpers — x86_64
386// ─────────────────────────────────────────────────────────────────────────
387
388#[cfg(target_arch = "x86_64")]
389#[inline]
390unsafe fn raw_syscall_getrandom(buffer: u64, count: u64, flags: u64) -> i64 {
391 let ret: i64;
392 // SAFETY:
393 // - SYS_getrandom (x86_64 = 318). Le kernel écrit au plus `count` octets
394 // à l'adresse `buffer` (validité garantie par l'appelant — référence mutable
395 // au buffer).
396 // - ABI x86_64 : numéro en RAX, args en RDI/RSI/RDX ; retour en RAX ;
397 // clobbe RCX (RIP) et R11 (RFLAGS).
398 // - Pas de `readonly` : le kernel écrit dans `*buffer`.
399 unsafe {
400 core::arch::asm!(
401 "syscall",
402 in("rax") 318_i64,
403 in("rdi") buffer,
404 in("rsi") count,
405 in("rdx") flags,
406 lateout("rax") ret,
407 lateout("rcx") _,
408 lateout("r11") _,
409 options(nostack, preserves_flags),
410 );
411 }
412 ret
413}
414
415#[cfg(target_arch = "x86_64")]
416#[inline]
417unsafe fn raw_syscall_uname(buffer: u64) -> i64 {
418 let ret: i64;
419 // SAFETY:
420 // - SYS_uname (x86_64 = 63). Le kernel écrit `sizeof(struct utsname)`
421 // (390 octets) à l'adresse `buffer` (validité garantie par l'appelant —
422 // `KernelUtsname` local).
423 // - ABI x86_64 : numéro en RAX, arg1 en RDI ; retour en RAX ; clobbe
424 // RCX et R11.
425 // - Pas de `readonly` : le kernel écrit dans `*buffer`.
426 unsafe {
427 core::arch::asm!(
428 "syscall",
429 in("rax") 63_i64,
430 in("rdi") buffer,
431 lateout("rax") ret,
432 lateout("rcx") _,
433 lateout("r11") _,
434 options(nostack, preserves_flags),
435 );
436 }
437 ret
438}
439
440#[cfg(target_arch = "x86_64")]
441#[inline]
442unsafe fn raw_syscall_sysinfo(buffer: u64) -> i64 {
443 let ret: i64;
444 // SAFETY:
445 // - SYS_sysinfo (x86_64 = 99). Le kernel écrit exactement 112 octets
446 // (struct sysinfo LP64) à l'adresse `buffer` (validité garantie par
447 // l'appelant — `KernelSysinfo` local, 112 octets vérifiés à la
448 // compilation).
449 // - ABI x86_64 : numéro en RAX, arg1 en RDI ; retour en RAX ; clobbe
450 // RCX et R11.
451 // - Pas de `readonly` : le kernel écrit dans `*buffer`.
452 unsafe {
453 core::arch::asm!(
454 "syscall",
455 in("rax") 99_i64,
456 in("rdi") buffer,
457 lateout("rax") ret,
458 lateout("rcx") _,
459 lateout("r11") _,
460 options(nostack, preserves_flags),
461 );
462 }
463 ret
464}
465
466#[cfg(target_arch = "x86_64")]
467#[inline]
468unsafe fn raw_syscall_sethostname(name: u64, length: u64) -> i64 {
469 let ret: i64;
470 // SAFETY:
471 // - SYS_sethostname (x86_64 = 170). Le kernel lit `length` octets à
472 // l'adresse `name` (validité garantie par l'appelant — bytes valide
473 // pour la durée du syscall).
474 // - ABI x86_64 : numéro en RAX, args en RDI/RSI ; retour en RAX ;
475 // clobbe RCX et R11.
476 // - `readonly` correct : le kernel lit uniquement la mémoire fournie,
477 // sans l'écrire.
478 unsafe {
479 core::arch::asm!(
480 "syscall",
481 in("rax") 170_i64,
482 in("rdi") name,
483 in("rsi") length,
484 lateout("rax") ret,
485 lateout("rcx") _,
486 lateout("r11") _,
487 options(nostack, preserves_flags, readonly),
488 );
489 }
490 ret
491}
492
493// ─────────────────────────────────────────────────────────────────────────
494// Raw syscall helpers — aarch64
495// ─────────────────────────────────────────────────────────────────────────
496
497#[cfg(target_arch = "aarch64")]
498#[inline]
499unsafe fn raw_syscall_getrandom(buffer: u64, count: u64, flags: u64) -> i64 {
500 let ret: i64;
501 // SAFETY:
502 // - SYS_getrandom (aarch64 = 278). Le kernel écrit au plus `count`
503 // octets à l'adresse `buffer` (validité garantie par l'appelant).
504 // - ABI aarch64 : numéro en X8, args en X0/X1/X2, retour en X0.
505 // `inout("x0")` passe buffer en entrée et lit ret en sortie.
506 // - Pas de `readonly` : le kernel écrit dans `*buffer`.
507 unsafe {
508 core::arch::asm!(
509 "svc 0",
510 in("x8") 278_i64,
511 inout("x0") buffer => ret,
512 in("x1") count,
513 in("x2") flags,
514 options(nostack, preserves_flags),
515 );
516 }
517 ret
518}
519
520#[cfg(target_arch = "aarch64")]
521#[inline]
522unsafe fn raw_syscall_uname(buffer: u64) -> i64 {
523 let ret: i64;
524 // SAFETY:
525 // - SYS_uname (aarch64 = 160). Identique x86_64 en sémantique.
526 // - ABI aarch64 : numéro en X8, arg1 en X0, retour en X0.
527 // - Pas de `readonly` : le kernel écrit dans `*buffer`.
528 unsafe {
529 core::arch::asm!(
530 "svc 0",
531 in("x8") 160_i64,
532 inout("x0") buffer => ret,
533 options(nostack, preserves_flags),
534 );
535 }
536 ret
537}
538
539#[cfg(target_arch = "aarch64")]
540#[inline]
541unsafe fn raw_syscall_sysinfo(buffer: u64) -> i64 {
542 let ret: i64;
543 // SAFETY:
544 // - SYS_sysinfo (aarch64 = 179). Identique x86_64 en sémantique.
545 // - ABI aarch64 : numéro en X8, arg1 en X0, retour en X0.
546 // - Pas de `readonly` : le kernel écrit dans `*buffer`.
547 unsafe {
548 core::arch::asm!(
549 "svc 0",
550 in("x8") 179_i64,
551 inout("x0") buffer => ret,
552 options(nostack, preserves_flags),
553 );
554 }
555 ret
556}
557
558#[cfg(target_arch = "aarch64")]
559#[inline]
560unsafe fn raw_syscall_sethostname(name: u64, length: u64) -> i64 {
561 let ret: i64;
562 // SAFETY:
563 // - SYS_sethostname (aarch64 = 161). Identique x86_64 en sémantique.
564 // - ABI aarch64 : numéro en X8, args en X0/X1, retour en X0.
565 // - `readonly` correct : le kernel lit uniquement la mémoire fournie.
566 unsafe {
567 core::arch::asm!(
568 "svc 0",
569 in("x8") 161_i64,
570 inout("x0") name => ret,
571 in("x1") length,
572 options(nostack, preserves_flags, readonly),
573 );
574 }
575 ret
576}
577
578// ─────────────────────────────────────────────────────────────────────────
579// Tests
580// ─────────────────────────────────────────────────────────────────────────
581
582#[cfg(test)]
583mod tests;