
Checker für Lifetimes und andere Refinement types
Checker für
Lebensdauern und andere
Raffinement-Typen
für Zig
Video: https://www.youtube.com/watch?v=mf0WzTOe-40 Sponsoring: https://buymeacoffee.com/dnautics
Diskussion auf HN: https://news.ycombinator.com/item?id=42923829
Diskussion auf lobste.rs: https://lobste.rs/s/9sitsj/clr_checker_for_lifetimes_other
Live-Demo-Video: https://www.youtube.com/watch?v=ZY_Z-aGbYm8
Dieses Projekt erstellt einen Zig-Transpiler für den Zig-Compiler, der AIR (Abstract Intermediate Representation) in Zig-Quellcode umwandelt, der zur Kompilierzeit eine statische Analyse durchführt. Der generierte Analyzer erkennt Speichersicherheitsprobleme wie Verwendung vor Zuweisung, Verwendung nach Freigabe, Stack-Zeiger-Eskapierungen sowie Zig-spezifisches UB wie Nicht-Null-Behauptungen, Verletzungen von getaggten Unions oder Missbrauch von fieldParentPtr.
Das Ziel ist es, Rust-ähnliche Speichersicherheitsgarantien für Zig durch statische Analyse von AIR zu erreichen, ohne die Sprache selbst zu ändern.
CLR hängt von einer geforkten Version des Zig-Compilers ab (als Submodul in zig/ enthalten), die das Routing von AIR an externe Plugins unterstützt. Wenn der Compiler mit -ofmt=air -fair-out=<plugin.so> aufgerufen wird, lädt er die angegebene Shared Library und übergibt das generierte AIR zur Verarbeitung.
CLR soll Programme zu Lebenszyklusmustern führen, die explizit und lokal überprüfbar sind, und nicht einfach jedes technisch gültige Zig-Programm erkennen. Wenn zwei Darstellungen möglich sind, bevorzugt CLR diejenige, die den Ressourcenzustand im Typ und in der Kontrollflussstruktur sichtbar macht.
Vermeiden Sie beispielsweise das bedingte Schließen eines nicht-optionalen Dateideskriptors:
const file = try std.fs.cwd().openFile(path, .{});
if (should_close) {
file.close(); // Schlecht: file ist nach diesem Zweig mehrdeutig offen.
}
Bevorzugen Sie die Darstellung bedingter Eigentümerschaft mit einem Optional:
var file: ?std.fs.File = null;
if (should_open) {
file = try std.fs.cwd().openFile(path, .{});
}
if (file) |open_file| {
open_file.close();
}
Das bedingte Schließen eines nicht-optionalen Deskriptors lässt seinen Lebenszyklus nach dem Zweig mehrdeutig. CLRs beabsichtigte Richtlinie ist es, dieses Muster abzulehnen, anstatt einen permanenten "vielleicht geschlossen"-Zustand zu tragen.
Das gleiche Prinzip gilt für allozierte Zeiger. Geben Sie keinen Speicher über einen abgeleiteten Zeiger frei:
const allocation = try allocator.alloc(u8, size);
const payload = allocation[header_size..];
allocator.free(payload); // Schlecht: payload ist nicht die Allokationsbasis.
Halten Sie den Allokationsbasis-Zeiger für die Freigabe verfügbar und verwenden Sie abgeleitete Zeiger nur für den Zugriff:
const allocation = try allocator.alloc(u8, size);
defer allocator.free(allocation);
const payload = allocation[header_size..];
use(payload);
Das Freigeben eines Feldzeigers, Teil-Slices oder eines durch Arithmetik erzeugten Zeigers wird abgelehnt, es sei denn, eine dokumentierte interne Regel stellt die Herkunft der Allokationsbasis wieder her.
Diese Richtlinien sind standardmäßig streng, da sie Code mit einfacheren, besser überprüfbaren Ressourcenlebenszyklen erzeugen. Ein zukünftiger unsafe-Annotationsmechanismus wird es ausgewählten GIDs oder Operationen ermöglichen, sich von einzelnen Analysen abzumelden. Dies wird Code unterstützen, der bewusst schwächere Prüfungen zugunsten der Leistung akzeptiert, ohne das Standardmodell für den Rest des Programms zu schwächen.
Dies ist eine aktive Neufassung des ursprünglichen auf Elixir basierenden Proof-of-Concept in Zig. Die Zig-Implementierung wird als Compiler-Plugin geladen und analysiert AIR direkt.
Derzeit implementiert:
std.mem.Allocator-Interface:
create/destroy – Allokation einzelner Elementealloc/free – Slice-Allokation (einschließlich alignedAlloc, allocSentinel usw.)realloc/remap – Slice-Neuzuweisung mit Verfolgung alter-freigegebener Slicesdupe/dupeZ – Slice-Duplizierungcreate/destroy vs. alloc/free)init/deinit/allocator – vollständiger Arena-Lebenszyklusdeinit freigegeben (keine falsch positiven Leckmeldungen)deinit, doppeltem deinit, Allokation nach deinitptr_add/ptr_sub auf Einzelelementzeigern)std.process.args,
std.mem.asBytes, std.HashMap und Allokator-/Datei-APIsstd.HashMap-Verfeinerungen mit kanonischer Metadaten-/Schlüssel-/Wert-Speicheridentität über put, get, getPtr und Wertiteration hinwegposix.open/close/dup/dup2/socket/accept/epoll_create/pipeGeplant (Details in LIMITATIONS.md):
sudo apt install batsDer mitgelieferte Zig-Compiler und das libclr-Plugin müssen mit übereinstimmenden Optimierungsstufen gebaut werden. Nicht übereinstimmende Optimierungsstufen verursachen Segfaults.
# Den benutzerdefinierten Zig-Compiler mit ReleaseFast bauen (nur beim ersten Mal oder nach Submodul-Änderungen)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseFast && cd ..
# Das CLR-Plugin mit passender Optimierung bauen
zig build -Doptimize=ReleaseFast
Für Entwicklung/Debugging verwenden Sie ReleaseSafe oder Debug für beide:
# ReleaseSafe (mit Sicherheitsüberprüfungen, etwas langsamer)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseSafe && cd ..
zig build -Doptimize=ReleaseSafe
# Debug (vollständige Debug-Informationen, am langsamsten)
cd zig && zig build --zig-lib-dir lib && cd ..
zig build
# Eine Zig-Datei mit dem AIR-Backend kompilieren
zig/zig-out/bin/zig build-exe -fair-out=zig-out/lib/libclr.so -ofmt=air -femit-bin=output.air.zig your_file.zig
# Den generierten Analyzer ausführen
zig run --dep clr -Mroot=output.air.zig -Mclr=lib/lib.zig
Die Ausgabe erfolgt auf stderr.
# Unit-Tests (Codegen/DLL)
zig build test
# Unit-Tests (Runtime-Bibliothek)
zig test lib/lib.zig
# Eine gezielte Integrationstest-Datei
bats test/integration/fd.bats
# Integrationstests (erfordert BATS)
# Standardmäßig ReleaseFast; überschreiben mit OPTIMIZE=ReleaseSafe oder OPTIMIZE=Debug
./run_integration.sh
# Manueller Test einer einzelnen Datei
./run_one.sh test/cases/undefined/use_before_assign.zig
Hinweis: Integrationstests bauen libclr mit der angegebenen Optimierungsstufe neu (Standard: ReleaseFast). Stellen Sie sicher, dass Ihr mitgelieferter Zig-Compiler mit einer passenden Optimierungsstufe gebaut wurde.
clr/
├── src/ # DLL/Plugin-Code (generiert .air.zig)
│ ├── clr.zig # Haupt-Einstiegspunkt des CLR-Plugins
│ ├── codegen.zig # Generiert .air.zig-Quellcode aus AIR-Instruktionen
│ └── allocator.zig # DLL-sicherer Allokator-Wrapper
├── lib/ # Runtime-Analyse-Bibliothek
│ ├── lib.zig # Bibliothekseinstiegspunkt
│ ├── tag.zig # AnyTag-Union, Type, Tag-Handler, Splat-Dispatch
│ ├── Inst.zig # Instruktionsergebnisse und interprozedurale Analyse
│ ├── Refinements.zig # Verfeinerungstypen (Zeiger, Struktur, Optional usw.)
│ ├── Analyte.zig # Analyse-Zustandsbehälter
│ ├── Context.zig # Ausführungskontext (Metadaten, Fehlerberichterstattung)
│ └── analysis/ # Analysemodule
│ ├── undefined_safety.zig # Verwendung-vor-Zuweisung-Verfolgung
│ ├── memory_safety.zig # Allokations-/Freigabeverfolgung
│ ├── null_safety.zig # Optional-Entpackungsprüfung
│ ├── variant_safety.zig # Zugriff auf getaggte Union-Felder
│ └── fd_safety.zig # Dateideskriptor-Verfolgung
├── test/
│ ├── integration/ # BATS-Integrationstests
│ │ ├── test_helper.bash
│ │ └── *.bats
│ └── cases/ # Testeingabedateien (.zig)
├── zig/ # Zig-Compiler-Submodul (instrumentierter Fork)
├── build.zig # Build-Konfiguration
└── build.zig.zon # Paketabhängigkeiten

