Dans un article précédent , Google a annoncé qu'Android prend désormais en charge le langage de programmation Rust utilisé dans le développement du système d'exploitation lui-même. À cet égard, les auteurs de cette publication ont également décidé d'évaluer à quel point le langage Rust est demandé dans le développement du noyau Linux. Cet article couvre les aspects techniques de ce travail avec quelques exemples simples.
Pendant près d'un demi-siècle, C a été le langage principal pour le développement du noyau, car C fournit le degré de contrôle et les performances prévisibles requis pour un composant aussi critique. La densité des bogues de sécurité de la mémoire dans le noyau Linux est généralement très faible car le code est de très haute qualité, les revues de code sont conformes à des normes strictes et les sauvegardes sont soigneusement mises en œuvre. Cependant, des bugs liés à la sécurité de la mémoire surgissent encore régulièrement . Sous Android, les vulnérabilités du noyau sont généralement considérées comme une faille sérieuse, car elles permettent parfois de contourner le modèle de sécurité en raison du fonctionnement du noyau en mode privilégié.
Rust est censé avoir suffisamment mûri pour coopérer avec C en tant que langage pour l'implémentation pratique du noyau. Rust aide à réduire les bogues potentiels et les vulnérabilités de sécurité dans le code privilégié, tout en s'intégrant bien avec le noyau central et en maintenant de bonnes performances.
Prise en charge de la rouille
Un prototype principal du pilote Binder a été développé pour comparer de manière adéquate les caractéristiques de sécurité et de performances de la version C existante et de son homologue Rust. Le noyau Linux compte plus de 30 millions de lignes de code, nous ne nous sommes donc pas fixé pour objectif de le réécrire entièrement en Rust, mais de fournir la possibilité d'ajouter le code requis en Rust. Nous pensons que cette approche incrémentielle permet de tirer parti de l'implémentation hautes performances dans le noyau tout en fournissant aux développeurs du noyau de nouveaux outils pour améliorer la sécurité de la mémoire et maintenir les performances lors de l'exécution.
Nous avons rejoint l'organisation Rust for Linuxoù la communauté a fait et continue de faire beaucoup pour ajouter la prise en charge de Rust au système de construction du noyau Linux. Nous devons également concevoir des systèmes pour que les fragments de code écrits dans deux langages puissent interagir les uns avec les autres : nous nous intéressons particulièrement aux abstractions sûres sans surcharge qui permettent au code Rust d'utiliser les fonctionnalités de base écrites en C, ainsi que la possibilité fonctionnalité dans Rust idiomatique, qui peut être appelée en douceur à partir de parties du noyau écrites en C.
Étant donné que Rust est un nouveau langage dans le noyau, nous avons également la possibilité d'obliger les développeurs à adhérer aux meilleures pratiques en matière de documentation et de cohérence. Par exemple, nous avons des exigences spécifiques vérifiables par machine concernant l'utilisation de code non sécurisé : pour chaque fonction non sécurisée, le développeur doit documenter les exigences que l'appelant doit satisfaire - garantissant ainsi que l'utilisation est sûre. De plus, chaque fois qu'une fonction non sécurisée est appelée, il est de la responsabilité du développeur de documenter les exigences qui doivent être remplies par l'appelant afin de garantir que l'utilisation sera sûre. De plus, pour chaque appel à des fonctions non sécurisées (ou utilisation de constructions non sécurisées, par exemple,lors du déréférencement d'un pointeur brut), le développeur doit documenter la raison pour laquelle il est si sûr de le faire.
Rust est célèbre non seulement pour sa sécurité, mais aussi pour son utilité et sa convivialité pour les développeurs. Ensuite, regardons quelques exemples qui montrent comment Rust peut être utile pour les développeurs du noyau lors de l'écriture de pilotes sûrs et corrects.
Exemple de pilote
Considérons la mise en œuvre d'un dispositif symbolique sémaphore. Chaque appareil a une valeur réelle ; lors de l'écriture de n octets, la valeur de l'appareil est augmentée de n ; à chaque lecture, cette valeur est diminuée de 1 jusqu'à ce que la valeur atteigne 0, auquel cas cet appareil est bloqué jusqu'à ce qu'une telle opération de décrémentation puisse être effectuée sur lui sans descendre en dessous de 0.
Disons
semaphore
qu'il s'agit d'un fichier représentant notre appareil. Nous pouvons interagir avec depuis le shell comme ceci :
> cat semaphore
Quand
semaphore
est le périphérique qui vient d'être initialisé, la commande indiquée ci-dessus est verrouillée car la valeur actuelle du périphérique est 0. Elle sera déverrouillée si nous exécutons la commande suivante à partir d'un autre shell, car elle incrémentera la valeur de 1, permettant ainsi au lecture de l'opération d'origine à terminer :
> echo -n a > semaphore
Nous pouvons également augmenter le compteur de plus de 1 si nous écrivons plus de données, par exemple :
> echo -n abc > semaphore
augmente le compteur de 3, afin que les 3 lectures suivantes ne se bloquent pas.
Pour illustrer d'autres aspects de Rust, ajoutons les fonctionnalités suivantes à notre pilote : rappelez-vous la valeur maximale atteinte pendant tout le cycle de vie et rappelez-vous également le nombre de lectures effectuées par chaque fichier sur l'appareil.
Montrons maintenant comment un tel pilote serait implémenté dans Rust , en comparant cette option avec l'implémentation en C.... Cependant, nous notons que le développement de ce sujet chez Google ne fait que commencer et qu'à l'avenir tout peut changer. Nous aimerions souligner comment Rust peut être utile à un développeur dans tous les aspects. Par exemple, au moment de la compilation, cela nous permet d'éliminer ou de réduire considérablement la probabilité que des classes entières de bogues s'infiltrent dans le code, tout en gardant le code flexible et en s'exécutant avec une surcharge minimale.
Dispositifs de caractères
Le développeur doit procéder comme suit pour implémenter le pilote pour le nouveau périphérique de caractère dans Rust :
- Implémenter un trait
FileOperations
: Toutes les fonctions qui lui sont associées sont facultatives, le développeur n'a donc besoin d'implémenter que celles qui sont pertinentes pour le scénario donné. Ils correspondent à des champs dans la structure Cstruct file_operations
. - Implémenter un trait
FileOpener
est l'équivalent de type sécurisé d'un champ C àopen
partir d'un structstruct file_operations
. - Enregistrez un nouveau type de périphérique pour le noyau : cela indiquera au noyau quelles fonctions devront être appelées en réponse aux opérations sur les fichiers d'un nouveau type.
Ce qui suit est une comparaison des deux premières étapes de notre premier exemple en Rust et en C
:
- Gestion de l'état du cycle de vie fichier par fichier :
FileOpener::open
renvoie un objet dont la durée de vie depuis lors appartient à l'appelant. Tout objet qui implémente le trait peut être renvoyéPointerWrapper
, et nous fournissons des implémentations pourBoîte <T>
etArc <T>
, , Rust, , .
FileOperations
self
( ),release
, ( ).release
, - , . «» ( , ).
, Rust , C. C , Rust, , Rust, , , . , C, Rust , Rust. , open , , , ,ioctl
/read
/write
( ) ,filp->private_data
, ..
- : ,
open
release
self
, , Rust , .
( ), :Mutex
SpinLock
( atomics) .
, ( ), ( ).
: , , Rust . , , ,FileOperation::open
.Arc, .
, FileOperation
( , ,open
,FileOperations
) – .
, , . , Cmiscdevice
,filp->private_data
;cdev
,inode->i_cdev
. , ,container_of
, . Rust .
: , Rust , . . C , , (void
*) : , , . Rust .
: ,FileOperations
, . ,impl FileOperations for Device
,Device
– , (FileState
). , , , . (neovim
LSP-rust-analyzer
.)
Rust, , C,struct file_operations
. (declare_file_operations
): , , ,const
, , .
Ioctl
ioctl,ioctl
,FileOperations
, .
Ioctl , , , , , (, , , , ) . Rust (cmd.dispatch
), .
. , , ioctl, Rust :cmd.raw
ioctl ( , ).
, , , - , :
- , .
- , , , TOCTOU ( ). , , . , Rust .
- : , , ioctl.
IOCTL_GET_READ_COUNT
UserSlicePtrWriter
,sizeof(u64)
, ioctl. - : ioctl , , . ,
UserSlicePtrWriter
UserSlicePtrReader
.
C, ( , ) ; Rustunsafe
, . Rust:
. ; , C Rust , , , , :
, C, , «» , unix , , .
Rust:
-
Semaphore::inner
, ,lock
. , . C, ,count
max_seen
semaphore_state
, . there is no enforcement that the lock is held while they're accessed. - (RAII): , (
inner
) . , : , , , , ; : , , ,drop
. - ,
Lock
, , ,Mutex
SpinLock
, , C, . , , , . - Rust , . , , . C
semaphore_consume
Linux: , ,mutex_unlock
prepare_to_wait
, . - : , , , , , . , ioctl , , . Rust , . , C,
atomic64_t
, , .
,open
,read
write
:
Rust:
-
?
operator:open
read
Rust ; , , , . C , - . - : Rust , , - . C .
open
, , Ckref_get
( ); Rustclone
( ), . - RAII: Rust , ,
inner
, , . - : Rust , .
write
, , . C , , .
.
10% !
