Верификация оптимизаций LuaJIT с помощью SMT

JIT-компиляторы - сердце современных динамических языков. LuaJIT, например, используется в Tarantool, OpenResty и игровых движках. Но оптимизации JIT могут содержать ошибки, которые проявляются редко и коварно. Как автоматически проверить, что оптимизированный код работает так же, как неоптимизированный?

Мы разработали ljopt - инструмент для верификации оптимизаций LuaJIT методом ограниченной трансляционной валидации (bounded translation validation). Подход прост: запускаем одну и ту же Lua-программу дважды - с оптимизациями и без них. Записываем внутренние IR-трассы компилятора (через штатный jit.util), транслируем их в логические формулы SMT-LIB и передаём SMT-решателю (например Z3). Если формула выполнима (SAT), то найден контрпример и значит есть ошибка в оптимизации или трансляция была выполнена некорректно. Если невыполнима (UNSAT), то оптимизация корректна в пределах модели.

Что удалось сделать?

  • Поддержали 78 из 96 IR-инструкций LuaJIT: арифметика, побитовые операции, работа с таблицами, преобразования типов, вызовы математических функций.
  • Модель памяти на основе трёхмерных массивов SMT, учитывающая мутабельные таблицы Lua и разделение на локальную и нелокальную память.
  • Пробрасывание констант через SMT-контекст - это позволило точно верифицировать свёртку вызовов math.sin() и других трансцендентных функций.
  • Реализовали развёртку циклов (loop unrolling) вместо замыканий Хорна из-за ограничений SMT-решателей на смеси теорий.

Результаты на реальных багах LuaJIT

Протестировано 33 известные регрессионные ошибки LuaJIT.

3 ошибки обнаружены методом SMT-верификации: LJ#783 (некорректная свёртка x - (-0.0)), LJ#791 (ошибка конкатенации строк), LJ#1079 (64-битные побитовые сдвиги). 1 ранее неизвестная ошибка найдена (LJ#1432). 2 ошибки обнаружены в самом SMT-решателе Z3 (Z3#9488, Z3#9234) и исправлены мейнтейнерами.

Инструмент не требует модификации компилятора - работает как внешний наблюдатель через jit.attach().

Ограничения: не поддерживаются вызовы произвольных C-функций (CALLS, CALLA), проверка границ массивов (ABC) и метаметоды. Также не ловит ошибки, приводящие к краху самого компилятора (например, переполнение стека в оптимизаторе) - для этого нужны фаззинг и санитайзеры.

Насколько нам известно, ljopt - первая работающая система SMT-валидации трансляции для промышленного трассового JIT-компилятора.

Проект может быть полезен всем, кто использует LuaJIT в продакшене (Tarantool, OpenResty) и хочет повысить доверие к оптимизациям.

Код проекта распространяется под лицензией MIT - ligurio/ljopt.

Теги: draft