Zig ist bekanntermaßen eine "unsichere" Sprache. Die Speicherverwaltung erfolgt manuell, was die Möglichkeit von Implementierungsfehlern eröffnet. Während Zig die Sicherheitsprobleme im Vergleich zu C reduziert, indem es Zugriffe außerhalb der Grenzen von Arrays und Nullzeiger-Dereferenzierungen in sicherheitsgeprüftem Code eliminiert, ist es dennoch weniger sicher als Rust, das Verwendung-nach-Freigabe, doppelte Freigabe und Datenrennen durch statische Analyse eliminiert.
Inspiriert von Rusts MIRI-Projekt führt CLR eine statische Analyse von Zigs AIR-Zwischenrepräsentation durch, um ein höheres Maß an Sicherheit zu erreichen, als Zig standardmäßig bietet. Im Gegensatz zu MIRI, das Rusts MIR in einer sandboxierten Pseudo-Laufzeit interpretiert, transpiliert CLR AIR in Zig-Quellcode, der die Analyse statisch ausführt. Beachten Sie, dass der von CLR ausgegebene AIR-Zig-Code grundsätzlich zur Kompilierzeit ausgeführt werden könnte, aber durch die Verwendung eines Zig-Zwischenschritts erzeugen wir einen leicht verständlichen und leicht zu debuggenden logischen Ablauf. Ein ambitionierter Mensch könnte diesen allgemeinen Ansatz verwenden, um ein anderes Ausgabeziel zu emittieren, z. B. eine Beweisassistentensprache, oder ihn so umgestalten, dass er vollständig im Zig-Compiler läuft!
Der entscheidende Einblick: Wenn Sie für sicherheitskritische Rust-Projekte sowieso MIRI benötigen, warum dann nicht eine einfachere Sprache nehmen und eine MIRI-ähnliche Analyse durchführen, um Borrow-Checking und andere Verfeinerungstyp-Analysen zu erhalten? Dieses Projekt zeigt, dass eine solche Zukunft für Zig eine reale Möglichkeit ist.
Die Zig-Compiler-Pipeline ist:
AIR ist die ideale Ebene für Analysen, weil es typisiert ist, als "minimal realisierbare" Liste generalisierter Programmierinstruktionen interpretiert werden kann und die Erweiterung von Typen um Verfeinerungsmetadaten ermöglicht.
Für einen tiefgehenden Einblick in die Funktionsweise von Zig AIR siehe Mitchell Hashimotos Blogbeitrag: https://mitchellh.com/zig/sema
MIT-Lizenz – siehe LICENSE für Details.