flawopen.com/Sécurité mémoire/Rust unsafe
Le bloc unsafe de Rust reste-t-il sûr pour la mémoire ?
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
Une API safe reçoit un itérateur
L’appelant transmet un itérateur ordinaire à une fonction qui n’expose aucune précondition unsafe.
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.
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.
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.
// 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: 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
- ✓Ne prenez pas les méthodes de traits safe pour des preuves de sûreté mémoire. Utilisez
size_hint()uniquement pour optimiser ; gardez les vérifications ou augmentez la capacité si nécessaire. - ✓Préservez les invariants en cas de panic. Le code fourni par l’appelant, comme
Iterator::next, un destructeur ou une closure, peut dérouler la pile. Veillez à ce que l’état partiellement initialisé reste valide et soit correctement détruit. - ✓Ajoutez un commentaire
// SAFETY:qui précise les conditions requises et leur justification. Un code unsafe inexpliqué ne peut pas être relu. - ✓Limitez la portée des blocs. Encapsuler une boucle entière pour éviter de répéter le mot-clé élargit le raisonnement demandé au relecteur.
- ✓Exécutez Miri sur les tests ciblés. Il peut détecter certains cas d’UB sur les chemins exécutés, notamment accès hors limites, use-after-free et aliasing invalide ; il ne prouve pas la soundness de tout le code unsafe.
- ✓Auditez les dépendances.
cargo auditvérifie les avis RustSec connus ;cargo geigeraide à inventorier l’usage d’unsafe. Aucun ne remplace la revue des contrats unsafe du code livré.
Questions fréquentes
#![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.
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.
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
- The Rust Reference — unsafe code and soundness
- Rust Reference — the unsafe keyword and proof obligations
- Rust standard library — Iterator::size_hint contract
- Rustonomicon — working with unsafe
- RustSec Advisory RUSTSEC-2021-0003 — smallvec buffer overflow
- GitHub Security Advisory GHSA-43w2-9j62-hq99 — CVE-2021-25900
- The Rustonomicon — the guide to unsafe Rust
- RustSec Advisory Database
- Miri — an interpreter that detects undefined behaviour
- CWE-787 — Out-of-bounds Write
- Case study: the smallvec overflow (CVE-2021-25900)