
Comprobador de duraciones y otros tipos de refinamiento
Comprobador de
Lifetimes (vida útil) y otros
Refinement types (tipos de refinamiento)
para Zig
Vídeo: https://www.youtube.com/watch?v=mf0WzTOe-40 Patrocinio: https://buymeacoffee.com/dnautics
discutir en hn: https://news.ycombinator.com/item?id=42923829
discutir en lobste.rs: https://lobste.rs/s/9sitsj/clr_checker_for_lifetimes_other
vídeo de demostración en vivo: https://www.youtube.com/watch?v=ZY_Z-aGbYm8
Este proyecto crea un transpilador Zig para el compilador Zig, que transforma AIR (Intermediate Representation Abstracta) en código fuente Zig que realiza análisis estático en tiempo de compilación. El analizador generado detecta problemas de seguridad de memoria como uso antes de asignación, uso después de liberación, fugas de puntero de pila, así como UB específicos de Zig como aserciones de no nulidad, violaciones de uniones etiquetadas o mal uso de fieldParentPtr.
El objetivo es aportar garantías de seguridad de memoria al nivel de Rust a Zig mediante análisis estático de AIR, sin cambiar el lenguaje en sí.
CLR depende de una versión bifurcada del compilador Zig (incluida como submódulo en zig/) que agrega soporte para enrutar AIR a complementos externos. Cuando se invoca con -ofmt=air -fair-out=<plugin.so>, el compilador carga la biblioteca compartida especificada y le pasa el AIR generado para su procesamiento.
CLR está diseñado para empujar los programas hacia patrones de ciclo de vida que sean explícitos y verificables localmente, no simplemente para reconocer todo programa Zig técnicamente válido. Cuando dos representaciones son posibles, CLR prefiere aquella que hace visible el estado del recurso en el tipo y en la estructura de flujo de control.
Por ejemplo, evite cerrar condicionalmente un descriptor de archivo no opcional:
const file = try std.fs.cwd().openFile(path, .{});
if (should_close) {
file.close(); // Mal: el archivo queda ambiguamente abierto después de esta rama.
}
Prefiera representar la propiedad condicional con un opcional:
var file: ?std.fs.File = null;
if (should_open) {
file = try std.fs.cwd().openFile(path, .{});
}
if (file) |open_file| {
open_file.close();
}
Cerrar condicionalmente un descriptor no opcional deja su ciclo de vida ambiguo después de la rama. La política prevista de CLR es rechazar ese patrón en lugar de llevar un estado permanente de "tal vez cerrado".
El mismo principio se aplica a los punteros asignados. No libere a través de un puntero derivado:
const allocation = try allocator.alloc(u8, size);
const payload = allocation[header_size..];
allocator.free(payload); // Mal: payload no es la base de la asignación.
Mantenga disponible el puntero base de la asignación para la desasignación, y use los punteros derivados solo para acceso:
const allocation = try allocator.alloc(u8, size);
defer allocator.free(allocation);
const payload = allocation[header_size..];
use(payload);
Liberar un puntero de campo, subsegmento o puntero producido por aritmética es rechazado a menos que una regla interna documentada restablezca la procedencia de la base de asignación.
Estas políticas son estrictas por defecto porque producen código con ciclos de vida de recursos
más simples y más revisables. Un mecanismo futuro de anotación unsafe permitirá
que GIDs u operaciones seleccionadas opten por no participar en análisis individuales. Eso
respaldará código que acepta deliberadamente comprobaciones más débiles a cambio de
rendimiento, sin debilitar el modelo predeterminado para el resto del programa.
Esta es una reescritura activa del prototipo original basado en Elixir en Zig. La implementación en Zig se carga como un complemento del compilador y analiza AIR directamente.
Actualmente implementado:
std.mem.Allocator:
create/destroy - asignación de un solo elementoalloc/free - asignación de segmentos (incluyendo alignedAlloc, allocSentinel, etc.)realloc/remap - reasignación de segmentos con seguimiento de segmento anterior liberadodupe/dupeZ - duplicación de segmentosPlaneado (consulte LIMITATIONS.md para más detalles):
sudo apt install batsEl compilador Zig incluido y el complemento libclr deben compilarse con niveles de optimización coincidentes. Los niveles de optimización no coincidentes causarán fallos de segmentación.
# Compilar el compilador Zig personalizado con ReleaseFast (solo la primera vez, o después de cambios en el submódulo)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseFast && cd ..
# Compilar el complemento CLR con optimización coincidente
zig build -Doptimize=ReleaseFast
Para desarrollo/depuración, use ReleaseSafe o Debug para ambos:
# ReleaseSafe (con comprobaciones de seguridad, ligeramente más lento)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseSafe && cd ..
zig build -Doptimize=ReleaseSafe
# Debug (información de depuración completa, más lento)
cd zig && zig build --zig-lib-dir lib && cd ..
zig build
# Compilar un archivo Zig usando el 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
# Ejecutar el analizador generado
zig run --dep clr -Mroot=output.air.zig -Mclr=lib/lib.zig
La salida va a stderr.
# Pruebas unitarias (código generado/DLL)
zig build test
# Pruebas unitarias (biblioteca en tiempo de ejecución)
zig test lib/lib.zig
# Un archivo de prueba de integración enfocado
bats test/integration/fd.bats
# Pruebas de integración (requiere BATS)
# Por defecto ReleaseFast; anular con OPTIMIZE=ReleaseSafe o OPTIMIZE=Debug
./run_integration.sh
# Prueba manual de un solo archivo
./run_one.sh test/cases/undefined/use_before_assign.zig
Nota: Las pruebas de integración reconstruyen libclr con el nivel de optimización especificado (por defecto: ReleaseFast). Asegúrese de que su compilador Zig incluido se haya compilado con un nivel de optimización coincidente.
clr/
├── src/ # Código DLL/complemento (genera .air.zig)
│ ├── clr.zig # Punto de entrada principal del complemento CLR
│ ├── codegen.zig # Genera código fuente .air.zig a partir de instrucciones AIR
│ └── allocator.zig # Envoltorio de asignador seguro para DLL
├── lib/ # Biblioteca de análisis en tiempo de ejecución
│ ├── lib.zig # Punto de entrada de la biblioteca
│ ├── tag.zig # Unión AnyTag, Tipo, manejadores de etiquetas, envío splat
│ ├── Inst.zig # Resultados de instrucciones y análisis interprocedimental
│ ├── Refinements.zig # Tipos de refinamiento (puntero, estructura, opcional, etc.)
│ ├── Analyte.zig # Contenedor de estado de análisis
│ ├── Context.zig # Contexto de ejecución (metadatos, informes de errores)
│ └── analysis/ # Módulos de análisis
│ ├── undefined_safety.zig # Seguimiento de uso antes de asignación
│ ├── memory_safety.zig # Seguimiento de asignación/liberación
│ ├── null_safety.zig # Comprobación de desenvolvimiento opcional
│ ├── variant_safety.zig # Acceso a campos de unión etiquetada
│ └── fd_safety.zig # Seguimiento de descriptores de archivo
├── test/
│ ├── integration/ # Pruebas de integración BATS
│ │ ├── test_helper.bash
│ │ └── *.bats
│ └── cases/ # Archivos de entrada de prueba (.zig)
├── zig/ # Submódulo del compilador Zig (bifurcación instrumentada)
├── build.zig # Configuración de compilación
└── build.zig.zon # Dependencias del paquete

