Modul 20 — Unsafe Code: Verträge, Rohe Zeiger & Undefined Behavior
unsafe-Blöcke, unsafe-Funktionen, rohe Zeiger, undefiniertes Verhalten, unsafe-Traits und Unions: Wenn Rusts Sicherheitsgarantien nicht reichen. Lerne, wann unsafe nötig ist, was Verträge sind und wie du es sicher einsetzt.
Was ist Unsafe? Verträge & Undefined Behavior
Lektion 95 von 7
Das Versprechen von Rust
Rusts Typ- und Borrow-Checker verhindern ganze Fehlerklassen zur Compile-Zeit. Aber manches lässt sich nicht automatisch prüfen. Ein unsafe-Feature ist eines, das einen Vertrag auferlegt: Regeln, die Rust nicht selbst erzwingen kann, die du aber befolgen musst, um undefiniertes Verhalten (UB) zu vermeiden.
Undefined Behavior ist Verhalten, von dem Rust annimmt, dass es nie eintritt. Trifft es doch ein, macht Rust keine Vorhersagen über die Folgen — von verwirrenden Abstürzen bis zur Übergabe deines Computers an einen Angreifer.
Beispiel: Ein roher Zeiger, der über das Ende seines Ursprungsobjekts hinaus läuft, verstößt gegen den Zeiger-Vertrag. Rust kompiliert das trotzdem — die Sicherheitsprüfung erkennt es nicht. Du trägst die Verantwortung.
Unsafe-Features
Diese Optionen stehen dir in unsafe zur Verfügung:
unsafe-Funktionen aufrufen- Rohe Zeiger dereferenzieren
- Felder von
unions lesen - Mutable
static-Variablen zugreifen - FFI-Funktionen/-Variablen (aus anderen Sprachen) benutzen
Ein Vertrag geht über Typ-Checks hinaus
Ein Vertrag sind Regeln zusätzlich zu den üblichen Typ- und Lifetime-Checks. Rust kennt den Vertrag meist gar nicht — er steht nur in der Doku des Unsafe-Features. Verstößt du dagegen, gibst du deinen Teil des Deals mit Rust auf, und Rust weigert sich, die Konsequenzen vorherzusagen.
Warum Rusts Sicherheitsmodell wissenschaftlich fundiert ist
Rusts Borrow-Checker verhindert zur Compile-Zeit genau die Fehlerklassen, die in C/C++ zu den häufigsten Sicherheitslücken führen: Null-Dereferenzierung, Use-after-Free und Double-Free. Aktuelle Forschung bestätigt dies:
- Neuro-symbolische Verifikation (2026): LLMs können nicht zuverlässig entscheiden, ob eine Zeiger-Dereferenzierung sicher ist — Rusts Typsystem löst dieses Problem deterministisch zur Compile-Zeit.
- RustMizan (2026): Ein Benchmark für Rust-Schwachstellen zeigt, welche Fehlermuster in unsafe-Code auftreten — von fehlender Signaturprüfung bis zu Pufferüberläufen.
- AutoVerus (2025): LLMs können automatisch formale Korrektheitsbeweise für Rust-Code generieren — über 90 % von 150 Beweisaufgaben wurden automatisch gelöst.
- DSFB Crate-Auditing (2026): Strukturelle Code-Qualitätsanalyse für Rust-
Crates mit
#![forbid(unsafe_code)]— ein Crate-Audit-Layer, der Sicherheits Signale deterministisch aggregiert.
unsafe ist der bewusste Austritt aus diesem Sicherheitsmodell — nutze es
sparsam und immer mit // SAFETY:-Kommentar.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
unsafe-Blöcke & unsafe-Funktionen
Lektion 96 von 7
unsafe-Block
Ein unsafe-Block sieht aus wie ein normaler Block, nur mit unsafe davor.
Innerhalb darfst du Unsafe-Features nutzen — z. B. String::from_utf8_unchecked:
fn main() {
let ascii = b"hello".to_vec();
// from_utf8_unchecked ist eine unsafe-Funktion:
let s = unsafe { String::from_utf8_unchecked(ascii) };
println!("{s}");
}
Ohne unsafe davor lehnt Rust den Aufruf ab. Der Block separiert Code,
der besondere Prüfung verdient, vom Rest des Programms.
unsafe-Funktion
Eine unsafe fn ist eine Funktion mit einem Vertrag:
/// Liefert einen String ohne Laufzeit-UTF-8-Prüfung.
///
/// # Safety
/// `bytes` muss gültiges UTF-8 enthalten.
unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String {
String::from_utf8_unchecked(bytes)
}
Der Aufruf erfordert einen unsafe-Block. Die # Safety-Doku erklärt den
Vertrag.
Block vs. Funktion — zwei Botschaften
unsafe fn= "Ich stelle ein gefährliches Werkzeug bereit." → Doku sollte# Safetyhaben.unsafe-Block = "Ich benutze gefährliche Werkzeuge." → idealerweise mit// SAFETY:-Kommentar, der erklärt, warum die Nutzung korrekt ist.
Selbst in einer unsafe fn ist es sinnvoll, den Unsafe-Code in einen
unsafe-Block zu setzen. Rust warnt sonst; mit
#![deny(unsafe_op_in_unsafe_fn)] wird daraus ein Fehler.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Rohe Zeiger: *mut T & *const T
Lektion 97 von 7
Zwei Arten roher Zeiger
Ein roher Zeiger ist ein unbeschränkter Zeiger — wie ein C-Zeiger. Du kannst
damit Strukturen bauen, die geprüfte Zeiger nicht zulassen (z. B. doppelt
verkettete Listen). Dafür kann Rust nicht prüfen, ob du sie sicher verwendest —
dereferenzieren darfst du sie nur im unsafe-Block.
*mut T— roher Zeiger, der das Referent ändern darf*const T— roher Zeiger, der das Referent nur lesen darf
Es gibt kein *T — du musst const oder mut angeben.
Zeiger erzeugen und dereferenzieren
fn main() {
let mut x = 10;
let ptr_x: *mut i32 = &raw mut x; // &raw-mut-Operator
let y = Box::new(20);
let ptr_y: *const i32 = &*y; // implizite Koerzion
unsafe {
*ptr_x += *ptr_y; // dereferenzieren nur hier
}
assert_eq!(x, 30);
}
Rohe Zeiger können null sein (wie C NULL) und werden nicht
automatisch gelöscht/geleert.
Konvertierung nur in eine Richtung
Referenzen koerzieren implizit zu rohen Zeigern — aber nicht umgekehrt. Aus
einem rohen Zeiger machst du eine Referenz nur mit explizitem &*ptr (oder
&mut *ptr), und das nur nach Prüfung, dass der Zeiger gültig und nicht null ist.
Übung: Zeiger erstellen und lesen
🧩 Übung m20-l3-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Undefined Behavior verstehen
Lektion 98 von 7
Warum vertraut der Compiler?
Ein Optimierer transformiert dein Programm schrittweise — schneller, kleiner. Dabei verlässt er sich darauf, dass manche Dinge nie passieren (UB). Diese Annahme erlaubt aggressive Optimierung.
Beispiel — der Compiler darf Folgendes annehmen:
let i = 10;
some_function(&i);
println!("{}", i * 100);
Wenn some_function Unsafe-Code enthält, der i über den Zeiger entgegen
seiner Referenz-Semantik verändert, ist das UB. Der Compiler kann das Ergebnis
von i * 100 dann vorab berechnen, als wäre i unverändert — und das
Programm überrascht dich.
Die meisten Fehler sind harmlos bis zufällig
UB ist nicht immer sichtbar. Mal hat es keine sichtbaren Folgen, mal crasht es, mal öffnet es eine Sicherheitslücke — und es kann sich zwischen Rust-Versionen ändern, ohne Vorwarnung. Die Konsequenzen von UB stehen nirgends garantiert fest.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Unsafe-Traits, Null-Zeiger & Sicherheit
Lektion 99 von 7
Unsafe-Traits
Genau wie Funktionen können Traits unsafe sein. Das Kennzeichnen einer Trait
als unsafe signalisiert: Wer sie implementiert, muss bestimmte Garantien
einhalten. Send und Sync sind klassische Unsafe-Traits.
unsafe trait MeineVertrag {
fn do_it(&self);
}
// Wo unrichtig unpingerherrn:
unsafe impl MeineVertrag for Typ { ... }
Nur Unsafe-Code darf eine unsafe-Trait implementieren — denn die
Implementierung trägt einen Vertrag.
Null-Zeiger
Rohe Zeiger dürfen null sein. Entwickle Konventionen, um das sicher zu handhaben:
fn option_to_raw<T>(opt: Option<&T>) -> *const T {
match opt {
Some(r) => r as *const T,
None => std::ptr::null(),
}
}
fn raw_to_option<'a, T>(raw: *const T) -> Option<&'a T> {
if raw.is_null() {
None
} else {
Some(unsafe { &*raw })
}
}
std::ptr::null()/null_mut()erzeugen Null-Zeiger.is_null()prüft auf null- Bevor du
&*rawmachst: sicherstellen, dass nicht null und gültig
Normale Referenzen sind nie null
In sicherem Rust kannst du nie einen Null-Referenz haben — das ist garantiert. Nur rohe Zeiger kennen null. Genau deshalb ist das Umwandeln Zeiger↔Referenz ein Unsafe-Bereich: Du musst die Null-Prüfung selbst übernehmen.
Übung: Null-Zeiger sicher behandeln
🧩 Übung m20-l5-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Pointer-Arithmetik, Sizes & Unions
Lektion 100 von 7
Zeiger-Arithmetik und Größen
Der Compiler kennt Größe und Ausrichtung jedes Typs. Für rohe Zeiger gibt es Methoden, die in Einheiten zählen (nicht in Bytes):
use std::mem;
let arr = [1u8, 2, 3, 4];
let ptr = arr.as_ptr();
// Zeiger n Elemente weiter bewegen (in T-Einheiten, hier Bytes):
let third = unsafe { *ptr.add(2) };
assert_eq!(third, 3);
println!("usize: {}", mem::size_of::<usize>());
.add(n)bewegt umnElemente (Typr-basiert),.offset(n)ähnlichsize_of::<T>()/align_of::<T>()geben Größe/Ausrichtung- Der Vertrag: nur innerhalb der Grenzen des ursprünglichen Objekts bewegen
Unions
Eine union ist ein Strukturtyp aus C, dessen Felder alle am selben
Speicherort liegen:
union UnspecifiedShape {
circle: u32,
rectangle: u32,
}
Anders als ein enum trägt eine Union keine Kennung, welches Feld
initialisiert ist. Deshalb ist das Lesen aus einem Feld einer Union unsafe
— nur das Schreiben ist sicher. Unions nutzt du fast nur für FFI / C-Kompatibilität.
Wie verhält sich enum vs union?
Ein enum hat eine Tag (Kennung), die angibt, welche Variante vorliegt —
der Compiler weiß das. Eine union hat keine solche Kennung: Alle Felder
liegen am selben Offset, und es gibt keine Information, welches gültig ist.
Daher ist Lesen aus einer Union unsafe, außer du kennst den aktiven Wert.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Übung: Eine sichere Hülle um Unsafe
Lektion 101 von 7
Projekt: Unsafe kapseln
Der Kern-Trick von gutem Unsafe: kleine Mengen Unsafe in sicheren Interfaces verstecken, damit der Rest des Programms sicher bleibt. Deine Aufgabe:
- Definiere eine Funktion, die eine
Vec<u8>(bekannt gültiges UTF-8) mitString::from_utf8_uncheckedin einenStringverwandelt — alsunsafe fnmit# Safety-Doku. - Rufe sie in
mainaus einemunsafe-Block auf, mit// SAFETY:-Kommentar. - Erstelle einen rohen Zeiger auf ein
i32, ändere den Wert imunsafe-Block über den Zeiger und zeige, dass die ursprüngliche Variable sich geändert hat. - Schreibe einen
// SAFETY-Kommentar, der GENAU erklärt, warum dein Code den Vertrag nicht verletzt.
Das prüft: unsafe-Funktionen, unsafe-Blöcke, rohe Zeiger, und die Denkweise — Verantwortung für Verträge übernehmen — die dieses Modul vermittelt.
Rezepte aus diesem Modul
- Unsafe-Funktion mit Vertrag:
unsafe fn ...+# Safety-Doku - Unsafe-Aufruf begründen:
unsafe { ... }+// SAFETY: warum korrekt - Rohen Zeiger erzeugen:
&raw mut xoder Koerzion&*x - Sicher kapseln: kleinen Unsafe-Block, gewrappt durch eine sichere Funktion
- Zeiger bewegen:
.add(n)/.offset(n)in Typ-Einheiten, innerhalb der Grenzen
Praxis-Szenario: Die Sensor-Library
Ein C-Treiber liest Sensordaten. Der Rust-Wrapper funktioniert — bis ein gestörter Sensor eine negative Länge liefert und der Dienst in undefined behavior läuft. Du sollst den Zugang absichern, ohne den C-Code anzufassen.
Übung: Sichere Hülle um Unsafe
🧩 Übung m20-l7-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren: