
Controllore per Lifetimes e altri tipi di raffinamento
Controllore di
Lifetime e altri
Raffinamenti di tipo
per Zig
Video: https://www.youtube.com/watch?v=mf0WzTOe-40 Sponsorizza: https://buymeacoffee.com/dnautics
discuti su HN: https://news.ycombinator.com/item?id=42923829
discuti su lobste.rs: https://lobste.rs/s/9sitsj/clr_checker_for_lifetimes_other
video demo dal vivo: https://www.youtube.com/watch?v=ZY_Z-aGbYm8
Questo progetto crea un transpiler Zig per il compilatore Zig, che trasforma AIR (Abstract Intermediate Representation) in codice sorgente Zig che esegue analisi statica in fase di compilazione. L'analizzatore generato rileva problemi di sicurezza della memoria come use-before-assignment, use-after-free, stack pointer escapes, oltre a UB specifici di Zig come asserzioni di non-nullità, violazioni di union taggate o uso improprio di fieldParentPtr.
L'obiettivo è portare le garanzie di sicurezza della memoria a livello di Rust a Zig tramite analisi statica di AIR, senza modificare il linguaggio stesso.
CLR dipende da una versione fork del compilatore Zig (inclusa come sottomodulo in zig/) che aggiunge il supporto per instradare AIR a plugin esterni. Quando invocato con -ofmt=air -fair-out=<plugin.so>, il compilatore carica la libreria condivisa specificata e passa l'AIR generato per l'elaborazione.
CLR ha lo scopo di spingere i programmi verso pattern di ciclo di vita che siano espliciti e verificabili localmente, non semplicemente riconoscere ogni programma Zig tecnicamente valido. Quando sono possibili due rappresentazioni, CLR preferisce quella che rende lo stato delle risorse visibile nel tipo e nella struttura del flusso di controllo.
Ad esempio, evita di chiudere condizionalmente un descrittore di file non opzionale:
const file = try std.fs.cwd().openFile(path, .{});
if (should_close) {
file.close(); // Male: file è ambiguamente aperto dopo questo ramo.
}
Preferisci rappresentare la proprietà condizionale con un optional:
var file: ?std.fs.File = null;
if (should_open) {
file = try std.fs.cwd().openFile(path, .{});
}
if (file) |open_file| {
open_file.close();
}
Chiudere condizionalmente un descrittore non opzionale lascia il suo ciclo di vita ambiguo dopo il ramo. La politica prevista da CLR è di rifiutare questo pattern piuttosto che portare uno stato permanente "forse chiuso".
Lo stesso principio si applica ai puntatori allocati. Non liberare tramite un puntatore derivato:
const allocation = try allocator.alloc(u8, size);
const payload = allocation[header_size..];
allocator.free(payload); // Male: payload non è la base dell'allocazione.
Mantieni il puntatore base dell'allocazione disponibile per la deallocazione, e usa i puntatori derivati solo per l'accesso:
const allocation = try allocator.alloc(u8, size);
defer allocator.free(allocation);
const payload = allocation[header_size..];
use(payload);
Liberare un puntatore a campo, sottosezione o puntatore prodotto da aritmetica è rifiutato a meno che una regola interna documentata non ripristini la provenienza della base di allocazione.
Queste politiche sono rigorose per impostazione predefinita perché producono codice con cicli di vita delle risorse più semplici e revisionabili. Un futuro meccanismo di annotazione unsafe permetterà a determinati GID o operazioni di rinunciare a singole analisi. Ciò supporterà codice che accetta deliberatamente controlli più deboli in cambio di prestazioni, senza indebolire il modello predefinito per il resto del programma.
Questa è una riscrittura attiva del proof-of-concept originale basato su Elixir in Zig. L'implementazione Zig si carica come plugin del compilatore e analizza AIR direttamente.
Attualmente implementato:
std.mem.Allocator:
create/destroy - allocazione di un singolo elementoalloc/free - allocazione di slice (incluse alignedAlloc, allocSentinel, ecc.)realloc/remap - riallocazione di slice con tracciamento della vecchia slice liberatadupe/dupeZ - duplicazione di sliceinit// - ciclo di vita completo dell'arenaPianificato (vedi LIMITATIONS.md per dettagli):
sudo apt install batsIl compilatore Zig fornito e il plugin libclr devono essere compilati con livelli di ottimizzazione corrispondenti. Livelli di ottimizzazione non corrispondenti causeranno segfault.
# Build the custom Zig compiler with ReleaseFast (first time only, or after submodule changes)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseFast && cd ..
# Build the CLR plugin with matching optimization
zig build -Doptimize=ReleaseFast
Per sviluppo/debug, usa ReleaseSafe o Debug per entrambi:
# ReleaseSafe (with safety checks, slightly slower)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseSafe && cd ..
zig build -Doptimize=ReleaseSafe
# Debug (full debug info, slowest)
cd zig && zig build --zig-lib-dir lib && cd ..
zig build
# Compila un file Zig usando il backend AIR
zig/zig-out/bin/zig build-exe -fair-out=zig-out/lib/libclr.so -ofmt=air -femit-bin=output.air.zig your_file.zig
# Esegui l'analizzatore generato
zig run --dep clr -Mroot=output.air.zig -Mclr=lib/lib.zig
L'output va su stderr.
# Test unitari (codegen/DLL)
zig build test
# Test unitari (libreria runtime)
zig test lib/lib.zig
# Un file di test di integrazione specifico
bats test/integration/fd.bats
# Test di integrazione (richiede BATS)
# Predefinito a ReleaseFast; sovrascrivi con OPTIMIZE=ReleaseSafe o OPTIMIZE=Debug
./run_integration.sh
# Test manuale di un singolo file
./run_one.sh test/cases/undefined/use_before_assign.zig
Nota: I test di integrazione ricostruiscono libclr con il livello di ottimizzazione specificato (predefinito: ReleaseFast). Assicurati che il compilatore Zig fornito sia stato compilato con un livello di ottimizzazione corrispondente.
clr/
├── src/ # Codice DLL/plugin (genera .air.zig)
│ ├── clr.zig # Punto di ingresso principale del plugin CLR
│ ├── codegen.zig # Genera codice sorgente .air.zig dalle istruzioni AIR
│ └── allocator.zig # Wrapper allocator sicuro per DLL
├── lib/ # Libreria di analisi runtime
│ ├── lib.zig # Punto di ingresso della libreria
│ ├── tag.zig # Unione AnyTag, Type, gestori di tag, invio splat
│ ├── Inst.zig # Risultati delle istruzioni e analisi interprocedurale
│ ├── Refinements.zig # Tipi di raffinamento (puntatore, struct, optional, ecc.)
│ ├── Analyte.zig # Contenitore dello stato di analisi
│ ├── Context.zig # Contesto di esecuzione (metadati, segnalazione errori)
│ └── analysis/ # Moduli di analisi
│ ├── undefined_safety.zig # Tracciamento use-before-assign
│ ├── memory_safety.zig # Tracciamento allocazione/liberazione
│ ├── null_safety.zig # Controllo unwrap di optional
│ ├── variant_safety.zig # Accesso ai campi di union taggate
│ └── fd_safety.zig # Tracciamento dei descrittori di file
├── test/
│ ├── integration/ # Test di integrazione BATS
│ │ ├── test_helper.bash
│ │ └── *.bats
│ └── cases/ # File di input dei test (.zig)
├── zig/ # Sottomodulo del compilatore Zig (fork strumentato)
├── build.zig # Configurazione di compilazione
└── build.zig.zon # Dipendenze dei pacchetti

