Skip to main content

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}