Que signifie dangereux Ă  Rust?

Bonjour, Habr! Je vous présente la traduction de l'article "Qu'est-ce qui est dangereux pour Rust?" auteur Nora Codes.


J'ai vu beaucoup de malentendus quant Ă  ce que le mot-clĂ© dangereux signifie pour l'utilitĂ© et l'exactitude du langage Rust et sa promotion en tant que «langage de programmation de systĂšme sĂ»r». La vĂ©ritĂ© est beaucoup plus compliquĂ©e que ce qui peut ĂȘtre dĂ©crit dans un court tweet, malheureusement. VoilĂ  comment je la vois.


En général, le mot clé unsafe ne désactive pas le systÚme de type qui maintient le code Rust correct . Il ne permet d'utiliser que certains «superpuissances», comme les pointeurs de déréférencement. unsafe est utilisé pour implémenter des abstractions sûres basées sur un monde fondamentalement dangereux afin que la plupart du code Rust puisse utiliser ces abstractions et éviter l'accÚs à la mémoire non sécurisé.


Garantie de sécurité


La rouille garantit la sécurité comme l'un de ses principes fondamentaux. On peut dire que c'est le sens de l'existence du langage. Cependant, il n'assure pas la sécurité au sens traditionnel, pendant l'exécution du programme et l'utilisation du garbage collector. Au lieu de cela, Rust utilise un systÚme de type trÚs avancé pour garder une trace du moment et des valeurs accessibles. Le compilateur analyse ensuite statiquement chaque programme Rust pour s'assurer qu'il est toujours dans le bon état.


Sécurité Python


Prenons l'exemple de Python. Le code Python pur ne peut pas corrompre la mémoire. L'accÚs aux éléments de la liste a des contrÎles pour dépasser les frontiÚres; les liens renvoyés par les fonctions sont comptés pour éviter l'apparition de liens pendants; Il n'y a aucun moyen de faire de l'arithmétique arbitraire avec des pointeurs.


Cela a deux consĂ©quences. PremiĂšrement, de nombreux types doivent ĂȘtre «spĂ©ciaux». Par exemple, il n'est pas possible d'implĂ©menter une liste ou un dictionnaire efficace en Python pur. Au lieu de cela, l'interprĂ©teur CPython a leur implĂ©mentation interne. DeuxiĂšmement, l'accĂšs aux fonctions externes (fonctions non implĂ©mentĂ©es en Python), appelĂ©es l'interface d'une fonction externe, nĂ©cessite l'utilisation d'un module ctypes spĂ©cial et viole les garanties de sĂ©curitĂ© du langage.


Dans un sens, cela signifie que tout ce qui est écrit en Python ne garantit pas un accÚs sécurisé à la mémoire.


Sécurité à Rust


Rust fournit également la sécurité, mais au lieu d'implémenter des structures dangereuses en C, il fournit une astuce: le mot clé unsafe. Cela signifie que les structures de données fondamentales de Rust, telles que Vec, VecDeque, BTreeMap et String, sont implémentées dans Rust.


Vous pouvez demander: "Mais, si Rust fournit une astuce contre ses garanties de sécurité de code, et que la bibliothÚque standard est implémentée en utilisant cette astuce, tout dans Rust ne sera-t-il pas considéré comme dangereux?"


En un mot, cher lecteur, oui , exactement comme c'était en Python. Examinons-le plus en détail.


Qu'est-ce qui est interdit dans la rouille sûre?


La sécurité à Rust est bien définie: nous y pensons beaucoup. En bref, les programmes Rust sûrs ne peuvent pas:


  • DĂ©rĂ©fĂ©rencer un pointeur qui pointe vers un type diffĂ©rent de celui que le compilateur connaĂźt . Cela signifie qu'il n'y a pas de pointeurs vers null (car ils ne pointent nulle part), pas d'erreurs de dĂ©passement de limites et / ou de segmentation (dĂ©fauts de segmentation), pas de dĂ©bordements de buffer. Mais cela signifie Ă©galement qu'il n'y a aucune utilisation aprĂšs avoir libĂ©rĂ© la mĂ©moire ou re-libĂ©rĂ© la mĂ©moire (car la libĂ©ration de la mĂ©moire est considĂ©rĂ©e comme un dĂ©rĂ©fĂ©rencement du pointeur) et aucun jeu de mots destinĂ© Ă  taper .
  • Avoir plusieurs rĂ©fĂ©rences mutables Ă  un objet ou des rĂ©fĂ©rences simultanĂ©ment mutables et immuables Ă  un objet . Autrement dit, si vous avez une rĂ©fĂ©rence mutable Ă  un objet, vous ne pouvez que l'avoir, et si vous avez une rĂ©fĂ©rence immuable Ă  l'objet, elle ne changera pas tant que vous ne la conserverez pas. Cela signifie que vous ne pouvez pas forcer une course aux donnĂ©es dans Safe Rust, ce qui est une garantie que la plupart des autres langues sĂ©curisĂ©es ne peuvent pas fournir.