Zig es un lenguaje famosamente "inseguro". La gestión de memoria se realiza manualmente, lo que abre la posibilidad de errores de implementación. Si bien Zig reduce los problemas de seguridad en relación con C al eliminar el acceso a matrices fuera de límites y la desreferencia de punteros nulos en código con comprobaciones de seguridad, sigue siendo menos seguro que Rust, que elimina el uso después de liberación, la doble liberación y las condiciones de carrera mediante análisis estático.
Inspirado en el proyecto MIRI de Rust, CLR realiza análisis estático en la representación intermedia AIR de Zig para lograr un mayor grado de seguridad del que Zig proporciona de serie. A diferencia de MIRI, que interpreta el MIR de Rust en un pseudoejecutable en caja de arena, CLR transpila AIR a código fuente Zig que ejecuta análisis estáticamente. Tenga en cuenta que el código zig de salida de AIR de CLR podría, en principio, ejecutarse en tiempo de compilación, pero al pasar a través de un intermediario zig, producimos un flujo lógico fácil de entender y depurar. Una persona ambiciosa podría usar este enfoque general para emitir un destino de salida diferente, como un lenguaje de asistente de pruebas, o refactorizarlo para que se ejecute completamente en el compilador zig.
La idea clave: si de todas formas necesita MIRI para proyectos de Rust que requieren seguridad, ¿por qué no elegir un lenguaje más simple y hacer un análisis al estilo MIRI para obtener comprobación de préstamos y otros análisis de tipos de refinamiento? Este proyecto muestra que ese futuro es una posibilidad real para Zig.
El pipeline de compilación de Zig es:
AIR es el nivel ideal para el análisis porque está tipificado, es interpretable como una lista "mínima viable" de instrucciones de programación generalizadas y permite extender tipos con metadatos de refinamiento.
Para una mirada en profundidad sobre cómo funciona AIR de Zig, consulte la publicación del blog de Mitchell Hashimoto: https://mitchellh.com/zig/sema
Licencia MIT - consulte LICENSE para más detalles.
initdeinitallocatorstd.process.args,
std.mem.asBytes, std.HashMap y APIs de asignador/archivostd.HashMap con identidad de metadatos canónicos/almacenamiento clave/valor
a través de put, get, getPtr e iteración de valoresposix.open/close/dup/dup2/socket/accept/epoll_create/pipe