air_sys_syscall/arch.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//! Syscalls **spécifiques à l'architecture** — `arch_prctl(2)` (ADR-051).
6//!
7//! `arch_prctl` n'existe **que sur x86_64** : il programme la base des registres
8//! de segment `%fs`/`%gs`. Air l'utilise pour fixer le registre TLS du **thread
9//! principal** (`%fs`) au démarrage (`air-rt`, ADR-049 D2). Sur **aarch64**, le
10//! registre TLS (`tpidr_el0`) est inscriptible directement en EL0 par une
11//! instruction `msr` (affaire d'`air-rt`, pas un syscall) ; ce module est donc
12//! **vide** sur aarch64. Les threads **spawnés** (toutes arches) reçoivent leur
13//! registre TLS du kernel via `CLONE_SETTLS` ([`crate::process::clone_thread`]).
14//!
15//! Conformément à ADR-021 §3 (pas de wrapper multiplexé), `arch_prctl` est
16//! exposé par des fonctions **dédiées** [`set_fs`]/[`get_fs`], jamais par un
17//! `arch_prctl(op, …)` générique.
18
19#[cfg(target_arch = "x86_64")]
20use air_sys_types::Errno;
21#[cfg(target_arch = "x86_64")]
22use core::num::NonZeroI32;
23
24/// `ARCH_SET_FS` (cf. `asm/prctl.h`).
25#[cfg(target_arch = "x86_64")]
26const ARCH_SET_FS: i64 = 0x1002;
27/// `ARCH_GET_FS` (cf. `asm/prctl.h`).
28#[cfg(target_arch = "x86_64")]
29const ARCH_GET_FS: i64 = 0x1003;
30
31/// Fixe la base du registre de segment `%fs` (registre TLS) du thread appelant.
32///
33/// Sert à `air-rt` pour ancrer le **TCB** du thread principal au registre TLS
34/// (ADR-049 D1/D2). N'existe **que sur x86_64**.
35///
36/// # Safety
37///
38/// **Hautement `unsafe`.** Après cet appel, **tout accès `thread_local`** du
39/// thread (y compris ceux insérés par le compilateur / la bibliothèque standard
40/// si elle est liée) résout via `base`. L'appelant doit garantir que `base`
41/// pointe une **image TLS valide et correctement initialisée** pour ce thread
42/// (sinon corruption mémoire silencieuse de tout l'état par-thread). Passer une
43/// base erronée est un comportement indéfini du programme, pas seulement de
44/// cette fonction.
45///
46/// # Errors
47///
48/// - [`Errno::EINVAL`] / [`Errno::EPERM`] : `base` rejetée par le kernel (p. ex.
49/// adresse **non canonique**). Dans ce cas le kernel **ne modifie pas** `%fs`.
50#[cfg(target_arch = "x86_64")]
51pub unsafe fn set_fs(base: usize) -> Result<(), Errno> {
52 let ret: i64;
53 // SAFETY:
54 // - SYS_arch_prctl (x86_64 = 158) avec ARCH_SET_FS programme la base de
55 // `%fs` sur `base`. La validité de `base` (image TLS valide) relève du
56 // contrat `unsafe` ci-dessus.
57 // - L'ABI syscall x86_64 : numéro en RAX, args en RDI/RSI, retour en RAX,
58 // clobbe RCX (RIP) et R11 (RFLAGS).
59 // - **Pas** d'option `readonly`/`pure` : l'effet sur `%fs` change la
60 // sémantique des accès TLS suivants ; on interdit donc au compilateur de
61 // réordonner librement autour de l'asm (barrière conservatrice).
62 unsafe {
63 core::arch::asm!(
64 "syscall",
65 in("rax") 158_i64,
66 in("rdi") ARCH_SET_FS,
67 in("rsi") base,
68 lateout("rax") ret,
69 lateout("rcx") _,
70 lateout("r11") _,
71 options(nostack),
72 );
73 }
74 if ret < 0 {
75 return Err(errno_from_negative_syscall_ret(ret));
76 }
77 Ok(())
78}
79
80/// Lit la base courante du registre de segment `%fs` (registre TLS) du thread
81/// appelant. N'existe **que sur x86_64**. Opération **sûre** (lecture seule).
82///
83/// # Errors
84///
85/// - [`Errno`] propagée si le kernel rejette l'appel (ne devrait pas survenir
86/// sur un kernel conforme).
87#[cfg(target_arch = "x86_64")]
88pub fn get_fs() -> Result<usize, Errno> {
89 let mut base: usize = 0;
90 let ret: i64;
91 // SAFETY:
92 // - SYS_arch_prctl (x86_64 = 158) avec ARCH_GET_FS écrit la base courante de
93 // `%fs` à l'adresse fournie en RSI (`&mut base`, mémoire locale valide).
94 // - Absence d'option `readonly` : l'asm **écrit** la mémoire pointée par
95 // RSI, donc `base` est correctement considéré comme modifié après l'appel.
96 // - Clobbers RCX/R11 comme tout `syscall` x86_64.
97 unsafe {
98 core::arch::asm!(
99 "syscall",
100 in("rax") 158_i64,
101 in("rdi") ARCH_GET_FS,
102 in("rsi") &mut base,
103 lateout("rax") ret,
104 lateout("rcx") _,
105 lateout("r11") _,
106 options(nostack),
107 );
108 }
109 if ret < 0 {
110 return Err(errno_from_negative_syscall_ret(ret));
111 }
112 Ok(base)
113}
114
115/// Convertit un retour de syscall strictement négatif en [`Errno`]. Copie locale
116/// du helper (convention couche 0 : un helper privé par module).
117#[cfg(target_arch = "x86_64")]
118fn errno_from_negative_syscall_ret(ret: i64) -> Errno {
119 debug_assert!(ret < 0 && ret > -4096);
120 #[allow(clippy::cast_possible_truncation)]
121 let raw = ret.wrapping_neg() as i32;
122 let nz = NonZeroI32::new(raw).expect("errno strictement positif par construction");
123 Errno::from_nonzero(nz)
124}
125
126#[cfg(all(test, target_arch = "x86_64"))]
127mod tests {
128 use super::*;
129
130 /// `get_fs` (lecture pure) réussit ; sous un binaire de test lié à la libc,
131 /// `%fs` est déjà programmé (base TLS non nulle).
132 #[test]
133 fn get_fs_returns_nonzero_base() {
134 let base = get_fs().expect("get_fs doit réussir");
135 assert_ne!(base, 0, "la base TLS d'un thread libc est non nulle");
136 }
137
138 /// `set_fs` chemin **succès** sans rien casser : réécrire la **base
139 /// courante** est un no-op effectif (le TLS de la libc reste valide) tout en
140 /// exerçant le chemin nominal du syscall. Cf. ADR-051 (stratégie de test).
141 #[test]
142 fn set_fs_to_current_base_is_ok() {
143 let base = get_fs().expect("get_fs");
144 // SAFETY: on réécrit l'EXACTE base courante → l'image TLS reste celle,
145 // valide, de la libc ; aucun état par-thread n'est invalidé.
146 unsafe { set_fs(base) }.expect("set_fs(base courante) doit réussir");
147 assert_eq!(get_fs().expect("get_fs"), base, "base inchangée");
148 }
149
150 /// `set_fs` chemin **erreur** : une adresse **non canonique** (bit 63 posé)
151 /// est rejetée par le kernel **sans** modifier `%fs` → le TLS reste intact.
152 #[test]
153 fn set_fs_non_canonical_is_rejected_without_changing_fs() {
154 let before = get_fs().expect("get_fs");
155 let non_canonical: usize = 0x8000_0000_0000_0000;
156 // SAFETY: adresse non canonique ⇒ le kernel valide et rejette AVANT de
157 // toucher `%fs` ; aucun accès TLS n'est compromis.
158 let err = unsafe { set_fs(non_canonical) };
159 assert!(err.is_err(), "une base non canonique doit être rejetée");
160 assert_eq!(
161 get_fs().expect("get_fs"),
162 before,
163 "%fs inchangé après rejet"
164 );
165 }
166}