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)