FlawOpen

flawopen.com/Speichersicherheit/Rust unsafe

Ist Rusts unsafe-Block weiterhin speichersicher?

CWE-787: Out-of-bounds WriteReferenzseite
Kurz erklärt

unsafe schaltet Rusts Prüfungen nicht ab. Es erlaubt bestimmte Operationen, deren Sicherheitsbedingungen der Compiler nicht beweisen kann; der Programmierer muss diese Pflichten erfüllen. Eine unsound unsafe-Abstraktion kann dazu führen, dass gewöhnliche safe-Aufrufe undefiniertes Verhalten (UB) auslösen, obwohl sie das Wort unsafe nie verwenden.

Begriffe

Unsafe-Operation:
Operation mit Sicherheitsbedingungen, die der Compiler nicht beweisen kann, etwa das Dereferenzieren eines Raw-Pointers.
Sound-Abstraktion:
Eine safe-API, über die safe-Aufrufer kein undefiniertes Verhalten auslösen können.
size_hint():
Größenabschätzung eines Iterators zur Optimierung; keine exakte Anzahl und kein Beweis der Speichersicherheit.
Undefiniertes Verhalten (UB):
Verhalten, das Rust nicht festlegt; unsafe-Code muss verhindern, dass safe-Aufrufer es auslösen.

Wie eine unsound safe-Abstraktion scheitert

Schritt 1

Eine safe-API erhält einen Iterator

Der Aufrufer übergibt einen gewöhnlichen Iterator an eine Funktion ohne unsafe-Vorbedingung.

Schritt 2

Die Implementierung reserviert anhand eines Hinweises

Die Funktion reserviert nur die gemeldete Untergrenze, die kleiner als die tatsächlich gelieferte Elementzahl sein kann.

Schritt 3

Ungeprüfte Schreibvorgänge überschreiten die Kapazität

Die Implementierung schreibt jedes Element über einen Raw-Pointer, ohne den Speicher zu erweitern oder die Kapazität pro Element zu prüfen.

Schritt 4

Safe-Nutzung erreicht UB

Liefert der Iterator mehr Elemente als Platz vorhanden ist, kann ein safe-Aufruf außerhalb des Puffers schreiben. Verwenden Sie Vec::extend oder eine Implementierung, die sicher wächst.

UNSOUND
// UNSOUND: reserve uses only the lower hint,
// then unchecked writes consume every item.
fn append_unsound(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) {
    let (lower, _) = iter.size_hint();
    dst.reserve(lower);
    let start = dst.len();
    let mut written = 0;
    unsafe {
        let out = dst.as_mut_ptr().add(start);
        for item in iter {
            // UB once writes exceed spare capacity.
            out.add(written).write(item);
            written += 1;
        }
        dst.set_len(start + written);
    }
}
SOUND
// SOUND: Vec::extend handles capacity
// growth and length updates safely.
fn append(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) {
    dst.extend(iter);
}

Was Rusts Garantie tatsächlich besagt

Die präzise Garantie lautet: Safe Rust kann kein undefiniertes Verhalten verursachen, sofern der zugrunde liegende unsafe-Code sound ist. Die Rust Reference definiert Soundness an der Schnittstelle: unsafe-Code muss verhindern, dass safe-Clients UB auslösen. Der Compiler überprüft nicht alle Sicherheitsbedingungen einer unsafe-Operation; Entwickler und Reviewer müssen sie begründen.

Das ist weiterhin ein großer praktischer Vorteil gegenüber C und C++: Rust macht speicherkritische Operationen und ihre Pflichten sichtbarer, sodass Reviews unsafe-Schnittstellen und darauf aufbauende safe-Abstraktionen prüfen können. Dazu gehören unsafe-Blöcke, Verträge von unsafe-Funktionen, unsafe-Trait-Implementierungen, FFI-Deklarationen und unsafe-Attribute; sie liegen nicht immer in einem einzigen kleinen Block. Ein konkreter Fall ist CVE-2021-25900 in smallvec: SmallVec::insert_many reservierte anhand der unteren Iterator-Größenschätzung und schrieb mehr Elemente, als in die Allokation passten; bewertet mit CVSS 9.8.

Soundness ist der Maßstab, nicht bloße Sorgfalt

Eine API ist unsound, wenn safe-Code sie verwenden kann, um undefiniertes Verhalten auszulösen. Iterator::size_hint liefert etwa für Optimierungen nützliche Grenzen, aber keine exakte Anzahl: Ein korrekter Iterator kann mehr Elemente liefern als die Untergrenze, ein fehlerhafter Iterator kann seine Schätzung verletzen. Die Dokumentation sagt ausdrücklich, dass der Hinweis nicht dazu dienen darf, Bounds-Checks in unsafe-Code wegzulassen. Fehlerhaftes Verhalten eines safe-Traits kann auch Logikfehler oder Panics verursachen; das ist nicht automatisch UB. Unsafe-Code muss Speicherinvarianten dennoch erhalten, wenn Aufrufercode unerwartet reagiert.

Unsafe-Code ist daher nicht lokal begrenzt: Safe-APIs können von einer Speicherinvariante abhängen, die an anderer Stelle hergestellt wird. Ist sie falsch, liegt der Fehler in der Abstraktion, die die safe-API bereitstellt, nicht bei safe-Aufrufern, die den Vertrag einhalten.

Praktische Regeln

Häufige Fragen

Kann ich unsafe vollständig verbieten?

#![forbid(unsafe_code)] kann unsafe-Syntax in dem Crate verbieten, in dem es aktiviert ist, und eignet sich für Anwendungs-Crates. Abhängigkeiten sind davon nicht betroffen; manche Implementierungen, FFI-Grenzen und Bibliotheken benötigen unsafe-Operationen.

Kann unsafe-Code einem Trait vertrauen?

Ja. Ein unsafe trait dokumentiert Pflichten, die Implementierungen erfüllen müssen; unsafe-Code darf sich darauf stützen. TrustedLen ist ein instabiles Standardbibliotheks-Trait für Iteratoren, die eine exakte Größenangabe versprechen. Iterator::size_hint bietet diese Garantie nicht.

Schwächt das die Argumente für Rust?

Nein. Rusts Sicherheitsmodell verringert die Stellen, an denen Speicher-Sicherheitspflichten entstehen, und macht sie an unsafe-Schnittstellen sichtbar. Unsafe-Code, Abhängigkeiten, FFI-Verträge und safe-APIs auf unsafe-Implementierungen müssen weiterhin geprüft werden.

Referenzen

Siehe auch Ist strncpy() sicherer als strcpy()? CWE vs. CVE: Was ist der Unterschied? Mass Assignment: Risiken und sichere Feldfreigaben Serverless State Bleed: Benutzerdaten pro Anfrage isolieren PHP == und Magic-Hash-Vergleiche