Rust für sichere und zuverlässige eingebettete Systeme
Julian Dickert, Joël Schulz-Andres September 03, 2024Da wir im Nachgang zu unserem Vortrag zusammen mit dem VDE Bayern am 25.07.2024 oft auf die Folien angesprochen wurden, haben wir die Inhalte des Vortrags hier noch einmal zusammengefasst.
Einführung
In der Softwareentwicklung ist es leider alltäglich, dass Sicherheitslücken durch unsicheren Umgang mit Speicher entstehen. Dies zeigt sich besonders deutlich in der Verbreitung von Memory-Safety-Vulnerabilities (MSVs), welche einen erheblichen Teil der Sicherheitslücken in Software ausmachen. Statistiken von führenden Technologieunternehmen verdeutlichen dies:
- Microsoft: Etwa 70 % der Common Vulnerabilities and Exposures (CVEs) von 2006–2018 sind MSVs.
- Google Chromium: Ebenfalls etwa 70 % der identifizierten Schwachstellen sind MSVs.
- Mozilla: 32 von 34 kritischen oder schweren Fehlern waren MSVs.
- Google Project Zero: 67 % der Zero-Day-Schwachstellen im Jahr 2021 waren MSVs.
Diese Zahlen unterstreichen die Notwendigkeit von Programmiersprachen, die Speicherfehler proaktiv verhindern. Memory-Safety ist oft der entscheidende Faktor zwischen einer sicheren und einer gefährdeten Software. Die Wahl einer sicheren Programmiersprache kann viele dieser Probleme bereits an ihrem Ursprung beheben.
Die Vergangenheit
Die unsichere und unschöne Vergangenheit eingebetteter Systeme.
Historisch gesehen basieren viele eingebettete Systeme auf der Programmiersprache C. Obwohl C eine leistungsfähige und weit verbreitete Sprache ist, bringt sie erhebliche Herausforderungen mit sich. Die Komplexität von C führt häufig dazu, dass Entwickler jahrelange Erfahrung benötigen, um sicheren und stabilen Code zu schreiben. Dies stellt insbesondere im Kontext des aktuellen Fachkräftemangels ein großes Problem dar.
Das Resultat dieser Situation sind oft unsichere und unzuverlässige Systeme. Viele dieser Systeme sind anfällig für schwerwiegende Sicherheitslücken, was in einer zunehmend vernetzten Welt zu erheblichen Risiken führt.
Um diesen Risiken zu begegnen, haben Gesetzgeber auf der ganzen Welt begonnen, Maßnahmen zu ergreifen. Der EU Cyber Resilience Act und die Maschinenverordnung der EU sind nur zwei Beispiele dafür, wie Regierungen auf diese Bedrohungen reagieren. Auch das Weiße Haus drängt auf die Nutzung sicherer Programmiersprachen.
Einige Regierungen und Organisationen haben erkannt, dass Programmiersprachen mit Memory-Safety der Schlüssel sind, um die häufigsten Sicherheitslücken zu vermeiden.
"The clear north star for security against memory corruption exploits is the broad usage of Memory Safe Languages [...] Anything less is playing the whack-a-mole of exploit mitigation."
— CISA, U.S. Cybersecurity & Infrastrucure Agency
Die Zukunft eingebetteter Systeme
Natürlich mit Rust.
Bei der Systemscape GmbH liegt der Fokus auf Softwareentwicklung und Systems Engineering mit besonderer Ausrichtung auf Rust. Rust ist mehr als nur eine Programmiersprache; sie ist ein modernes Werkzeug, das Entwickler befähigt, zuverlässige und effiziente Software zu erstellen. Viele der Probleme, die in der Vergangenheit durch C verursacht wurden, können mit Rust behoben werden.
Rust bietet eine moderne, stark typisierte Syntax, die es ermöglicht, Code von sehr hoher Qualität zu schreiben. Zudem gibt es keine Garbage Collection, was Rust besonders effizient und geeignet für eingebettete Systeme macht. Das hervorragende Tooling und der hilfreiche Compiler machen es Entwicklern leicht, sicheren Code zu schreiben. Besonders hervorzuheben ist das Ownership-Modell von Rust, welches Speichersicherheit "by Design" sicherstellt.
Was ist Rust?
Rust hat sich in den letzten Jahren als die beliebteste Programmiersprache unter Entwicklern etabliert. Seit 2016 steht Rust kontinuierlich an der Spitze, während über die Hälfte der C- und C++-Nutzer angibt, dass sie C(++) lieber nicht weiter verwenden würden. Dies spiegelt die Herausforderungen und Frustrationen wider, die mit älteren Programmiersprachen einhergehen, und hebt die Vorteile von Rust hervor.
Wer nutzt Rust und wofür?
Rust wird bereits von vielen etablierten Unternehmen genutzt, darunter:
- Microsoft
- Mozilla
- Dropbox
- Atlassian
- Cloudflare
Diese Unternehmen verwenden Rust sowohl für Low-Level-Anwendungen wie Bootloader und Gerätetreiber als auch für High-Level-Anwendungen wie Netzwerkdienste und Web-Apps. Ein beeindruckendes Beispiel ist Cloudflares neuer HTTP Proxy "Pingora", der täglich über 1 Billion Anfragen verarbeitet.
Wie sieht Rust aus?
Die Syntax von Rust ist modern und ausdrucksstark. Ein Beispiel für Enums und Pattern-Matching zeigt, wie Rust durch starke Typisierung und klare Syntax hilft, Fehler zu vermeiden und gleichzeitig leistungsstarken Code zu schreiben:
Dieses Beispiel verdeutlicht, wie Rust Entwicklern ermöglicht, komplexe Logik auf einfache und sichere Weise auszudrücken.
Sicherheit und Zuverlässigkeit
Rust sorgt durch Memory Safety für mehr Sicherheit, indem potenzielle Speicherfehler bereits beim Kompilieren verhindert werden. Das Ownership- und Borrowing-Modell von Rust stellt sicher, dass jeder Wert einen klaren Besitzer hat, der für die Lebensdauer des Wertes im Speicher verantwortlich ist. Dadurch werden Fehler wie "Use After Free" oder "Double Free" vermieden. Zusätzlich gibt es strenge Regeln für Referenzen: Entweder gibt es nur eine mutable Referenz (&mut x
), oder es können unendlich viele immutable Referenzen (&x
) existieren. Dies verhindert sogenannte "data races", die zu unvorhersehbarem Verhalten führen können.
Zur Laufzeit prüft Rust weiterhin auf häufige Fehler wie Array-Längenüberschreitungen und Zugriffe auf ungültige Werte (None
oder Err
). Auch Überläufe bei Ganzzahlen werden im Debug-Modus erkannt und abgefangen.
Beispiel: Ownership
Das folgende Beispiel zeigt, wie Rust das Ownership-Modell durchsetzt. Wird ein Wert "gemoved", ist der ursprüngliche Wert danach nicht mehr verfügbar:
Nach dem "Move" von x
nach y
verhindert Rust den Zugriff auf x
, da es nicht mehr gültig ist. Dies schützt vor den häufigsten Fehlern, die in anderen Sprachen auftreten.
Der Zugriff führt dann zu folgender Fehlermeldung:
Thread Safety
Explizit statt implizit.
Rust verlangt, dass Datentypen explizit als threadsicher gekennzeichnet werden, was durch die beiden Marker-Traits Send
und Sync
erreicht wird:
Send
: Ein Wert kann sicher an einen anderen Thread gesendet werden. Fast alle Werte, die keine Referenzen sind, sind auchSend
.Sync
: Ein Wert kann sicher zwischen mehreren Threads geteilt werden. Dies ist nur möglich, wenn der Wert bereitsSend
implementiert.
Im Gegensatz dazu gibt es Typen, wie den Reference-Counted Pointer (Rc
), die Send
nicht implementieren und daher nicht sicher zwischen Threads verschoben werden können. Der folgende Code zeigt ein Beispiel dafür:
let rc = new; // Reference Counted Pointer
// error[E0277]: `Rc<i32>` cannot be sent between threads safely
;
spawn
Der Rust-Compiler verhindert hier die Verwendung von Rc
zwischen Threads, da dieser Typ nicht Send
ist und daher nicht threadsicher.
Unsafe Code
Kontrollierte Unsicherheit.
Obwohl Rust ein hohes Maß an Sicherheit bietet, gibt es Situationen, in denen potentiell unsicherer Code notwendig ist. Derartige Operationen müssen jedoch explizit als unsafe
gekennzeichnet werden.
Hier ein Beispiel, in welchem ein slice
RAM "aus dem Nichts" erzeugt wird, was bei späterer Verwendung potentiell fatale Folgen haben kann:
// Example: Access arbitrary memory address
let ram_ptr: *mut u32 = sdram.init as *mut _; // e.g. 0xC0000000
// Unsafe! You should know what you're doing...
let ram_slice = unsafe ;
// Use like ordinary Rust slice / array. Safe from here!
info!;
Durch unsafe
wird darauf hingewiesen, dass hier potentiell unsichere Operationen stattfinden und Vorsichtsmaßnahmen getroffen werden sollten.
Dementsprechend wird durch Rust sichergestellt, dass der unsichere Code leicht auffindbar und isoliert bleibt, sodass er nicht versehentlich in anderen Teilen des Programms verwendet wird.
Multi-core Nebenläufigkeit
Für Multi-Core-Systeme bietet Rust sichere und effiziente Möglichkeiten zur Nebenläufigkeit. Der Einsatz von atomaren referenzgezählten Zeigern (Arc
) in Kombination mit Mutex
stellt sicher, dass Daten sicher zwischen mehreren Threads geteilt werden können:
use ;
use thread;
let counter = new;
let mut handles = vec!;
for _ in 0..10
for handle in handles
println!;
Dieses Beispiel zeigt, wie Rust sicherstellt, dass mehrere Threads sicher auf eine gemeinsame Ressource zugreifen können, ohne dass es zu Datenkorruption kommt.
Typen und Hardware-Eigenschaften
Rust ermöglicht es, Hardware-Eigenschaften durch Typen exakt abzubilden. Ein Beispiel ist die Verwendung von Traits, um bestimmte Funktionen nur für spezifische Hardware-Komponenten verfügbar zu machen:
; // Pin PA9
// Mark PA9 as clock pin
// This function only works for Clock Pins
In diesem Beispiel wird sichergestellt, dass nur Pins, die als ClkPin
gekennzeichnet sind, in der Funktion take_clk_pin
verwendet werden können. Dies erhöht die Zuverlässigkeit, indem der Compiler überprüft, ob der richtige Typ verwendet wird und es keine unangenehmen Überraschungen zur Laufzeit gibt.
Performance ohne Kompromisse
Rust ermöglicht durch Zero-cost Abstractions, auf einem hohen Abstraktionsniveau zu programmieren, ohne Performance-Einbußen hinnehmen zu müssen. Dies ist besonders in eingebetteten Systemen wichtig, wo sowohl Effizienz als auch Sicherheit von größter Bedeutung sind.
Ein Beispiel zeigt, wie dies in der Praxis aussehen kann:
; // No data -> size = 0 bytes -> optimized away by compiler
// In practice: obtained from HAL, e.g., `hal.peripherals.SPI1`
spi1_driver;
// This won't work. No other driver can be created from the same SPI instance!
spi1_driver; // error[E0382]: use of moved value: `spi`
In diesem Codebeispiel wird ein leerer Struct SPI1
verwendet. In einem echten Szenario würde dieser Struct von der Hardware Abstraction Layer (HAL) bereitgestellt werden. Beispielsweise gibt es für STM32-Chips wie z.B. den STM32F469 das Projekt stm32f4xx-hal. Dadurch wird sichergestellt, dass nur ein SPI1
-Objekt existieren kann, wodurch versehentliche doppelte Zugriffe auf die gleiche Hardware vermieden werden.
Single-core Nebenläufigkeit
Neben der Performance bietet Rust auch Lösungen für Nebenläufigkeit auf Single-Core-Prozessoren. Mit Rust lassen sich Aufgaben asynchron abarbeiten, was besonders in eingebetteten Systemen nützlich ist.
async
async
Dieses Beispiel zeigt, wie eine einfache LED-Blinker-Anwendung mit Rust realisiert werden kann. Der Code verwendet Asynchronität, um die CPU effizient zu nutzen, ohne komplexe Threading-Modelle oder Interrupts zu benötigen.
Produktivitätssteigerung
Rust bietet eine Vielzahl von Werkzeugen, die Entwicklern helfen, effizienter und produktiver zu arbeiten. Diese Tools machen es einfach, qualitativ hochwertigen Code zu schreiben und zu verwalten, ohne dass dabei die Sicherheit beeinträchtigt wird.
Compiler und Toolchain Manager
Der Rust-Compiler rustc
bietet nicht nur leistungsstarke Optimierungen, sondern auch hilfreiche Fehlermeldungen, die es Entwicklern ermöglichen, Probleme schnell zu identifizieren und zu beheben. Mit dem Toolchain-Manager rustup
lassen sich verschiedene Rust-Versionen und Zielarchitekturen verwalten, was besonders für Cross-Kompilierung wichtig ist.
Ein Beispiel zeigt, wie einfach es ist, die Entwicklungsumgebung für verschiedene Plattformen einzurichten:
Pakete, Formatierung, Dokumentation
Zusätzlich zu rustc
und rustup
bietet Rust eine Reihe weiterer nützlicher Tools, wie zum Beispiel den Paketmanager cargo
. cargo
ermöglicht es, Projekte zu bauen, zu formatieren und zu dokumentieren. Einheitliche Codeformatierung wird mit rustfmt
bzw. cargo fmt
erreicht, und HTML-Dokumentation kann aus Docstrings mit rustdoc
bzw. cargo doc
erstellt werden.
Um ein Projekt zu kompilieren und auszuführen, reicht folgendes Kommando:
Ein Beispiel für eine einfache Cargo.toml
-Konfigurationsdatei:
# "Cargo.toml" project file
[]
= { = "0.7.6", = ["critical-section-single-core"] }
Mit diesen Tools lässt sich der gesamte Entwicklungsprozess in Rust effizienter und sicherer gestalten.
Die HTML-Basierte Dokumentation sieht beispielsweise so aus:
Als Interaktives Beispiel könnte mitunter die Dokumentation der Rust Standardbibliothek dienen.
Tools für Embedded
Für die Embedded-Entwicklung gibt es spezielle Tools, die Rust besonders gut unterstützen:
- Flashing und Debugging:
Mitprobe-rs
lassen sich ARM-Chips flashen und debuggen. Dies ist besonders nützlich in Embedded-Systemen, da es eine einfache und effiziente Methode bietet, den Code direkt auf der Hardware auszuführen und zu testen.
- Ressourcenschonendes Logging:
Das Logging-Systemdefmt
ist extrem ressourcenschonend und eignet sich perfekt für Embedded-Systeme, in denen Speicher und Rechenleistung knapp sind.
!;
error// Output:
0.000000 ERROR This is very bad!
└─ @ src/bin/myapp.rs:100
- Embassy:
Ein leistungsstarkes Async-Framework, das speziell für Embedded-Systeme entwickelt wurde. Embassy unterstützt verschiedene Mikrocontroller-Plattformen und bietet eine Vielzahl von nützlichen Crates.
Aktuelle Herausforderungen
... und wann Rust nicht die beste Wahl ist.
Trotz der vielen Vorteile von Rust gibt es auch einige Herausforderungen, die berücksichtigt werden sollten.
Komplexität und Lernkurve
Das Verständnis des Typensystems sowie des Ownership-Modells ist notwendig, um effizient in Rust zu arbeiten. Dies kann anfangs überwältigend wirken, besonders für Entwickler, die von weniger strikten Sprachen wie C oder Python kommen. Rust setzt hohe Maßstäbe für den geschriebenen Code und der Compiler agiert eher als strenger Wächter denn als stiller Helfer:
Auch wenn dies den Lernprozess auf den Ersten Blick erschwert, führt es letztendlich zu sichererem und robusterem Code.
Interoperabilität
Die Integration von Rust mit bestehendem C/C++-Code kann mühsam sein. Es gibt wenig offiziellen Support durch Chiphersteller und Entwickler müssen zwei Sprachen beherrschen, um komplexe Projekte umzusetzen. Auch gibt es einige Treiber, die trotz aktiver Community noch nicht in Rust verfügbar sind.
Dennoch können dank Rusts Foreign Function Interface (FFI) auch C Bibliotheken aufgerufen werden, sodass immer die Möglichkeit besteht, auf bestehenden C-Code aufzubauen.
Wann Rust nicht die beste Wahl ist
Es gibt Fälle, in denen Rust möglicherweise nicht die beste Wahl ist. Dies ist zum Beispiel der Fall, wenn spezifische Treiber nur in C/C++ verfügbar sind oder wenn bestimmte Zertifizierungen notwendig sind, wie sie im Automobilbereich, der Luftfahrt oder im Militär gefordert werden.
Praxisbeispiele
Rust wird bereits in einer Vielzahl von Embedded-Systemen eingesetzt und hat sich als vielseitige und zuverlässige Sprache etabliert:
-
Betriebssysteme:
Rust wird in großen Betriebssystemen wie Windows, Android und Linux verwendet. Besonders bemerkenswert ist, dass Rust die einzige offiziell unterstützte Kernel-Sprache neben C im Linux-Kernel ist. -
Server:
Rust wird in Serverinfrastrukturen eingesetzt, wie z. B. bei oxide.computer, wo die komplette Firmware in Rust geschrieben ist. -
Zugangskontrollsysteme:
akiles.app nutzt Rust für ihre Hauptentwicklungen, und ist Hauptentwickler des Embassy Frameworks. -
Ladeinfrastruktur:
sksignet.us verwendet Rust für die Firmware und das Embedded UI. -
Wearables:
Bei hoptech.ca wird mindestens das User Interface in Rust entwickelt, oft mithilfe von Slint. -
Umweltsensoren:
anyleaf.org nutzt Rust teilweise für die Treiber ihrer Sensoren.
Diese Beispiele zeigen, dass Rust nicht nur eine theoretische Option ist, sondern in der Praxis bereits weit verbreitet und erfolgreich eingesetzt wird. Immer wieder dringen Informationen über große Firmen durch, die ebenfalls Rust Embedded verwenden, aber nicht sehr viel darüber sprechen.
Rust lernen / Unterstützung
Für das Lernen von Rust stehen viele Ressourcen zur Verfügung:
- Das Rust Book bietet eine umfassende Einführung in die Sprache.
- Das Rust Embedded Book ist eine wertvolle Ressource für alle, die Rust in Embedded-Systemen einsetzen möchten.
- Die Sammlung Awesome Embedded Rust bietet eine Fülle von Tools und Crates speziell für Embedded-Entwickler.
- Für eine gezielte Einführung in Embedded Rust gibt es bei Systemscape ein eigenes Training.
Falls professionelle Unterstützung erforderlich ist, bieten verschiedene Anbieter Trainings und Beratung an, darunter Ferrous Systems GmbH und Tweede Golf B.V.. Auch die Systemscape GmbH bietet maßgeschneiderte Trainings und Entwicklungsdienstleistungen für Embedded Rust an.
Abschluss
Rust in a Nutshell
Rust liefert:
- Sicherheit: Durch das Ownership-Modell und die strikte Typprüfung werden viele der häufigsten Fehlerquellen bereits zur Kompilierzeit eliminiert.
- Zuverlässigkeit: Der Code, der mit Rust geschrieben wird, ist nicht nur sicher, sondern auch vorhersehbar und zuverlässig in seiner Ausführung.
- Performance: Durch Zero-cost Abstractions und die effiziente Speicherverwaltung bietet Rust Performance auf dem Niveau von C/C++.
- Tooling: Die hervorragenden Werkzeuge rund um Rust machen die Entwicklung schneller und weniger fehleranfällig.
Für die Entwicklung bedeutet das:
- Mehr Effizienz: Durch die starken Werkzeuge und die hohe Sicherheit wird Zeit und Aufwand gespart.
- Weniger Kopfschmerzen: Viele Fehler, die in anderen Sprachen auftreten, werden in Rust von vornherein vermieden.
- Mehr Freude an der Entwicklung: Mit Rust liegt der Fokus auf der kreativen Seite der Softwareentwicklung, ohne sich ständig über Speicherprobleme sorgen zu müssen.
Nächste Schritte
Um mehr über Rust zu erfahren und direkt in die Praxis einzusteigen, empfehlen sich folgende Schritte:
- Die Folien herunterladen und die wichtigsten Punkte noch einmal durchgehen.
- Die LinkedIn Seite von Systemscape abonnieren, um über die neuesten Entwicklungen und Angebote informiert zu bleiben.
- Unsere Einführung in Embedded Rust lesen und ein Beispiel ausprobieren, um direkt in die Praxis einzutauchen.
- Sichere und zuverlässige eingebettete Systeme mit Rust entwickeln.
Kontakt
Bei Fragen und Anmerkungen bzw. Lob und Kritik stehen wir jederzeit gerne zur Verfügung.
Systemscape GmbH