Retour aux leçons
Scrollytelling

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 :

overflow.c
#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'attaquant

En 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 :

safe.rs
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 :

dangling.c
#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 malveillant

En 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 :

impossible.rs
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.

ownership.rs
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 automatiquement
  Aprè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 :

borrow_error.rs
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 :

erreur du compilateur
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 here

Pourquoi 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émoireC / C++Rust
Buffer overflowPossible, silencieuxPanic ou erreur de compilation
Use-after-freeComportement indéfiniRefusé par le compilateur
Double-freeCrash ou corruptionImpossible (ownership)
Data raceDifficile à détecterRefusé par le compilateur
Null pointerSegfaultPas 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.