Skip to content
Lektion 95 von 1150/115 abgeschlossen
Modul 20 — Unsafe Code

Was ist Unsafe? Verträge & Undefined Behavior

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.

📝 Schnellprüfung

Was bedeutet Undefined Behavior (UB) in Rust?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 96 von 1150/115 abgeschlossen
Modul 20 — Unsafe Code

unsafe-Blöcke & unsafe-Funktionen

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 # 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.

📝 Schnellprüfung

Was markiert eine Funktion als 'unsafe fn'?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 97 von 1150/115 abgeschlossen
Modul 20 — Unsafe Code

Rohe Zeiger: *mut T & *const T

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.

📝 Schnellprüfung

Wo darfst du einen rohen Zeiger dereferenzieren?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 98 von 1150/115 abgeschlossen
Modul 20 — Unsafe Code

Undefined Behavior verstehen

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.

📝 Schnellprüfung

Warum erlaubt Rust so viel Optimierung trotz UB-Risiko?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 99 von 1150/115 abgeschlossen
Modul 20 — Unsafe Code

Unsafe-Traits, Null-Zeiger & Sicherheit

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.

📝 Schnellprüfung

Dürfen rohe Zeiger in Rust null sein?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 100 von 1150/115 abgeschlossen
Modul 20 — Unsafe Code

Pointer-Arithmetik, Sizes & Unions

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.

📝 Schnellprüfung

Warum ist das Lesen aus einem Union-Feld unsafe?

✅ Wichtige Erkenntnisse

Haken setzen, um deinen Lernfortschritt zu markieren:

Lektion 101 von 1150/115 abgeschlossen
Modul 20 — Unsafe Code

Übung: Eine sichere Hülle um Unsafe

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

Ü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

Haken setzen, um deinen Lernfortschritt zu markieren:

Modul-Checkpoint

Bestehe das Modul mit 80 %. Der Versuch zählt — beantwortete Fragen werden nicht erneut angezeigt; erst nach dem Absenden gibt es Auflösung und Erklärung.

Checkpoint: Modul 20 — Unsafe Code

Bestehensgrenze: 80 %. Der Versuch wird bewertet und zählt für das Modul-Gate.

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