FlawOpen

flawopen.com/Sécurité mémoire/Rust unsafe

Le bloc unsafe de Rust reste-t-il sûr pour la mémoire ?

CWE-787: Out-of-bounds WritePage de référence
Réponse courte

unsafe ne désactive pas les vérifications de Rust. Il autorise certaines opérations dont le compilateur ne peut pas prouver la sûreté ; le programmeur doit satisfaire leurs obligations. Une abstraction unsafe non sound peut permettre à des appels safe ordinaires de provoquer un comportement indéfini (UB), même s’ils n’emploient jamais le mot unsafe.

Termes clés

Opération unsafe :
Opération dont le compilateur ne peut pas prouver les conditions de sûreté, comme déréférencer un pointeur raw.
Abstraction sound :
API safe dont les appelants safe ne peuvent pas provoquer de comportement indéfini.
size_hint() :
Estimation de la taille d’un itérateur pour optimiser ; ce n’est ni un décompte exact ni une preuve de sûreté mémoire.
Comportement indéfini (UB) :
Comportement que Rust ne définit pas ; le code unsafe doit empêcher les appelants safe de le provoquer.

Échec d’une abstraction safe non sound

Étape 1

Une API safe reçoit un itérateur

L’appelant transmet un itérateur ordinaire à une fonction qui n’expose aucune précondition unsafe.

Étape 2

La réservation utilise une estimation

La fonction ne réserve que la borne inférieure indiquée, qui peut être inférieure au nombre d’éléments produits.

Étape 3

Les écritures non vérifiées dépassent la capacité

L’implémentation écrit chaque élément via un pointeur raw sans agrandir l’allocation ni vérifier la capacité à chaque élément.

Étape 4

Un appel safe atteint l’UB

Si l’itérateur produit plus d’éléments que l’espace disponible, un appel safe peut écrire hors limites. Utilisez Vec::extend ou une méthode qui agrandit l’allocation correctement.

NON SOUND
// UNSOUND: reserve uses only the lower hint,
// then unchecked writes consume every item.
fn append_unsound(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) {
    let (lower, _) = iter.size_hint();
    dst.reserve(lower);
    let start = dst.len();
    let mut written = 0;
    unsafe {
        let out = dst.as_mut_ptr().add(start);
        for item in iter {
            // UB once writes exceed spare capacity.
            out.add(written).write(item);
            written += 1;
        }
        dst.set_len(start + written);
    }
}
SOUND
// SOUND: Vec::extend handles capacity
// growth and length updates safely.
fn append(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) {
    dst.extend(iter);
}

Ce que garantit réellement Rust

La garantie précise est la suivante : Rust safe ne peut pas provoquer de comportement indéfini si le code unsafe dont il dépend est sound. La Rust Reference définit la soundness à la frontière : le code unsafe doit empêcher ses clients safe de provoquer l’UB. Le compilateur ne vérifie pas toutes les obligations de sûreté des opérations unsafe ; les programmeurs et les relecteurs doivent les justifier.

C’est un avantage pratique majeur par rapport à C et C++ : Rust rend plus visibles les opérations sensibles à la mémoire et leurs obligations, afin que les relectures ciblent les frontières unsafe et les abstractions safe construites dessus. Ces frontières comprennent les blocs unsafe, les contrats de fonctions unsafe, les implémentations de traits unsafe, les déclarations FFI et les attributs unsafe ; elles ne se limitent pas toujours à un petit bloc. Exemple concret : CVE-2021-25900 dans smallvec : SmallVec::insert_many réservait selon la borne basse de l’itérateur puis écrivait davantage d’éléments que l’allocation ne pouvait en contenir ; la gravité était CVSS 9.8.

La soundness est une exigence, pas une question de prudence

Une API est non sound si du code safe peut l’utiliser pour provoquer un comportement indéfini. Par exemple, Iterator::size_hint fournit des bornes utiles à l’optimisation, pas un décompte exact : un itérateur valide peut produire plus d’éléments que la borne basse, et un itérateur défectueux peut contredire son estimation. La documentation précise que cette indication ne doit pas servir à supprimer les vérifications de bornes en code unsafe. Un trait safe incorrect peut aussi causer des erreurs logiques ou des panics ; ce n’est pas automatiquement de l’UB, mais le code unsafe doit préserver les invariants mémoire même si le code fourni par l’appelant se comporte de façon inattendue.

Le code unsafe n’est donc pas local : des API safe peuvent dépendre d’un invariant mémoire établi ailleurs. Si cet invariant est faux, le défaut appartient à l’abstraction qui expose l’API safe, et non aux appelants qui respectent son contrat.

Règles pratiques

Questions fréquentes

Puis-je interdire complètement unsafe ?

#![forbid(unsafe_code)] peut interdire la syntaxe unsafe dans le crate où il est activé, ce qui est utile pour les crates applicatifs. Il ne régit pas les dépendances ; certaines implémentations, interfaces FFI et bibliothèques nécessitent des opérations unsafe.

Le code unsafe peut-il faire confiance à un trait ?

Oui. Un unsafe trait documente les obligations que ses implémentations doivent respecter ; le code unsafe peut s’y fier. TrustedLen est un trait instable de la bibliothèque standard pour les itérateurs qui promettent une indication de taille exacte. Iterator::size_hint ordinaire ne donne pas cette garantie.

Cela affaiblit-il l’argument en faveur de Rust ?

Non. Le modèle de Rust réduit les endroits où naissent les obligations de sûreté mémoire et les rend explicites aux frontières unsafe. Il reste nécessaire d’auditer le code unsafe, les dépendances, les contrats FFI et les API safe construites sur des implémentations unsafe.

Références

À voir aussi strncpy() est-elle plus sûre que strcpy() ? CWE vs CVE : quelle est la différence ? Mass assignment : risques et listes de champs autorisés Fuite d’état serverless : isoler les données par requête PHP == et les comparaisons de magic hash