Code-Fehler im Programm führen nicht nur zu späteren Sicherheits-Lücken in der Anwendung, sondern erhöhen die Entwicklungs-Kosten, da ein großer Teil der Entwicklungs-Zeit für die Fehler-Behebung teils schwer zu findender Programm-Fehler investiert werden muss.
Speicher-Fehler sowie Fehler in der parallelen Programmierung werden ermöglicht, wenn die verwendete Programmier-Sprache diesbezüglich zu schwache Restriktionen besitzt. In den weit verbreiteten alten System-Sprachen C und C++ wurde bewusst auf derartige Einschränkungen verzichtet, um eine möglichst vollumfängliche Kontrolle über die Anwendung und den verwendeten Speicher zu erhalten. Zudem soll damit auch die bestmögliche Effizienz bezüglich dem Speicher-Verbrauch und der Laufzeit der Anwendung ermöglicht werden. Andere Programmier-Sprachen wie etwa Java besitzen eine größere Sicherheit und vermeiden Speicher-Fehler zum großen Teil. Dadurch wird jedoch die Laufzeit beeinträchtigt, aber vor allem der Speicher-Verbrauch durch Garbage-Collection stark erhöht. Dies ist für den Großteil der heutigen Anwendungen außreichend, vor allem, da die Hardware seit der Veröffentlichung von C schneller geworden ist und mehr Speicher zur Verfügung stellt. Für Performance-kritische Systeme wie beispielsweise Betriebssysteme, Firmware, Treiber oder eingebettete Systeme wird aber weiterhin C und C++ verwendet, da hier die komplette Kontrolle über den Speicher sowie den Prozessor ermöglicht wird [38].
Rust, eine weitere System-Sprache, welche seit 2010 existiert, versucht, die Schwächen von C und C++ zu beseitigen und dennoch eine etwa gleichbleibende Performance zu C und C++ zu bieten. Dies ist jedoch im Kontrast zu der Annahme, dass Sicherheit, also zusätzliche Überprüfungen, nicht umsonst zu bekommen ist, also immer auch Nachteile wie etwa Performance-Reduzierung mit sich bringt.
Daher ist das Ziel dieses Artikels, die Schwachstellen der Kategorien Memory-Safety, Thread-Safety sowie Type-Safety des Top 25-Artikels des CVE entsprechend zu analysieren. Es wird auf Nachteile aber auch die daraus resultierenden Möglichkeiten von C und C++ bezüglich der jeweiligen Schwachstelle eingegangen, aber auch ein Lösungs-Konzept in Rust für das entsprechende Problem beschrieben. Dabei wird unter anderem auf Performance und Speicher-Verbrauch geachtet, um herauszufinden, ob Rust die lang etablierten System-Sprachen in diesem Bereich in Zukunft ersetzen könnte.
In folgender Tabelle ist eine Übersicht der für diesen Artikel relevanten Schwachstellen, sortiert nach den Sicherheits-Bereichen, gegeben. Die Schwachstellen ‘Out-Of-Bounds-Read‘ und ‘-Write’ sowie ‘Improper Restriction of Operations within the Bounds of a Memory Buffer‘ werden zusammen in einem Kapitel behandelt, da diese größtenteils thematisch übereinstimmen.
Bereich | Platzierung | Schwachstelle | Name |
---|---|---|---|
Memory-Safety | 1 | CWE-787 | Out-of-Bounds Write |
Memory-Safety | 3 | CWE-125 | Out-of-bounds Read |
Memory-Safety | 17 | CWE-119 | Improper Restriction of Operations within the Bounds of a Memory Buffer |
Memory-Safety | 7 | CWE-416 | Use After Free (Dangling Pointer) |
Memory-Safety | 15 | CWE-476 | Null-Pointer Dereferenzierung |
Memory-Safety | 32 | CWE-401 | Missing Release of Memory after Effective Lifetime (Memory Leak) |
Memory-Safety | 12 | CWE-190 | Integer Overflow or Wraparound |
Thread-Safety | 33 | CWE-362 | Concurrent Execution using Shared Resource with Improper Synchronization (‘Race Condition’) |
Type-Safety | 36 | CWE-843 | Access of Resource Using Incompatible Type (‘Type Confusion’) |
Rust
Im folgenden Kapitel wird zunächst auf drei wichtige Konzepte in Rust eingegangen, auf welche in den darauffolgenden Kapiteln immer wieder Bezug genommen wird.
Ownership-Modell
Um auf Lösungen seitens von Rust bezüglich der Schwachstellen Bezug zu nehmen, wird zunächst ein wichtiges Konzept in Rust beschrieben. Dieses ermöglicht Memory-Safety ohne Garbage-Collection, was die Stabilität von Anwendungen mit gleichbleibender Performance bezogen auf C/C++ ermöglicht. Dies wird durch eine Prüfung zur Compile-Zeit erreicht.
Das Konzept dient als Zugangs-Kontrolle, um zu überprüfen, dass Code nicht auf beliebige Daten, sondern nur auf für ihn erlaubte Daten Zugriff hat. Das Ownership-Prinzip besagt, dass immer nur eine Variable gleichzeitig, eine bestimmte Ressource besitzen kann. Andere Variablen können diese Ressource ausleihen, um ebenfalls Zugriff zu erhalten. Dieses Prinzip wird Borrowing genannt. Wird eine Ressource neu erzeugt, so besitzt die Variable, der die Ressource zugewiesen wird, zunächst die Verantwortung darüber. Der Besitzer einer Ressource kann gewechselt werden, indem die Ressource über eine Move-Operation, ähnlich wie in C++, einer anderen Variable, per Parameter zugewiesen oder als Rückgabe-Wert dem Aufrufer übergeben wird [19].
Die Ressource ist an die verantwortliche Variable gebunden. Sollte die Variable zerstört werden, wird die Ressource automatisch mit zerstört beziehungsweise freigegeben. Zusätzlich kann über die besitzende Variable die Ressource verändert werden [19].
Borrowing (Referenzen)
Beim Borrowing wird eine Referenz auf die Ressource übergeben, sodass eine andere Software-Komponente darauf zugreifen kann, ohne dass die Verantwortung über die Ressource wechselt. Es wird dabei vom Compiler garantiert, dass die Referenz nicht über die Lebenszeit der Ressource hinaus existiert. Sollten Referenz-Variablen länger existieren, wird ein Compiler-Fehler geworfen [19].
Es gibt zwei Arten von Referenzen, welche jeweils eigene Restriktionen und Möglichkeiten bieten. Immutable, also nicht veränderbare Referenzen auf eine Ressource können beliebig oft im Programm existieren, da hier keine der Zugriffe, die Ressource verändern darf. Damit ist gewährleistet, dass alle Software-Komponenten mit der selben stabilen Version der Ressource arbeiten können, ohne dass sie sich zwischendurch verändert. Solange eine immutable-Referenz existiert, darf die Ressource nicht geändert werden [19].
Die andere Referenz-Art ist die veränderbare (mutable) Referenz. Existiert eine veränderbare Referenz, so darf es keine weitere mutable oder immutable Referenz geben. So wird garantiert, dass Software-Komponenten, welche eine unveränderbare Referenz besitzen, davon aus gehen können, dass sich die Ressource während der Lebenszeit der Referenz nicht ändert [19].
Clone / Copy
Rust führt die Move-Operation bei Zuweisungen oder Parameter-Übergaben automatisch aus, anders als in C++, wo das explizit geschrieben werden muss. Um jedoch einen Wert in Rust zu kopieren, muss eine spezielle Kopier-Funktion (.clone()) auf der Ressource aufgerufen werden [40][21].
Beispiele
Ein Beispiel, wie das Ownership-Modell mögliche Speicher-Fehler verhindern kann, ist wie folgt gegeben.
|
Wenn die Main-Funktion ausgeführt wird, wird zunächst die ‘fct1‘-Funktion aufgerufen. Diese erzeugt ein Array und befüllt es mit einem Element. Damit ist die Variable ‘vec’ zunächst der Besitzer. Das Array wird am Schluss als Rückgabe-Wert zurückgegeben. Da es sich um eine Move-Operation handelt, wird die Verantwortung über das Array an die aufrufende Funktion übergeben. In der Main-Funktion wird anschließend eine immutable Referenz an ‘fct2‘ übergeben. Ist ‘fct2‘ fertig abgearbeitet, wird der Kontext aufgeräumt und die Referenz auf die Ressource wieder zerstört. Am Schluss wird der dritten Funktion ‘fct3‘ das Array per Move-Operation als Parameter übergeben, sodass nun der Besitzer der Ressource zu ‘fct3‘ wechselt. Kehrt ‘fct3‘ nun wieder zum Aufrufer zurück, so wird das Array automatisch zerstört und beanspruchter Speicher wieder frei gegeben. Der Entwickler muss sich nun keine Gedanken mehr darüber machen, welche Software-Komponente nun für das Freigeben von Speicher zuständig ist, oder ob noch Referenzen auf die Ressource existieren. All diese Prüfungen werden zur Compilezeit durchgeführt, sodass die Performance kaum betroffen ist. [19]
Folgendes Beispiel soll die Bedeutung von ‘clone()‘, also das Kopieren einer Ressource, verdeutlichen.
let v1 = vec![1,2,3];<br>let v2 = v1.clone(); // copy<br>let v3 = v1; // move<br>let v4 = v1; // compiler error |
Zunächst wird wieder ein Array erzeugt welches anschließend über ‘clone()‘ kopiert wird. Dabei verbleibt die Verantwortung über die alte Ressource bei ‘v1‘ und ‘v2‘ wird der Besitzer der neu erstellten Ressource. Danach wird jedoch die Verantwortung der zuerst erstellten Ressource per Move-Operation an ‘v3‘ übergeben, wodurch ‘v1‘ invalide wird. Da ‘v1‘ invalide ist, gibt es auch keine Verantwortung mehr, welche der Variablen ‘v4‘ übergeben werden könnte. Daher erzeugt der Rust-Compiler hier einen Fehler.
Rust-Pointer
Pointer, wie sie in C oder C++ existieren, sind in Rust nur unter Einschränkungen nutzbar. Dies muss zusätzlich dem Rust-Compiler explizit mitgeteilt werden, indem der unsichere Modus von Rust genutzt wird, was im folgenden Abschnitt näher erläutert wird.
Für gewöhnlich werden aber Rust-Pointer wie Box<T> verwendet, um eine Adresse auf eine Ressource zu verwalten. Hierbei dient Box<T> als Wrapper-Objekt für die darunter liegende Speicher-Adresse. Wird der Pointer durch das Verlassen des Kontext zerstört, so wird die Ressource, auf die der Pointer zeigt, automatisch frei gegeben [25].
Safe-Rust und Unsafe-Rust
Rust besitzt einen sehr konservativen Compiler. Dies bedeutet, dass er auch funktionierenden Code nicht kompiliert, wenn dieser in einer anderen Anwendungs-Umgebung Fehler verursachen würde. Zusätzlich ist der Entwickler an das Schreiben sicheren Codes gebunden, was bei Low-Level-Programmierung teilweise nicht möglich ist, wenn mit der Hardware oder dem Betriebssystem kommuniziert werden soll, oder eigene Bibliothekserweiterungen geschrieben werden sollen [39].
Hierfür bietet Rust eine Möglichkeit, unter gewissen Einschränkungen auch ausgewählte unsichere Aktionen wie C-ähnliche Pointer zu verwenden, um zum Beispiel mit C oder dem dem Betriebssystem zu kommunizieren. Diese Möglichkeit heißt Unsafe-Rust und muss bei Nutzung dem Rust-Compiler explizit mitgeteilt werden [39].
Memory-Safety
Memory-Safety ist die Eigenschaft einer Programmiersprache oder Anwendung, keine Sicherheits-Schwachstellen oder andere Programm-Fehler in Bezug auf Speicher-Zugriffe zu ermöglichen. [15]
C und C++ sind in diesem Fall klar Memory-Unsafe, da die Sprachen Pointer intern als ganz normale Integer behandeln. Dies bedeutet, dass als Speicher-Adresse ein beliebiger Wert angegeben werden kann, auf welchen versucht werden könnte, zuzugreifen. Zusätzlich kann durch Pointer-Arithmetik beliebig mit den Adressen gerechnet werden. Das Objekt-Layout von Strukturen kann Byte für Byte gelesen und beschrieben werden, was beliebige Möglichkeiten der Programmierung mit sich bring, jedoch auch viel Platz für Speicher-Fehler bietet.
CWE-787/125: Out-Of-Bounds-Read / -Write | CWE-119 Improper Restriction of Operations within the Bounds of a Memory Buffer
Eine auch als Buffer-Overflow oder Buffer-Overrun bzw. Buffer-Underflow bezeichnete Schwachstelle, bei der es möglich ist, Speicher-Zugriffe außerhalb des im Kontext vorgesehenen Speicher-Bereichs zu tätigen [1][2][3][7]. In Tabelle 2 ist ein Beispiel gegeben.
Die Variable ‘arr‘ ist dabei wie folgt definiert: int arr[7];
Typ | Array-Syntax (C/C++) | Pointer-Syntax (C/C++) | Assembly (MASM) |
---|---|---|---|
Out-Of-Bounds-Read |
|
|
|
Out-Of-Bounds-Write |
|
|
|
In obigem Beispiel ist gut ersichtlich, dass C/C++ bei Array-Operationen Pointer-Arithmetik zum Berechnen der Speicher-Adresse verwendet. Damit existiert keine Überprüfung des Index zur Laufzeit. Da Adressen auch subtrahiert werden können, ist daher ein negativer Index in C/C++ erlaubt.
Verhindert werden kann der Speicher-Fehler durch ‘Bounds-Checking‘, also dass Überprüfen des Index bzw. der Position, an der im Speicher zugegriffen werden soll. Liegt die Position in einem validen Speicher-Bereich, kann im Programm-Code weiter fortgefahren werden, ansonsten sollte die Anwendung angehalten werden, um dem Entwickler die Möglichkeit zu geben, den Fehler schnellstmöglich zu finden, sowie weiteren Schaden für die Anwendung zu vermeiden [2][3][7].
Ein möglicher ‘Bounds-Check‘ könnte dabei wie folgt aussehen:
|
Dies kann teilweise zur Compilezeit geschehen, wenn die größe des Arrays bereits bekannt ist, teilweise aber auch erst zur Laufzeit durchgeführt werden, wenn Speicher dynamisch zur Laufzeit allokiert wird. Um die Performance nicht allzu stark zu beeinträchtigen, werden in der STL (Standard Template Library) von C++ oft zwei Varianten einer Funktion angeboten, eine mit und eine ohne ‘Bounds-Checking‘. Damit braucht nur dann getestet werden, wenn die Eingaben nicht vorher bestimmbar sind, wie die eines Benutzers. Ein Beispiel für verschiedene Funktions-Versionen sind hierbei ‘std::vector::at()’ für die sichere, sowie ‘std::vector::operator[]()’ für die unsichere Variante eines Array-Zugriffs [26]. Dies könnte aber weiterhin durch ein Makro ergänzt werden, welches eine Typprüfung ausschließlich im Debug-Modus der Anwendung durchführt und im Release-Modus die Überprüfung weglässt. Bei externen Eingaben muss aber nachwievor eine Eingabe-Validierung stattfinden.
Rust-Lösung
Rust bietet im sicheren Modus mit Pointer-Wrappern wie ‘Box<T>‘ keine Möglichkeit für Pointer-Arithmetik. Box<T> verhält sich im Grunde ähnlich wie eine Referenz-Variable in Java, da die Speicher-Adresse fix ist [25]. Datenstrukturen wie der Vector der Standard-Bibliothek von Rust, welche Low-Level-Speicher-Verwaltung betreiben, führen Bounds-Checking zur Laufzeit aus. Eine zweite Variante ohne Bounds-Checking existiert nicht, weil dies nicht sicher wäre [27].
CWE-416: Use After Free (Dangling Pointer)
Bei ‘Use-After-Free‘ handelt es sich um einen Speicher-Fehler, bei welchem versucht wird, auf einen bereits freigegebenen Speicher-Bereich zuzugreifen [4]. Dies kann sowohl auf dem Stack, als auch auf dem Heap passieren, wie folgendes Beispiel zeigt:
Speicher-Art | Code (C/C++) | Beschreibung |
---|---|---|
Stack |
| Rückgabe eines Pointers, welcher auf Stack-Speicher zeigt. Speicher wird automatisch bei Beendigung der Funktion frei gegeben. |
Heap |
| Manuelle Speicher-Verwaltung: Anfrage und Freigabe des Speichers liegt in der Verantwortung des Programmierers. |
Vor allem in der manuellen Speicher-Verwaltung bezüglich dem Heap ist der Hauptgrund für diese Schwachstelle, dass es keine klare Rollenverteilung beziehungsweise Zuständigkeit gibt, die durch den Compiler geprüft werden kann. Daher ist es die Aufgabe des Entwicklers, welche Komponenten für welchen Speicher-Bereich verantwortlich sind, diesen also freigeben dürfen, was zu Speicher-Fehlern führen kann.
Die Folgen belaufen sich bestenfalls auf einen Absturz der Anwendung durch eine Zugriffs-Verweigerung (Access-Violation) hin. Anderenfalls können sensible Daten wie Zugangs-Codes ausgelesen oder geschrieben werden, wenn sich die Daten an dieser Stelle noch im Speicher befinden, sowie Fremd-Code angesprungen und ausgeführt werden. Das Manipulieren bereits freigegebener Speicher-Bereiche kann beliebige andere Software-Komponenten beeinflussen und führt daher zu undefiniertem Verhalten der Anwendung und daher auch teilweise zu Fehlern, welche nur schwer reproduzierbar sind [4].
Die einfachste Lösung für dieses Problem wäre, den Pointer nach der Freigabe auf Null zu setzen, um per Prüfung zu erkennen, ob ein Pointer noch auf validen Speicher zeigt. Sollte ein Null-Pointer dereferenziert werden, wird eine Null-Pointer-Exception geworfen und die Anwendung beendet, was zu einer schnellen Erkennung des Fehler führt [4]. Diese Lösung liegt jedoch wieder in der Verantwortung des Entwicklers und kann daher vergessen werden. Desweiteren handelt es sich hier um eine weitere Zuweisung (p = nullptr;), welche evtl. auf manchen Systemen zu einer schlechteren Performance führen könnte.
Rust-Lösung
C++ | Rust |
---|---|
|
|
In obigem Beispiel wird der Variablen ‘str’ eine Referenz auf das erste Feld im Array zugewiesen. Darüber wird am Schluss versucht, auf das erste Feld zuzugreifen, nachdem das Array verändert wurde. Hierbei kann es passieren, dass die Referenz ungültig wird und auf einen invaliden Speicher-Bereich zeigt. Dies ist dann der Fall, wenn im Array nicht genügent Speicher reserviert ist, um das nueue Element ‘world’ einzufügen. Dadurch muss ein neuer Speicher-Bereich allokiert werden, in welchen die alten Elemente und das Neue hinein kopiert werden. Der alte Speicher-Bereich wird anschließend frei gegeben. Die Referenz zeigt aber noch auf den frei gegebenen Speicher-Bereich. Wird die Referenz nun verwendet, besitzt diese Anwendung eine Use-After-Free-Schwachstelle mit den entsprechenden möglichen Folgen. [21]
Rust kann durch das Ownership-Modell unter anderem Use-After-Free-Schwachstellen vermeiden, wie der Rust-Code im obigen Beispiel zeigt. Die Referenz ‘str‘ ist immutable. Wird die Ressource vom Inhaber der Ressource verändert, darf auf die immutable Referenz nicht mehr zugegriffen werden. Dies wird zur Compilezeit gewährleistet. Dadurch verursacht obiger Code von Rust einen Compiler-Fehler, sodass ein möglicher Speicher-Fehler von vornherein verhindert werden kann. Anzumerken ist noch, dass dieser Code in Rust generell nicht erlaubt ist, auch wenn der Entwickler garantieren kann, dass in dieser Situation genügend Speicher im Array vorhanden ist. Dadurch muss der Code an manchen Stellen evtl. umständlicher geschrieben werden, verhindert aber Speicher-Fehler, ohne großartigen Performance-Verlust. [21]
Generell können die sicheren Rust-Pointer nicht auf invalide Speicher-Bereiche zeigen. Darüber hinaus entspricht ‘Box<T>‘ einem Pointer, welcher ausschließlich auf Speicher im Heap zeigt. Daher sind im abgesicherten Modus von Rust, Pointer auf lokale Variablen nicht möglich [25]. Eine mögliche Lösung für Use-After-Free auf dem Stack ist daher wie folgt gegeben.
|
In obigem Beispiel wird im Heap speicher allokiert und mit dem Wert 4 der Variablen ‘x’ beschrieben. Anschließend wird der Pointer per Move-Operation der aufrufenden Routine übergeben.
CWE-476: Null-Pointer Dereferenzierung
Hierbei handelt es sich um das Dereferenzieren eines Null-Pointers, was zum Absturz der Anwendung führt. Sollte der Prozess jedoch Rechte besitzen, auf die Speicher-Adresse 0 zuzugreifen, könnte auch hier externe Code-Ausführung möglich sein. [6] Die Schwachstelle wird durch fehlende Abfragen beziehungsweise Überprüfungen auf Null hervorgerufen, zum Beispiel, wenn Funktionen Null als Rückgabewert für einen Fehler zurück geben. Null-Pointer-Dereferenzierung könnte aber auch durch konkurierende Threads entstehen. [6] Folgendes Beispiel, veranschaulicht die Schwachstelle:
Code (C/C++) | Beschreibung |
---|---|
| Sollte die Adresse keinem Host zugeordnet werden können, wird ein Null-Pointer bei ‘getHostAddr()’ zurück gegeben, was zu einer Null-Pointer-Dereferenzierung bei ‘pHost->m_name’ führt. |
Als Lösung in C/C++ sollte der Pointer vor der Verwendung auf Null geprüft werden und ggf. bei mehreren Threads der Zugriff zusätzlich gelockt werden. Der Compiler kann den Programmierer hierbei auch verpflichten, uninitialisierte Pointer vor der ersten Verwendung zu initialisieren, jedoch nur wenn diese im selben Kontext definiert sind. [6]
Rust-Lösung
Die sicheren Pointer von Rust wie ‘Box<T>’ können keinen Null-Wert besitzen. Eine Null-Pointer-Dereferenzierung ist dadurch ausgeschlossen. In Rust ist gewährleistet, das sichere Pointer auf einen validen Speicher-Bereich zeigen [25]. Ein Beispiel für Null-ähnliche Verarbeitung ist in der Rust-Lösung im nachsten Abschnitt gegeben.
CWE-401: Missing Release of Memory after Effective Lifetime
Als Memory-Leak wird eine Schwachstelle bezeichnet, bei welcher eine Anwendung Speicher belegt, welcher für sie aber nicht erreichbar ist, da kein Pointer darauf zeigt. Dies kann bei der manuellen Speicherverwaltung im Heap passieren, wenn es versäumt wurde den nicht mehr benötigten Speicher frei zu geben. Auch hier kann die unklare Zuständigkeit der Software-Komponenten für die jeweiligen Speicher-Bereiche eine Rolle spielen.[8] Folgendes Beispiel soll die Schwachstelle veranschaulichen:
Code (C/C++) | Beschreibung |
---|---|
| Sollte eine Bedingung ‘bCond’ fehlschlagen, wird direkt zurück gekehrt, ohne den Speicher davor wieder frei zu geben. |
Die Folgen dieser Schwachstelle sind das Verbrauchen von immer mehr Speicher. Dies führt irgendwann dazu, dass immer öffters Speicher-Bereiche auf die Festplatte zurück und neueSpeicher-Bereiche wieder herein geladen werden müssen (Trashing). Hierbei erhöhen sich die Zugriffszeiten auf den Speicher, wobei die Anwendung auch einfrieren kann. Sollte nicht mehr genügen Speicher für die Anwendung verfügbar sein, wird diese in der Regel vom Betriebssystem automatisch beendet. [8]
Das Verwenden einer automatischen Speicher-Verwaltung wie etwa Garbage-Collection ist eine mögliche Lösung für das Problem. [8] Der große Zusatz-Speicher, der dabei aber benötigt wird, ist für die System-Programmierung für SPeicher-sensible Anwendungen keine Option. Daher werden nur einzelne Bestandteile der automatischen Speicher-Verwaltung in C/C++ verwendet, wie etwa dem Reference-Counting. [8]
Beim Reference-Counting werden die Referenzen auf Speicher-Bereiche gezählt, um zu erkennen, wann ein Speicher-Bereich nicht mehr referenziert wird und daher wieder frei gegeben werden kann. Dies wird normalerweise mit Objekten erreicht, welche sich nach außen hin wie ein normaler Pointer verhalten, jedoch intern den Zähler des referenzierten Objektes inkrementieren, wenn die Referenz zugewiesen wurde, und wieder dekrementieren, wenn die Referenz verloren geht. Dies erhöht zwar auch den Speicher-Verbrauch und die Laufzeit, aber deutlich geringer, als bei der vollständigen Garbage-Collection. Beispiele hierfür sind die Smart-Pointer in C++ wie ‘std::shared_ptr’, und ‘std::unique_ptr’.
Rust-Lösung
Aber auch Rust besitzt als mögliche Technik, ‘Reference-Counting’ mittels Objekten wie ‘Rc<T>‘ [22]. Sollte aber klar sein, dass einer Ressource immer nur eine verantwortliche Variable zugeteilt sein muss, kann auch ein einzelner Pointer wie ‘Box<T>‘ verwendet werden, um Memory-Leaks zu verhindern. Rust kennt für sicheren Programmcode keine Null-Pointer. Um ein ähnliches Verhalten wie in C/C++ zu erhalten, muss ‘Option<T>‘ verwendet werden. ‘Option<T>‘ ist ein Datentyp, welcher einen Wert von None, oder einen Wert von Some(x) annehmen kann. ‘x’ muss hierbei vom Typ T sein. Der Wert kann über eine Switch-Kontrollstruktur abgefragt werden [24]. Folgendes Beispiel gibt eine Lösung für obigen C/C++-Code in Rust mittels eines Box<T>-Objekts sowie dem ‘Option<T>‘-Wert.
|
In obigem Rust-Code wird die Funktion ‘fct‘ aufgerufen. Sollte in der Funktion ‘fct‘ der Wert ‘None‘ zurückgegeben werden, wird durch das Ownership-Prinzip von Rust, der Pointer ‘Box<i8>‘ automatisch zerstört, wodurch der Speicher-Bereich, auf den der Pointer zeigt, frei gegeben wird. Sollte jedoch ‘Some(x)‘ zurück gegeben werden, wird die Verantwortung über den ‘Box<i8>‘-Wert an die aufrufende Funktion ‘main‘ übergeben. Der Rückgabe-Wert kann dann über ‘match‘, was in etwa einem ‘switch’ in C/C++ entspricht, abgefragt werden. Bei ‘Some(x)‘ kann über ‘x‘ auf den ‘Box<i8>‘-Wert zugegriffen werden.
Optimierung: Null Pointer Optimization
Bei bestimmten Datentypen wie zum Beispiel ‘Box<T>‘ kann Rust eine Optimierung bezüglich der Speicher-Größe des ‘Option<T>‘-Wertes durchführen. Hierbei wird garantiert, dass die Größe von ‘Option<T>‘ dem enthaltenen Datentyp ‘T’ entspricht. Damit gibt es bezüglich der Größe keinen Overhead [24].
CWE-190: Integer Overflow or Wraparound
Beim Integer-Überlauf handelt es sich um einen normalen Zahlen-Überlauf aufgrund begrenzter Speicher-Kapazitäten im Registern der CPU. Er entsteht, wenn der Ergebnisdatentyp zu klein für das Ergebnis der Berechnung oder einer Zuweisung ist und das Ergebnis somit abgeschnitten werden muss, um noch in den Speicher zu passen [28][5]. Der Integer-Überlauf ist in C/C++ besonders gefährlich, da im Standard die genaue Größe von Daten-Typen nicht festgelegt ist, also je nach eingesetztem Compiler unterschiedlich sein kann. [29] Mögliche Folgen können Out-Of-Bounds-Zugriffe sowie Endlosschleifen, aber auch unerwartetes System-Verhalten sein, bis hin zum Absturz der Anwendung [5]. Folgendes Beispiel soll das unbeabsichtigte Verfälschen von Werten durch Code visualisieren:
Code (C/C++) | Beschreibung |
---|---|
| Wenn die Summe ‘currAmount’ größer als der für ‘unsigned int’ mögliche maximale Wert ist, entsteht ein falsches Ergebnis. Sollte ‘currAmount’ größer als für ‘unsigned short’ möglich sein, wird das Ergebnis bei der Parameter-Übergabe teilweise abgeschnitten. |
Das auch solche Fehler zu schwerwiegenden Katastrophen führen können, zeigt das Unglück der Ariane V88-Rakete, welche im Jahr 1996 kurz nach dem Start durch einen arithmetischen Überlauf in einem Steuermodul die Flugbahn verließ und zerstört werden musste. [16]
Zusätzlich können Folge-Schwachstellen, wie etwa Out-Of-Bounds durch oben genannte Gegenmaßnahmen entdeckt und somit Rückschlüsse auf die eigentliche Ursache, den Integer-Overflow, gezogen werden.
Microsoft bietet für C++ die SafeInt-Bibliothek an, mit welcher Ganzzahl-Überläufe bei arithmetischen Operationen oder Castings erkannt werden können. Hierbei können einzelne Funktionen, aber auch Objekte verwendet werden, welche die primitiven Datentypen kapseln. [17][5]
Rust-Lösung
Rust verhält sich hier je nach Compiler-Einstellung anders. Wird das Projekt als Debug-Version erstellt, werden für die primitiven Datentypen zusätzliche Prüfungen bei arithmetischen Operationen eingebaut. Tritt ein arithmetischer Überlauf auf, so wird dies erkannt und die Anwendung mit einem Fehler beendet. Im Release-Modus werden keine zusätzlichen Prüfungen eingebaut, sodass Zahlen-Überläufe zur Laufzeit unbemerkt bleiben können, aber eine bessere Performance erzielt wird [23].
Sollten dennoch arithmetische Operationen auf Zahlen-Überläufe geprüft werden, so existieren wie bei der SafeInt-Bibliothek auch, diverse Funktionen wie zum Beispiel ‘overflowing_add’, um für einzelne Berechnungen eine Prüfung auf Integer-Overflows zu ermöglichen. Hierbei wird ein zusätzlicher boolscher Wert zurüc gegeben, welcher angibt, ob es einen Overflow gab [23].
Rust ist daher sehr ähnlich zu den herkömmlichen Sprachen und ignoriert standardmäßig das Überlaufen von Zahlen-Werten zur Laufzeit im Release-Modus.
Thread-Safety
Eine Programm-Komponente oder ein Programm werden als Thread-Safe bezeichnet, wenn diese von mehreren Threads parallel ausgeführt werden können, ohne das erwartete Laufzeit-Verhalten der Anwendung zu verändern, also keine Race-Condition besitzen. Unter anderem soll die Anwendung auch keine Deadlocks enthalten, was teilweise durch Callback-Funktionen aber nicht ganz auszuschließen ist. [18]
CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (‘Race Condition’)
Hierbei handelt es sich um die typische Race-Condition. Eine Race-Condition liegt vor, wenn mehrere Threads einen nicht synchronisierten geteilten Speicher-Bereich beschreiben wollen. Dies führt zu unerwartetem und vor allem undefiniertem Verhalten, dessen Fehlerquelle teilweise sehr schwer zu finden ist, da der Fehler schlecht reproduzierbar ist. Weitere Folgen können das mehrfache Allokieren von Speicher sein, was zu Folgen des Memory-Leaks weiter oben im Artikel führen würde. [9]
Eine einfache Race Condition könnte das Inkrementieren einer globalen Variable x sein, da es sich hierbei um keine atomare Operation handelt, also aus einzelnen Teilschritten besteht, welche unterbrochen werden können, wie in folgendem Beispiel gezeigt [30].
C++ | MASM | MASM |
---|---|---|
|
|
|
Der Ablauf einer möglichen Race-Condition mit dieser Operation ist wie folgt gegeben. T1 und T2 sind zwei Threads. Die Aktion T1-1 bedeutet, dass Therad 1 die Aktion 1 ausführt, bezogen auf die drei Assembly-Befehle in obiger Tabelle.
Aktion | T1 | T2 | Speicher |
---|---|---|---|
– | – | – | 7 |
T1-1 | 7 | – | 7 |
T2-1 | 7 | 7 | 7 |
T2-2 | 7 | 8 | 7 |
T2-3 | 7 | 8 | 8 |
T1-2 | 8 | 8 | 8 |
T1-3 | 8 | 8 | 8 |
In obigem Beispiel ist zu sehen, dass die letzte Zeile der Tabelle keine Wirkung hat, da der erste Thread noch auf einem veralteten Zustand der globalen Variablen gearbeitet hat.
C und C++ sowie Java und andere Sprachen unterstützen verschiedene Synchronisations-Mechanismen, um einen exklusiven Zugriff auf geteilte Ressourcen in einer parallel ausführbaren Umgebung zu ermöglichen. Atomare Operationen sind hierbei die kleinste Synchronisations-Primitive, da es sich hierbei um Synchronisation auf Instruktions-Level handelt [30]. Aus atomaren Instruktionen können Locking-Mechanismen gebaut werden, welche größere Code-Bereiche für nur einen Thread parallel ausführbar machen können, um den Synchronisations-Aufwand zu reduzieren [31]. Mutexes [32] und Semaphoren [33] bieten als Synchronisations-Objekte eine angenehme Anwendung diverser Patterns aus der parallelen Programmierung wie etwa ‘Producer/Consumer’ [34].
Aber keine der Sprachen bis auf Rust hatte bisher Mechanismen in der Sprache eingebunden, um eine Thread-sichere Entwicklung zu gewährleisten. Das korrekte Locking beziehungsweise Synchronisieren in C/C++ oder Java ist Aufgabe des Entwicklers. Zum Beispiel ist nicht gewährleistet, dass die geteilte Ressource nicht von einer anderen Stelle der Anwendung manipuliert werden kann.
Mutexes und Locking in Rust
Dem Versucht Rust entgegenzuwirken, indem in Rust ein Mutex immer an die zu schützende Ressource gekoppelt ist. Wird versucht, auf die Ressource zuzugreifen, muss die Ressource über das Mutex angefragt werden. Es wird also garantiert, dass diese Ressource nie von zwei Threads parallel zugegriffen werden kann [19].
Ein weiterer Mechanismus von Rust ist das MutexGuard<T>-Objekt, welches bei der Anfrage des Mutex nach dem Lock zurück gegeben wird. Hierbei handelt es sich um ein Wrapper-Objekt ähnlich einem Scoped-Lock. Wenn das Objekt zerstört, also der Kontext oder die Funktion verlassen wird, wird die Ressource wieder über das Mutex frei gegeben. Damit werden Deadlocks durch ein fehlendes unlock() vermieden. Über das MutexGuard-Objekt kann schließlich auf die Ressource zugegriffen werden, wobei vom Compiler zur Compile-Zeit garantiert wird, dass die Referenz auf die Ressource nicht über die Lebenszeit des MutexGuard hinaus überlebt. Dadurch werden zum Beispiel Fehler wie der Zugriff auf die Ressource ohne Locking über die Referenz vermieden [19]. Folgende Tabelle zeigt die dafür nötigen Funktionen in Rust, um eine Ressource über das Mutex anzufragen.
Code (Rust) | Beschreibung |
---|---|
| Mutex-Erzeugung |
| Lock-Anfrage |
| Ressourcen-Abfrage |
Ein entsprechender Beispielcode für die Abfrage einer Ressource über das Mutex-Objekt ist wie folgt gegeben:
|
Thread-Spawning
Beim Erstellen von Threads bietet Rust weitere Sicherheiten, wie etwa den JoinGuard<T>. Dieses Objekt verhält sich ähnlich wie der Scoped-Lock, ruft aber join(), anstatt unlock() im Destruktor auf, und auch nur wenn join() nicht bereits aufgerufen wurde. Damit können Referenzen auf Speicher-Inhalte des Stacks, welche den entsprechenden Threads bei der Erzeugung mitgegeben wurden, valide gehalten werden. Beim Verlassen des Kontext wird zunächst auf alle erstellten Threads des Kontext gewartet und anschließend der Stack-Speicher frei gegeben. [19]
In folgendem Beispiel wird ein dynamisches Array (Vector) auf dem Stack erzeugt und bei der Erzeugung des Threads eine Referenz darauf übergeben.
|
Soll nicht auf erzeugte Threads gewartet, also die Funktion direkt beendet werden, kann auch eine statische Prüfung zur Compile-Zeit stattfinden. Hier ist es nun nicht mehr erlaubt, Referenzen auf dem Stack-Speicher dem Thread bei der Erzeugung mitzugeben [19].
Folgendes Beispiel würde nicht kompilieren und entspricht obigem Beispiel mit einer anderen Erzeugungs-Funktion ‘spawn’, welche die statische Prüfung ermöglicht.
|
Send und !Send
Rust unterscheidet beim Multi.Threading zwei unterschiedliche Typen: Send und !Send. ‘Send‘ sind alle Objekte beziehungsweise Datenstrukturen, aber auch Funktionen, welche Thread-safe sind, also auch von mehreren Threads parallel ausgeführt werden können, ohne dass sich der Programmierer Gedanken machen muss. ‘!Send‘ hingegen werden alle Programm-Module genannt, welche nicht Thread-safe sind. Durch diese Unterscheidung kann nun eingeschränkt werden, welche Objekte oder Typen an welchen Bereichen der Anwendung eingesetzt werden können. [19]
Zum Beispiel erlaubt die Funktion zur Erzeugung eines Mutex im obigen Beispiel ‘ fn mutex<T: Send>(t: T) -> Mutex<T>; ‘ nur Daten-Typen, welche Thread-safe sind, um den exklusiven Zugriff auf die Ressource durch das Mutex gewährleisten zu können.
Type-Safety
Typ-Sicherheit bedeutet, dass alle Typ-Verletzungen erkannt, also alle Daten-Typen gemäß ihrer Definition korrekt verwendet werden. Datentypen werden hierbei meist vom Compiler statisch zur Compile-Zeit, aber auch dynamisch vom Interpreter oder der Laufzeitumgebung mittels eines Typsystems geprüft. [20]
Statische Typprüfung führt in der Regel zu einer schnelleren Ausführung des Programmcodes, da nicht zur Laufzeit erst der entsprechende Typ geprüft werden muss. Zusätzlich wird die Code-Qualität und die Zuverlässigkeit der Anwendung erhöht, da im Vornherein Programmier-Fehler durch den Compiler vermieden werden können. Zusätzlich kann der Code leichter Refactored werden, wenn die Sprache strenger typisiert ist. [20]
Dynamische Typprüfung wird vor allem bei der Objektorientierung eingesetzt, um dynamisch und flexibel prüfen zu können, ob eine Instanz kompatibel zum Typ ist, über den sie angesprochen wird. Dies ist über eine statische Typprüfung nicht möglich, da zur Compilezeit nicht bekannt ist, welche Objekt-Instanz sich hinter welchem Typ verbirgt. [20]
CWE-843: Access of Resource Using Incompatible Type (‘Type Confusion’)
Diese Schwachstelle wird durch wenig oder gar keine Typ-Sicherheit hervorgerufen. Durch zu wenig Typprüfung und daher entsprechende Freiheiten, ist es möglich, dass zum Beispiel ein Typ-1 erzeugt wird und über Typ-2 angesprochen werden kann, obwohl diese beiden Typen nicht kompatibel zueinander sind. Das bedeutet, dass ein un der selbe Speicher-Bereich unterschiedlich interpretiert werden kann. Dies ist insbesondere dann problematisch, wenn es unbeabsichtigt passiert und somit für den Verwendungszweck, invalide Daten ausgelesen werden oder zu viele Daten ausgelesen werden, wenn der Datentyp größer ist, als der zugegriffene Speicher-Bereich [10].
Dies kann zu verschiedensten Fehlern wie Out-Of-Bounds-Zugriffen, Schadcode-Ausführung, Daten-Manipulation, dem Auslesen sensibler Daten oder einem Programm-Absturz führen. [10]
Ein gutes Beispiel für eine Typ-unsichere Sprache ist C und C++. Bei diesen Sprachen ist es möglich einen Speicher-Bereiche unterschiedlich zu interpretieren. Die Struktur ‘Union’ beispielsweise dient der effizienten Speicher-Ausnutzung, wenn immer nur ein Feld in der Struktur gleichzeitig benötigt wird. Alle Felder im ‘Union’ beginnen an der selben Adresse im Speicher und verwenden damit den selben Speicher. Jenachdem, welchen Typ das Feld hat, auf das zugegriffen wird, wird der Speicher unterschiedlich interpretiert [35], was folgendes Beispiel aus [10] zeigt.
|
Die Gefahr in obigem Beispiel zielt auf das Verändern der Adresse ‘m_pBuffer‘ ab. Sollte ‘m_int‘ durch einen Angreifer manipulierbar sein, könnte damit die Adresse ‘m_pBuffer‘ vom Angreifer so geändert werden, sodass diese auf einen anderen Speicher-Bereich mit evtl. sensiblen Daten oder auf invaliden Speicher zeigen, was die Anwendung zum Absturz bringen würde.
Ein weiteres Beispiel könnte das Casting zwischen Pointer-Typen sein, welches bei statischer Typprüfung komplett ohne Einschränkungen funktioniert, was folgender Code zeigt. Hierbei ist anzumerken, das ‘Type1’ und ‘Type2’ beliebige Datentypen sein können. Zur Laufzeit könnte jedoch die Anwendung abstürzen wenn auf falsch interpretierte Daten zugegriffen wird.
|
Rust-Lösung
Im sicheren Modus von Rust sind alle Casts zwischen Datentypen wohl definiert und daher auch Typ-sicher. Casts werden mithilfe des ‘as’-Schlüsselworts explizit angegeben. Einen sicheren Pointer wie ‘Box<T>‘ als Daten-Typ zu interpretieren, welcher inkompatibel zum Pointer ist, wird vom Rust-Compiler nicht ermöglicht [36].
|
Fazit
Der Artikel hat mit Rust gezeigt, dass auch sichere System-Programmierung mittels strenger Typisierung durch den Compiler möglich ist.
Rust kann bereits zur Compilezeit häufig gemachte Fehler in den Bereichen Memory/-, Thread/- und Type-Safety verhindern, was zu einer sichereren und stabileren Anwendung mit vergleichsweise wenig Speicher-Aufwand und Nachteilen in der Laufzeit-Geschwindigkeit führt [19].
Die Haupt-Vorteile gegenüber anderen Sprachen sind hierbei das Gewährleisten von Speicher-Sicherheit ohne Garbage-Collection sowie Thread-Sicherheit [19].
Der unsichere Modus von Rust zeigt aber, dass es ohne unsichere Programmierung in manchen Bereichen auch nicht funktioniert, da die darunter liegende Hardware oder Betriebssystem-Kommunikation ebenfalls hoch unsicher ist und manche notwendige Aktionen vom Rust-Compiler im sicheren Modus nicht erlaubt würden [39].
Große Konzerne wie Microsoft denken bereits an eine Umstellung auf Rust [37], wodurch sich erahnen lässt, dass Rust in Zukunft C und C++ zurück drängen könnte. Vermutlich wird Rust die Sprachen C oder C++ nicht komplett verdrängen. Sehr performance-kritische Systeme wie etwa eingebettete Systeme werden in Zukunft vermutlich weiterhin in C++, aber vor allem in C entwickelt werden.
Quellen
- ] “CWE Top 25.” https://cwe.mitre.org/top25/archive/2021/2021_cwe_top25.html .Zugegriffen am: 2021-08-18.
- ] “CWE-787: Out-of-bounds Write” https://cwe.mitre.org/data/definitions/787.html .Zugegriffen am: 2021-08-18.
- ] “CWE-125: Out-of-bounds Read” https://cwe.mitre.org/data/definitions/125.html .Zugegriffen am: 2021-08-18.
- ] “CWE-416: Use After Free” https://cwe.mitre.org/data/definitions/416.html .Zugegriffen am: 2021-08-18.
- ] “CWE-190: Integer Overflow or Wraparound” https://cwe.mitre.org/data/definitions/190.html .Zugegriffen am: 2021-08-18.
- ] “CWE-476: NULL Pointer Dereference” https://cwe.mitre.org/data/definitions/476.html .Zugegriffen am: 2021-08-18.
- ] “CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer” https://cwe.mitre.org/data/definitions/119.html .Zugegriffen am: 2021-08-18.
- ] “CWE-401: Missing Release of Memory after Effective Lifetime” https://cwe.mitre.org/data/definitions/401.html .Zugegriffen am: 2021-08-18.
- ] “CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (‘Race Condition’)” https://cwe.mitre.org/data/definitions/362.html .Zugegriffen am: 2021-08-18.
- ] “CWE-843: Access of Resource Using Incompatible Type (‘Type Confusion’)” https://cwe.mitre.org/data/definitions/843.html .Zugegriffen am: 2021-08-18.
- ] “Microsoft: 70 percent of all security bugs are memory safety issues” https://www.zdnet.com/article/microsoft-70-percent-of-all-security-bugs-are-memory-safety-issues/ .Zugegriffen am: 2021-08-18.
- ] “Qualys Security Advisory” https://www.qualys.com/2021/05/04/21nails/21nails.txt .Zugegriffen am: 2021-08-18.
- ] “Sicherheitsupdates: Angreifer könnten Samba-LDAP-Server crashen” https://www.heise.de/news/Sicherheitsupdates-Angreifer-koennten-Samba-LDAP-Server-crashen-5999401.html .Zugegriffen am: 2021-08-18.
- ] “Apple macOS bis 11.3.0 WebKit Pufferüberlauf” https://vuldb.com/de/?id.174514 .Zugegriffen am: 2021-08-18.
- ] “Memory safety” https://en.wikipedia.org/wiki/Memory_safety .Zugegriffen am: 2021-08-18.
- ] “Ariane V88” https://de.wikipedia.org/wiki/Ariane_V88 .Zugegriffen am: 2021-08-18.
- ] “SafeInt-Bibliothek” https://docs.microsoft.com/de-de/cpp/safeint/safeint-library?view=msvc-160 .Zugegriffen am: 2021-08-18.
- ] “Threadsicherheit” https://de.wikipedia.org/wiki/Threadsicherheit .Zugegriffen am: 2021-08-18.
- ] “Fearless Concurrency with Rust” https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html .Zugegriffen am: 2021-08-18.
- ] “Typsicherheit” https://de.wikipedia.org/wiki/Typsicherheit .Zugegriffen am: 2021-08-18.
- ] “Rust for C++ developers – What you need to know to get rolling with crates – Pavel Yosifovich” https://www.youtube.com/watch?v=k7nAtrwPhR8 .Zugegriffen am: 2021-08-18.
- ] “
Rc<T>
, the Reference Counted Smart Pointer” http://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/second-edition/ch15-04-rc.html .Zugegriffen am: 2021-08-18. - ] “Data Types” https://doc.rust-lang.org/book/ch03-02-data-types.html .Zugegriffen am: 2021-08-18.
- ] “Module std::option” https://doc.rust-lang.org/std/option/ .Zugegriffen am: 2021-08-18.
- ] “Box, stack and heap” https://doc.rust-lang.org/rust-by-example/std/box.html .Zugegriffen am: 2021-08-18.
- ] “std::vector” https://en.cppreference.com/w/cpp/container/vector .Zugegriffen am: 2021-08-18.
- ] “Struct std::vec::Vec” https://doc.rust-lang.org/std/vec/struct.Vec.html .Zugegriffen am: 2021-08-18.
- ] “Integer overflow” https://en.wikipedia.org/wiki/Integer_overflow .Zugegriffen am: 2021-08-18.
- ] “Fundamental types” https://en.cppreference.com/w/cpp/language/types .Zugegriffen am: 2021-08-18.
- ] “Atomare Operation” https://de.wikipedia.org/wiki/Atomare_Operation .Zugegriffen am: 2021-08-18.
- ] “Lock” https://de.wikipedia.org/wiki/Lock .Zugegriffen am: 2021-08-18.
- ] “Mutex” https://de.wikipedia.org/wiki/Mutex .Zugegriffen am: 2021-08-18.
- ] “Semaphor (Informatik)” https://de.wikipedia.org/wiki/Semaphor_(Informatik) .Zugegriffen am: 2021-08-18.
- ] “Erzeuger-Verbraucher-Problem” https://de.wikipedia.org/wiki/Erzeuger-Verbraucher-Problem .Zugegriffen am: 2021-08-18.
- ] “Union declaration” https://en.cppreference.com/w/cpp/language/union .Zugegriffen am: 2021-08-18.
- ] “Casting” https://doc.rust-lang.org/rust-by-example/types/cast.html .Zugegriffen am: 2021-08-18.
- ] “Microsoft will Rust statt Go auch in der Cloud” https://www.golem.de/news/sichere-programmiersprache-microsoft-will-rust-statt-go-auch-in-der-cloud-2005-148235.html .Zugegriffen am: 2021-08-18.
- ] “C (Programmiersprache)” https://de.wikipedia.org/wiki/C_(Programmiersprache) .Zugegriffen am: 2021-08-18.
- ] “Meet Safe and Unsafe” https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html .Zugegriffen am: 2021-08-18.
- ] “Trait std::clone::Clone” https://doc.rust-lang.org/std/clone/trait.Clone.html .Zugegriffen am: 2021-08-18.
Leave a Reply
You must be logged in to post a comment.