
Представьте, что в основе вашего коммерческого продукта используется компонент с исходным кодом, который написан на смеси языка С и самописного ассемблера. Из-за слабой детерминированности поиск репродьюсеров сложен, а без репродьюсера мейнтейнер проекта заявляет: «Сделайте так, чтобы я про вас больше не слышал». Я расскажу, как мы построили процесс активной поддержки LuaJIT в СУБД Tarantool, сократили количество инцидентов в продакшене, сократили затраты на бэкпорт патчей из основного проекта и какую роль во всем этом сыграл фаззинг и его специфика.
Команда разработки продукта полностью отвечает за весь код этого продукта, в том числе за компоненты с открытым исходным кодом от третьих лиц. К сожалению, не все мейнтейнеры проектов с открытым исходным кодом готовы сотрудничать с разработчиками или их сотрудничество ограничивается жесткими рамками, что усложняет использование этих компонентов в коммерческих продуктах.
В СУБД Tarantool используется LuaJIT в качестве языкового рантайма, но в Tarantool используется не оригинальный проект, а его форк. Я расскажу, как мы прошли путь от пассивного использования кода LuaJIT к процессу поддержки форка, с которым количество инцидентов на продакшене установилось около нуля, сократились усилия по бэкпортингу патчей из основного проекта, а основной проект получил активных контрибьюторов.
Я рассмотрю специфику работы с проектом исходного кода на примере LuaJIT, расскажу, как устроено тестирование в нашем форке и какую роль там играет фаззинг. Расскажу о специфике фаззинга LuaJIT и о том, каких результатов мы в этом достигли за последние два года.
Знакомство с Lua и LuaJIT
Чтобы понимать контекст проблем, с которыми мы сталкивались, и способов их решения, стоит начать с базы — познакомиться с Lua и LuaJIT.
Язык Lua: Знакомство
Lua — легковесный мультипарадигменный язык программирования, который спроектирован простым и легко встраиваемым. Для Lua нет спецификации языка. Вместо нее есть основной документ — Lua Reference Manual, который является единственным источником правды в вопросах допустимого поведения языка.
Lua изредка используется как основной язык в проекте (например, Prosody, Kong) и широко используется для программирования внутренней логики приложений. Например, он используется в OpenResty, HAProxy, Redis, Roblox, Wireshark, NeoVim, mpv, VLC, Snort, Pandoc, PowerDNS, Snort, NMap, LÖVE, Darktable, WoW, Angry Birds, Adobe Lightroom, Torch, Tarantool.
PUC Rio Lua: Знакомство
PUC Rio Lua — референсная реализация языка Lua, написанная на языке C (С99). Разрабатывается с 1993 года группой из трех разработчиков университета Рио-де-Жанейро. Разработка PUC Rio Lua закрытая, но весь код доступен под свободной лицензией MIT.
Примечательно, что реализация PUC Rio Lua содержит около 30 тысяч строк, что кратно меньше, чем у других runtime-языков программирования.

LuaJIT: Знакомство
LuaJIT — одна из наиболее производительных реализаций динамического языка программирования Lua, которая комбинирует интерпретатор и трассирующий JIT-компилятор. Проект публично разрабатывается с 2005 года.
LuaJIT реализует Lua 5.1 с некоторой примесью Lua 5.2+ и является мультиплатформенным решением — есть поддержка x86/x64, ARM, ARM64, MIPS, PPC.
Как и PUC Rio Lua, LuaJIT — закрытая разработка. Но исходный код свободно доступен под лицензией MIT.
Реализация LuaJIT содержит около 80 тысяч строк кода.

