
Rex est un framework d'extension de noyau sûr et utilisable qui permet de charger et d'exécuter des programmes d'extension en Rust à la place d'eBPF.
____ _______ __ _____ _ _
| _ \| ____\ \/ / | ____|_ _| |_ ___ _ __ ___(_) ___ _ __ ___
| |_) | _| \ / | _| \ \/ / __/ _ \ '_ \/ __| |/ _ \| '_ \/ __|
| _ <| |___ / \ | |___ > <| || __/ | | \__ \ | (_) | | | \__ \
|_| \_\_____/_/\_\ |_____/_/\_\\__\___|_| |_|___/_|\___/|_| |_|___|
Rex est un framework d'extension du noyau sûr et utilisable qui permet de charger et d'exécuter des programmes d'extension du noyau en Rust à la place de eBPF. Contrairement aux frameworks basés sur eBPF comme Aya, les programmes Rex ne passent pas par le vérificateur interne du noyau ; au lieu de cela, les programmes sont implémentés dans le sous-ensemble sûr de Rust, sur lequel le compilateur Rust effectue les vérifications de sécurité nécessaires et génère directement du code natif. Cette approche évite les exigences de vérification trop restrictives (par exemple, les contraintes de complexité des programmes) et les erreurs de vérification obscures qui en résultent, tout en offrant potentiellement une meilleure opportunité d'optimisation dans le backend du compilateur natif (c'est-à-dire LLVM) que l'approche backend eBPF + JIT interne au noyau.
Rex prend actuellement en charge les fonctionnalités suivantes :
kprobe, perf_event, tracepoint, xdp, et
tc.L'exemple suivant implémente un programme kprobe qui se rattache à un
appel système sélectionné et injecte une erreur (spécifiée par errno)
dans l'appel système sur un processus (spécifié par son pid). L'exemple
complet, y compris le programme de chargement, se trouve sous
samples/error_injector.
#![no_std]
#![no_main]
use rex::kprobe::kprobe;
use rex::map::RexHashMap;
use rex::pt_regs::PtRegs;
use rex::rex_kprobe;
use rex::rex_map;
use rex::Result;
#[allow(non_upper_case_globals)]
#[rex_map]
static pid_to_errno: RexHashMap<i32, u64> = RexHashMap::new(1, 0);
#[rex_kprobe]
pub fn err_injector(obj: &kprobe, ctx: &mut PtRegs) -> Result {
obj.bpf_get_current_task()
.map(|t| t.get_pid())
.and_then(|p| obj.bpf_map_lookup_elem(&pid_to_errno, &p).cloned())
.map(|e| obj.bpf_override_return(ctx, e))
.ok_or(0)
}
D'autres exemples de programmes se trouvent sous samples.
Vous trouverez le guide détaillé ici.
Des documentations de conception supplémentaires se trouvent sous docs.
L'extension eBPF existante repose sur le vérificateur eBPF interne au noyau pour fournir des garanties de sécurité. Cela conduit malheureusement à des problèmes d'utilisabilité où des programmes sûrs sont rejetés par le vérificateur, y compris, sans s'y limiter :
Rex vise à résoudre ces problèmes en exploitant directement la garantie de sécurité de Rust sûr. Les développeurs peuvent implémenter leurs programmes de toute manière qui peut être écrite en Rust sûr avec peu de restrictions, et n'ont plus à se soucier de la complexité du programme, du générateur de code, ou de trouver la manière (souvent contre-intuitive) d'exprimer la même logique pour satisfaire le vérificateur.
Nous le démontrons avec l'implémentation du BPF Memcached Cache (BMC), un programme d'extension de pointe pour l'accélération de Memcached. En tant que programme eBPF complexe, BMC est forcé d'être divisé en plusieurs composants connectés par des tail-calls BPF et d'utiliser des implémentations de boucles/branchements maladroites pour satisfaire le vérificateur, ce qui n'est pas du tout nécessaire dans son implémentation Rex.
Par exemple, nous montrons le code de la logique d'invalidation de cache
du BPF-BMC qui recherche une commande SET dans le payload du paquet :
// Searches for SET command in payload
for (unsigned int off = 0;
off < BMC_MAX_PACKET_LENGTH && payload + off + 1 <= data_end;
off++) {
if (set_found == 0 && payload[off] == 's' &&
payload + off + 3 <= data_end && payload[off + 1] == 'e' &&
payload[off + 2] == 't') {
off += 3;
set_found = 1;
}
...
}
Le code introduit non seulement une contrainte supplémentaire dans la
boucle (off < BMC_MAX_PACKET_LENGTH) uniquement pour passer le
vérificateur, mais utilise aussi du code passe-partout répété pour
vérifier les fins de paquet (data_end) et une logique lourde pour
trouver la chaîne "set" dans le paquet.
Aucun de ces fardeaux n'est nécessaire avec la puissance de Rust sûr dans Rex, qui n'a pas de limites de complexité et offre plus de liberté sur l'implémentation :
let set_iter = payload.windows(4).enumerate().filter_map(|(i, v)| {
if v == b"set " {
Some(i)
} else {
None
}
});
L'implémentation complète de BMC dans Rex se trouve sous samples/bmc.
Rex est sous licence GPLv2. Les sous-modules (Linux, Rust, LLVM) dans ce dépôt sont sous leurs propres licences. Veuillez consulter les fichiers de licence correspondants pour plus de détails. De plus, le benchmark memcached est sous licence MIT.