
Проверка времен жизни и других уточняющих типов
Проверка
времени жизни и других
уточняющих типов
для Zig
Видео: https://www.youtube.com/watch?v=mf0WzTOe-40 Спонсорство: https://buymeacoffee.com/dnautics
обсуждение на hn: https://news.ycombinator.com/item?id=42923829
обсуждение на lobste.rs: https://lobste.rs/s/9sitsj/clr_checker_for_lifetimes_other
видео с демонстрацией: https://www.youtube.com/watch?v=ZY_Z-aGbYm8
Этот проект создаёт транспилятор Zig для компилятора Zig, который преобразует AIR (абстрактное промежуточное представление) в исходный код Zig, выполняющий статический анализ во время компиляции. Сгенерированный анализатор выявляет проблемы безопасности памяти, такие как использование до присваивания, использование после освобождения, утечки указателей на стек, а также специфические для Zig UB, такие как утверждения о ненулевости, нарушения размеченных объединений или неправильное использование fieldParentPtr.
Цель — обеспечить гарантии безопасности памяти на уровне Rust в Zig с помощью статического анализа AIR, без изменения самого языка.
CLR зависит от форкнутой версии компилятора Zig (включённой как подмодуль в zig/), которая добавляет поддержку маршрутизации AIR во внешние плагины. При вызове с -ofmt=air -fair-out=<plugin.so> компилятор загружает указанную разделяемую библиотеку и передаёт ей сгенерированный AIR для обработки.
CLR предназначен для продвижения программ к шаблонам жизненного цикла, которые являются явными и локально проверяемыми, а не просто для распознавания каждого технически корректного Zig-программы. Когда возможны два представления, CLR предпочитает то, которое делает состояние ресурса видимым в типе и структуре потока управления.
Например, избегайте условного закрытия не-опционального файлового дескриптора:
const file = try std.fs.cwd().openFile(path, .{});
if (should_close) {
file.close(); // Плохо: после этой ветки file неоднозначно открыт.
}
Предпочтительнее представлять условное владение с помощью опционального типа:
var file: ?std.fs.File = null;
if (should_open) {
file = try std.fs.cwd().openFile(path, .{});
}
if (file) |open_file| {
open_file.close();
}
Условное закрытие не-опционального дескриптора оставляет его жизненный цикл неоднозначным после ветки. Предполагаемая политика CLR — отклонять такой шаблон, а не нести постоянное состояние «возможно закрыто».
Тот же принцип применим к выделенным указателям. Не освобождайте память через производный указатель:
const allocation = try allocator.alloc(u8, size);
const payload = allocation[header_size..];
allocator.free(payload); // Плохо: payload — это не базовый адрес выделения.
Держите указатель на базовый адрес выделения доступным для освобождения, а производные указатели используйте только для доступа:
const allocation = try allocator.alloc(u8, size);
defer allocator.free(allocation);
const payload = allocation[header_size..];
use(payload);
Освобождение указателя на поле, подмассива или указателя, полученного арифметикой, отклоняется, если документированное внутреннее правило не восстанавливает происхождение от базового выделения.
Эти политики по умолчанию строги, поскольку они создают код с более простыми и легко проверяемыми жизненными циклами ресурсов. В будущем механизм unsafe аннотаций позволит отдельным GID или операциям отказываться от отдельных анализов. Это поддержит код, который намеренно принимает более слабые проверки в обмен на производительность, не ослабляя модель по умолчанию для остальной программы.
Это активная переработка исходного прототипа на Elixir в Zig. Реализация на Zig загружается как плагин компилятора и анализирует AIR напрямую.
На данный момент реализовано:
std.mem.Allocator:
create/destroy — выделение одного элементаalloc/free — выделение среза (включая alignedAlloc, allocSentinel и т.д.)realloc/remap — перевыделение среза с отслеживанием освобождения старого срезаdupe/dupeZ — дублирование срезаinit// — полный жизненный цикл ареныЗапланировано (подробнее см. LIMITATIONS.md):
sudo apt install batsПоставляемый компилятор Zig и плагин libclr должны быть собраны с одинаковыми уровнями оптимизации. Несовпадение уровней оптимизации приведёт к segfault.
# Сборка пользовательского компилятора Zig с ReleaseFast (только в первый раз или после изменений подмодуля)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseFast && cd ..
# Сборка плагина CLR с соответствующей оптимизацией
zig build -Doptimize=ReleaseFast
Для разработки/отладки используйте ReleaseSafe или Debug для обоих:
# ReleaseSafe (с проверками безопасности, немного медленнее)
cd zig && zig build --zig-lib-dir lib -Doptimize=ReleaseSafe && cd ..
zig build -Doptimize=ReleaseSafe
# Debug (полная отладочная информация, самый медленный)
cd zig && zig build --zig-lib-dir lib && cd ..
zig build
# Компиляция Zig-файла с помощью бэкенда 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
# Запуск сгенерированного анализатора
zig run --dep clr -Mroot=output.air.zig -Mclr=lib/lib.zig
Вывод идёт в stderr.
# Модульные тесты (codegen/DLL)
zig build test
# Модульные тесты (библиотека времени выполнения)
zig test lib/lib.zig
# Отдельный интеграционный тестовый файл
bats test/integration/fd.bats
# Интеграционные тесты (требуется BATS)
# По умолчанию ReleaseFast; можно переопределить через OPTIMIZE=ReleaseSafe или OPTIMIZE=Debug
./run_integration.sh
# Ручной тест отдельного файла
./run_one.sh test/cases/undefined/use_before_assign.zig
Примечание: Интеграционные тесты пересобирают libclr с указанным уровнем оптимизации (по умолчанию: ReleaseFast). Убедитесь, что ваш поставляемый компилятор Zig собран с таким же уровнем оптимизации.
clr/
├── src/ # Код DLL/плагина (генерирует .air.zig)
│ ├── clr.zig # Главная точка входа плагина CLR
│ ├── codegen.zig # Генерирует исходный код .air.zig из инструкций AIR
│ └── allocator.zig # Аллокаторная обёртка, безопасная для DLL
├── lib/ # Библиотека анализа времени выполнения
│ ├── lib.zig # Точка входа библиотеки
│ ├── tag.zig # AnyTag union, Type, обработчики тегов, диспетчеризация splat
│ ├── Inst.zig # Результаты инструкций и межпроцедурный анализ
│ ├── Refinements.zig # Уточняющие типы (указатель, структура, опционал и т.д.)
│ ├── Analyte.zig # Контейнер состояния анализа
│ ├── Context.zig # Контекст выполнения (метаданные, отчёт об ошибках)
│ └── analysis/ # Модули анализа
│ ├── undefined_safety.zig # Отслеживание использования до присваивания
│ ├── memory_safety.zig # Отслеживание выделения/освобождения
│ ├── null_safety.zig # Проверка распаковывания опционала
│ ├── variant_safety.zig # Доступ к полям размеченного объединения
│ └── fd_safety.zig # Отслеживание файловых дескрипторов
├── test/
│ ├── integration/ # Интеграционные тесты BATS
│ │ ├── test_helper.bash
│ │ └── *.bats
│ └── cases/ # Входные файлы тестов (.zig)
├── zig/ # Подмодуль компилятора Zig (инструментированный форк)
├── build.zig # Конфигурация сборки
└── build.zig.zon # Зависимости пакета

