PREVAIL принял BPF-программу как безопасную. Штатный верификатор ядра Linux ту же самую программу отклонил. Кода не было в статье про уязвимости. Ошибки в логике верификатора тоже не нашлось. Разбираемся, почему отказ верификатора — не синоним бага и как отличить ложный вердикт от реальной проблемы.
Суть события
eBPF-программы перед загрузкой в ядро проходят статический анализ — верификатор. Его задача доказать, что код не выйдет за границы памяти, не обратится к неверным указателям и не нарушит ограничений ядра. Обычно вердикт однозначный: либо принимают, либо отклоняют.
Но если один и тот же объектный файл прогнать через два разных верификатора, картина меняется. PREVAIL (внешний статический анализатор) принял объект correlated_branch.o. Верификатор ядра Linux тот же самый объект отклонил. Программа одна, условия одни.
Никакого бага в коде не было — и именно в этом главный урок.
Детали
Программа ConvergedBranch из набора ebpf-samples читает Ethernet-заголовок сетевого пакета. Перед чтением функция check_packet убеждается, что данных хватает: если размер пакета недостаточен, она возвращает нулевой указатель. Если проверка проходит — возвращается указатель на начало пакета, после чего выполняется проверка и чтение поля.
В исходном коде на C порядок безупречен: сначала проверка границ, потом чтение. Но верификатор ядра работает не с исходным кодом, а с BPF-инструкциями и строит собственную модель состояния программы.
Верификатор ядра проверку границ видел: получил ограничение на размер доступной области (не меньше 14 байт). Однако это ограничение не связалось с указателем r7, который используется при последующем чтении. Верификатор остановился на отказе: invalid access to packet, off=12 size=2.
PREVAIL после проверки сформировал состояние, в котором связь между указателем и проверенной областью пакета сохранена. Поэтому при следующем чтении он воспользовался результатом предыдущей проверки — и доступ был разрешён.
Ключевое отличие: верификатор ядра отслеживает состояние через типы указателей и диапазоны значений регистров; PREVAIL использует графовую модель состояний, что позволяет ему сохранять больше контекста между проверкой и использованием.
Перенос проверки границ непосредственно в место использования вместо вызова отдельной функции заставил верификатор ядра принять программу. Код был безопасен с самого начала.
Контекст
Когда верификатор ядра отказывает в загрузке, первая реакция — искать баг в коде. Иногда он действительно есть. Но иногда проблема в том, что нужные гарантии в программе записаны так, что анализ их не связал.
Цена ошибки в обратную сторону тоже реальна: если разработчик видит отказ верификатора и решает, что это ложное срабатывание, не разобравшись, — в продакшен может уйти код с неочевидной уязвимостью.
Для фрилансеров и удалённых разработчиков, которые разворачивают eBPF-фильтры на серверах клиентов, это означает дополнительный этап отладки: прежде чем править «очевидный» баг, стоит посмотреть в лог верификатора по состоянию регистров — запись вида R7 pkt(off=0,r=0) говорит больше, чем сам текст отказа. А сравнение вердиктов PREVAIL и ядра может уберечь от часов напрасной переработки.
Что дальше
Если верификатор отклонил программу — не паникуйте и не правиль сразу. Оцените, вынесена ли проверка границ в отдельную функцию. Попробуйте перенести её в место использования указателя: if (ptr + N > data_end) return 0;. Проверьте вердикт PREVAIL. Если оба верификатора дали один вердикт — скорее всего, проблема в коде. Если разошлись — возможно, вы имеете дело с ограничением анализа, а не с уязвимостью.
Для команд, которые массово деплоят eBPF-фильтры, имеет смысл добавить в CI-пайплайн прогон через внешний верификатор в дополнение к ядерному — это даёт второе мнение и помогает приоритизировать, над каким отказом стоит разбираться в первую очередь.