У LuaJIT есть некоторая специфика.
Во-первых, проект единолично разрабатывает Майк Полл (Mike Pall), а все общение с ним ведется только через тикеты. Причем это общение не совсем открытое: тикеты без стабильного минимизированного репродьюсера Майк закрывает, а за споры с мейнтейнером в тикетнице можно получить бан. Ну и поскольку разработка полностью закрыта, об изменениях в проекте становится известно по факту, из-за чего не всегда есть возможность подготовиться к нововведениям.
Во-вторых, у проекта нет регрессионных тестов. То есть все патчи, написанные мейнтейнером, попадают сразу в мастер. Также отсутствует документация, а код LuaJIT несколько переусложнен, из-за чего работать с ним и решать проблемы не всегда легко и просто.
Наличие упомянутых аспектов проекта, а также практически полное отсутствие динамики в их устранении привело к тому, что у LuaJIT начали появляться форки. Например, такие развивает Tarantool, OpenResty, LuaVela, RaptorJIT. Но примечательно, что каждый форк живет собственной жизнью — команды не сотрудничают.
Принцип выполнения Lua кода в LuaJIT
Теперь перейдем от базовой теории к обзору принципа работы с LuaJIT и его устройству.
Так, алгоритм выполнения кода на Lua в LuaJIT следующий:
Запускается LuaJIT, и Lua код передается в лексический анализатор, где разбивается на токены.
Далее полученные токены передаются в синтаксический анализатор, который выделяет в коде синтаксические конструкции.
Следом код преобразуется в байткод и передается на исполнение в BC-интерпретатор.
Примечание: Помимо Lua-кода. можно передавать уже готовый байткод, который пройдет через BC-фронтенд и также будет передан для исполнения в интерпретатор.
Если некоторые участки кода в интерпретаторе воспроизводятся часто, то LuaJIT начинает выделять горячие пути исполнения и начинается запись трассы.
Далее эта трасса записывается и транслируется в промежуточное представление (IR, Intermediate Representation).
Затем происходит оптимизация промежуточного представления и его ассемблирование. Ассемблер транслируется в машинный код для исполнения.
После того как трасса записана и ассемблирована, мы возвращаемся в интерпретатор и патчим байткод, чтобы выполнение записанной трассы происходило в машинном коде.

Помимо интерпретатора, в архитектуре LuaJIT предусмотрены FFI-библиотека, JIT-библиотека, стандартная библиотека и некоторые расширения.
Внутреннее устройство LuaJIT можно разделить на несколько крупных блоков:
виртуальная машина;
JIT-компилятор;
компонент, отвечающий за интеграцию с языком С.
В этой статье мы подробнее остановимся на обзоре собственного интерпретатора, стандартных библиотек и JIT-компилятора.
LuaJIT: Интерпретатор
Интерпретатор для обычного языка программирования выглядит примерно следующим образом:
switch {
case BC_<NAME1>:
…
case BC_<NAME2>:
…
…
}
Это конструкция case-switch, которая обрабатывает каждый байткод, запускает и выполняет его.
В LuaJIT это реализовано иначе.
У нас есть описание для каждого байткода, которое позже транслируется с помощью DynASM. В свою очередь DynASM преобразовывает макроассемблер в код, который затем сам генерирует код.
case BC_CAT:
| ins_ABC
| mov L:CARG1, SAVE_L
| mov L:CARG1->base, BASE
| lea CARG2, [BASE+RC*8]
| mov CARG3d, RCd
| sub CARG3d, RBd
Преимущество такого описания в том, что мы можем смешивать конструкции языка С и макросы ассемблера, а также использовать удобные обращения к конструкциям языка С без разыменования указателей и получения смещения.
После трансляции DynASM-описания для каждого байткода получается такой код на С:
…
case BC_CAT:
dasm_put(
Dst,
3783,
Dt1(->base),
Dt1(->base),
GG_G2DISP
);
…
А затем происходит преобразование кода на С в бинарный формат:
ljBC_CAT:
.long 0xd3587e11,0x92401f9c
.long 0xf90012f3,0xcb110382
.long 0x8b1c0e61,0xaa1703e0
.long 0xf90007f5
bl ljmeta_cat
.long 0x385ff2b1,0xf94012f3
,long 0xb500cbe0,0xf8717a68
.long 0xf83b7a68,0xb84046b0
.long 0x8b300ec9,0xd3483e1b
.long 0xf947f528,0xd3507e1c
.long 0xd61f0100
LuaJIT: Стандартная библиотека
«Под капотом» стандартной библиотеки LuaJIT сочетает важные компоненты. Среди них:
встроенные (builtin) функции: loadstring(), pcall(), getmetatable() и другие;
стандартная библиотека: os, table, math, string и другие;
библиотека LuaJIT FFI;
библиотека для управления JIT-компилятором.
Любая реализация может содержать так называемый быстрый путь исполнения (fast pass) и более долгий путь, который содержит обработку ошибок.
Рассмотрим, как реализованы функции стандартной библиотеки в LuaJIT, на примере функции tonumber(), которая преобразует строку в число.
У нас есть так называемый быстрый путь, который реализован на ассемблере. Но если при исполнении нужна обработка ошибок, то управление передается в реализацию на С, которая представляет более долгий путь и содержит обработку всех ошибок.
Например, в подобной ситуации управление дважды передается в fallback, которое обрабатывает ошибки.

