В статическом анализаторе кода PVS-Studio реализован механизм символьного выполнения. И сейчас предоставилась хорошая возможность на практике продемонстрировать, как этот механизм помогает выявлять ошибки.
Каждый день система для регулярного мониторинга проекта Blender присылает мне отчёт о потенциальных ошибках в новом или изменённом коде. Описывать каждый новый баг и писать 100500 заметок про это неинтересно, но сегодня особый случай.
Статический анализатор кода PVS-Studio использует различные технологии для выявления ошибок и потенциальных уязвимостей.
Суть технологии символьного выполнения в том, чтобы вычислять выражения, не зная значений переменных. Звучит таинственно, но сейчас мы разберём это на практическом примере, и всё сразу станет понятно. Рассмотрим вот этот коммит в проекте Blender.
PVS-Studio выдаёт предупреждение на 868-ую строчку кода:
memset(&path->ptr[i], 0, sizeof(path->ptr[i]) * (path->len - i));
Анализатор считает подозрительным, что функция memset на самом деле не заполняет память:
[CWE-628] V575: The 'memset' function processes '0' elements. Inspect the third argument.
Давайте разберёмся, как он пришёл к такому выводу.
Анализатор не знает, какие числовые значения могут храниться в переменной path->len. Он знает про эту переменную кое-что другое, но про это позже.
Про переменную i известно чуть больше.
for (int i = 0; i < path->len; i++) {
....
if (i != 0) {
....
memset(&path->ptr[i], 0, sizeof(path->ptr[i]) * (path->len - i));
Из этого кода анализатор может извлечь следующую информацию:
- Переменная i меньше path->len. Это известно из анализа цикла.
- Переменная i больше 0. Это следует из инициализации этой переменной в цикле и последующей проверки на неравенство нулю.
Итого, значения переменной i лежат в диапазоне от 1 до path->len.
Впрочем, этой информации всё равно мало, чтобы делать какие-то выводы. И тут на помощь приходит механизм символьного выполнения.
Анализатор видит, что перед вызовом функции memset значение path->len меняется следующим образом:
path->len = i;
if (i != 0) {
memset(&path->ptr[i], 0, sizeof(path->ptr[i]) * (path->len - i));
Переменная path->len равна значению i. Поэтому, даже не зная диапазоны возможных значений переменных, можно вычислить выражение. Для этого анализатор делает подстановку:
sizeof(path->ptr[i]) * (i - i)
И получает 0 в качестве третьего аргумента функции:
sizeof(path->ptr[i]) * 0
Это явная аномалия, о чём PVS-Studio и сообщает программистам. Перед нами какая-то ошибка, возникшая в процессе редактирования кода. Прикольно, что её можно сразу выявить и исправить, если использовать инструмент статического анализа.
Примечание. Поскольку в статье приводится только маленькая часть кода, то присваивание path->len = i может показаться вообще очень странным. Ведь это означает, что цикл всегда завершается после первой итерации. На самом деле, рассмотренный в статье фрагмент находится под условиями, и такой код имеет смысл. Вы можете сами изучить тело цикла целиком.
Предыдущие публикации:
- Как PVS-Studio защищает от поспешных правок кода, пример N4.
- Как PVS-Studio защищает от поспешных правок кода, пример N3.
- Как PVS-Studio защищает от поспешных правок кода, пример N2.
- Как PVS-Studio защищает от поспешных правок кода.
- PVS-Studio, Blender: цикл заметок о пользе регулярного использования статического анализа.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. How PVS-Studio prevents rash code changes, example N5.