Introduction
Rust est un langage multiparadigmes orienté vers la sûreté mémoire.
Il est entre autres orienté programmation système, en permettant par exemple une gestion de la mémoire sans ramasse-miettes et sans nécessiter d'allocations et de libérations manuelles, ou encore protège la mémoire contre les accès concurrents (data race).
Le langage atteint ce but en introduisant un système d'ownership (fortement lié à l'aliasing des variables). À tout point d'un programme Rust, le compilateur recense les variables qui se réfèrent à une même donnée, et applique un ensemble de règles qui permettent la récupération automatique de la mémoire, la sûreté mémoire et l'absence de problèmes d'accès concurrents.
Le langage est également axé sur la performance, avec des constructions permettant des abstractions à coût nul et un compilateur proposant de puissantes optimisations.
De plus, le langage Rust fournit des fonctionnalités de programmation de haut niveau. Grâce aux fonctions d'ordre supérieur, aux fermetures, aux itérateurs, etc., il permet d'écrire tout ou parties des programmes dans un style proche des langages de programmation fonctionnelle. En outre, le typage statique, l'inférence de types et le polymorphisme ad hoc (sous la forme de traits) sont d'autres moyens que Rust propose pour construire des bibliothèques et des programmes de façon sûre.
Enfin, les outils d'accès à la chaîne de compilation (rustup, cargo) facilitent
grandement l'utilisation de Rust en simplifiant la configuration de la construction
du logiciel, tout en privilégiant les bonnes pratiques de sécurité de compilation.
Néanmoins, le langage offre des constructions et fonctionnalités qui, si elles ne sont pas utilisées correctement, peuvent potentiellement introduire des problèmes de sécurité, soit par construction, soit en permettant d'écrire du code qui serait mal interprété par un développeur ou un relecteur. De plus, comme pour la plupart des outils dans le domaine de la compilation et de la vérification logicielles, les outils utilisés pour développer, mettre au point, compiler et exécuter des programmes proposent des options et des possibilités de configuration qui, si mal utilisées, peuvent introduire des vulnérabilités.
L'objet de ce document est ainsi de rassembler une collection de conseils et de recommandations pour rester autant que possible dans une zone sûre pour le développement d'applications sécurisées, tout en profitant de la variété de possibilités que le langage Rust peut offrir.
Public visé
Ce guide compile une liste de recommandations qui doivent être observées pour le développement d'applications aux besoins de sécurité élevés. Il peut toutefois être suivi par quiconque souhaite s'assurer que les garanties offertes par la plateforme Rust ne sont pas invalidées par l'utilisation d'une fonctionnalité non sûre, trompeuse ou peu claire.
Il ne vise pas à constituer un cours sur la manière d'écrire des programmes en Rust, il existe déjà une grande quantité de ressources de qualité sur le sujet (par exemple, la page principale de documentation de Rust). L'intention est plutôt de guider le développeur et de l'informer à propos de certains pièges. Ces recommandations forment un complément au bon niveau de confiance que le langage Rust apporte déjà. Ceci étant dit, des rappels peuvent parfois être nécessaires pour la clarté du discours, et le développeur Rust expérimenté peut s'appuyer principalement sur le contenu des encarts (Règle, Recommandation, Avertissement, etc.).
Contributions
Ce guide est rédigé de manière collaborative et ouverte, via la plateforme GitHub (https://github.com/ANSSI-FR/rust-guide). Toutes les contributions pour de futures versions sont les bienvenues, que ce soit directement sous la forme de propositions (pull requests) ou bien de suggestions et discussions (issues).
Organisation du guide
La structure de ce document vise à discuter successivement des différentes phases typiques (et simplifiées) d'un processus de développement. Tout d'abord, nous proposons des recommandations concernant l'utilisation des outils de l'écosystème Rust dans un cadre sécurisé, ainsi que les précautions à prendre durant le choix et l'utilisation de bibliothèques externes. Ensuite, les recommandations à propos des constructions du langage sont présentées.
Enfin, nous discutons du bon usage de certaines librairies.
Il est à noter qu'à ce jour ce document n'aborde pas le sujet du Rust async.
Un résumé des règles et recommandations est disponible à la fin de ce guide.
Convention de lecture
Pour chacune des recommandations de ce guide, l’utilisation du verbe devoir est volontairement plus prescriptive que la formulation il est recommandé.
Pour certaines recommandations, il est proposé, au vu des menaces constatées lors de la rédaction de ce guide, plusieurs solutions qui se distinguent par le niveau de sécurité qu’elles permettent d’atteindre. Le lecteur a ainsi la possibilité de choisir une solution offrant la meilleure protection en fonction du contexte et de ses objectifs de sécurité.
Ainsi, les recommandations sont présentées de la manière suivante :
Cette recommandation est formulée de manière conceptuelle, sans prise en compte du contexte d’application ni des modalités de sa mise en œuvre.
L'implémentation technique de cette recommandation offre un niveau de sécurité adapté à un besoin de sécurité plus élevé.
Dans une démarche permanente de gestion du risque numérique et d’amélioration continue de la sécurité des systèmes d’information 1, la pertinence de mise en œuvre des recommandations décrites dans ce document doit être périodiquement réévaluée.
La liste récapitulative des recommandations est disponible en .
Références
- Maîtrise du risque numérique - l’atout confiance (anssi-risque-numerique)
-
Se reporter au guide ANSSI relatif à la maîtrise du risque numérique anssi-risque-numerique ↩
Environnement de développement
Rustup
Rustup est l'installateur des outils de la chaîne de compilation pour Rust. Entre autres choses, il permet de basculer entre différentes variantes de la chaîne d'outils (stable, beta, nightly), de gérer l'installation des composants additionnels et de maintenir le tout à jour.
Du point de vue de la sécurité, rustup effectue tous les téléchargements en
HTTPS, mais ne valide pas les signatures des fichiers téléchargés. Les
protections contre les attaques par déclassement, le pinning de certificats
et la validation des signatures sont des travaux actuellement en cours. Pour
les cas les plus sensibles, il peut être préférable d'opter pour une méthode
d'installation alternative comme celles listées dans la section Install du
site officiel du langage Rust.
Éditions Rust
Il existe plusieurs variantes du langage Rust, appelées éditions. Le concept d'éditions a été introduit afin de distinguer la mise en place de nouvelles fonctionnalités dans le langage, et de rendre ce processus incrémental. Toutefois, comme mentionné dans le rust-edition-guide, cela ne signifie pas que de nouvelles fonctionnalités et améliorations ne seront incluses que dans la dernière édition.
Certaines éditions peuvent introduire de nouvelles constructions dans le langage et de nouveaux mots-clés. Les recommandations concernant ces fonctionnalités deviennent alors fortement liées à une édition en particulier. Dans le reste de ce guide, un effort sera réalisé pour mettre en évidence les règles qui ne s'appliqueraient qu'à certaines éditions de Rust en particulier.
Aucune édition spécifique n'est recommandée, tant que le développement se conforme aux recommandations exprimées à propos des fonctionnalités que l'édition utilisée propose.
Chaînes d'outils stable, nightly et beta
De manière orthogonale aux éditions qui permettent d'opter pour une variante du langage en termes de fonctionnalités, la chaîne d'outils du langage Rust est déclinée en trois variantes appelées release channels.
- La version nightly est produite une fois par jour.
- La version nightly est promue en version beta toutes les six semaines.
- La version beta est promue en version stable toutes les six semaines.
Lors du développement d'un projet, il est important de vérifier non seulement la version de la chaîne d'outils couramment sélectionnée par défaut, mais aussi les potentielles surcharges qui peuvent être définies en fonction des répertoires :
$ pwd
/tmp/foo
$ rustup toolchain list
stable-x86_64-unknown-linux-gnu (default)
beta-x86_64-unknown-linux-gnu
nightly-x86_64-unknown-linux-gnu
$ rustup override list
/tmp/foo nightly-x86_64-unknown-linux-gnu
$
Le développement d'applications sécurisées DOIT être mené en utilisant la chaîne d'outils dans sa version stable, afin de limiter les potentiels bugs à la compilation, à l'exécution et lors de l'utilisation d'outils complémentaires.
Enfin, lorsque l'utilisation d'un outil (par exemple, une sous-commande cargo)
requiert l'installation d'un composant dans une chaîne d'outils non stable,
le basculement de variante doit être effectué de la façon la plus locale
possible (idéalement, uniquement pendant la commande concernée) au lieu
d'explicitement basculer la version courante. Par exemple, pour lancer la
version nightly de la commande rustfmt :
$ rustup toolchain list
stable-x86_64-unknown-linux-gnu (default)
beta-x86_64-unknown-linux-gnu
nightly-x86_64-unknown-linux-gnu
$ rustup run nightly cargo fmt
$ # or
$ cargo +nightly fmt
$
Garantie de niveau pour Rustc
Rustc utilise LLVM comme backend, il hérite donc du support de ce dernier et classe ses cibles prises en charge en différents niveaux afin d’indiquer le degré de stabilité et de tests effectué.
Tier 1 - Fonctionnement garanti
La cible est entièrement examinée par la communauté. Elle réussit l’ensemble complet de la batterie de tests, fait l’objet de tests de régression réguliers et est maintenue à jour avec les nouvelles versions. En pratique, vous pouvez compter sur une génération de code cohérente, une ABI stable et des performances prévisibles d’une version à l’autre. Les cibles de tier 1 offrent une garantie de fonctionnement.
Tier 2 - Compilation garantie
La cible se compile correctement, mais elle ne bénéficie pas du même niveau de tests ni de la même maintenance que les cibles de niveau 1. Elle peut ne pas être entièrement couverte par les tests, et certaines optimisations ou fonctionnalités récentes peuvent être absentes ou instables. Les utilisateurs peuvent tout de même générer du code pour ces cibles, mais ils doivent s’attendre à d’éventuels problèmes occasionnels ou à devoir appliquer des correctifs manuels. Les cibles de tier 2 offrent une garantie de compilation mais pas de fonctionnement.
Tier 3
Les cibles de niveau 3 ne sont tout simplement pas prises en charge officiellement.
La distinction entre les différents niveaux aide les développeurs à choisir une cible adaptée à leur tolérance au risque : le niveau 1 pour des applications de production, le niveau 2 pour des architectures plus expérimentales ou de niche dont le support complet n’est pas encore assuré.
Les cibles Rustc de niveau 1 et les chaînes de compilation certifiées DOIVENT être utilisées pour les systèmes de sûreté critiques.
Une liste complète des cibles prises en charge est disponible dans rustc-book.
Cargo
Une fois que la chaîne d'outils appropriée a été sélectionnée avec Rustup,
l'outil Cargo est disponible pour exécuter ces différents outils en
fournissant la commande cargo. Cargo est le gestionnaire de paquetages de Rust.
Il joue plusieurs rôles fondamentaux tout au long d'un développement en Rust. Il
permet notamment de :
- structurer le projet en fournissant un squelette de projet (
cargo new) ; - lancer la compilation du projet (
cargo build) ; - lancer la génération de la documentation (
cargo doc) ; - lancer les tests (
cargo test) et les benchmarks (cargo bench) ; - gérer le téléchargement des dépendances ;
- rendre le projet distribuable et le publier sur crates.io
(
cargo publish) ; - lancer des outils complémentaires tels que ceux décrits ci-après, sous la forme de sous-commandes.
Cargo permet la récupération automatique des dépendances avant compilation.
L'utilitaire permet de vérifier l'intégrité des dépendances après téléchargement.
Il utilise pour cela un fichier Cargo.lock qui, s'il est présent au moment de la compilation,
contraint les sommes de contrôle des dépendances. En cas de différence entre
les sources téléchargées et le fichier Cargo.lock, une erreur apparaît.
error: checksum for `sha256 v1.6.0` changed between lock files
this could be indicative of a few possible errors:
* the lock file is corrupt
* a replacement source in use (e.g., a mirror) returned a different checksum
* the source itself may be corrupt in one way or another
unable to verify that `sha256 v1.6.0` is the same as when the lockfile was generated
En cas d'absence du fichier Cargo.lock, il est créé automatiquement à la première compilation en suivant les
sommes de contrôle des sources téléchargées (selon le principe TOFU : Trust On First Use).
Ce fichier peut également être créé manuellement avec cargo generate-lockfile, et s'il
est déjà présent, un nouveau fichier est créé avec les dernières versions compatibles de chaque crate.
Le fichier Cargo.lock DOIT être versionné avec le code source du programme Rust.
Des discussions sont en cours pour
déterminer le meilleur moyen de protéger et de valider les crates lors de leur ajout au projet
(les téléchargements suivants sont vérifiés par le fichier Cargo.lock). Pour le
moment, la sécurité des premiers téléchargements de cargo repose sur la bonne sécurité du site web
crates.io ainsi que celle du dépôt, hébergé sur GitHub, contenant l'index du
registre de crates. Pour les cas les plus sensibles, il peut être préférable
d'opter pour une méthode d'installation alternative pour les dépendances.
Cargo propose différentes commandes et options pour adapter le processus de
compilation aux besoins de chaque projet, principalement au travers du fichier
Cargo.toml. Pour une présentation complète, voir le cargo-book.
Certaines de ces options requièrent une attention particulière.
La section [profile.*] permet de configurer la façon dont le compilateur est
invoqué. Par exemple :
- La variable
debug-assertionscontrôle l'activation des assertions de debug. - La variable
overflow-checkscontrôle l'activation de la vérification des dépassements d'entiers lors d'opérations arithmétiques.
Changer les options par défaut pour ces variables peut entraîner l'apparition de bugs non détectés, même si le profil de debug qui active normalement les vérifications (par exemple, les vérifications de dépassements d'entiers) est utilisé.
Les variables debug-assertions et overflow-checks NE DOIVENT PAS être
modifiées dans les sections de profils de développement ([profile.dev] et
[profile.test]).
Cargo propose d'autres moyens de configuration afin de modifier son comportement
sur un système donné. Cela peut être très pratique, mais il peut alors aussi
être difficile de connaître et de se souvenir de toutes les options qui sont
effectivement passées à cargo, et en particulier passées ensuite au
compilateur Rust. Finalement, cela peut affecter la robustesse du processus de
compilation et la confiance qu'on lui accorde. Il est préférable de centraliser
les options de compilation dans le fichier de configuration Cargo.toml. Pour
le cas spécifique de la variable d'environnement RUSTC_WRAPPER, utilisée par
exemple pour générer une partie du code ou pour invoquer un outil externe avant
la compilation, il est préférable d'utiliser la fonctionnalité de scripts de
compilation de Cargo.
Les variables d'environnement RUSTC, RUSTC_WRAPPER et RUSTFLAGS NE
DOIVENT PAS être modifiées lorsque Cargo est appelé pour compiler un projet.
Rustfmt
Rustfmt est un outil offrant la possibilité de formater du code en fonction de consignes de style (style guidelines).
Les règles de convention de style peuvent être personnalisées au besoin dans
le fichier rustfmt.toml ou .rustfmt.toml à la racine du projet. Il sera
utilisé par l'outil en écrasant les préférences par défaut. Par exemple :
# Set the maximum line width to 120
max_width = 120
# Maximum line length for single line if-else expressions
single_line_if_else_max_width = 40
Pour plus d'informations à propos des règles de convention de style que
rustfmt propose, voir rust-style.
L'outil de formatage rustfmt DEVRAIT être utilisé pour assurer le respect de
règles de convention de style sur une base de code.
Cargo fix
La commande cargo fix est un
outil dédié à la réparation des avertissements de compilation et facilitant la
transition entre éditions.
$ cargo fix
Pour préparer la transition d'un projet de l'édition Rust 2021 à Rust 2024, il est possible de lancer la commande suivante :
$ cargo fix --edition
Rustfix va soit réparer le code afin de le rendre compatible avec Rust 2024, soit afficher un avertissement décrivant le problème. Le problème devra alors être réparé manuellement. En exécutant la commande (et en réparant potentiellement les problèmes manuellement) jusqu'à ce qu'elle n'affiche plus aucun avertissement, il est possible de s'assurer que le code est compatible tant avec Rust 2021 qu'avec Rust 2024.
Pour basculer définitivement le projet sous Rust 2024 :
$ cargo fix --edition-idioms
Il est important de noter que l'outil ne fournit que peu de garanties quant
à la correction (soundness) des réparations proposées. Dans une certaine
configuration, certaines réparations (comme celles proposées avec l'option
--edition-idioms) sont connues pour casser la compilation ou pour modifier
la sémantique d'un programme dans certains cas.
Clippy
Clippy est un outil permettant la vérification de nombreux lints (bugs,
style et lisibilité du code, problèmes de performances, etc.). Depuis la chaîne
d'outils stable en version 1.29, clippy peut être installé dans
l'environnement rustup stable. Il est aussi recommandé d'installer clippy en
tant que composant (rustup component add clippy) dans la chaîne d'outils
stable plutôt que de l'installer comme une dépendance de chaque projet.
L'outil fournit plusieurs catégories de lints, selon le type de problème qu'il
vise à détecter dans le code. Les avertissements doivent être revérifiés par le
développeur avant d'appliquer la réparation suggérée par clippy, en
particulier dans le cas des lints de la catégorie clippy::nursery puisque
ceux-ci sont encore en cours de développement et de mise au point.
clippy dispose maintenant d'un outils fix similaire à celui de rustfix.
Un linter comme clippy DOIT être utilisé régulièrement tout au long du
développement d'une application sécurisée.
Dans le cadre du développement d'une application sécurisée, toute réparation
automatique (comme celles appliquées par rustfix ou clippy par exemple) DOIT être
vérifiée par le développeur.
Autres
D'autres outils ou sous-commandes cargo utiles pour renforcer la sécurité
d'un programme existent, par exemple, en recherchant des motifs de code
particuliers. Nous en discutons dans les chapitres suivants en fonction de leurs
portées et de leurs objectifs.
Références
- The Rust Edition Guide (rust-edition-guide)
- The Cargo Book (cargo-book)
- Rust Style Guide (rust-style)
- The rustc book (rustc-book)
Bibliothèques
Dépôts de dépendances
La gestion de bibliothèques externes est intégrée dans l'outil Cargo. Plusieurs moyens existent pour définir la provenance de ces bibliothèques, certains sont donnés dans la suite.
On rappelle que le traçage exact des versions de ces bibliothèques est une condition importante de la bonne sécurité des logiciels écrits en Rust. Ce besoin est matérialisé par la règle DENV-CARGO-LOCK.
Crates
En complément de la bibliothèque standard du langage, l'outil cargo fournit un
moyen pratique d'intégrer des bibliothèques tierces dans un projet en Rust. Ces
bibliothèques, appelées crates dans l'écosystème Rust, sont importées depuis
un dépôt central de composants en sources ouvertes.
Un exemple de déclaration de dépendances dans le fichier Cargo.toml.
[dependencies]
mdbook = { version = "0.4.52" }
anyhow = "1.0.99"
clap = { version = "4.5.47", features = ["derive"] }
markdown = { version = "1.0.0", features = ["serde"] }
semver = "1.0.26"
serde_json = "1.0.143"
serde = "1.0.219"
Le dépôt par défaut est crates.io. Il est aussi possible d'utiliser son propre dépôt.
Dépendances Git
Chaque dépendance du fichier Cargo.toml peut également faire référence à un dépôt Git. Par exemple :
[dependencies]
regex = { git = "https://github.com/rust-lang/regex.git" }
Il est possible de spécifier plus en détail la version souhaitée en donnant soit une branche, soit un tag, soit un hash de commit.
Le système de verrou des dépendances est opérant même dans le cas d'un dépôt Git : dans le cas où la dépendance ne spécifie pas un commit en particulier, le commit le plus récent répondant aux critères du fichier Cargo.toml est récupéré au moment de la première compilation et est pérennisé dans le fichier Cargo.lock. Toutes les compilations suivantes utiliseront le même commit (sauf si le fichier Cargo.lock est mis à jour).
Sécurité des dépendances
Quelle que soit la méthode de récupération des dépendances (crate ou commit Git), si elles proviennent d'organisations extérieures, les dépendances doivent faire l'objet d'une validation.
Chaque dépendance tierce directe DOIT être dûment validée, et chaque validation DOIT être tracée.
Concernant les dépendances transitives, il est également recommandé de les valider individuellement.
Chaque dépendance tierce DEVRAIT être dûment validée, et chaque validation DEVRAIT être tracée.
Outils de vérification des dépendances
Cargo-outdated
L'outil Cargo-outdated permet de faciliter la gestion des versions des dépendances.
Pour une crate donnée, l'outil liste les versions utilisées des dépendances
(dépendances listées dans le fichier Cargo.toml), et vérifie s'il s'agit de la
dernière version compatible disponible ainsi que la dernière version en général.
L'outil cargo-outdated DOIT être utilisé pour vérifier le statut des
dépendances. Ensuite, chaque dépendance importée en version obsolète DEVRAIT
être mise à jour ou bien, le cas échéant, le choix de la version DOIT être
justifié.
Cargo-audit
Cargo-audit est un outil permettant de vérifier s'il existe des vulnérabilités connues dans la RustSec Advisory Database pour les dépendances utilisées dans un projet.
L'outil cargo-audit DOIT être utilisé pour rechercher des vulnérabilités
connues dans les dépendances d'un projet.
Nommage
La convention de nommage employée par la bibliothèque standard est de facto le standard pour le nommage des éléments des programmes écrits en Rust. Un effort a été fait pour formaliser ces conventions de nommage, d'abord dans la RFC-430, puis dans le document des rust-guidelines.
La règle de base (C-CASE) consiste à utiliser :
UpperCamelCasepour les types, traits, variants d'énumérations et paramètres génériques de type ;snake_casepour les fonctions, méthodes, macros, variables et modules ;SCREAMING_SNAKE_CASEpour les variables statiques, les constantes et les paramètres génériques constants ;'lowercasepour les durées de vie (lifetimes).
Les rust-guidelines recommandent également des conventions de nommage plus précises pour certaines constructions particulières :
- (C-CONV) pour les méthodes de conversion (
as_,to_,into_) ; - (C-GETTER) pour les accesseurs ;
- (C-ITER) pour les méthodes produisant des itérateurs ;
- (C-ITER-TY) pour les types itérateur ;
- (C-FEATURE) pour les noms de features (fonctionnalités activables par configuration) ;
- (C-WORD-ORDER) pour la cohérence sur l'ordre des mots.
Les règles de base sont vérifiées par le compilateur (jeu d'avertissements nonstandard_style).
En complément du compilateur, l'outil clippy permet de faciliter l'adoption des conventions de nommage usuelles à travers la catégorie style.
Par exemple, la vérification wrong_self_convention contrôle la cohérence entre les noms des méthodes de conversion et le type du receveur (self, &self, &mut self), suivant (C-CONV).
Le développement d'une application sécurisée DOIT suivre les conventions de nommage décrites dans les rust-guidelines.
Références
- General naming conventions (RFC-430)
- Rust API Guidelines (rust-guidelines)
Traitements des entiers
Dépassement d'entiers
Bien que des vérifications soient effectuées par Rust en ce qui concerne les potentiels dépassements d'entiers, des précautions doivent être prises lors de l'exécution d'opérations arithmétiques sur les entiers.
En particulier, il faut noter que le profil de compilation (généralement dev, la compilation de débogage par défaut, ou release, la compilation optimisée standard) modifie le comportement en cas de dépassement d'entier. En configuration dev, un dépassement provoque l'arrêt du programme (panic), tandis qu'en configuration release, la valeur calculée est silencieusement tronquée au nombre de bits du type numérique, ce qui donne une sémantique d'arithmétique circulaire (wrap-around).
Le comportement en cas de dépassement d'entier peut être défini explicitement par l'option de compilation -C overflow-checks=true (ou false).
Il peut également être modifié dans la définition du profil dans Cargo.toml :
[profile.release]
overflow-checks = true # enable overflow checks in release builds
Lorsqu'un dépassement est possible, le comportement peut être rendu explicite soit en utilisant des méthodes spécifiques, soit en utilisant des types enveloppants spécifiques.
Les méthodes sont de la forme <mode>_<op>, où <mode> est checked, overflowing, wrapping ou saturating, et <op> est add, mul, sub, shr, etc. Les sémantiques sont les suivantes :
checked_<op>retourneNoneen cas de dépassement,overflowing_<op>retourne à la fois un résultat selon l'arithmétique circulaire et un booléen indiquant si un dépassement a eu lieu,wrapping_<op>retourne toujours le résultat selon l'arithmétique circulaire,saturating_<op>retourne toujours le résultat saturé.
Les types enveloppants sont Wrapping<T> et Saturating<T> (de std::num), où T est un type entier. Le premier fournit une sémantique d'arithmétique circulaire pour toutes les opérations arithmétiques, tandis que le second fournit une sémantique de saturation. Une fois les valeurs enveloppées, toutes les opérations suivantes sont effectuées avec la sémantique correspondante.
#![allow(unused)] #![allow(dead_code)] fn main() { use std::num::Wrapping; use std::panic; fn use_integer() { let x: u8 = 242; let result = panic::catch_unwind(|| { println!("{}", x + 50); // panics in debug, prints 36 in release. }); if result.is_err() { println!("panic"); } // always prints 36. println!("{}", x.overflowing_add(50).0); // always prints 36. println!("{}", x.wrapping_add(50)); // always prints 36. println!("{}", Wrapping(x) + Wrapping(50)); // always panics: let (res, c) = x.overflowing_add(50); let result = panic::catch_unwind(|| { if c { panic!("custom error"); } else { println!("{}", res); } }); if result.is_err() { println!("panic"); } } }
Lorsqu'une opération arithmétique peut produire un dépassement, les opérateurs classiques sur les entiers NE DOIVENT PAS être utilisés.
Les méthodes spécialisées comme checked_<op>, overflowing_<op>, wrapping_<op>, ou saturating_<op>, ou des types enveloppants spécialisés comme Wrapping ou Saturating, DOIVENT être utilisés pour rendre le comportement explicite et homogène, quel que soit le profil de compilation.
Gestion des erreurs
Le type Result est la façon privilégiée en Rust pour décrire le type de retour
des fonctions dont le traitement peut échouer. Un objet Result doit être
testé et jamais ignoré.
Une crate PEUT implanter son propre type Error qui peut contenir toutes
les erreurs possibles. Des précautions supplémentaires DOIVENT être prises :
ce type DOIT être exception-safe (RFC 1236) et implémenter les traits
Error + Send + Sync + 'static ainsi que Display.
Des crates tierces peuvent être utilisées pour faciliter la gestion d'erreurs. La plupart (snafu, thiserror) proposent la création de types d'erreurs personnalisées qui implémentent les traits nécessaires et permettent l'encapsulation d'autres erreurs.
Une autre approche (notamment proposée dans anyhow) consiste à envelopper automatiquement les erreurs dans un seul type d'erreur universel. Une telle approche ne devrait pas être utilisée dans des bibliothèques ou des systèmes complexes parce qu'elle ne permet pas de fournir de contexte sur les erreurs ainsi initialement enveloppées, contrairement à la première approche.
Panics
La gestion explicite des erreurs (Result) doit être préférée à la place de
l'utilisation de la macro panic. La cause de l'erreur doit être rendue
disponible, et les erreurs trop génériques doivent être évitées.
Les crates fournissant des bibliothèques ne doivent pas utiliser de fonctions
ou d'instructions qui peuvent échouer en engendrant un panic.
Des motifs courants de code qui provoquent des panic sont :
- une utilisation de
unwrapou deexpect; - une utilisation de
assert; - un accès non vérifié à un tableau ;
- un dépassement d'entier (en mode debug) ;
- une division par zéro ;
- l'utilisation de
format!pour le formatage d'une chaîne de caractères.
Dans certains domaines critiques pour la sécurité, il est obligatoire de passer en mode sans échec dès qu'une erreur susceptible d'entraîner un comportement indéfini se produit. Dans ces situations, il est judicieux de déclencher délibérément un panic (ou d'interrompre l'exécution) puisque cela permet d'arrêter le système avant que des données ne soient corrompues, ou des défaillances liées à la sûreté ou la sécurité ne se propagent.
Pour un avion ou d'autres types de véhicule, ce comportement « fail-fast » peut être crucial : l'unité de contrôle principale doit s'arrêter immédiatement en cas de défaillance grave, puis transférer le contrôle à un sous-système redondant ou de secours capable d'arrêter le véhicule en toute sécurité ou de poursuivre son fonctionnement en mode réduit. Le redémarrage sur un système secondaire fiable garantit que le véhicule reste contrôlable, protège les occupants et évite les conséquences dangereuses qui pourraient résulter de la poursuite de l'exécution dans un état imprévisible.
Dans le cas où le développement n'est pas soumis à ce type de normes:
Les fonctions et instructions qui peuvent causer des panic à l'exécution
NE DOIVENT PAS être utilisées.
L'indice d'accès à un tableau DOIT être testé, ou la méthode get DOIT être
utilisée pour récupérer une Option.
FFI et panics
Lorsque du code Rust est appelé depuis du code écrit dans un autre langage (par exemple, du code C), le code Rust doit être écrit de sorte à ne jamais pouvoir paniquer. Dérouler (unwinding) depuis le code Rust vers le code étranger résulte en un comportement indéfini.
Le code Rust appelé depuis une FFI DOIT :
- soit être assuré de ne pas paniquer,
- soit utiliser
catch_unwindou le modulestd::panicpour s'assurer qu'il ne va pas abandonner un traitement puis laisser l'exécution retourner dans le langage appelant dans un état instable.
Il est porté à l'attention du développeur que catch_unwind ne va traiter que
les cas de panic, et va préserver les abandons de processus causés par
d'autres raisons.
Garanties du langage
Comportements indéfinis
Le comportement d'un programme est indéfini (UB pour Undefined Behavior) lorsque sa sémantique n'est pas décrite dans le langage Rust.
Selon rust-reference, l'existence d'UB est considéré comme une erreur.
Par exemple le déréférencement d'un pointeur null est un UB.
A contrario, un unwrap sur l'objet None est bien défini car c'est le langage qui traite cette erreur
(en lançant un panic).
La liste actuelle des UB est donnée dans la référence Rust. On notera les garanties suivantes :
- Pas de déréférencement de pointeur vers une adresse mémoire non allouée (dangling pointer) ou non alignée, ce qui implique
- Pas de dépassement de tableau
- Pas d'accès à de la mémoire libérée
- Accès toujours aligné quelque soit la plateforme
- Les valeurs pointées sont cohérentes avec le type du pointeur. Par exemple, une valeur pointée par un pointeur booléen sera l'octet 1 ou 0.
- Respect des règles d'aliasing (voir aussi le nomicon pour des exemples): une référence mutable ne peux être partagée.
- Pas d'accès concurrent (un accès en lecture et un autre en écriture ou en lecture) à la même adresse mémoire (voir aussi le nomicon pour des exemples)
Garantie de Rust
La volonté du langage est d'assurer l'absence d'UB dans un programme utilisant uniquement la partie non unsafe de Rust.
Cependant, le langage ne protège pas contre les erreurs suivantes :
- fuites de resources (mémoire, IO, ...) (voir la section sur la gestion mémoire) ;
- dépassements numériques (voir la section sur le traitement des entiers).
Références
- The Rust Reference (rust-reference)
- The Rustonomicon (nomicon)
Rust unsafe
Généralités sur l'utilisation de unsafe
Comportements ajoutés par Rust unsafe
Les capacités du langage peuvent être étendues en utilisant du code unsafe. La liste complète de ces capacités est donnée dans le [manuel de référence de Rust][r-unsafety]. On notera les capacités suivantes :
- Déréférencer un raw pointer
- Lire ou écrire une variable statique mutable et/ou externe
- Accéder aux champs d'une
union - Implémenter un trait
unsafe - Déclarer un bloc
extern
D'autres exemples peuvent être trouvés dans le nomicon
Si ces capacités sont nécessaires à la programmation système, elles font perdre au langage ses propriétés de sûreté, et permettent l'apparition de comportements indéfinis.
AUCUN comportement indéfini NE DOIT se produire.
Un mot-clé, deux usages
Comme décrit dans rust-reference, le mot-clé unsafe a deux usages : le marquage dans une API et le déverrouillage dans une implémentation.
Marquage unsafe
Le marquage unsafe est une délégation de responsabilité sur la sûreté mémoire du programme en développement.
L'usage de ce mot-clé dans une API met en garde l'utilisateur de l'API contre les potentiels effets néfastes de l'usage de l'API.
- Dans une signature de fonction (r-unsafe.fn),
unsafesignifie que le comportement de la fonction peut conduire à des UB si le contrat d'usage de la fonction (dans sa documentation) n'est pas respecté. - Dans la déclaration d'un trait (r-unsafe.trait),
unsafesignifie qu'une implémentation erronée de ce trait peut conduire à des UB si le contrat d'implémentation du trait (de préférence documenté) n'est pas respecté.
Déverrouillage unsafe
Le déverrouillage unsafe est une prise de responsabilité sur la sûreté mémoire du programme en développement.
L'usage d'un bloc unsafe dans le corps d'une fonction ou dans la définition d'une constante est imposé par le compilateur (r-unsafe.block) pour empêcher l'usage par inadvertance d'opérations unsafe. Parmi ces opérations, on trouve :
- l'utilisation de fonctions marquées unsafe
- la modification de variable mutables statiques
- l'utilisation de fonctions externes
De manière similaire, l'implémentation d'un trait marqué unsafe nécessite unsafe (r-unsafe.impl) pour indiquer la prise en compte explicite par le développeur des contrats de sûreté du trait. Il permet donc de déverrouiller l'implémentation de traits unsafe.
Enfin, depuis l'édition 2024 de Rust, il est nécessaire également de déverrouiller à l'aide du mot-clé unsafe :
- les blocs
extern(r-unsafe.extern) contenant les déclarations externes pour le FFI ; - certains attributs (par exemple,
no_mangle, cf. r-attributes.safety).
Limitations et précautions d'usage
Paraphrasant le Rustonomicon, le principe fondamental de Rust pourrait se résumer à :
un code sans
unsafene peut pas mal se comporter
L'utilisation conjointe du système de types et du système d'ownership assure l'absence de comportement indéfini (UB) et apporter un haut niveau de sûreté quant à la gestion de la mémoire dans les programmes écrits en Rust. Le langage permet alors d'éviter les débordements mémoire, la construction de pointeurs nuls ou invalides, et les problèmes d'accès concurrents à la mémoire.
Cette promesse faite au développeur de code sans unsafe est perdue lors de l'utilisation de code unsafe.
C'est donc au développeur de s'assurer qu'aucun UB ne peut se produire dans son code.
Aussi, il est important de limiter l'usage de unsafe au strict nécessaire :
Pour un développement sécurisé, les blocs unsafe DEVRAIENT être évités,
ou DOIVENT être justifiés par au moins l'une des raisons suivantes :
-
L'interfaçage entre Rust et d'autres langages (FFI) permet la déclaration de fonctions dont l'implantation est faite en C, en utilisant le préfixe
extern "C". Pour utiliser une telle fonction, le mot-cléunsafeest requis. Un wrapper "sûr" doit être défini pour que le code C soit finalement appelé de façon souple et sûre. -
Pour la programmation des systèmes embarqués, on accède souvent aux registres et à d'autres ressources au travers d'adresses mémoire fixées. Dans ce cas, des blocs
unsafesont nécessaires afin de pouvoir initialiser et déréférencer des pointeurs en Rust pour ces adresses. Afin de minimiser le nombre de déclarationsunsafepour permettre au développeur de facilement identifier les accès critiques, une abstraction adaptée (structure de données ou module) doit être mise en place. -
Lorsque les performances sont impactées sur une petite partie de code (Buffer zero-copy modifié directement en mémoire, gestion des allocations, etc.).
À l'exception de l'un ou plusieurs de ces cas #![forbid(unsafe_code)] DOIT
apparaître dans à la racine de la crate (typiquement main.rs ou lib.rs)
afin de générer des erreurs de compilation dans le cas où le mot-clé unsafe
est utilisé dans le projet.
En cas d'usage d'unsafe, il est donc de la responsabilité du développeur du programme:
- de s'assurer qu'aucun UB n'est possible en cas de déverrouillage ;
- de s'assurer que les conditions d'usage (invariants) soient exhaustives et correctes en cas de marquage.
Au delà du code unsafe lui-même, il est important d'encapsuler correctement les opérations unsafe dans un composant (crate ou module) de manière à rétablir les garanties usuelles de sûreté mémoire de Rust:
Dans un développement sécurisé d'un composant logiciel Rust (crate ou module),
tout code unsafe DOIT être encapsulé de manière :
- soit à exposer un comportement safe à l'utilisateur dans lequel aucune interaction safe ne peut aboutir à un UB (comportement indéfini) ;
- soit à exposer des fonctionnalités marquées
unsafeet dont les conditions d'usages (préconditions, séquencements, etc.) sont commentées exhaustivement et correctement (c'est-à-dire qu'elles impliquent la sûreté mémoire).
Ainsi, une fonction utilisant des opérations unsafe peut-être sûre (et donc sans marque unsafe) si les opérations unsafe ne présentent pas d'UB étant donnés les invariants du composant (typiquement l'invariant de type pour une méthode). Inversement, une fonction sans bloc unsafe doit être marquée unsafe si elle casse ces invariants. Le choix et la connaissance de ces invariants sont donc cruciaux pour le développement sécurisé.
Exemple : préservation d'un invariant de type
La protection des invariants d'une bibliothèque est primordiale pour se prémunir de bugs en général, d'UB en particulier.
L'exemple qui suit est extrait du Rustonomicon.
Si l'on souhaite réimplémenter le type Vec, on pourrait utiliser le code suivant :
#![allow(unused)] fn main() { use std::ptr; pub struct Vec<T> { ptr: *mut T, len: usize, cap: usize, } // Note this implementation does not correctly handle zero-sized types. impl<T> Vec<T> { pub fn push(&mut self, elem: T) { if self.len == self.cap { // reallocate new array with bigger capacity } unsafe { ptr::write(self.ptr.add(self.len), elem); self.len += 1; } } } }
La sûreté de ce code repose sur plusieurs invariants, dont l'un stipule que
la plage d'octets allant de self.ptr à self.ptr + self.cap * size_of<T>() est allouée.
Or, il est possible de casser cet invariant avec du code safe. Par exemple, considérons la méthode suivante :
#![allow(unused)] fn main() { impl<T> Vec<T> { pub fn make_room(&mut self) { // grow the capacity self.cap += 1; } } }
Si elle peut être tout à fait légitime pour du code interne à l'API,
cette méthode ne doit pas être exposée par l'API ou alors doit être annotée
par unsafe car elle peut conduire à des UB (même si elle ne comporte pas de blocs unsafes).
Références
- The Rust Reference (rust-reference)
Gestion de la mémoire
Dans la très grande majorité des cas, en Rust non-unsafe, le compilateur détermine automatiquement quand il peut libérer la mémoire occupée par une valeur du programme. Mais, comme rappelé plus tôt dans ce guide, ce n'est pas une garantie : un code non-unsafe peut mener à des fuites mémoires. Aussi, certaines règles présentées dans ce chapitre ne sont pas strictement unsafe. Cependant,
même si certaines des fonctions présentées dans la suite ne sont pas
unsafe, elles ne devraient être utilisées qu'en Rust unsafe.
forget et fuites de mémoire
Rust fournit
des fonctions spéciales pour réclamer manuellement la mémoire : les fonctions
forget et drop du module std::mem (ou core::mem). drop déclenche
simplement une récupération prématurée de la mémoire tout en appelant les
destructeurs associés lorsque nécessaire, forget quant à elle n'appelle pas
ces destructeurs.
#![allow(unused)] fn main() { let pair = ('↑', 0xBADD_CAFEu32); drop(pair); }
Les deux fonctions sont considérées comme sûres du point de vue mémoire par
Rust. Toutefois, forget rendra toute ressource gérée par la valeur libérée
inaccessible, mais non libérée.
#![allow(unused)] fn main() { let s = String::from("Hello"); forget(s); // Leak memory }
En particulier, l'utilisation de forget peut causer la rétention en mémoire de
ressources critiques, menant à des interblocages et à la persistance de données
sensibles en mémoire. C'est pourquoi forget doit être considérée comme
non sécurisée.
Dans un développement sécurisé en Rust (unsafe ou non), la fonction forget de std::mem
(core::mem) NE DOIT PAS être utilisée.
Le lint mem_forget de Clippy DEVRAIT être utilisé pour automatiquement
détecter toute utilisation de la fonction forget. Pour s'assurer de l'absence
d'appel à forget, ajouter la directive suivante en début de fichier racine
(en général src/lib.rs ou src/main.rs) :
#![deny(clippy::mem_forget)]
La bibliothèque standard inclut d'autres moyens d'oublier une valeur :
Box::leakpour libérer une ressource ;Box::into_rawpour exploiter une valeur dans un bloc unsafe, notamment dans une FFI ;ManuallyDrop(dansstd::memoucore::mem) pour assurer la libération manuelle d'une valeur.
Ces alternatives peuvent mener au même type de problème de sécurité, mais ont l'avantage de faire apparaître explicitement leur but.
Dans un développement sécurisé (unsafe ou non) en Rust, le code NE DOIT PAS faire fuiter de la
mémoire ou des ressources via Box::leak.
ManuallyDrop et Box::into_raw passent la responsabilité de la libération de
la ressource concernée du compilateur au développeur.
Dans un développement sécurisé en Rust, toute valeur wrappée dans le type
ManuallyDrop DOIT être unwrapped pour permettre sa libération automatique
(ManuallyDrop::into_inner) ou bien DOIT être manuellement libérée (unsafe
ManuallyDrop::drop).
Raw pointers
L'utilisation principale des pointeurs raw est de traduire les pointeurs C en Rust. Comme leur nom l'indique, ces types sont bruts et n'ont pas toutes les capacités des pointeurs intelligents (smart pointer) de Rust. En particulier, leur libération est à la charge du programmeur.
Dans un développement sécurisé en Rust non-unsafe, les références et les smart pointers
NE DOIVENT PAS être convertis en raw pointers. En particulier, les fonctions into_raw ou into_non_null
des smart pointers Box, Rc, Arc ou Weak NE DOIVENT PAS être utilisées dans un code Rust non-unsafe.
Dans un développement sécurisé en Rust, tout pointeur créé par un appel à
into_raw (ou into_non_null) depuis un des types suivants DOIT
finalement être transformé en valeur avec l'appel à la fonction from_raw
correspondant, pour permettre sa libération :
std::boxed::Box(oualloc::boxed::Box) ;std::rc::Rc(oualloc::rc::Rc) ;std::rc::Weak(oualloc::rc::Weak) ;std::sync::Arc(oualloc::sync::Arc) ;std::sync::Weak(oualloc::sync::Weak) ;std::ffi::CString;std::ffi::OsString.
#![allow(unused)] fn main() { let boxed = Box::new(String::from("Crab")); let raw_ptr = Box::into_raw(boxed); let _ = unsafe { Box::from_raw(raw_ptr) }; // will be freed }
La réciproque est aussi vraie, c'est-à-dire que les fonctions from_raw ne
devraient pas être utilisées sur des raw pointers qui ne sont pas issus de la fonction
into_raw associée. En effet, pour les cas comme Rc, la documentation officielle
limite explicitement ces fonctions
à ce cas d'usage, et, dans le cas de Box, la conversion de pointeurs C en Box
n'est pas sûre,
Dans un développement de sécurité en Rust, les fonctions from_raw NE DOIVENT être appelées QUE sur des
valeurs issues de la fonction into_raw
Dans le cas de Box::into_raw, le nettoyage automatique est possible, mais
est bien plus compliqué que de re-boxer le pointeur brut et doit être
évité :
#![allow(unused)] fn main() { // Excerpt from the standard library documentation use std::alloc::{Layout, dealloc}; use std::ptr; let x = Box::new(String::from("Hello")); let p = Box::into_raw(x); unsafe { ptr::drop_in_place(p); dealloc(p as *mut u8, Layout::new::<String>()); } }
Puisque les autres types (Rc et Arc) sont opaques et plus complexes, la
libération manuelle n'est pas possible.
Mémoire non initialisée
Par défaut, le langage Rust impose que toutes les valeurs soient initialisées, pour
prévenir l'utilisation de mémoire non initialisée (à l'exception de
l'utilisation de std::mem::uninitialized ou de std::mem::MaybeUninit).
La fonction std::mem::uninitialized (dépréciée depuis la version 1.38) NE DOIT PAS être utilisée.
Le type std::mem::MaybeUninit (stabilisé dans la version 1.36) NE DOIT être
utilisé QU'en fournissant une justification pour chaque cas d'usage.
L'utilisation de mémoire non initialisée peut induire deux problèmes de sécurité distincts :
- la libération de mémoire non initialisée (étant également un problème de sûreté mémoire) ;
- la non-libération de mémoire initialisée.
Le type std::mem::MaybeUninit est une amélioration de la fonction
std::mem::uninitialized. En effet, il rend la libération des valeurs non
initialisées bien plus difficile. Toutefois, cela ne change pas le second
problème : la non-libération de la mémoire initialisée est bien possible.
C'est problématique en particulier si l'on considère l'utilisation de Drop
pour effacer des valeurs sensibles.
Cycle dans les références comptées (Rc et Arc)
La combinaison de la mutabilité intérieure, des références comptées et des types récursifs n'est pas sûre. En effet, elle peut conduire à des fuites mémoire, et donc éventuellement à des attaques par déni de service et à des fuites de secrets.
L'exemple non-unsafe suivant montre, la création d'une fuite mémoire en utilisant la mutabilité intérieure et les références comptées.
use std::{cell::Cell, rc::Rc}; struct LinkedStruct { other: Cell<Option<Rc<LinkedStruct>>>, } fn main() { println!("Hello, world!"); let a = Rc::new(LinkedStruct { other: Cell::new(None), }); let b = Rc::new(LinkedStruct { other: Cell::new(None), }); let aa = a.clone(); let bb = b.clone(); a.other.set(Some(bb)); b.other.set(Some(aa)); }
La fuite peut être mise en évidence grâce à valgrind :
$ valgrind --leak-check=full target/release/safe-rust-leak
==153637== Memcheck, a memory error detector
==153637== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==153637== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==153637== Command: target/release/safe-rust-leak
==153637==
Hello, world!
==153637==
==153637== HEAP SUMMARY:
==153637== in use at exit: 48 bytes in 2 blocks
==153637== total heap usage: 10 allocs, 8 frees, 3,144 bytes allocated
==153637==
==153637== 48 (24 direct, 24 indirect) bytes in 1 blocks are definitely lost
in loss record 2 of 2
==153637== at 0x48417B4: malloc (vg_replace_malloc.c:381)
==153637== by 0x10F8D4: safe_rust_leak::main
(in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637== by 0x10F7E2: std::sys::backtrace::__rust_begin_short_backtrace
(in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637== by 0x10F7D8: std::rt::lang_start::{{closure}}
(in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637== by 0x12A90F: std::rt::lang_start_internal
(in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637== by 0x10FA54: main
(in /home/toto/src/safe-rust-leak/target/release/safe-rust-leak)
==153637==
==153637== LEAK SUMMARY:
==153637== definitely lost: 24 bytes in 1 blocks
==153637== indirectly lost: 24 bytes in 1 blocks
==153637== possibly lost: 0 bytes in 0 blocks
==153637== still reachable: 0 bytes in 0 blocks
==153637== suppressed: 0 bytes in 0 blocks
==153637==
==153637== For lists of detected and suppressed errors, rerun with: -s
==153637== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Les types récursifs dont la récursion se base sur l'utilisation des références comptées Rc ou Arc NE DOIVENT PAS être mutables intérieurement.
Interfaçage avec des fonctions externes (FFI)
L'approche de Rust en ce qui concerne l'interfaçage avec des fonctions d'autres langages repose sur une compatibilité forte avec le C. Toutefois, cette frontière est par nature non sûre (voir Rust Book: Unsafe Rust).
Les fonctions marquées comme externes (mot-clé extern) sont rendues
compatibles avec du code C à la compilation. Elles peuvent être appelées depuis
un code C avec n'importe quelle valeur en argument. La syntaxe complète est
extern "<ABI>" où "<ABI>" décrit la convention d'appel et dépend de la
plateforme d'exécution visée. Par défaut, elle vaut "C", ce qui correspond à
la manière standard en C d'appeler des fonctions.
#![allow(unused)] fn main() { /// Export C-compatible function #[unsafe(no_mangle)] extern "C" fn mylib_f(param: u32) -> i32 { if param == 0xCAFEBABE { 0 } else { -1 } } }
Pour que la fonction mylib_f soit accessible avec le même nom, la fonction
doit être annotée avec l'attribut #[unsafe(no_mangle)]).
À l'inverse, il est possible d'appeler des fonctions écrites en C depuis du code
Rust si celles-ci sont déclarées dans un bloc extern :
#![allow(unused)] fn main() { use std::os::raw::c_int; // import C function unsafe extern "C" { fn abs(args: c_int) -> c_int; } fn use_abs() { let x = -1; println!("{} {}\n", x, unsafe { abs(x) }); } }
Toute fonction écrite dans un autre langage et importée dans Rust par l'usage
d'un bloc extern est automatiquement unsafe. C'est pourquoi tout
appel à une telle fonction doit être fait dans un contexte unsafe.
Les blocs extern peuvent également contenir des déclarations de variables
globales externes, préfixées alors par le mot-clé static :
#![allow(unused)] fn main() { // A direct way to access environment variables (on Unix) // should not be used! Not thread safe, have a look at `std::env`! unsafe extern "C" { // Global variable #[link_name = "environ"] static libc_environ: *const *const std::os::raw::c_char; } fn use_static_extern() { let mut next = unsafe { libc_environ }; while !next.is_null() && !unsafe { *next }.is_null() { let env = unsafe { std::ffi::CStr::from_ptr(*next) } .to_str() .unwrap_or("<invalid>"); println!("{}", env); next = unsafe { next.offset(1) }; } } }
Organisation du code
L'interfaçage entre une bibliothèque écrite dans un autre langage et du code Rust DEVRAIT être réalisé en deux parties :
- un module bas-niveau, potentiellement caché, qui traduit de façon très
proche l'API originale en des blocs
extern; - un module qui assure la sûreté mémoire et les invariants de sécurité au niveau de Rust.
Si l'API bas-niveau est exposée, cela DEVRAIT être fait dans un crate dédiée
ayant un nom de la forme *-sys.
La crate rust-bindgen peut être utilisée pour générer automatiquement la partie bas-niveau du binding depuis les fichiers header C.
Typage
Le typage est le moyen qu'utilise Rust pour assurer la sûreté mémoire. Lors de l'interfaçage avec d'autres langages, qui n'offrent peut-être pas les mêmes garanties, le choix des types lors du binding est essentiel pour maintenir au mieux cette sûreté mémoire.
Agencement des données
Rust ne fournit aucune garantie, que ce soit sur un court ou un long terme,
vis-à-vis de la façon dont sont agencées les données en mémoire. La seule
manière de rendre les données compatibles avec d'autres langages est
la déclaration explicite de la compatibilité avec le C, avec l'attribut repr
(voir Rust Reference: Type Layout). Par exemple, les types Rust suivants :
#![allow(unused)] fn main() { #[repr(C)] struct Data { a: u32, b: u16, c: u64, } #[repr(C, packed)] struct PackedData { a: u32, b: u16, c: u64, } }
sont compatibles avec les types C suivants :
struct Data
{
uint32_t a;
uint16_t b;
uint64_t c;
};
__attribute__((packed)) struct PackedData
{
uint32_t a;
uint16_t b;
uint64_t c;
};
Dans un développement sécurisé, les types non compatibles avec le C NE DOIVENT PAS être utilisés comme argument ou type de retour des fonctions importées ou exportées et comme type de variables globales importées ou exportées.
La seule exception à cette règle est l'utilisation de types considérés comme opaques du côté du langage externe.
Les types suivants sont considérés comme compatibles avec le C :
- les types primitifs entiers et à virgule flottante ;
- les
structs annotées avecrepr(C); - les
enums annotées avecrepr(C)ourepr(Int)(oùIntest un type primitif entier), contenant au moins un variant et dont tous les variants ne comportent pas de champ ; - les pointeurs ;
- les
Option<T>oùTest:core::ptr::NonNull<U>etUest un type compatible avec le C etSized, auquel cas le type est équivalent à un pointeur*const Tet*mut T,core::num::NonZero*, auquel cas le type est équivalent au type primitif entier correspondant ;
- les
structs annotées avecrepr(transparent)possédant un seul champ, qui est d'un type C-compatible.
Les types suivants ne sont pas compatibles avec le C :
- les types à taille variable ;
- les
trait objects ; - les
enums dont les variants comportent des champs ; - les n-uplets (sauf les
structs à n-uplet annotées avecrepr(C)).
Certains types sont compatibles, mais avec certaines limitations :
- les types à taille nulle, qui ne sont pas spécifiés pour le C et mènent à des contradictions dans les spécifications du C++ ;
- les
enums avec champs annotés avecrepr(C),repr(C, Int)ourepr(Int)(voir RFC-2195).
Cohérence du typage
Les types DOIVENT être cohérents entre les deux côtés des frontières des FFI.
Bien que certains détails peuvent être masqués de la part d'un côté envers l'autre (typiquement, pour rendre un type opaque), les types des deux parties DOIVENT avoir la même taille et respecter le même alignement.
En ce qui concerne les enums avec des champs en particulier, les types
correspondant en C (ou en C++) ne sont pas évidents (RFC-2195).
Les outils permettant de générer automatiquement des bindings, comme rust-bindgen ou cbindgen, peuvent aider à assurer la cohérence entre les types du côté C et ceux du côté Rust.
Dans un développement sécurisé en Rust, les outils de génération automatique de bindings DEVRAIENT être utilisés lorsque cela est possible, et ce en continu.
Pour les bindings C/C++ vers Rust, rust-bindgen est capable de générer
automatiquement des bindings de bas niveau. L'écriture d'un binding de
plus haut niveau est fortement recommandée (voir Recommandation
FFI-SAFEWRAPPING). Attention également à certaines
options dangereuses de rust-bindgen, en particulier rustified_enum.
Types dépendants de la plateforme d'exécution
Lors de l'interfaçage avec un langage externe, comme C ou C++, il est souvent
nécessaire d'utiliser des types dépendants de la plateforme d'exécution, comme
les ints C, les longs, etc.
En plus du type c_void de std::ffi (ou core::ffi) pour le type C void,
la bibliothèque standard offre des alias de types portables dans std::os::raw
(or core::os::raw) :
c_charpourchar(soiti8ou bienu8) ;c_scharpoursigned char(toujoursi8) ;c_ucharpourunsigned char(toujoursu8) ;c_shortpourshort;c_ushortpourunsigned short;c_intpourint;c_uintpourunsigned int;c_longpourlong;c_ulongpourunsigned long;c_longlongpourlong long;c_ulonglongpourunsigned long long;c_floatpourfloat(toujoursf32) ;c_doublepourdouble(toujoursf64).
La crate libc offre des types supplémentaires compatibles avec le C qui couvrent la quasi-entièreté de la bibliothèque standard du C.
Dans un développement sécurisé en Rust, lors de l'interfaçage avec du code
faisant usage de types dépendants de la plateforme d'exécution, comme les
ints et les longs du C, le code Rust DOIT utiliser les alias portables de
types, comme ceux fournis dans la bibliothèque standard ou dans la crate
libc, au lieu des types spécifiques à la plateforme, à l'exception du cas
où les bindings sont générés automatiquement pour chaque plateforme (voir
la note ci-dessous).
Les outils de génération automatiques de bindings (par exemple cbindgen ou rust-bindgen) sont capables d'assurer la cohérence des types dépendants de la plateforme. Ils doivent être utilisés durant le processus de compilation pour chaque cible afin d'assurer que la génération est cohérente pour la plateforme visée.
Types non-robustes : références, pointeurs de fonction, énumérations
Une représentation piégeuse d'un type particulier est une représentation (motif d'octets) qui respecte les contraintes de représentation du type (telles que sa taille et son alignement), mais qui ne représente pas une valeur valide de ce type et mène à des comportements indéfinis.
En d'autres termes, si une telle valeur invalide est affectée à une variable Rust, tout peut arriver ensuite, d'un simple crash à une exécution de code arbitraire. Quand on écrit du code Rust sûr, ce genre de comportement ne peut arriver (à moins d'un bug dans le compilateur Rust). Toutefois, en écrivant du code Rust non sûr, et en particulier dans des FFI, cela peut facilement avoir lieu.
Dans la suite, on appelle des types non-robustes les types dont les valeurs peuvent avoir ces représentations piégeuses (au moins une). Beaucoup de types Rust sont non-robustes, même parmi les types compatibles avec le C :
bool(1 octet, 256 représentations, seules deux d'entre elles valides) ;- les références ;
- les pointeurs de fonction ;
- les énumérations ;
- les flottants (même si de nombreux langages ont la même compréhension de ce qu'est un flottant valide) ;
- les types composés qui contiennent au moins un champ ayant pour type un type non-robuste.
De l'autre côté, les types entiers (u*/i*), les types composés packés qui
ne contiennent pas de champs de type non-robuste, sont par exemple des
types robustes.
Les types non-robustes engendrent des difficultés lors de l'interfaçage entre deux langages. Cela revient à décider quel langage des deux est le plus responsable pour assurer la validité des valeurs hors bornes et comment mettre cela en place.
Dans un développement sécurisé en Rust, toute valeur externe de type non- robuste DOIT être vérifiée.
Plus précisément, soit une conversion (en Rust) est effectuée depuis des types robustes vers des types non-robustes à l'aide de vérifications explicites, soit le langage externe offre des garanties fortes quant à la validité des valeurs en question.
Dans un développement Rust sécurisé, la vérification des valeurs provenant d'un langage externe DEVRAIT être effectuée du côté Rust lorsque cela est possible.
Ces règles génériques peuvent être adaptées à un langage externe spécifique ou selon les risques associés. En ce qui concerne les langages, le C est particulièrement inapte à offrir des garanties de validité. Toutefois, Rust n'est pas le seul langage à offrir de telles possibilités. Par exemple, un certain sous-ensemble de C++ (sans la réinterprétation) permet au développeur de faire beaucoup dans ce domaine à l'aide du typage. Parce que Rust sépare nativement les segments sûrs des segments non-sûrs, la recommandation est de toujours utiliser Rust pour les vérifications lorsque c'est possible. En ce qui concerne les risques, les types présentant le plus de dangers sont les références, les références de fonction et les énumérations, qui sont discutées ci-dessous.
Le type bool de Rust a été rendu équivalent au type _Bool (renommé bool
dans <stdbool.h>) de C99 et au type bool de C++. Toutefois, charger une
valeur différente de 0 ou 1 en tant que _Bool/bool est un comportement
indéfini des deux côtés. La partie sûre de Rust assure ce fait. Les
compilateurs C et C++ assurent qu'aucune autre valeur que 0 et 1 ne peut être
stockée dans un _Bool/bool mais ne peuvent garantir l'absence d'une
réinterprétation incorrecte (par exemple dans un type union ou via un
cast de pointeur). Pour détecter une telle réinterprétation, un
sanitizer tel que l'option -fsanitize=bool de LLVM peut être utilisé.
Références et pointeurs
Bien qu'autorisée par le compilateur Rust, l'utilisation des références Rust dans une FFI peut casser la sûreté mémoire. Parce que leur côté non sûr est plus explicite, les pointeurs sont préférés aux références Rust pour un interfaçage avec un autre langage.
De plus, les types des références ne sont pas robustes : ils permettent seulement de pointer vers des objets valides en mémoire. Toute déviation mène à des comportements indéfinis.
Dans un développement sécurisé en Rust, les références externes transmises au côté Rust par le biais d'une FFI DOIVENT être vérifiées du côté du langage externe, que ce soit de manière automatique (par exemple, par un compilateur) ou de manière manuelle.
Les exceptions comprennent les références Rust wrappées de façon opaque et
manipulées uniquement du côté Rust, et les références wrappées dans un type
Option (voir note ci-dessous).
Lors d'un binding depuis et vers le C, le problème peut être particulièrement sévère, parce que le langage C n'offre pas de références (dans le sens de pointeurs valides) et le compilateur n'offre pas de garantie de sûreté.
Lors d'un binding avec le C++, les références Rust peuvent en pratique être
liées aux références C++ bien que l'ABI d'une fonction extern "C" en C++ avec
des références soit défini par l'implémentation. Enfin, le code C++ doit être
vérifié pour éviter toute confusion de pointeurs et de références.
Les références Rust peuvent être raisonnablement utilisées avec d'autres langages compatibles avec le C, incluant les variantes de C qui mettent en œuvre la vérification que les pointeurs sont non nuls, comme du code annoté à l'aide de Microsoft SAL par exemple.
Dans un développement sécurisé en Rust, le code Rust de la partie
bas-niveau de la liaison à une bibliothèque écrite dans un langage externe
(la crate *-sys)
NE DOIT PAS utiliser de types références aux frontières avec le langage externe, mais des types pointeurs.
Les exceptions sont :
- les références qui sont opaques dans le langage externe et qui sont seulement manipulées du côté Rust ;
- les références wrappées dans un type
Option(voir note ci-dessous) ; - les références liées à des références sûres dans le langage externe, par
exemple dans des variantes du C ou dans du code compilé en C++ dans un
environnement où les références de fonctions
extern "C"sont encodées comme des pointeurs.
D'un autre côté, les types pointeur Rust peuvent aussi mener à des
comportements indéfinis, mais sont plus aisément vérifiables, principalement
par la comparaison avec std/code::ptr::null() ((void*)0 en C), mais aussi
dans certains contextes par la vérification de l'appartenance à une plage
d'adresses mémoire (en particulier dans des systèmes embarqués ou pour un
développement au niveau noyau). Un autre avantage à utiliser les pointeurs Rust
dans des FFI est que tout chargement de valeur pointée est clairement marqué
comme appartenant à un bloc ou à une fonction unsafe.
Dans un développement sécurisé en Rust, tout code Rust qui déréférence un pointeur externe DOIT vérifier sa validité au préalable. En particulier, les pointeurs DOIVENT être vérifiés comme étant non nuls avant toute utilisation.
Des approches plus strictes sont recommandées lorsque cela est possible. Elles comprennent la vérification des pointeurs comme appartenant à une plage d'adresses mémoire valides ou comme étant des pointeurs avérés (étiquetés ou signés). Cette approche est particulièrement applicable si la valeur pointée est seulement manipulée depuis le code Rust.
Le code suivant est un simple exemple d'utilisation de pointeur externe dans une fonction Rust exportée :
/// Add in place
#[unsafe(no_mangle)]
unsafe extern "C" fn add_in_place(a: *mut u32, b: u32) {
// checks for nullity of `a`
// and takes a mutable reference on it if it's non-null
if let Some(a) = unsafe { a.as_mut() } {
*a += b
}
}
Il faut noter que les méthodes as_ref et as_mut (pour les pointeurs
mutables) permettent d'accéder facilement à la référence tout en assurant une
vérification du caractère non nul de manière très idiomatique en Rust. Du côté
du C, la fonction peut alors être utilisée comme suit :
#include <stdint.h>
#include <inttypes.h>
//! Add in place
void add_in_place(uint32_t *a, uint32_t b);
int use_add_in_place()
{
uint32_t x = 25;
add_in_place(&x, 17);
printf("%" PRIu32 " == 42", x);
return 0;
}
Les valeurs de type Option<&T> ou Option<&mut T>, pour tout T tel que
T: Sized, sont admissibles dans un FFI à la place de pointeurs avec
comparaison explicite avec la valeur nulle. En raison de la garantie de Rust
vis-à-vis des optimisations de pointeurs pouvant être nuls, un pointeur nul
est acceptable du côté C. La valeur C NULL est comprise par Rust comme la
valeur None, tandis qu'un pointeur non nul est encapsulé dans le
constructeur Some.
Bien qu'ergonomique, cette fonctionnalité ne permet pas en revanche des validations fortes des valeurs de pointeurs comme l'appartenance à une plage d'adresses mémoire valides.
Pointeurs de fonction
Les pointeurs de fonction qui traversent les frontières d'une FFI peuvent mener à de l'exécution de code arbitraire et impliquent donc des risques réels de sécurité.
Dans un développement sécurisé en Rust, tout type de pointeur de fonction dont
les valeurs sont amenées à traverser les frontières d'une FFI DOIT être
marqué comme extern (si possible avec l'ABI spécifiée) et comme unsafe.
Les pointeurs de fonction en Rust ressemblent bien plus aux références qu'aux pointeurs simples. En particulier, la validité des pointeurs de fonction ne peut pas être vérifiée directement du côté Rust. Toutefois, Rust offre deux alternatives possibles :
-
l'utilisation de pointeurs de fonctions wrappé dans une valeur de type
Option, accompagnée d'un test contre la valeur nulle :#[unsafe(no_mangle)] pub unsafe extern "C" fn repeat( start: u32, n: u32, f: Option<unsafe extern "C" fn(u32) -> u32>, ) -> u32 { if let Some(f) = f { let mut value = start; for _ in 0..n { value = unsafe { f(value) }; } value } else { start } }Du côté C :
uint32_t repeat(uint32_t start, uint32_t n, uint32_t (*f)(uint32_t)); -
l'utilisation de pointeurs bruts avec une transformation
unsafevers un type pointeur de fonction, permettant des tests plus poussés au prix de l'ergonomie.
Dans un développement sécurisé en Rust, tout pointeur de fonction provenant de l'extérieur de l'écosystème Rust DOIT être vérifié à la frontière des FFI.
Lors d'un binding avec le C ou encore le C++, il n'est pas simple de garantir la validité d'un pointeur de fonction. De plus, les objets fonction C++ (parfois appelés foncteurs) ne sont pas compatibles avec le C.
Enumérations
Les valeurs (motifs de bits) valides pour une énumération donnée sont en général
assez peu nombreuses par rapport à l'ensemble des valeurs qu'il est possible
d'exprimer avec le même nombre de bits. Ne pas traiter correctement une valeur
d'enum fournie par un code externe peut mener à une confusion de types et
avoir de sérieuses conséquences sur la sécurité d'un programme. Malheureusement,
vérifier la valeur d'une énumération aux bornes d'une FFI n'est pas une tâche
triviale des deux côtés.
Du côté Rust, cette vérification consiste à utiliser un type entier dans la
déclaration du bloc extern, un type robuste donc, et d'effectuer une
conversion contrôlée vers le type enum.
Du côté externe, cela est possible uniquement si l'autre langage permet la mise
en place de tests plus stricts que ceux proposés en C. C'est par exemple
possible en C++ avec les enum class. Notons toutefois pour référence que
l'ABI extern "C" d'une enum class est définie par l'implémentation et doit
être vérifiée pour chaque environnement d'exécution.
Dans un développement sécurisé en Rust, lors de l'interfaçage avec un
langage externe, le code Rust NE DOIT PAS accepter de valeurs provenant de
l'extérieur en tant que valeur d'un type enum.
Les exceptions incluant des types enum Rust sont :
- les types opaques du langage externe dont les valeurs sont uniquement manipulées du côté Rust ;
- les types liés à des types d'énumération sûrs du côté du langage externe,
comme les
enum classde C++ par exemple.
Concernant les énumérations ne contenant aucun champ, des crates comme
num_derive ou num_enum permettent au développeur de fournir facilement
des opérations de conversions sûres depuis une valeur entière vers une
énumération et peuvent être utilisées pour convertir de manière contrôlée un
entier (fourni par une énumération C) vers une énumération C.
Types opaques
Rendre opaques des types est une bonne méthode pour augmenter la modularité dans un développement logiciel. C'est notamment une pratique assez courante dans un développement impliquant plusieurs langages de programmation.
Dans un développement sécurisé en Rust, lors d'un binding avec des types
opaques externes, des pointeurs vers des types opaques dédiés DEVRAIENT être
utilisés au lieu de pointeurs c_void.
La pratique recommandée pour récupérer des valeurs externes de type opaque est illustrée comme suit :
#[repr(C)]
pub struct OpaqueFoo {
_private: [u8; 0],
}
unsafe extern "C" {
fn handle_opaque_foo(arg: *mut OpaqueFoo);
}
La proposition RFC-1861, non stabilisée à la rédaction de ce guide, propose
de faciliter cette situation en permettant de déclarer des types opaques dans
des blocs extern.
Dans un développement sécurisé en Rust, lors de l'interfaçage avec du C ou du
C++, les valeurs de types Rust considérés comme opaques dans la partie C/C++
DEVRAIENT être transformées en valeurs de type struct incomplet (c'est-à-dire
déclaré sans définition) et être fournies avec un constructeur et un
destructeur dédiés.
Un exemple d'utilisation de type opaque Rust :
pub struct Opaque {
// (...) hide details
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn new_opaque() -> *mut Opaque {
catch_unwind(|| // Catch panics, see below
Box::into_raw(Box::new(Opaque {
// (...) construction
})))
.unwrap_or(std::ptr::null_mut())
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn destroy_opaque(o: *mut Opaque) {
catch_unwind(|| {
if !o.is_null() {
// only necessary when `Opaque` or any of its fields is `Drop`
drop(unsafe { Box::from_raw(o) })
}
})
.unwrap_or_default();
}
Mémoire et gestion des ressources
Les langages de programmation ont de nombreuses façons de gérer la mémoire. En résultat, il est important de savoir quel langage est responsable de la réclamation de l'espace mémoire d'une donnée lorsqu'elle est échangée entre Rust et un autre langage. Il en va de même pour d'autres types de ressources comme les descripteurs de fichiers ou les sockets.
Rust piste le responsable ainsi que la durée de vie des variables pour
déterminer à la compilation si et quand la mémoire associée doit être libérée.
Grâce au trait Drop, il est possible d'exploiter ce système pour libérer
toutes sortes de ressources comme des fichiers ou des accès au réseau.
Déplacer une donnée depuis Rust vers un langage signifie également abandonner
de possibles réclamations de la mémoire qui lui est associée.
Dans un développement sécurisé en Rust, le code Rust NE DOIT PAS implémenter
Drop pour les valeurs de types qui sont directement transmis à du code
externe (c'est-à-dire ni par pointeur, ni par référence).
En fait, il est même recommandé de n'utiliser que des types qui implémentent
Copy. Il faut noter que *const T est Copy même si T ne l'est pas.
Si ne pas récupérer la mémoire et les ressources est une mauvaise pratique, en termes de sécurité, utiliser de la mémoire récupérée plus d'une fois ou libérer deux fois certaines ressources peut être pire. Afin de libérer correctement une ressource une seule et unique fois, il faut savoir quel langage est responsable de la gestion de son allocation et de sa libération.
Dans un développement sécurisé en Rust, lorsqu'une donnée, quel que soit son type, est échangée par une FFI, il est nécessaire de s'assurer que :
- un seul langage DOIT gérer l'allocation et de la libération d'une donnée ;
- l'autre langage NE DOIT NI allouer, NI libérer la donnée directement, mais peut utiliser une fonction externe dédiée fournie par le langage responsable choisie.
L'identification d'un langage responsable de la gestion des données en mémoire ne suffit pas. Il reste à s'assurer de la durée de vie correcte de ces données, principalement qu'elles ne sont plus utilisées après leur libération. C'est une étape bien plus difficile. Lorsque le langage externe est responsable de la mémoire, la même approche est de fournir un wrapper sûr autour du type externe.
Dans un développement sécurisé en Rust, toute donnée à caractère non sensible
allouée et libérée du côté du langage externe DEVRAIT être encapsulée dans un
type implémentant Drop, de telle sorte que Rust fournisse l'appel
automatique au destructeur Rust.
Voici un simple exemple d'encapsulation autour d'un type opaque externe :
/// Private “raw” opaque foreign type Foo
#[repr(C)]
struct RawFoo {
_private: [u8; 0],
}
// Private “raw” C API
unsafe extern "C" {
fn foo_create() -> *mut RawFoo;
fn foo_do_something(this: *const RawFoo);
fn foo_destroy(this: *mut RawFoo);
}
/// Foo
pub struct Foo(*mut RawFoo);
impl Foo {
/// Create a Foo
pub fn new() -> Option<Foo> {
let raw_ptr = unsafe { foo_create() };
if raw_ptr.is_null() {
None
} else {
Some(Foo(raw_ptr))
}
}
/// Do something on a Foo
pub fn do_something(&self) {
unsafe { foo_do_something(self.0) }
}
}
impl Drop for Foo {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { foo_destroy(self.0) }
}
}
}
Parce que des panics peuvent mener à ne pas exécuter la méthode
Drop::drop, cette solution n'est pas satisfaisante pour le cas de la
libération de ressources sensibles (pour effacer les données sensibles par
exemple), à moins que le code soit garanti exempt de panic potentiel.
Pour le cas de l'effacement des données sensibles, le problème peut être géré
par l'utilisation d'un handler de panic.
Lorsque le langage externe exploite des ressources allouées depuis le côté Rust, il est encore plus difficile d'offrir quelque garantie qui soit.
En C par exemple, il n'y a pas de moyen simple qui permette de vérifier que le destructeur correspondant est appelé. Il est possible d'utiliser des callbacks pour assurer que la libération est effectivement faite.
Le code Rust suivant est un exemple unsafe du point de vue des threads d'une API compatible avec le C qui fournit une callback pour assurer la libération d'une ressource :
pub struct XtraResource {/* fields */}
impl XtraResource {
pub fn new() -> Self {
XtraResource { /* ... */ }
}
pub fn dosthg(&mut self) {
/* ... */
}
}
impl Drop for XtraResource {
fn drop(&mut self) {
println!("xtra drop");
}
}
pub mod c_api {
use super::XtraResource;
use std::panic::catch_unwind;
const INVALID_TAG: u32 = 0;
const VALID_TAG: u32 = 0xDEAD_BEEF;
const ERR_TAG: u32 = 0xDEAF_CAFE;
static mut COUNTER: u32 = 0;
pub struct CXtraResource {
tag: u32, // to detect accidental reuse
id: u32,
inner: XtraResource,
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn xtra_with(cb: Option<unsafe extern "C" fn(*mut CXtraResource) -> ()>) {
let inner = if let Ok(res) = catch_unwind(XtraResource::new) {
res
} else {
return;
};
let id = unsafe { COUNTER };
let tag = VALID_TAG;
unsafe { COUNTER = COUNTER.wrapping_add(1) };
// Use heap memory and do not provide pointer to stack to C code!
let mut boxed = Box::new(CXtraResource { tag, id, inner });
if let Some(cb) = cb {
unsafe { cb(boxed.as_mut() as *mut CXtraResource) };
}
if boxed.id == id && (boxed.tag == VALID_TAG || boxed.tag == ERR_TAG) {
boxed.tag = INVALID_TAG; // prevent accidental reuse
// implicit boxed drop
} else {
// (...) error handling (should be fatal)
boxed.tag = INVALID_TAG; // prevent reuse
std::mem::forget(boxed); // boxed is corrupted it should not be
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn xtra_dosthg(cxtra: *mut CXtraResource) {
let do_it = || {
if let Some(cxtra) = unsafe { cxtra.as_mut() }
&& cxtra.tag == VALID_TAG
{
cxtra.inner.dosthg();
return;
}
println!("do noting with {:p}", cxtra);
};
if catch_unwind(do_it).is_err()
&& let Some(cxtra) = unsafe { cxtra.as_mut() }
{
cxtra.tag = ERR_TAG;
};
}
}
Un appel C compatible :
struct XtraResource;
void xtra_with(void (*cb)(struct XtraResource *xtra));
void xtra_sthg(struct XtraResource *xtra);
void cb(struct XtraResource *xtra)
{
// ()...) do anything with the proposed C API for XtraResource
xtra_sthg(xtra);
}
int use_xtra()
{
xtra_with(cb);
}
Panics et code externe
Lors de l'appel à du code Rust depuis un autre langage (par exemple, du C), le
code Rust ne doit pas provoquer de panic : dérouler (unwinding) depuis le
code Rust dans du code externe résulte en un comportement indéfini.
Le code Rust appelé depuis un langage externe DOIT :
- soit s'assurer que la fonction ne peut pas provoquer de
panic, - soit utiliser un mécanisme de
récupération de
panic(commestd::panic::catch_unwind,std::panic::set_hook,#[panic_handler]), afin d'assurer que la fonction Rust ne peut pas quitter ou retourner dans un état instable.
Il faut noter que catch_unwind rattrapera seulement les unwinding panics
mais pas ceux provoquant un arrêt du processus.
fn may_panic() {
/* Something may panic... */
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn no_panic() -> i32 {
let result = catch_unwind(may_panic);
match result {
Ok(_) => 0,
Err(_) => -1,
}
}
no_std
Dans le cas des programmes n'utilisant pas la bibliothèque standard
Rust (#[no_std]), un gestionnaire de panic (#[panic_handler]) doit être
défini pour la sécurité du programme. Le gestionnaire de panic doit être écrit
avec la plus grande précaution pour garantir non seulement la sécurité, mais
aussi la sûreté du programme.
Un approche alternative est de simplement s'assurer qu'il n'y a aucune
utilisation de panic! avec la crate panic-never. Comme no-panic,
panic-never repose sur une astuce au moment de l'édition de liens : le
programme d'édition de liens échoue si une branche non trivialement
inaccessible mène à un appel à panic!.
Liaison entre une bibliothèque Rust et du code d'un autre langage
Dans un développement sécurisé en Rust, exposer un bibliothèque Rust à un langage externe DOIT être uniquement fait par le biais d'une API dédiée et compatible avec le C.
La crate cbindgen peut être utilisée pour générer automatiquement les bindings C ou C++ pour l'API Rust compatible avec le C d'une bibliothèque Rust.
Exemple minimal d'une bibliothèque Rust exportée vers du C
src/lib.rs:
/// Opaque counter
pub struct Counter(u32);
impl Counter {
/// Create a counter (initially at 0)
fn new() -> Self {
Self(0)
}
/// Get the current value of the counter
fn get(&self) -> u32 {
self.0
}
/// Increment the value of the counter if there's no overflow
fn incr(&mut self) -> bool {
if let Some(n) = self.0.checked_add(1) {
self.0 = n;
true
} else {
false
}
}
}
// C-compatible API
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_create() -> *mut Counter {
Box::into_raw(Box::new(Counter::new()))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_incr(counter: *mut Counter) -> std::os::raw::c_int {
if let Some(counter) = unsafe { counter.as_mut() } {
if counter.incr() { 0 } else { -1 }
} else {
-2
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_get(counter: *const Counter) -> u32 {
if let Some(counter) = unsafe { counter.as_ref() } {
counter.get()
} else {
0
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_destroy(counter: *mut Counter) -> std::os::raw::c_int {
if !counter.is_null() {
let _ = unsafe { Box::from_raw(counter) }; // get box and drop
0
} else {
-1
}
}
En utilisant cbindgen ([cbindgen] -l c > counter.h), il est possible de
générer un header C cohérent, counter.h :
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
typedef struct Counter Counter;
Counter *counter_create(void);
int counter_destroy(Counter *counter);
uint32_t counter_get(const Counter *counter);
int counter_incr(Counter *counter);
counter_main.c:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include "counter.h"
int main(int argc, const char **argv)
{
if (argc < 2)
{
return -1;
}
size_t n = (size_t)strtoull(argv[1], NULL, 10);
Counter *c = counter_create();
for (size_t i = 0; i < n; i++)
{
if (counter_incr(c) != 0)
{
printf("overflow\n");
counter_destroy(c);
return -1;
}
}
printf("%" PRIu32 "\n", counter_get(c));
counter_destroy(c);
return 0;
}
Références
- Really tagged unions (RFC-2195)
- Extern types (RFC-1861)
Bibliothèque standard
Les traits Send et Sync
Les traits Send et Sync (définis dans std::marker ou core::marker) sont
des marqueurs utilisés pour assurer la sûreté des accès concurrents en Rust.
Lorsqu'ils sont correctement implémentés, ils permettent au compilateur Rust de
garantir l'absence de problèmes d'accès concurrents. Leurs sémantiques sont
définies comme suit :
- Un type est
Sends’il est sûr d'envoyer (move) des valeurs de ce type vers un autre fil d'exécution. - Un type est
Syncs’il est sûr de partager des valeurs de ce type par une référence immutable avec un autre fil d'exécution.
Ces deux traits sont des traits unsafe, c'est-à-dire que le compilateur Rust ne vérifie d'aucune manière que leur implémentation est correcte. Le danger est réel : une implémentation incorrecte peut mener à un comportement indéfini.
Heureusement, dans la plupart des cas, il n'est pas nécessaire de fournir une
implémentation. En Rust, la quasi-totalité des types primitifs implémente
Send et Sync, et dans la majorité des cas, Rust fournit de manière
automatique une implémentation pour les types composés. Comme rappelé dans
nomicon, quelques exceptions notables sont :
- les pointeurs
raw, qui n'implémentent niSend, niSync, puisqu'ils n'offrent aucune garantie quant à la sûreté ; - les références
UnsafeCell, qui n'implémentent pasSync(et par extension, les référencesCelletRefCellnon plus), puisqu'elles autorisent la mutabilité des valeurs contenues (interior mutability) ; - les références
Rc, qui n'implémentent niSend, niSync, puisque les compteurs de références seraient partagés de manière désynchronisée.
L'implémentation automatique de Send (respectivement Sync) a lieu pour les
types composés (structures ou énumérations) lorsque tous les champs contenus
implémentent Send (respectivement Sync).
Afin d’empêcher artificiellement qu'un type n'implémente Send ou Sync,
il est possible d'utiliser un champ typé par un type fantôme (PhantomData) :
use std::marker::PhantomData;
struct SpecialType(u8, PhantomData<*const ()>);
Dans un développement sécurisé en Rust, l'implémentation manuelle des traits
Send et Sync DEVRAIT être évitée, et, si nécessaire, DOIT être justifiée
et documentée.
Les traits de comparaison : PartialEq, Eq, PartialOrd, Ord
Les comparaisons (==, !=, <, <=, >, >=) en Rust reposent sur quatre
traits de la bibliothèque standard disponibles dans std::cmp (ou core::cmp
pour une compilation avec no_std) :
PartialEq<Rhs>qui définit la relation d'équivalence partielle entre objets de typesSelfetRhs;PartialOrd<Rhs>qui définit la relation d'ordre partiel entre les objets de typesSelfetRhs;Eqqui définit la relation d'équivalence totale entre les objets du même type. Il s'agit d'un trait de marquage qui requiert le traitPartialEq<Self>;Ordqui définit la relation d'ordre total entre les objets du même type. Le traitPartialOrd<Self>est alors requis.
Comme stipulé dans la documentation de la bibliothèque standard, Rust présuppose de nombreux invariants lors de l'implémentation de ces traits :
-
Pour
PartialEq:-
Cohérence interne :
a.ne(b)est équivalent à!a.eq(b), c.-à-d.,neest le strict inverse deeq. Cela correspond précisément à l'implémentation par défaut dene. -
Symétrie :
a.eq(b)etb.eq(a)sont équivalents. Du point de vue du développeur, cela signifie que :PartialEq<B>est implémenté pour le typeA(notéA: PartialEq<B>).PartialEq<A>est implémenté pour le typeB(notéB: PartialEq<A>).- Les deux implémentations sont cohérentes l'une avec l'autre.
-
Transitivité :
a.eq(b)etb.eq(c)impliquenta.eq(c). Cela signifie que :A: PartialEq<B>.B: PartialEq<C>.A: PartialEq<C>.- Les trois implémentations sont cohérentes les unes avec les autres (ainsi qu'avec leurs implémentations symétriques).
-
-
Pour
Eq:-
PartialEq<Self>est implémenté. -
Réflexivité :
a.eq(a). Cela signifie quePartialEq<Self>est implémenté (Eqne fournit aucune méthode).
-
-
Pour
PartialOrd:-
Consistance de la relation d'égalité :
a.eq(b)est équivalent àa.partial_cmp(b) == Some(std::ordering::Eq). -
Consistence interne :
a.lt(b)ssia.partial_cmp(b) == Some(std::ordering::Less).a.gt(b)ssia.partial_cmp(b) == Some(std::ordering::Greater).a.le(b)ssia.lt(b) || a.eq(b).a.ge(b)ssia.gt(b) || a.eq(b).
Il faut noter qu'en définissant seulement
partial_cmp, la cohérence interne est garantie par les implémentations par défaut delt,le,gt, etge. -
Antisymétrie :
a.lt(b)(respectivementa.gt(b)) impliqueb.gt(a)(respectivementb.lt(a)). Du point de vue du développeur, cela signifie que :A: PartialOrd<B>.B: PartialOrd<A>.- Les deux implémentations sont cohérentes l'une avec l'autre.
-
Transitivité :
a.lt(b)etb.lt(c)impliquenta.lt(c)(également avecgt,leetge). Cela signifie que :A: PartialOrd<B>.B: PartialOrd<C>.A: PartialOrd<C>.- Les trois implémentations sont cohérentes les unes avec les autres (et avec leurs implémentations symétriques).
-
-
Pour
Ord:-
PartialOrd<Self> -
Totalité :
a.partial_cmp(b) != Noneest toujours vrai. En d'autres mots, exactement une assertion parmia.eq(b),a.lt(b)eta.gt(b)est vraie. -
Cohérence avec
PartialOrd<Self>:Some(a.cmp(b)) == a.partial_cmp(b).
-
Le compilateur ne vérifie aucun de ces prérequis, à l'exception des
vérifications sur les types. Toutefois, les comparaisons sont des éléments
importants puisqu'elles jouent un rôle tant dans les propriétés de vivacité
des systèmes critiques comme des ordonnanceurs ou des répartiteurs de charge
que dans les algorithmes optimisés qui peuvent éventuellement utiliser des
blocs unsafe. Dans le premier cas d'usage, une mauvaise relation d'ordre
peut causer des problèmes de disponibilité comme des interblocages. Dans le
second cas, cela peut mener à des problèmes classiques de sécurité liés à des
violations de propriétés de sûreté mémoire. C'est là encore un atout que de
limiter au possible l'utilisation des blocs unsafe.
Dans un développement sécurisé en Rust, l'implémentation des traits de comparaison standards DOIT respecter les invariants décrits dans la documentation.
Dans un développement sécurisé en Rust, l'implémentation des traits de comparaison standard NE DEVRAIT être effectuée que par l'implémentation des méthodes ne fournissant pas d'implémentation par défaut, dans le but de réduire le risque de violation des invariants associés auxdits traits.
Il existe un lint Clippy qui permet de vérifier que PartialEq::ne n'est pas
défini lors d'une implémentation du trait PartialEq.
Rust propose une façon de fournir automatiquement des implémentations par défaut
pour les traits de comparaison, au travers de l'attribut #[derive(...)] :
- La dérivation de
PartialEqimplémentePartialEq<Self>avec une égalité structurelle à condition que chacun des types des données membres implémentePartialEq<Self>. - La dérivation de
Eqimplémente le trait de marquageEqà condition que chacun des types des données membres implémenteEq. - La dérivation de
PartialOrdimplémentePartialOrd<Self>comme un ordre lexicographique à condition que chacun des types des données membres implémentePartialOrd. - La dérivation de
OrdimplémenteOrdcomme un ordre lexicographique à condition que chacun des types des données membres implémenteOrd.
Par exemple, le court extrait de code suivant montre comment comparer deux
valeurs de type T1 facilement. Toutes les assertions sont vraies.
#[derive(PartialEq, Eq, PartialOrd, Ord)] struct T1 { a: u8, b: u8 } fn main() { assert!(&T1 { a: 0, b: 0 } == Box::new(T1 { a: 0, b: 0 }).as_ref()); assert!(T1 { a: 1, b: 0 } > T1 { a: 0, b: 0 }); assert!(T1 { a: 1, b: 1 } > T1 { a: 1, b: 0 }); println!("tous les tests sont validés."); }
La dérivation des traits de comparaison pour les types composites dépend de l'ordre de déclaration des champs et non de leur nom.
D'abord, cela implique que changer l'ordre des champs modifie l'ordre des valeurs. Par exemple, en considérant le type suivant :
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct T2{
b: u8, a: u8
};
on a T1 {a: 1, b: 0} > T1 {a: 0, b: 1} mais
T2 {a: 1, b: 0} < T2 {a: 0, b: 1}.
Ensuite, si une comparaison sous-jacente provoque un panic, l'ordre peut
changer le résultat à cause de l'utilisation d'un opérateur logique court-
circuitant dans l'implémentation automatique.
Pour les énumérations, les comparaisons dérivées dépendent d'abord de l'ordre des variants, puis de l'ordre des champs.
En dépit de ces avertissements sur les ordres dérivés, les comparaisons dérivées automatiquement sont bien moins sujettes à erreurs que des implémentations manuelles, et rendent le code plus court et plus simple à maintenir.
Dans un développement sécurisé en Rust, l'implémentation des traits de
comparaison standard DEVRAIT être automatiquement dérivée à l'aide de
#[derive(...)] lorsque l'égalité structurelle et l'ordre lexicographique
sont nécessaires. Toute implémentation manuelle d'un trait de comparaison
standard DEVRAIT être justifiée et documentée.
Trait Drop : le destructeur
Les types implémentent le trait std::ops::Drop dans le but d'effectuer
certaines opérations lorsque la mémoire associée à une valeur est réclamée.
Drop est l'équivalent Rust d'un destructeur en C++ ou un finaliseur en Java.
Implémenter ce trait modifie la sémantique d'exécution du langage. En effet, contrairement au fonctionnement classique des traits 1, l'exécution d'un même code se verra différente avec et sans cette implémentation.
Drop agit récursivement, depuis la valeur externe vers les valeurs imbriquées.
Lorsqu'une valeur sort du scope (ou est explicitement relâchée avec
std::mem::drop), elle est relâchée en deux étapes. La première étape a lieu
uniquement si le type de la valeur en question implémente le trait Drop et
consiste en l'appel de la méthode drop. La seconde étape consiste en la
répétition du processus de drop récursivement sur tous les champs que contient
la valeur. Il est à noter que l'implémentation de Drop est
responsable uniquement de la valeur extérieure.
Tout d'abord, l'implémentation de Drop ne doit pas être systématique. Elle est
nécessaire uniquement lorsque le type requiert un traitement logique à la
destruction. Drop est typiquement utilisé dans le cas du relâchement des
ressources externes (connexions réseau, fichier, etc.) ou de ressources mémoire
complexes (smart pointers comme les Box ou les Rc par exemple). Au
final, il est probable que l'implémentation du trait Drop contienne des blocs
unsafe ainsi que d'autres opérations critiques du point de vue de la sécurité.
Dans un développement sécurisé en Rust, l'implémentation du trait
std::ops::Drop DOIT être justifiée et documentée.
Ensuite, le système de types de Rust assure seulement la sûreté mémoire et,
du point de vue du typage, des drops peuvent tout à fait être manqués.
Plusieurs situations peuvent mener à manquer des drops, comme :
- un cycle de références (par exemple avec
RcouArc) ; - un appel explicite à
std::mem::forget(oucore::mem::forget) (voir paragraphe à propos deforgetet des fuites de mémoire) ; - un
panicdans undrop; - un arrêt du programme (et un
paniclorsqueabort-on-panicest activé).
Les drops manqués peuvent mener à l'exposition de données sensibles ou bien
encore à l'épuisement de ressources limitées et par là même à des problèmes
d'indisponibilité.
Dans un développement sécurisé en Rust, l'implémentation du trait
std::ops::Drop NE DOIT PAS causer de panic.
En plus des panics, les drops contenant du code critique doivent être
protégés.
Les valeurs dont le type implémente Drop NE DOIVENT PAS être incluses,
directement ou indirectement, dans un cycle de références à compteurs.
Certaines opérations liées à la sécurité d'une application à la fin d'un
traitement (comme l'effacement de secrets cryptographiques par exemple) NE DOIVENT PAS
reposer uniquement sur l'implémentation du trait Drop.
Références
- Specialization (RFC-1210)
- The Rustonomicon (nomicon)
-
excepté l'usage de contraintes négatives de type, permis par exemple par la RFC-1210, pas encore stabilisé pour la version actuelle de Rust (1.91.0) ↩
LICENCE OUVERTE / OPEN LICENCE
- Version 2.0
- Avril 2017
« RÉUTILISATION » DE L’« INFORMATION » SOUS CETTE LICENCE
Le « Concédant » concède au « Réutilisateur » un droit non exclusif et gratuit de libre « Réutilisation » de l’« Information » objet de la présente licence, à des fins commerciales ou non, dans le monde entier et pour une durée illimitée, dans les conditions exprimées ci-dessous.
Le « Réutilisateur » est libre de réutiliser l’« Information » :
- de la reproduire, la copier,
- de l’adapter, la modifier, l’extraire et la transformer, pour créer des « Informations dérivées », des produits ou des services,
- de la communiquer, la diffuser, la redistribuer, la publier et la transmettre,
- de l’exploiter à titre commercial, par exemple en la combinant avec d’autres informations, ou en l’incluant dans son propre produit ou application.
Sous réserve de :
- mentionner la paternité de l’« Information » : sa source (au moins le nom du « Concédant ») et la date de dernière mise à jour de l’« Information » réutilisée.
Le « Réutilisateur » peut notamment s’acquitter de cette condition en renvoyant, par un lien hypertexte, vers la source de « l’Information » et assurant une mention effective de sa paternité.
Par exemple :
« Ministère de xxx - Données originales téléchargées sur http://www.data.gouv.fr/fr/datasets/xxx/, mise à jour du 14 février 2017 ».
Cette mention de paternité ne confère aucun caractère officiel à la « Réutilisation » de l’« Information », et ne doit pas suggérer une quelconque reconnaissance ou caution par le « Concédant », ou par toute autre entité publique, du « Réutilisateur » ou de sa « Réutilisation ».
« DONNÉES À CARACTÈRE PERSONNEL »
L’« Information » mise à disposition peut contenir des « Données à caractère personnel » pouvant faire l’objet d’une « Réutilisation ». Si tel est le cas, le « Concédant » informe le « Réutilisateur » de leur présence.
L’« Information » peut être librement réutilisée, dans le cadre des droits accordés par la présente licence, à condition de respecter le cadre légal relatif à la protection des données à caractère personnel.
« DROITS DE PROPRIÉTÉ INTELLECTUELLE »
Il est garanti au « Réutilisateur » que les éventuels « Droits de propriété intellectuelle » détenus par des tiers ou par le « Concédant » sur l’« Information » ne font pas obstacle aux droits accordés par la présente licence.
Lorsque le « Concédant » détient des « Droits de propriété intellectuelle » cessibles sur l’« Information », il les cède au « Réutilisateur » de façon non exclusive, à titre gracieux, pour le monde entier, pour toute la durée des « Droits de propriété intellectuelle », et le « Réutilisateur » peut faire tout usage de l’« Information » conformément aux libertés et aux conditions définies par la présente licence.
RESPONSABILITÉ
L’« Information » est mise à disposition telle que produite ou reçue par le « Concédant », sans autre garantie expresse ou tacite que celles prévues par la présente licence. L’absence de défauts ou d’erreurs éventuellement contenues dans l’« Information », comme la fourniture continue de l’« Information » n’est pas garantie par le « Concédant ». Il ne peut être tenu pour responsable de toute perte, préjudice ou dommage de quelque sorte causé à des tiers du fait de la « Réutilisation ».
Le « Réutilisateur » est seul responsable de la « Réutilisation » de l’« Information ».
La « Réutilisation » ne doit pas induire en erreur des tiers quant au contenu de l’« Information », sa source et sa date de mise à jour.
DROIT APPLICABLE
La présente licence est régie par le droit français.
COMPATIBILITÉ DE LA PRÉSENTE LICENCE
La présente licence a été conçue pour être compatible avec toute licence libre qui exige au moins la mention de paternité et notamment avec la version antérieure de la présente licence ainsi qu’avec les licences :
- « Open Government Licence » (OGL) du Royaume-Uni,
- « Creative Commons Attribution » (CC-BY) de Creative Commons et
- « Open Data Commons Attribution » (ODC-BY) de l’Open Knowledge Foundation.
DÉFINITIONS
Sont considérés, au sens de la présente licence comme :
Le « Concédant » : toute personne concédant un droit de « Réutilisation » sur l’« Information » dans les libertés et les conditions prévues par la présente licence
L’« Information » :
- toute information publique figurant dans des documents communiqués ou publiés par une administration mentionnée au premier alinéa de l’article L.300-2 du CRPA;
- toute information mise à disposition par toute personne selon les termes et conditions de la présente licence.
La « Réutilisation » : l’utilisation de l’« Information » à d’autres fins que celles pour lesquelles elle a été produite ou reçue.
Le « Réutilisateur »: toute personne qui réutilise les « Informations » conformément aux conditions de la présente licence.
Des « Données à caractère personnel » : toute information se rapportant à une personne physique identifiée ou identifiable, pouvant être identifiée directement ou indirectement. Leur « Réutilisation » est subordonnée au respect du cadre juridique en vigueur.
Une « Information dérivée » : toute nouvelle donnée ou information créées directement à partir de l’« Information » ou à partir d’une combinaison de l’« Information » et d’autres données ou informations non soumises à cette licence.
Les « Droits de propriété intellectuelle » : tous droits identifiés comme tels par le Code de la propriété intellectuelle (notamment le droit d’auteur, droits voisins au droit d’auteur, droit sui generis des producteurs de bases de données…).
À PROPOS DE CETTE LICENCE
La présente licence a vocation à être utilisée par les administrations pour la réutilisation de leurs informations publiques. Elle peut également être utilisée par toute personne souhaitant mettre à disposition de l’« Information » dans les conditions définies par la présente licence.
La France est dotée d’un cadre juridique global visant à une diffusion spontanée par les administrations de leurs informations publiques afin d’en permettre la plus large réutilisation.
Le droit de la « Réutilisation » de l’« Information » des administrations est régi par le code des relations entre le public et l’administration (CRPA).
Cette licence facilite la réutilisation libre et gratuite des informations publiques et figure parmi les licences qui peuvent être utilisées par l’administration en vertu du décret pris en application de l’article L.323-2 du CRPA.
Etalab est la mission chargée, sous l’autorité du Premier ministre, d’ouvrir le plus grand nombre de données publiques des administrations de l’Etat et de ses établissements publics. Elle a réalisé la Licence Ouverte pour faciliter la réutilisation libre et gratuite de ces informations publiques, telles que définies par l’article L321-1 du CRPA.
Cette licence est la version 2.0 de la Licence Ouverte.
Etalab se réserve la faculté de proposer de nouvelles versions de la Licence Ouverte. Cependant, les « Réutilisateurs » pourront continuer à réutiliser les informations qu’ils ont obtenues sous cette licence s’ils le souhaitent.
Checklist
-
Environnement de développement:
- Règle - Utilisation de la chaîne d'outils stable (DENV-STABLE)
-
Rule - Utilisation exclusive du tier 1 de
rustcpour les logiciels de sûreté critiques (DENV-TIERS) - Règle - Mise en dépôt du fichier Cargo.lock (DENV-CARGO-LOCK)
- Règle - Conservation des valeurs par défaut des variables critiques dans les profils cargo (DENV-CARGO-OPTS)
- Règle - Conservation des valeurs par défaut des variables d'environnement à l'exécution de cargo (DENV-CARGO-ENVVARS)
- Recommandation - Utilisation d'un outil de formatage (rustfmt) (DENV-FORMAT)
- Règle - Utilisation régulière d'un linter (DENV-LINTER)
- Règle - Vérification manuelle des réparations automatiques (DENV-AUTOFIX)
-
Bibliothèques:
- Règle - Validation des dépendances tierces directes (LIBS-VETTING-DIRECT)
- Recommandation - Validation des dépendances tierces transitives (LIBS-VETTING-TRANSITIVE)
- Règle - Vérification des dépendances obsolètes (cargo-outdated) (LIBS-OUTDATED)
- Règle - Vérification des vulnérabilités connues pour les dépendances (cargo-audit) (LIBS-AUDIT)
-
Nommage:
- Règle - Respect des conventions de nommage (LANG-NAMING)
-
Gestion des entiers:
- Règle - Utilisation des opérations arithmétiques appropriées au regard des potentiels dépassements (LANG-ARITH)
-
Gestion des erreurs:
-
Recommandation - Mise en place d'un type
Errorpersonnalisé pouvant contenir toutes les erreurs possibles (LANG-ERRWRAP) -
Règle - Non-utilisation de fonctions qui peuvent causer des
panic(LANG-NOPANIC) -
Règle - Test des indices d'accès aux tableaux ou utilisation de la méthode
get(LANG-ARRINDEXING) -
Règle - Gestion correcte des
panic!dans les FFI (LANG-FFIPANIC)
-
Recommandation - Mise en place d'un type
-
Généralités:
- Règle - Interdiction des comportements non définis (UNSAFE-NOUB)
- Règle - Non-utilisation des blocs unsafe (LANG-UNSAFE)
- Règle - Encapsulation des fonctionnalités unsafe (LANG-UNSAFE-ENCP)
-
Gestion de la mémoire:
-
Règle - Non-utilisation de
forget(MEM-FORGET) -
Recommandation - Utilisation du lint clippy pour détecter l'utilisation de
forget(MEM-FORGET-LINT) -
Règle - Non-utilisation de
Box::leak(MEM-LEAK) -
Règle - Libération des valeurs wrappées dans
ManuallyDrop(MEM-MANUALLYDROP) - Règle - Pas de conversion en pointeur raw en Rust non-usafe (MEM-NORAWPOINTER)
-
Règle - Appel systématique à
from_rawpour les valeurs créées avecinto_raw(MEM-INTOFROMRAWALWAYS) -
Règle - Appel de
from_rawuniquement pour les valeurs issues deinto_raw(MEM-INTOFROMRAWONLY) - Règle - Pas de mémoire non initialisée (MEM-UNINIT)
- Règle - Éviter les références comptées récursives mutables (MEM-MUT-REC-RC)
-
Règle - Non-utilisation de
-
FFI:
- Recommandation - Mise en place d'une encapsulation sûre pour les bibliothèques externes (FFI-SAFEWRAPPING)
- Règle - Utilisation exclusive de types compatibles avec le C dans les FFI (FFI-CTYPE)
- Règle - Utilisation de types cohérents pour les FFI (FFI-TCONS)
- Recommandation - Utilisation des outils de génération automatique de bindings (FFI-AUTOMATE)
-
Règle - Utilisation des alias portables
c_*pour faire correspondre les types dépendants de la plateforme d'exécution (FFI-PFTYPE) - Règle - Vérification des valeurs de types non-robustes (FFI-CKNONROBUST)
- Recommandation - Vérification des valeurs externes en Rust (FFI-CKINRUST)
- Règle - Vérification des références provenant d'un langage externe (FFI-CKREF)
- Règle - Non-utilisation des types références au profit des types pointeurs à la frontière avec un langage externe (FFI-NOREF)
- Règle - Vérification des pointeurs externes (FFI-CKPTR)
-
Règle - Marquage des types de pointeurs de fonction dans les FFI comme
externetunsafe(FFI-MARKEDFUNPTR) - Règle - Vérification des pointeurs de fonction provenant d'une FFI (FFI-CKFUNPTR)
-
Règle - Non-utilisation d'
enums Rust provenant de l'extérieur par une FFI (FFI-NOENUM) - Recommandation - Utilisation de types Rust dédiés pour les types opaques externes (FFI-R-OPAQUE)
-
Recommandation - Utilisation de pointeurs vers des
structs C/C++ pour rendre des types opaques (FFI-C-OPAQUE) -
Règle - Non-utilisation de types qui implémentent
Dropdans des FFI (FFI-MEM-NODROP) - Règle - Identification du langage responsable de la libération des données dans les FFI (FFI-MEM-OWNER)
-
Recommandation - Encapsulation des données externes dans un type
Drop(FFI-MEM-WRAPPING) -
Règle - Gestion correcte des
panics dans les FFI (FFI-NOPANIC) - Règle - Exposition exclusive d'API dédiée et compatible avec le C (FFI-CAPI)
-
Bibliothèque standard:
-
Règle - Justification de l'implémentation des traits
SendetSync(LANG-SYNC-TRAITS) - Règle - Respect des invariants des traits de comparaison standards (LANG-CMP-INV)
- Recommandation - Utilisation des implémentations par défaut des traits de comparaison standards (LANG-CMP-DEFAULTS)
- Recommandation - Dérivation des traits de comparaison lorsque c'est possible (LANG-CMP-DERIVE)
-
Règle - Justification de l'implémentation du trait
Drop(LANG-DROP) -
Règle - Absence de
panicdans l'implémentation deDrop(LANG-DROP-NO-PANIC) -
Règle - Absence de cycles de références avec valeurs
Dropables (LANG-DROP-NO-CYCLE) -
Règle - Sécurité assurée par d'autres mécanismes en plus du trait
Drop(LANG-DROP-SEC)
-
Règle - Justification de l'implémentation des traits