LuaJIT: Компилятор
Теперь немного подробнее остановимся на JIT-компиляторе и посмотрим, как работает JIT.
Допустим, в виртуальной машине у нас есть несколько участков кода, которые последовательно исполняются. И в какой-то момент времени один из участков кода начинает исполняться гораздо чаще других, то есть становится «горячим».
В таком случае JIT-компилятор записывает трассу и не начинает выполнять этот фрагмент байткода, а передает управление на скомпилированную трассу.

Но таким образом может быть записан не любой фрагмент кода, а только те, которые начинаются со специальных инструкций в байткоде. В LuaJIT к таким «горячим» инструкциям относятся FORL, LOOP, ITERL, FUNCF, FUNCV. При этом каждое исполнение любой из этих инструкций модифицирует хеш-таблицу счетчиков — при достижении счетчиком значения 0 начинается запись трассы.
Немного остановимся на трассировке в LuaJIT.
Допустим, у нас есть код, где в переменную присваиваем число. А далее у нас появляется условие, которое проверяет, что присвоено в переменной: число или нет. На основе проверки выбирается ветка.
Далее нам нужно проверить, отрицательное это число или положительное. Результат также определяет выбор ветки. Таких проверок по пути может быть множество с разными условиями и ограничениями. При этом LuaJIT фиксирует весь конкретный путь исполнения в коде.