Rust code ces informations dans un systĂšme de types ou Ă  l'aide de types de donnĂ©es algĂ©briques , comme Option pour indiquer l'existence / absence d'une valeur et RĂ©sultat <T, E> pour indiquer l'erreur / le succĂšs, ou les rĂ©fĂ©rences et leur durĂ©e de vie , par exemple, & T vs & mut T pour indiquer un lien commun (immuable) et un lien exclusif (mutable) et & 'a T vs &' b T pour distinguer les liens qui sont corrects dans diffĂ©rents contextes (ceci est gĂ©nĂ©ralement omis car le compilateur est assez intelligent pour le comprendre vous-mĂȘme) .


Des exemples


Par exemple, le code suivant ne sera pas compilĂ© car il contient un lien pendant. Plus prĂ©cisĂ©ment, my_struct ne vit pas assez . En d'autres termes, la fonction renverra un lien vers quelque chose qui n'existe plus, et donc le compilateur ne peut pas (et, en fait, ne sait mĂȘme pas comment) le compiler.


fn dangling_reference(v: &u64) -> &MyStruct { //     MyStruct   ,  v,   . let my_struct = MyStruct { value: v }; //      my_struct. return &my_struct; //  - my_struct  (  ). } 

Ce code fait de mĂȘme, mais il essaie de contourner ce problĂšme en plaçant la valeur sur le tas (Box est le nom du pointeur intelligent de base dans Rust).


 fn dangling_heap_reference(v: &u64) -> &Box<MyStruct> { let my_struct = MyStruct { value: v }; //    Box         . let my_box = Box::new(my_struct); //      my_box. return &my_box; // my_box   .   "" my_struct       - , //    - MyStruct  . } 

Le code correct est renvoyĂ© par Box lui-mĂȘme au lieu d'une rĂ©fĂ©rence Ă  celui-ci. Cela encode le transfert de propriĂ©tĂ© - la responsabilitĂ© de libĂ©rer de la mĂ©moire - dans la signature de la fonction. En regardant la signature, il devient clair que le code appelant est responsable de ce qui se passe avec Box et, en effet, le compilateur le traite automatiquement.

 fn no_dangling_reference(v: &u64) -> Box<MyStruct> { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); //    my_box  . return my_box; //    .         , //    ;       //  Box<MyStruct>       ,      . } 

Certaines mauvaises choses ne sont pas interdites dans la rouille sûre. Par exemple, il est autorisé du point de vue du compilateur:
  • provoquer un blocage dans le programme
  • fuite d'une quantitĂ© de mĂ©moire arbitrairement grande
  • ne pas fermer les poignĂ©es de fichier, les connexions Ă  la base de donnĂ©es ou les couvercles d'arbre de missile


La force de l'Ă©cosystĂšme Rust est que de nombreux projets choisissent d'utiliser un systĂšme de type pour s'assurer que le code est aussi prĂ©cis que possible, mais le compilateur ne nĂ©cessite pas une telle contrainte, sauf dans les cas oĂč un accĂšs sĂ©curisĂ© Ă  la mĂ©moire est fourni.

Qu'est-ce qui est autorisé dans la rouille dangereuse?


Le code Rust non sĂ©curisĂ© est un code Rust avec le mot clĂ© unsafe. dangereux peut ĂȘtre appliquĂ© Ă  une fonction ou Ă  un bloc de code. Lorsqu'elle est appliquĂ©e Ă  une fonction, cela signifie "cette fonction nĂ©cessite que le code appelĂ© fournisse manuellement l'invariant qui est gĂ©nĂ©ralement fourni par le compilateur". Lorsqu'il est appliquĂ© Ă  un bloc de code, cela signifie "ce bloc de code fournit manuellement l'invariant nĂ©cessaire pour empĂȘcher l'accĂšs non sĂ©curisĂ© Ă  la mĂ©moire, et par consĂ©quent il est autorisĂ© Ă  faire des choses dangereuses".


En d'autres termes, dangereux pour la fonction signifie "vous devez tout vérifier", et sur le bloc de code - "J'ai déjà tout vérifié".


Comme indiqué dans The Rust Programming Language , le code dans un bloc marqué avec le mot clé unsafe peut:


  • DĂ©rĂ©fĂ©rencer un pointeur. Il s'agit d'une "superpuissance" clĂ© qui vous permet d'implĂ©menter des listes doublement liĂ©es, une table de hachage et d'autres structures de donnĂ©es fondamentales.
  • Appelez une fonction ou une mĂ©thode non sĂ©curisĂ©e. Plus d'informations Ă  ce sujet ci-dessous.
  • AccĂ©dez ou modifiez une variable statique mutable. Les variables statiques dont la portĂ©e n'est pas contrĂŽlĂ©e ne peuvent pas ĂȘtre vĂ©rifiĂ©es statiquement, donc leur utilisation n'est pas sĂ»re.
  • Mettre en Ɠuvre un trait dangereux. Des traits non sĂ©curisĂ©s sont utilisĂ©s pour signaler si des types particuliers garantissent certains invariants. Par exemple, Send et Sync dĂ©terminent si un type peut ĂȘtre envoyĂ© entre les limites de threads ou peut ĂȘtre utilisĂ© par plusieurs threads en mĂȘme temps.

Rappelez-vous ces pointeurs suspendus ci-dessus? Ajoutez le mot dangereux, et le compilateur jurera deux fois plus car il n'aime pas utiliser dangereux lĂ  oĂč il n'est pas nĂ©cessaire.


Au lieu de cela, le mot clé unsafe est utilisé pour implémenter des abstractions sûres basées sur des opérations de pointeur arbitraires. Par exemple, le type Vec est implémenté en utilisant dangereux, mais il est sûr de l'utiliser, car il vérifie les tentatives d'accÚs aux éléments et n'autorise pas les débordements. Bien qu'il fournisse des opérations telles que set_len, qui peuvent entraßner un accÚs non sécurisé à la mémoire, elles sont marquées comme non sécurisées.


Par exemple, nous pourrions faire la mĂȘme chose que dans l'exemple no_dangling_reference, mais avec une utilisation dĂ©raisonnable de dangereux:


 fn manual_heap_reference(v: u64) -> *mut MyStruct { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct); //  Box    . let struct_pointer = Box::into_raw(my_box); return struct_pointer; //   ;     . // MyStruct     . } 

Remarquez l'absence du mot dangereux. La création de pointeurs est absolument sûre. Comme cela a été écrit, il existe un risque de fuite de mémoire, mais rien de plus, et les fuites de mémoire sont sûres. L'appel de cette fonction est également sûr. dangereux n'est requis que lorsque quelque chose tente de déréférencer un pointeur. En prime, le déréférencement libérera automatiquement la mémoire allouée.


 fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct = unsafe { Box::from_raw(my_pointer) }; //  "Value: 1337" println!("Value: {}", my_boxed_struct.value); // my_boxed_struct    .       ,  //    - MyStruct } 

AprĂšs optimisation, ce code Ă©quivaut Ă  renvoyer simplement une Box. Box est une abstraction sĂ©curisĂ©e basĂ©e sur un pointeur car elle empĂȘche la distribution de pointeurs partout. Par exemple, la prochaine version de main entraĂźnera une double mĂ©moire libre (double libre).


 fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct_1 = unsafe { Box::from_raw(my_pointer) }; // DOUBLE FREE BUG! let my_boxed_struct_2 = unsafe { Box::from_raw(my_pointer) }; //  "Value: 1337" . println!("Value: {}", my_boxed_struct_1.value); println!("Value: {}", my_boxed_struct_2.value); // my_boxed_struct_2    .     ,  //    - MyStruct. //  my_boxed_struct_1    .      , //      - MyStruct.  double-free bug. } 

Alors, quelle est l'abstraction sûre?


L'abstraction sĂ»re est une abstraction qui utilise un systĂšme de type pour fournir une API qui ne peut pas ĂȘtre utilisĂ©e pour violer les garanties de sĂ©curitĂ© mentionnĂ©es ci-dessus. Box est plus sĂ»r * mut T, car il ne peut pas conduire Ă  une double dĂ©sallocation de mĂ©moire, comme illustrĂ© ci-dessus.


Un autre exemple est le type Rc dans Rust. Il s'agit d'un pointeur de comptage de rĂ©fĂ©rence - une rĂ©fĂ©rence non modifiable aux donnĂ©es du tas. Puisqu'il permet plusieurs accĂšs simultanĂ©s Ă  une zone de mĂ©moire, il doit empĂȘcher le changement afin d'ĂȘtre considĂ©rĂ© comme sĂ»r.


De plus, il n'est pas sĂ»r pour les threads. Si vous avez besoin de la sĂ©curitĂ© des threads, vous devrez utiliser le type d'arc (comptage de rĂ©fĂ©rence atomique), qui prĂ©sente une pĂ©nalitĂ© de performance en raison de l'utilisation de valeurs atomiques pour le comptage de liens et empĂȘchant d'Ă©ventuelles courses de donnĂ©es dans des environnements multithreads.


Le compilateur ne vous permettra pas d'utiliser Rc lĂ  oĂč vous devriez utiliser Arc, car les crĂ©ateurs comme Rc ne l'ont pas marquĂ© comme thread-safe. S'ils le faisaient, ce serait dĂ©raisonnable: une fausse promesse de sĂ©curitĂ©.


Quand faut-il utiliser la rouille dangereuse?


La rouille dangereuse est toujours nĂ©cessaire lorsqu'il est nĂ©cessaire d'effectuer une opĂ©ration qui viole l'une de ces deux rĂšgles dĂ©crites ci-dessus. Par exemple, dans une liste doublement liĂ©e, l'absence de liens mutables vers les mĂȘmes donnĂ©es (pour l'Ă©lĂ©ment suivant et l'Ă©lĂ©ment prĂ©cĂ©dent) la prive complĂštement de bĂ©nĂ©fice. Avec unsafe, un implĂ©menteur de liste doublement liĂ© peut Ă©crire du code Ă  l'aide des pointeurs * mut Node, puis l'encapsuler dans une abstraction sĂ»re.

Un autre exemple est de travailler avec des systĂšmes embarquĂ©s. Les microcontrĂŽleurs utilisent souvent un ensemble de registres dont les valeurs sont dĂ©terminĂ©es par l'Ă©tat physique de l'appareil. Le monde ne peut pas s'arrĂȘter pendant que vous prenez & mut u8 Ă  partir d'un tel registre, donc dangereux n'est pas nĂ©cessaire pour travailler avec des caisses de support d'appareil. En rĂšgle gĂ©nĂ©rale, ces caisses encapsulent l'Ă©tat dans des emballages transparents et sĂ©curisĂ©s qui copient les donnĂ©es chaque fois que possible, ou utilisent d'autres techniques qui fournissent des garanties de compilation.


Parfois, il est nécessaire d'effectuer une opération qui peut conduire à une lecture et à une écriture simultanées, ou à un accÚs non sécurisé à la mémoire, et c'est là que la sécurité est nécessaire. Mais tant qu'il y a une possibilité de s'assurer que les invariants sûrs sont maintenus avant qu'un utilisateur touche quelque chose (c'est-à-dire dangereux dangereux), tout va bien.


Sur qui repose cette responsabilité?


Nous arrivons à une déclaration faite plus tÎt - oui , l'utilité du code Rust est basée sur un code dangereux. Malgré le fait que cela soit fait d'une maniÚre légÚrement différente de l'implémentation non sécurisée des structures de données de base en Python, l'implémentation de Vec, Hashmap, etc., devrait utiliser des manipulations de pointeurs dans une certaine mesure.


Nous disons que Rust est sĂ»r, avec l'hypothĂšse fondamentale que le code dangereux que nous utilisons via nos dĂ©pendances sur la bibliothĂšque standard ou le code d'autres bibliothĂšques est correctement Ă©crit et encapsulĂ©. L'avantage fondamental de Rust est que le code non sĂ©curisĂ© est entraĂźnĂ© dans des blocs non sĂ©curisĂ©s qui doivent ĂȘtre soigneusement vĂ©rifiĂ©s par leurs auteurs.


En Python, la charge de vérifier la sécurité des manipulations de mémoire incombe uniquement aux développeurs des interprÚtes et aux utilisateurs des interfaces des fonctions externes. En C, ce fardeau incombe à chaque programmeur.


Dans Rust, il appartient aux utilisateurs du mot-clĂ© dangereux. Cela est Ă©vident, car les invariants doivent ĂȘtre maintenus manuellement Ă  l'intĂ©rieur de ce code, et il est donc nĂ©cessaire de rechercher la plus petite quantitĂ© de ce code dans la bibliothĂšque ou le code d'application. L'insĂ©curitĂ© est dĂ©tectĂ©e, mise en Ă©vidence et indiquĂ©e. Par consĂ©quent, si des erreurs de segmentation se produisent dans votre code Rust, vous trouvez une erreur dans le compilateur ou une erreur dans plusieurs lignes de votre code non sĂ©curisĂ©.


Ce n'est pas un systĂšme parfait, mais si vous avez besoin de vitesse, de sĂ©curitĂ© et de multithreading en mĂȘme temps, c'est la seule option.

Source: https://habr.com/ru/post/fr460295/


All Articles