Sécurité mémoire
Pourquoi Rust empêche les bugs mémoire à la compilation
Le problème de la mémoire
Selon Microsoft et Google, environ 70 % des vulnérabilités de sécurité dans les logiciels systèmes sont des bugs liés à la mémoire : buffer overflows, use-after-free, double-free, dangling pointers...
Ces bugs existent depuis des décennies dans les programmes C et C++. Ils sont la cause de failles célèbres comme Heartbleed, WannaCry, ou encore des dizaines de CVE chaque mois dans le noyau Linux.
Bugs de sécurité dans les logiciels systèmes ┌──────────────────────────────────────────┐ │██████████████████████████████████░░░░░░░░│ │ ~70 % mémoire │ autre │ └──────────────────────────────────────────┘ Source : Microsoft Security Response Center, 2019
Et si un langage pouvait éliminer ces bugs à la compilation, sans sacrifier les performances ? C'est exactement ce que fait Rust.
Buffer Overflow
Un buffer overflow (dépassement de tampon) se produit quand un programme écrit au-delà des limites d'un tableau. En C, rien ne vous en empêche :
#include <string.h>
void vulnerable(const char *input) {
char buffer[8]; // seulement 8 octets
strcpy(buffer, input); // aucune vérification !
// Si input fait plus de 8 octets,
// on écrase la pile -> exécution de code arbitraire
}Voici ce qui se passe en mémoire lors de cet overflow :
Pile (Stack) — avant overflow :
┌──────────┬──────────────┬───────────────┐
│ buffer[8]│ saved EBP │ return addr │
│ "Hello" │ 0xBFFF1234 │ 0x08041234 │
└──────────┴──────────────┴───────────────┘
Pile (Stack) — après overflow :
┌──────────┬──────────────┬───────────────┐
│ buffer[8]│ ÉCRASÉ !!! │ ÉCRASÉ !!! │
│ "AAAAAA" │ "AAAA" │ 0xDEADBEEF │
└──────────┴──────────────┴───────────────┘
↑ adresse contrôlée
par l'attaquantEn Rust, les accès aux tableaux sont vérifiés automatiquement. Un accès hors limites provoque un panic! au lieu de corrompre silencieusement la mémoire :
fn main() {
let buffer = [0u8; 8];
// Erreur à l'exécution : index out of bounds
// Au lieu de corrompre la mémoire silencieusement
println!("{}", buffer[10]);
// ^^ panic: index out of bounds
}Use-After-Free
Un use-after-free se produit quand on utilise un pointeur vers une zone mémoire qui a déjà été libérée. En C, le compilateur ne détecte rien :
#include <stdlib.h>
#include <stdio.h>
int main() {
int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr); // la mémoire est libérée
// Comportement indéfini !
// ptr pointe vers une zone libérée
printf("%d\n", *ptr); // use-after-free
return 0;
} Tas (Heap) :
┌──────────────┐
│ 42 │ ← ptr pointe ici
└──────────────┘
↓ free(ptr)
┌──────────────┐
│ ???????? │ ← mémoire libérée !
└──────────────┘
↓ *ptr → comportement indéfini
Peut lire des données d'un autre objet,
crasher, ou pire : exécuter du code malveillantEn Rust, le système d'ownership rend ce bug impossible. Quand une valeur est libérée (drop), le compilateur interdit toute utilisation ultérieure :
fn main() {
let s = String::from("hello");
drop(s); // libère la mémoire
// Erreur de compilation !
// println!("{}", s);
// ^^^^^^^^^^^^^^^ error[E0382]: borrow of moved value: `s`
}Le compilateur Rust refuse catégoriquement de produire un exécutable qui contiendrait un use-after-free. Le bug est détecté avant même que le programme ne s'exécute.
La solution de Rust : l'ownership
Le modèle d'ownership de Rust repose sur trois règles simples, vérifiées à la compilation par le borrow checker :
Règle 1 — Chaque valeur a un seul propriétaire
Une donnée en mémoire appartient à exactement une variable.
Règle 2 — Le transfert de propriété (move)
Quand on assigne une valeur à une autre variable, la propriété est transférée.
Règle 3 — Libération automatique (drop)
Quand le propriétaire sort de la portée, la mémoire est automatiquement libérée.
fn main() {
let s1 = String::from("bonjour");
let s2 = s1; // s1 est "moved" vers s2
// println!("{}", s1); // ERREUR : s1 n'est plus valide
println!("{}", s2); // OK : s2 est le propriétaire
} // s2 sort de la portée → mémoire libérée automatiquementAprès let s2 = s1; : Pile (Stack) Tas (Heap) ┌──────────┐ ┌───────────────┐ │ s1 │ ──╳── │ "bonjour" │ │ (invalidé)│ │ │ ├──────────┤ │ │ │ s2 │ ──────→ │ │ │ ptr, len │ └───────────────┘ └──────────┘ Un seul propriétaire → pas de double-free
Le compilateur comme gardien
Le borrow checker de Rust analyse votre code à la compilation et refuse tout programme qui pourrait provoquer un bug mémoire. Voyons un exemple concret :
fn main() {
let mut data = vec![1, 2, 3];
let first = &data[0]; // emprunt immutable
data.push(4); // mutation pendant un emprunt !
println!("{}", first); // utilisation de l'emprunt
}Le compilateur Rust refuse ce code avec un message clair :
error[E0502]: cannot borrow `data` as mutable because it
is also borrowed as immutable
--> src/main.rs:5:5
|
4 | let first = &data[0];
| ---- immutable borrow occurs here
5 | data.push(4);
| ^^^^^^^^^^^^ mutable borrow occurs here
6 | println!("{}", first);
| ----- immutable borrow later used herePourquoi est-ce important ? Parce que push peut réallouer le vecteur en mémoire, ce qui invaliderait le pointeur first. En C++, ce serait un dangling pointer silencieux. En Rust, c'est une erreur de compilation.
Pourquoi push() est dangereux :
Avant push : Après push (réallocation) :
┌───┬───┬───┐ ┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ │ 1 │ 2 │ 3 │ 4 │ ← nouveau bloc
└───┴───┴───┘ └───┴───┴───┴───┘
↑ (ancien bloc libéré)
first pointe ici first → ??? DANGLING !
Rust empêche cela à la compilation.Résumé
Rust apporte la sécurité mémoire sans ramasse-miettes (garbage collector). Là où Java ou Go utilisent un GC qui tourne en arrière-plan et consomme des ressources, Rust vérifie tout à la compilation.
| Bug mémoire | C / C++ | Rust |
|---|---|---|
| Buffer overflow | Possible, silencieux | Panic ou erreur de compilation |
| Use-after-free | Comportement indéfini | Refusé par le compilateur |
| Double-free | Crash ou corruption | Impossible (ownership) |
| Data race | Difficile à détecter | Refusé par le compilateur |
| Null pointer | Segfault | Pas de null (Option<T>) |
Le coût de cette sécurité ? Zéro à l'exécution. Toutes les vérifications se font pendant la compilation. Le binaire produit est aussi rapide qu'un programme C équivalent, mais sans les bugs mémoire.