Каждое подобное ограничение в LuaJIT называется guard.
Guard нам нужны, чтобы понимать, что при исполнении записанный путь сохраняется. И если guard нарушается и выполнение идет по другому пути, мы выходим с трассы в интерпретатор и исполнение продолжается уже в интерпретаторе.
Примечательно, что в LuaJIT также есть поддержка снапшотов, которые:
позволяют переходить между трассами и ВМ;
дают отображение между регистрами и слотами стека ВМ;
синхронизируют только изменившиеся значения.
Зачем в LuaJIT нужно внутреннее представление
Чтобы понять, зачем в LuaJIT нужно внутреннее представление, рассмотрим небольшой пример.
Допустим, у нас есть пример кода, который в цикле складывает некоторые значения.
local sum = 0
for i = 1, 4 do
sum = sum + i
end
Байткод этого кода выглядит недостаточно информативно:
---- TRACE 1 start
0011 ADDVV 0 0 4
0012 FORL 1 => 0011
Но с внутренним представлением ситуация другая:
---- TRACE 1 IR
0001 int SLOAD #3 I
0002 u8 XLOAD [0x101004521] V
0003 int BAND 0002 +12
0004 > int EQ 0003 +0
0005 > int SLOAD #2 T
0006 >+ int ADDOV 0005 0001
0007 + int ADD 0001 +1
0008 > int LE 0007 +4
0009 ------ LOOP ------------
0010 u8 XLOAD [0x101004521] V
0011 int BAND 0010 +12
0012 > int EQ 0011 +0
0013 >+ int ADDOV 0007 0006
0014 + int ADD 0007 +1
0015 > int LE 0014 +4
0016 int PHI 0007 0014
0017 int PHI 0006 0013
Здесь код уже обогащен дополнительной информацией, которая позволяет компилятору перед ассемблированием трассы выполнять некоторые оптимизации, чтобы сделать выполнение кода более эффективным.
Примечание: Оптимизация — трансформация кода без нарушения его семантики, направленная на то, чтобы сделать выполнение кода более эффективным.
В LuaJIT возможно несколько оптимизаций в IR. Среди них:
General (Control-Flow Specialization, Hash Key Specialization, Fast Function Inlining, Bytecode Patching);
Fold Engine;
Common Subexpression Elimination;
Array Bounds Check Elimination;
Narrowing;
Memory Access Optimizations;
Loop Optimizations;
FP Value Splitting and Soft-Float Calls;
Allocation Sinking and Store Sinking.
Чтобы понять, зачем нужны все сложности с интерпретатором на ассемблере, записью трассы и компилятором, можно посмотреть на пример кода, который создает список из пяти значений, потом возводит каждое число в квадрат и добавляет к каждому числу ноль. Благодаря всем реализациям конструкция преобразуется в компактный ассемблер из десяти инструкций.

Вместе с тем LuaJIT нарушает принцип DRY, который заключается в том, что не нужно дублировать сущности без необходимости. Именно к таким примерам нарушения относятся «fast» functions (упомянутые выше), обход таблиц, индексирование таблиц, листер байткода, функция jit.dump(), которая дублирует часть компилятора.
Примечание: Подробнее о том, как и зачем LuaJIT нарушает DRY, можно узнать в докладе Антона Солдатова.
Известные проблемы LuaJIT
Помимо уже упомянутых, у LuaJIT есть еще ряд проблем. Причем с каждой из них мы сталкивались последовательно — по мере работы с LuaJIT. К подобным можно отнести некоторые сложности:
вспомогательный код для сборки проекта LuaJIT содержит проблемы работы с памятью, и эти проблемы никогда не будут исправлены;
«грязные» чтения при работе со строками;
хрупкие парсеры байткода и С-объявлений для FFI;
известные неисправленные проблемы в LuaJIT с неопределенным поведением с точки зрения языка C;
в LuaJIT и PUC Rio Lua необдуманное использование функций библиотеки debug может приводить к крешам и неожиданным эффектам.
Таким образом, LuaJIT со всей своей спецификой создает довольно много вызовов для тестирования. Среди них:
реализация с помощью низкоуровневых языков с ручным управлением памятью;
платформозависимый код;
нарушение принципа Don’t Repeat Yourself;
два режима работы: интерпретатор и компилятор;
восстановление с трассы в интерпретатор (снапшоты);
оптимизации в интерпретаторе и компиляторе;
известные неисправленные проблемы;
два API: С и Lua.
LuaJIT в Tarantool
Теперь остановимся на LuaJIT в Tarantool.
До некоторых пор у нас в Tarantool были разные проблемы с поддержкой LuaJIT.
Иногда Tarantool крашился на продакшене из-за неправильной работы LuaJIT. Но разбираться с проблемами в проде было сложно, поскольку мы работали с незнакомым окружением и кодом.
Был страх добавления новых изменений в свой форк, поскольку не было никаких гарантий, что обновления не сломают какие-то компоненты и не сделают LuaJIT еще хуже.
Были проблемы с бэкпортом патчей. Поскольку у нас свой форк, мы стараемся синхронизироваться с ванильной версией и забирать себе наиболее полезные изменения. Но поскольку в LuaJIT нет тестов, некоторые патчи могли или не до конца исправлять существующую проблему, или вносить регрессию в другом месте кода.
По ряду причин у нас сложились сложные отношения с мейнтейнером LuaJIT, что создавало дополнительные трудности для развития нашего форка.
Примерно с такими входными данными мы начали работу над LuaJIT в своем проекте.
Работаем над LuaJIT в Tarantool
В первую очередь в рамках работы над LuaJIT в Tarantool мы собрали все доступные для Lua runtime-тесты и интегрировали к нам в репозиторий. Так мы смогли достичь покрытия кода тестами на уровне 78% для строк и 67% для ветвлений.
После этого мы приступили к бэкпортингу патчей. Чтобы обезопасить свой форк от внесения багов и уязвимостей, мы выработали правило, согласно которому каждая строчка приносимого из апстрима патча должна покрываться тестами. Здесь сложность в том, что с каждым новым патчем требования к тестам могут меняться, как и сами тесты.
Также мы начали работу с багфиксами в режиме upstream first. Это значит, что на любую проблему, которую мы находили в LuaJIT, мы в первую очередь делали репорт в upstream, дожидались исправления этой проблемы и только после этого патч приносили к себе.
Но к сожалению, этих усилий было недостаточно, чтобы полностью исключить креши Tarantool в проде из-за проблем с LuaJIT. Именно поэтому мы решили попробовать фаззинг LuaJIT.
Фаззинг LuaJIT
Начнем с обзора наших тестов.
С точки зрения дизайна было решено построить тесты таким образом, чтобы они покрывали API, которые предоставляет LuaJIT (функции C API, функции Lua API), и проверяли входные данные для LuaJIT, то есть код Lua или байткод.
Немного контекста: Lua API и C API
Чтобы понимать наши дальнейшие действия, нужно немного погрузиться в контекст и понять, как выглядит Lua API и C API.
Для наглядности возьмем пример простого кода.
Так, чтобы в Lua API получить число 300, нужно сложить два числа (100 и 200) и присвоить переменную foo. То есть всего одна строчка кода на Lua.
Все функции в C API работают с Lua-стеком, то есть все взаимодействие между ними строится через стек. Вместе с тем, чтобы решить аналогичную задачу и получить число 300, при работе с C API нужно написать уже четыре функции:
положить на стек 100;
положить на стек 200;
вызвать функцию сложения двух элементов на стеке;
вызвать функцию lua_setglobal, которая забирает элемент на вершине стека и присваивает его в переменную foo.