Zig è un linguaggio notoriamente "unsafe". La gestione della memoria è manuale, il che apre la possibilità di errori di implementazione. Mentre Zig riduce i problemi di sicurezza rispetto a C eliminando l'accesso fuori dai limiti degli array e il dereferenziamento di puntatori nulli nel codice con controlli di sicurezza, è comunque meno sicuro di Rust, che elimina use-after-free, double-free e data race tramite analisi statica.
Ispirato dal progetto MIRI di Rust, CLR esegue analisi statica sulla rappresentazione intermedia AIR di Zig per raggiungere un grado di sicurezza maggiore di quello offerto da Zig di default. A differenza di MIRI, che interpreta il MIR di Rust in un pseudo-runtime in sandbox, CLR transpila AIR in codice sorgente Zig che esegue analisi in modo statico. Nota che il codice zig di output di CLR potrebbe in linea di principio essere eseguito in fase di compilazione, ma passando attraverso un intermedio zig, produciamo un flusso logico facile da capire e da debuggare. Una persona ambiziosa potrebbe usare questo approccio generale per emettere un target di output diverso, come un linguaggio di assistenza alla dimostrazione, o rifattorizzarlo per essere eseguito interamente nel compilatore zig!
Il concetto chiave: se comunque hai bisogno di MIRI per progetti Rust attenti alla sicurezza, perché non scegliere un linguaggio più semplice e fare analisi in stile MIRI per ottenere borrow checking e altri tipi di analisi di raffinamento? Questo progetto mostra che un tale futuro è una possibilità reale per Zig.
La pipeline di compilazione di Zig è:
AIR è il livello ideale per l'analisi perché è tipizzato, interpretabile come una lista "minima vitale" di istruzioni di programmazione generalizzate, e permette di estendere i tipi con metadati di raffinamento.
Per uno sguardo approfondito su come funziona AIR di Zig, vedi il post sul blog di Mitchell Hashimoto: https://mitchellh.com/zig/sema
Licenza MIT - vedi LICENSE per i dettagli.
deinitallocatorstd.process.args, std.mem.asBytes, std.HashMap e API di allocator/filestd.HashMap con identità canonica di archiviazione metadati/chiave/valore attraverso put, get, getPtr e iterazione dei valoriposix.open/close/dup/dup2/socket/accept/epoll_create/pipe