Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels "Was ist Rust unsicher?" Autor Nora Codes.
Ich habe viele Missverständnisse darüber gesehen, was das unsichere Schlüsselwort für die Nützlichkeit und Korrektheit der Rust-Sprache und ihre Förderung als "sichere Systemprogrammiersprache" bedeutet. Die Wahrheit ist leider viel komplizierter als in einem kurzen Tweet beschrieben werden kann. So sehe ich sie.
Im Allgemeinen deaktiviert das unsichere Schlüsselwort nicht das Typsystem, das den Rust-Code korrekt hält . Es ist nur möglich, einige „Superkräfte“ zu verwenden, z. B. Dereferenzierungszeiger. Unsicher wird verwendet, um sichere Abstraktionen basierend auf einer grundlegend unsicheren Welt zu implementieren, sodass die meisten Rust-Codes diese Abstraktionen verwenden und unsicheren Speicherzugriff vermeiden können.
Sicherheitsgarantie
Rost garantiert Sicherheit als eines seiner Grundprinzipien. Wir können sagen, dass dies die Bedeutung der Existenz von Sprache ist. Es bietet jedoch keine Sicherheit im herkömmlichen Sinne während der Programmausführung und der Verwendung des Garbage Collector. Stattdessen verwendet Rust ein sehr fortschrittliches Typsystem, um zu verfolgen, wann und auf welche Werte zugegriffen werden kann. Der Compiler analysiert dann statisch jedes Rust-Programm, um sicherzustellen, dass es immer im richtigen Zustand ist.
Python-Sicherheit
Nehmen wir als Beispiel Python. Reiner Python-Code kann den Speicher nicht beschädigen. Beim Zugriff auf Listenelemente wird überprüft, ob Grenzen überschritten werden. Von Funktionen zurückgegebene Links werden gezählt, um das Auftreten von baumelnden Links zu vermeiden. Es gibt keine Möglichkeit, mit Zeigern willkürlich zu rechnen.
Dies hat zwei Konsequenzen. Erstens müssen viele Typen "speziell" sein. Beispielsweise ist es nicht möglich, eine effektive Liste oder ein Wörterbuch in reinem Python zu implementieren. Stattdessen hat der CPython-Interpreter seine interne Implementierung. Zweitens erfordert der Zugriff auf externe Funktionen (Funktionen, die in Python nicht implementiert sind), die als Schnittstelle einer externen Funktion bezeichnet werden, die Verwendung eines speziellen ctypes-Moduls und verletzt die Sicherheitsgarantien der Sprache.
In gewissem Sinne bedeutet dies, dass alles, was in Python geschrieben ist, keinen sicheren Zugriff auf den Speicher garantiert.
Sicherheit in Rust
Rust bietet auch Sicherheit, aber anstatt unsichere Strukturen in C zu implementieren, bietet es einen Trick: das unsichere Schlüsselwort. Dies bedeutet, dass die grundlegenden Datenstrukturen in Rust wie Vec, VecDeque, BTreeMap und String in Rust implementiert sind.
Sie können fragen: "Aber wenn Rust einen Trick gegen seine Codesicherheitsgarantien bereitstellt und die Standardbibliothek mit diesem Trick implementiert wird, wird dann nicht alles in Rust als unsicher angesehen?"
Mit einem Wort, lieber Leser, ja , genau so, wie es in Python war. Schauen wir uns das genauer an.
Was ist in sicherem Rost verboten?
Sicherheit in Rust ist klar definiert: Wir denken viel darüber nach. Kurz gesagt, sichere Rust-Programme können nicht:
- Dereferenzieren eines Zeigers, der auf einen anderen Typ verweist, als der Compiler kennt . Dies bedeutet, dass es keine Zeiger auf Null gibt (weil sie nirgendwo zeigen), keine Fehler beim Überschreiten von Grenzen und / oder Segmentierungsfehler (Segmentierungsfehler), keine Pufferüberläufe. Es bedeutet aber auch, dass es nach dem Freigeben des Speichers oder dem erneuten Freigeben des Speichers keine Verwendung gibt (da das Freigeben des Speichers als Dereferenzierung des Zeigers angesehen wird) und kein Wortspiel zum Tippen vorgesehen ist .
- Haben Sie mehrere veränderbare Verweise auf ein Objekt oder gleichzeitig veränderbare und unveränderliche Verweise auf ein Objekt . Das heißt, wenn Sie einen veränderlichen Verweis auf ein Objekt haben, können Sie ihn nur haben, und wenn Sie einen unveränderlichen Verweis auf das Objekt haben, ändert sich dieser erst, wenn Sie ihn behalten. Dies bedeutet, dass Sie in Safe Rust kein Datenrennen erzwingen können. Dies ist eine Garantie, die die meisten anderen sicheren Sprachen nicht bieten können.
Rust codiert diese Informationen in einem Typsystem oder verwendet algebraische Datentypen , z. B. Option, um das Vorhandensein / Fehlen eines Werts anzuzeigen, und Ergebnis <T, E>, um Fehler / Erfolg anzuzeigen, oder Referenzen und deren Lebensdauer , z. B. & T vs & mut T, um anzuzeigen ein gemeinsamer (unveränderlicher) Link und ein exklusiver (veränderlicher) Link und & 'a T vs &' b T, um Links zu unterscheiden, die in verschiedenen Kontexten korrekt sind (dies wird normalerweise weggelassen, da der Compiler klug genug ist, es selbst herauszufinden). .
Beispiele
Beispielsweise wird der folgende Code nicht kompiliert, da er einen baumelnden Link enthält. Insbesondere lebt my_struct nicht genug . Mit anderen Worten, die Funktion gibt einen Link zu etwas zurück, das nicht mehr existiert, und daher kann der Compiler dies nicht kompilieren (und weiß sogar nicht einmal, wie).
fn dangling_reference(v: &u64) -> &MyStruct {
Dieser Code macht dasselbe, versucht jedoch, dieses Problem zu umgehen, indem er den Wert auf den Heap legt (Box ist der Name des Basis-Smart-Zeigers in Rust).
fn dangling_heap_reference(v: &u64) -> &Box<MyStruct> { let my_struct = MyStruct { value: v };
Der korrekte Code wird von Box selbst anstelle eines Verweises darauf zurückgegeben. Dies kodiert die Übertragung des Eigentums - die Verantwortung für die Freigabe des Speichers - in der Signatur der Funktion. Beim Betrachten der Signatur wird deutlich, dass der aufrufende Code für das, was mit Box passiert, verantwortlich ist, und der Compiler verarbeitet ihn tatsächlich automatisch.
fn no_dangling_reference(v: &u64) -> Box<MyStruct> { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct);
Einige schlechte Dinge sind in sicherem Rost nicht verboten. Zum Beispiel ist es aus Sicht des Compilers erlaubt:
- Deadlock im Programm verursachen
- eine beliebig große Menge an Speicher verlieren
- Schließen Sie keine Dateigriffe, Datenbankverbindungen oder Raketenschachtabdeckungen
Die Stärke des Rust-Ökosystems besteht darin, dass viele Projekte ein Typsystem verwenden, um sicherzustellen, dass der Code so genau wie möglich ist. Der Compiler erfordert jedoch keinen solchen Zwang, außer in Fällen, in denen ein sicherer Speicherzugriff bereitgestellt wird.
Was ist in unsicherem Rost erlaubt?
Unsicherer Rustcode ist Rustcode mit dem Schlüsselwort unsicher. Unsicher kann auf eine Funktion oder einen Codeblock angewendet werden. Wenn es auf eine Funktion angewendet wird, bedeutet dies, dass "diese Funktion erfordert, dass der aufgerufene Code manuell die Invariante bereitstellt, die normalerweise vom Compiler bereitgestellt wird." Wenn es auf einen Codeblock angewendet wird, bedeutet dies, dass "dieser Codeblock manuell die Invariante bereitstellt, die erforderlich ist, um einen unsicheren Zugriff auf den Speicher zu verhindern, und daher unsichere Dinge tun darf."
Mit anderen Worten bedeutet unsicher für die Funktion "Sie müssen alles überprüfen" und im Codeblock "Ich habe bereits alles überprüft".
Wie in The Rust Programming Language angegeben , kann der Code in einem Block, der mit dem unsicheren Schlüsselwort gekennzeichnet ist, Folgendes tun:
- Dereferenzieren Sie einen Zeiger. Dies ist eine wichtige "Supermacht", mit der Sie doppelt verknüpfte Listen, Hashmaps und andere grundlegende Datenstrukturen implementieren können.
- Rufen Sie eine unsichere Funktion oder Methode auf. Mehr dazu weiter unten.
- Zugriff auf oder Änderung einer veränderlichen statischen Variablen. Statische Variablen, deren Umfang nicht gesteuert wird, können nicht statisch überprüft werden, daher ist ihre Verwendung unsicher.
- Implementieren Sie unsichere Eigenschaften. Unsichere Merkmale werden verwendet, um zu kennzeichnen, ob bestimmte Typen bestimmte Invarianten garantieren. Beispielsweise bestimmen Senden und Synchronisieren, ob ein Typ zwischen Thread-Grenzen gesendet oder von mehreren Threads gleichzeitig verwendet werden kann.
Erinnerst du dich an die hängenden Zeiger oben? Fügen Sie das Wort unsicher hinzu, und der Compiler schwört doppelt so viel, weil er es nicht mag, unsicher dort zu verwenden, wo es nicht benötigt wird.
Stattdessen wird das unsichere Schlüsselwort verwendet, um sichere Abstraktionen basierend auf beliebigen Zeigeroperationen zu implementieren. Zum Beispiel wird der Vec-Typ mit unsicher implementiert, aber es ist sicher, ihn zu verwenden, da er Versuche überprüft, auf Elemente zuzugreifen, und keine Überläufe zulässt. Obwohl Operationen wie set_len bereitgestellt werden, die einen unsicheren Speicherzugriff verursachen können, werden sie als unsicher markiert.
Zum Beispiel könnten wir dasselbe tun wie im Beispiel no_dangling_reference, jedoch mit einer unangemessenen Verwendung von unsicher:
fn manual_heap_reference(v: u64) -> *mut MyStruct { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct);
Beachten Sie das Fehlen des Wortes unsicher. Das Erstellen von Zeigern ist absolut sicher. Wie bereits geschrieben, besteht die Gefahr eines Speicherverlusts, jedoch nicht mehr, und Speicherverluste sind sicher. Das Aufrufen dieser Funktion ist ebenfalls sicher. Unsicher ist nur erforderlich, wenn etwas versucht, einen Zeiger zu dereferenzieren . Als zusätzlichen Bonus gibt die Dereferenzierung automatisch den zugewiesenen Speicher frei.
fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct = unsafe { Box::from_raw(my_pointer) };
Nach der Optimierung entspricht dieser Code der einfachen Rückgabe einer Box. Box ist eine sichere zeigerbasierte Abstraktion, da sie die Verteilung von Zeigern überall verhindert. Zum Beispiel führt die nächste Version von main zu einem doppelten freien Speicher (doppelt frei).
fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct_1 = unsafe { Box::from_raw(my_pointer) };
Was ist also sichere Abstraktion?
Sichere Abstraktion ist eine Abstraktion, die ein Typsystem verwendet, um eine API bereitzustellen, mit der die oben genannten Sicherheitsgarantien nicht verletzt werden können. Box ist sicherer * mut T, da es nicht zu einer doppelten Speicherfreigabe kommen kann, wie oben dargestellt.
Ein weiteres Beispiel ist der Rc-Typ in Rust. Dies ist ein Referenzzählzeiger - eine nicht änderbare Referenz auf Daten auf dem Heap. Da mehrere gleichzeitige Zugriffe auf einen Speicherbereich möglich sind, müssen Änderungen verhindert werden, um als sicher zu gelten.
Darüber hinaus ist es nicht threadsicher. Wenn Sie Thread-Sicherheit benötigen, müssen Sie den Arc-Typ (Atomic Reference Counting) verwenden, der aufgrund der Verwendung von Atomwerten zum Zählen von Links und zur Verhinderung möglicher Datenrennen in Umgebungen mit mehreren Threads einen Leistungsverlust verursacht.
Der Compiler erlaubt Ihnen nicht, Rc dort zu verwenden, wo Sie Arc verwenden sollten, da Ersteller wie Rc es nicht als threadsicher markiert haben. Wenn sie dies tun würden, wäre dies unvernünftig: ein falsches Sicherheitsversprechen.
Wann wird unsicherer Rost benötigt?
Unsicheres Rost ist immer dann erforderlich, wenn eine Operation ausgeführt werden muss, die gegen eine der beiden oben beschriebenen Regeln verstößt. In einer doppelt verknüpften Liste wird beispielsweise durch das Fehlen veränderlicher Verknüpfungen zu denselben Daten (für das nächste Element und das vorherige Element) der Nutzen vollständig beeinträchtigt. Mit unsicher kann ein doppelt verknüpfter Listenimplementierer Code mit * mut Node-Zeigern schreiben und ihn dann in eine sichere Abstraktion kapseln.
Ein weiteres Beispiel ist die Arbeit mit eingebetteten Systemen. Oft verwenden Mikrocontroller einen Satz von Registern, deren Werte durch den physischen Zustand des Geräts bestimmt werden. Die Welt kann nicht aufhören, während Sie & mut u8 aus einem solchen Register entnehmen. Daher ist es unsicher, mit Geräteunterstützungskisten zu arbeiten. In der Regel kapseln solche Kisten den Status in transparenten, sicheren Wrappern, die Daten nach Möglichkeit kopieren, oder verwenden andere Techniken, die Compiler-Garantien bieten.
Manchmal ist es notwendig, einen Vorgang auszuführen, der zum gleichzeitigen Lesen und Schreiben oder zu einem unsicheren Zugriff auf den Speicher führen kann. Hier ist unsicher. Solange jedoch die Möglichkeit besteht, sicherzustellen, dass sichere Invarianten beibehalten werden, bevor ein Benutzer etwas berührt (dh nicht als unsicher markiert), ist alles in Ordnung.
Auf wessen Schultern liegt diese Verantwortung?
Wir kommen zu einer früher gemachten Aussage - ja , die Nützlichkeit von Rust-Code basiert auf unsicherem Code. Trotz der Tatsache, dass dies etwas anders erfolgt als die unsichere Implementierung grundlegender Datenstrukturen in Python, sollte die Implementierung von Vec, Hashmap usw. in gewissem Umfang Zeigermanipulationen verwenden.
Wir sagen, dass Rust sicher ist, mit der Grundannahme, dass der unsichere Code, den wir aufgrund unserer Abhängigkeiten von der Standardbibliothek oder dem Code anderer Bibliotheken verwenden, korrekt geschrieben und gekapselt ist. Der grundlegende Vorteil von Rust besteht darin, dass unsicherer Code in unsichere Blöcke geschrieben wird, die von ihren Autoren sorgfältig geprüft werden müssen.
In Python liegt die Last der Überprüfung der Sicherheit von Speichermanipulationen nur bei den Entwicklern der Interpreter und Benutzern der Schnittstellen externer Funktionen. In C liegt diese Belastung bei jedem Programmierer.
In Rust liegt es bei Benutzern des unsicheren Schlüsselworts. Dies ist offensichtlich, da Invarianten in einem solchen Code manuell verwaltet werden müssen und daher die kleinste Menge dieses Codes in der Bibliothek oder im Anwendungscode angestrebt werden muss. Unsicherheit wird erkannt, hervorgehoben und angezeigt. Wenn daher in Ihrem Rust-Code Segfaults auftreten, finden Sie entweder einen Fehler im Compiler oder einen Fehler in mehreren Zeilen Ihres unsicheren Codes.
Dies ist kein perfektes System, aber wenn Sie gleichzeitig Geschwindigkeit, Sicherheit und Multithreading benötigen, ist dies die einzige Option.