При этом любая функция, входящая в С API, содержит в себе индикатор функции, следующего вида:
[-o, +p, x],
где:
-о — описывает, сколько элементов функция «забирает» со стека;
+р — описывает, сколько элементов функция добавляет на стек;
х — указывает, бросает ли функция исключение.
Примечание: Все индикаторы функции задокументированы в Lua Reference Manual: 3.7 – Functions and Types.
С помощью индикатора функций мы можем проверять корректность поведения каждой функции для C API, то есть можем применять его в качестве тестового оракула.
Сам тест построен следующим образом: у нас есть набор функций, которые мы случайным образом вызываем, передаем им случайные аргументы и проверяем выполнимость этого индикатора функций.
Примечательно, что в референсной реализации PUC Rio Lua с помощью фаззинга можно обеспечить покрытие кода Lua C API на уровне 91% — настолько высок процент переиспользования кода. Но с LuaJIT достичь таких показателей сложно, поэтому для увеличения покрытия нам нужно было увеличивать количество тестов.
Тесты для функций Lua API
В LuaJIT не очень много функций — около 200. Для каждой из них мы сделали свою обертку, где вызываем эту функцию и передаем случайные входные данные, и использовали нативный фаззинг для Lua — проект luzer.
Примечание: Не для всех функций у нас получилось реализовать тестовый оракул. Например, в математической библиотеке все просто, но с функциями, которые работают со строками и таблицами, такие проверки придумать сложно.
Luzer — фаззинг Lua-кода и нативных расширений на С, «под капотом» которого libFuzzer. Для обратной связи он использует статическую инструментацию для кода на С (SanitizerCoverage) и динамическую инструментацию для Lua (хук на строки из стандартной библиотеки Lua). Luzer включен в состав Crusher от ИСП РАН, а в планах — добавление фаззера в OSS Fuzz.
Примечание: Подробнее о luzer можно узнать в одном из моих предыдущих докладов «Добавляем поддержку скриптового языка в AFL и LibFuzzer на примере Lua».
LuaJIT: Входные данные
Как я упоминал ранее, в виртуальную машину могут попадать входные данные двух форматов:
код Lua;
байткод.
Мы реализовали тесты из предположения, что любой корректный код Lua или байткод могут быть исполнены в LuaJIT.
Чтобы тестировать LuaJIT с точки зрения кода, мы разработали генератор Lua-программ. Программа генерируется с помощью схемы случайным образом, и далее protobuf сериализуется в код на Lua с помощью LibProtoBufMutator. Грамматика языка Lua описана в формате EBNF, и мы ее транслировали в схему в формате Google Protocol Buffers.
Схема работы теста следующая:
у нас есть грамматика в формате Google Protocol Buffers;
LibProtoBufMutator из этой грамматики генерирует случайную схему;
эту схему через protobuf мы сериализуем с помощью сериализатора в код на Lua;
полученный код на Lua передается на исполнение в LuaJIT;
после этого выполняется проверка на наличие багов, крешей и других аномалий.
Тест работал хорошо. Но нам было важно понимать, насколько он эффективен, корректен и достаточен. Для этого мы:
оценили полноту покрытия с помощью анализа покрытия кода;
провели анализ работы LuaJIT по нескольким метрикам: ошибки синтаксического анализатора, количество записанных трасс, причины выхода с трасс.
В результате мы поняли, что сериализатор работает не совсем корректно: есть довольно много разноформатных проблем.
Например, среди синтаксических проблем можно выделить:
некорректное использование return, break (42% ошибок!);
некорректное использование vararg: cannot use '...' outside a vararg function;
использование некорректного индекса: table index is {nil, NaN};
некорректные значения для оператора for;
использование зарезервированных слов.
Также мы выявили ряд проблем с семантикой:
«attempt to index» (16%);
«attempt to perform arithmetic on» (12%);
«attempt to call» (11%);
«attempt to concatenate» (3%).
Помимо этого, мы сталкивались с таймаутами из-за зацикливаний и рекурсий в коде.
Исправление таймаутов
Чтобы исправить проблему с тайм-аутами, мы добавили счетчики, которые инициализировались до цикла, в цикле инкрементировались, и при достижении некоторого значения мы выходили из цикла.
То есть, вместо:
while (true) do
foo = 'bar'
...
end
перешли к другому формату:
counter_0 = 0
while (true) do
if counter_0 > 5 then
break
end
...
end
Примечание: Циклы — одни из тех участков кода, которые компилятор может превратить в трассу. Поэтому полностью избавиться от циклов мы не можем. Но нам важно, чтобы зацикливания не возникали часто. Поэтому мы оставили число 5, чтобы код хотя бы иногда компилировался, но не приводил к зацикливанию.
Исправление семантики
Например, наш сериализатор мог сгенерировать код, который складывал две булевые переменные, что абсолютно бессмысленно.
true + false -- attempt to perform arithmetic on a boolean value
Но в Lua есть метатаблицы, которые позволяют добавить поддержку некоторых операций для объектов или типов данных. Поэтому мы реализовали метатаблицу для булевых переменных, которая реализует метаметод add.
always_number = function(v)
return tonumber(val) or 10
end
debug.setmetatable(true, { __add = function(v1, v2)
return always_number(v1) + always_number(v2)
end })
true + false -- 20
В результате сложение двух булевых переменных перестало приводить к ошибке и сбои в работе сгенерированных программ прекратились.
Тестирование BC-фронтенда
Так же как и в случае с Lua кодом, мы хотели бы проверять, что выполнение байткода не приводит к проблемам в LuaJIT.
Но у байткода есть некоторое ограничение. Например, мы не можем передавать в LuaJIT случайно сгенерированный байткод, поскольку парсер написан таким образом, что в LuaJIT могут возникать различные проблемы, связанные с неправильным использованием памяти.
Поэтому, чтобы тестировать BC-фронтенд, мы предусмотрели следующий алгоритм:
берем генератор Lua-кода, который генерирует случайные программы;
передаем Lua-код в байткод в функцию lua_dump() / string.dump() и на выходе получаем байткод;
полученный байткод передаем в функцию lua_load() и запускаем исполнение.
Интеграция в процесс разработки
Все упомянутые тесты мы интегрировали в OSS Fuzz, что позволяет нам тестировать как свой форк LuaJIT, так и референсную реализацию PUC Rio Lua.
Но интегрировать LuaJIT в OSS Fuzz не получилось, поэтому для его тестирования мы используем ClusterFuzz. То есть, помимо своего форка, мы также тестируем ванильную реализацию, что позволяет нам действовать на опережение: мы не ждем, когда кто-то внесет регрессию, которая со временем будет обнаружена и начнет создавать проблемы, — мы заранее фаззим ванильную версию, чтобы обнаруживать баги заблаговременно, репортить их в апстрим, бэкпортить себе патчи. Благодаря этому мы одновременно делаем стабильнее как наш форк, так и ванильную версию LuaJIT.
Примечательно, что использование инфраструктуры OSS Fuzz и ClusterFuzz позволяет нам автоматизировать рутину в части:
оформления тикета с артефактами;
верификации исправленных багов в тикетнице;
минимизации тест-кейса.
Планы
Мы проделали большую работу. Но LuaJIT — довольно сложный компонент, и некоторые его части покрываются нашими тестами неявно, а некоторые покрываются плохо. Поэтому мы не останавливаемся и продолжаем работу над решением проблем LuaJIT. Так, в наших планах:
устранение недетерминированного поведения;
исправление проблем с инструментацией кода, написанного вручную на ассемблере;
фаззинг LuaJIT FFI, регистрового аллокатора, сборщика мусора;
использование формальной верификации с помощью моделей.
Полученные результаты и выводы
Проделанная работа позволила нам исправить ошибки и решить текущие проблемы.
Примечательно, что проблем с LuaJIT мы обнаружили значительно больше, чем с PUC Rio Lua. Причем основная масса из них относилась к категории CWE-617: Reachable Assertion. Отчасти это может быть связано с тем, что авторы кода активно применяют assertion и тем самым используют защитное программирование, что упрощает поиск аномалий в коде.

Наряду с устранением ошибок, мы смогли получить и некоторые сопутствующие выгоды, среди которых:
снижение количества инцидентов в продакшене Tarantool;
положительное влияние на сертификацию Tarantool во ФСТЭК;
возможность проведения автономного тестирования;
покрытие LuaJIT фаззинг-тестами до 61% по строкам;
покрытие PUC Rio Lua фаззинг-тестами до 99% по строкам;
возможность переиспользования тестов для разных реализаций за счет единого API;
повышение надежности и корректности кода Tarantool / LuaJIT / PUC Rio Lua;
Shift left за счет непрерывного фаззинга PUC Rio Lua, LuaJIT, Tarantool LuaJIT.
Примечание: Помимо этого, мы получили благодарности от мейнтейнеров за репорты дефектов.
Код Tarantool’s LuaJIT, тестов и luzer есть в свободном доступе — все желающие могут изучить их и задать свои вопросы, а также поделиться в комментариях своим опытом применения фаззинга для поиска багов и уязвимостей.
Вступайте в чат сообщества Tarantool и подписывайтесь на наш канал в Telegram.