Zig — это famously «небезопасный» язык. Управление памятью осуществляется вручную, что открывает возможность ошибок реализации. Хотя Zig снижает проблемы безопасности относительно C за счёт устранения выхода за границы массива и разыменования нулевого указателя в коде с проверками безопасности, он всё ещё менее безопасен, чем Rust, который устраняет использование после освобождения, двойное освобождение и гонки данных с помощью статического анализа.
Вдохновлённый проектом MIRI от Rust, CLR выполняет статический анализ промежуточного представления AIR в Zig для достижения более высокой степени безопасности, чем предоставляет Zig «из коробки». В отличие от MIRI, который интерпретирует MIR Rust в изолированной псевдо-среде выполнения, CLR транспилирует AIR в исходный код Zig, который статически выполняет анализ. Обратите внимание, что сгенерированный воздушный код Zig в принципе может быть выполнен во время компиляции, но, пропуская его через промежуточный Zig, мы получаем легко понимаемый и отлаживаемый логический поток. Амбициозный человек мог бы использовать этот общий подход для создания другого целевого вывода, например, языка для доказательства теорем, или реорганизовать его, чтобы он работал полностью внутри компилятора Zig!
Ключевая идея: если для проектов на Rust, ориентированных на безопасность, всё равно нужен MIRI, почему бы не выбрать более простой язык и не выполнять анализ в стиле MIRI, чтобы получить проверку заимствований и другие уточняющие типы? Этот проект показывает, что такое будущее реально для Zig.
Конвейер компиляции Zig:
AIR — идеальный уровень для анализа, поскольку он типизирован, интерпретируем как «минимально жизнеспособный» список обобщённых инструкций программирования и позволяет расширять типы с помощью метаданных уточнения.
Подробнее о том, как работает AIR в Zig, см. в блоге Mitchell Hashimoto: https://mitchellh.com/zig/sema
Лицензия MIT — подробнее см. LICENSE.
deinitallocatorstd.process.args,
std.mem.asBytes, std.HashMap, и API аллокаторов/файловstd.HashMap с канонической идентичностью метаданных/ключей/значений
при put, get, getPtr и итерации по значениямposix.open/close/dup/dup2/socket/accept/epoll_create/pipe