>
>
>
Как PVS-Studio защищает от поспешных пр…

Андрей Карпов
Статей: 670

Как PVS-Studio защищает от поспешных правок кода, пример N5

В статическом анализаторе кода 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 может показаться вообще очень странным. Ведь это означает, что цикл всегда завершается после первой итерации. На самом деле, рассмотренный в статье фрагмент находится под условиями, и такой код имеет смысл. Вы можете сами изучить тело цикла целиком.

Предыдущие публикации: