Skip to content

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.

Fortschritt0/7 Lektionen
95

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:

  1. unsafe-Funktionen aufrufen
  2. Rohe Zeiger dereferenzieren
  3. Felder von unions lesen
  4. Mutable static-Variablen zugreifen
  5. 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

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

96

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-WerkstattBereit
Editor wird geladen…

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 # Safety haben.
  • 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

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

97

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?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

98

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

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

99

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 &*raw machst: 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?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

100

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 um n Elemente (Typr-basiert), .offset(n) ähnlich
  • size_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

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

101

Ü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:

  1. Definiere eine Funktion, die eine Vec<u8> (bekannt gültiges UTF-8) mit String::from_utf8_unchecked in einen String verwandelt — als unsafe fn mit # Safety-Doku.
  2. Rufe sie in main aus einem unsafe-Block auf, mit // SAFETY:-Kommentar.
  3. Erstelle einen rohen Zeiger auf ein i32, ändere den Wert im unsafe-Block über den Zeiger und zeige, dass die ursprüngliche Variable sich geändert hat.
  4. 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 x oder 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?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

This site uses essential cookies for Stripe payments. No tracking cookies.