0908_Technology_ru/image1.png


PVS-Studio предоставляет статические анализаторы для языков C, C++, C# и Java на платформах Windows, Linux и macOS. Несмотря на некоторые различия, накладываемые особенностями отдельных языков, в целом все перечисленные анализаторы используют общие технологии и подходы.


В составе PVS-Studio можно выделить 3 отдельных программных инструмента для статического анализа кода:


  • анализатор для языков C, C++ и диалектов (C++/CLI, C++/CX). Написан на C++ и основан на библиотеке с закрытым исходным кодом VivaCore, также разрабатываемой командой PVS-Studio;
  • анализатор для языка C#. Написан на C#, использует открытую платформу Roslyn для разбора кода (построения абстрактного синтаксического дерева и семантической модели) и для интеграции с проектной системой MSBuild \ .NET;
  • анализатор для языка Java. Написан на Java, использует возможности внутренней C++ библиотеки VivaCore для анализа потока данных. Для разбора кода (построения абстрактного синтаксического дерева и семантической модели) анализатор использует открытую библиотеку Spoon.

Перечисленные анализаторы реализуют алгоритмы и механизмы для проведения data-flow анализа (включая символьное выполнение и межпроцедурный анализ), основанные на наших собственных разработках.


Рассмотрим подходы и технологии, на которых базируется работа статического анализатора кода PVS-Studio.


Абстрактное синтаксическое дерево (abstract syntax tree) и методика сопоставления с шаблоном (pattern-based analysis)


Вначале рассмотрим два термина, применяющихся в теории разработки компиляторов и статических анализаторов кода.


Абстрактное синтаксическое дерево (AST) — это конечное ориентированное дерево, в котором внутренние вершины соответствуют операторам языка программирования, а листья —операндам. Абстрактные деревья синтаксиса используются компиляторами и интерпретаторами как промежуточное представление между деревьями разбора и внутренним представлением кода. Преимущество AST — достаточная компактность (абстрактность) структуры, достигаемая за счёт отсутствия узлов для конструкций, не влияющих на семантику программы.


Основным преимуществом использования абстрактного синтаксического дерева, по сравнению с прямым анализом текста программ (исходного кода), является независимость построенных на его основе анализаторов от конкретного синтаксиса: имён, стиля написания кода, его форматирования и т.п.


Дерево разбора (Parse Tree, PT, DT). Результат грамматического анализа. Дерево разбора отличается от абстрактного синтаксического дерева наличием узлов для тех синтаксических правил, которые не влияют на семантику программы. Классическим примером таких узлов являются группирующие скобки, в то время как в AST группировка операндов явно задаётся структурой дерева.


0908_Technology_ru/image2.png


Высокоуровнево можно говорить, что ядра всех анализаторов PVS-Studio для разных языков работают с абстрактным синтаксическим деревом (AST). Однако на практике всё немного сложнее. В некоторых случаях диагностики требуют информации о необязательных узлах или даже о количестве пробелов в начале строки. В этом случае анализ спускается на уровень дерева разбора и извлекает дополнительную информацию. Все используемые библиотеки (Roslyn, Spoon, VivaCore) предоставляют возможность получать информацию на уровне дерева разбора, и анализатор в ряде случаев этим пользуется.


Анализаторы PVS-Studio используют AST представление программы для поиска потенциальных дефектов методом сопоставления с шаблоном (pattern-based analysis). Это класс простых диагностических правил, которым для принятия решения об опасности кода достаточно сопоставить конструкцию, встретившуюся в коде, с заранее заданным шаблоном потенциальной ошибки. Данный подход к анализу достаточно точен, но позволяет находить только относительно простые дефекты — для более сложных диагностических правил PVS-Studio дополняет анализ AST другими методиками, которые мы рассмотрим чуть позже.


Следует отметить, что поиск по шаблону — более эффективная технология, чем использование регулярных выражений. Регулярные выражения вообще не подходят для построения эффективного статического анализатора по множеству причин. Поясним это на простом примере. Допустим, требуется найти опечатки, когда выражение сравнивается само с собой. Это можно сделать регулярными выражениями для простейших случаев:


if (A + B == A + B)
if (V[i] == V[i])

Однако, если ошибочные выражения записаны чуть по-другому, регулярные выражения бессильны. Вернее, их просто невозможно написать для всех случаев альтернативной записи:


if (A + B == B + A)
if (A + (B) == (B) + A)
if (V[i] == ((V[i])))
if (V[(i)] == (V[i]))

В свою очередь, для сопоставления по шаблону, если используется AST, выявление подобных ошибок не является проблемой.


Примечание. В нашей коллекции ошибок вы можете посмотреть, как разнообразно могут выглядеть подобные дефекты, найденные с помощью диагностик V501 (C, C++), V3001 (C#), V6001 (Java).


Представление кода в виде абстрактного синтаксического дерева также является подготовкой к проведению следующего уровня анализа — построения семантической модели и вывода типов.


Семантическая модель (semantic model) кода и вывод типов (type inference)


Все анализаторы PVS-Studio на основе построенного на предыдущем этапе представления кода в виде абстрактного синтаксического дерева проводят семантический анализ — построение полной семантической модели проверяемого кода.


0908_Technology_ru/image3.png


Обобщённая семантическая модель представляет собой словарь соответствий семантических (смысловых) символов и элементов синтаксического представления этого же кода.


Каждый такой символ определяет семантику (смысл) соответствующей синтаксической конструкции языка. Эта семантика может быть неочевидна и невыводима из самого локального синтаксиса. В таком случае для выведения такой семантики требуется обращаться к другим частям синтаксического представления кода. Поясним это на примере фрагмента кода на языке C:


A = B(C);

Не зная, что собой представляет B, невозможно сказать, какая перед нами конструкция языка. Это может быть как вызов функции, так и явное приведение типа (functional cast expression).


Семантическая модель, таким образом, позволяет анализировать семантику кода. Без неё пришлось бы постоянно совершать обход синтаксического представления этого кода, чтобы разрешить невыводимые из локального контекста семантические факты. Семантическая модель "запоминает" семантику по ходу разбора кода для дальнейшего её использования. Поясним на примере:


void B(int);
....
A = B(C);

Встретив объявление функции B, анализатор запомнит, что символ B является именем функции с определёнными характеристиками. Встретив выражение A = B(С), анализатор сразу поймёт, что такое B, ему не нужно вновь обходить большой фрагмент AST.


На основе семантической модели анализаторы PVS-Studio получают возможность проводить вывод типов (type inference) у любой встречаемой синтаксической конструкции. Такими конструкциями могут быть идентификаторы переменных, выражения и так далее.


Информация о типах требуется большинству диагностик. На её основе делается предположение о потенциальных ошибках или наоборот — срабатывают исключения из правил.


Даже если мы говорим о диагностиках, основанных на шаблонах, многие из них учитывают типы. Например, в тех случаях, когда одного синтаксического представления недостаточно для принятия решения об опасности проверяемой конструкции.


Рассмотрим в качестве примера очень простую диагностику V772 для кода на языке C++:


void *ptr = new Example();
....
delete ptr;

Вызов оператора delete для указателя типа (void *) приводит к неопределённому поведению. Сам по себе шаблон для поиска крайне прост: это любой вызов оператора delete. Можно сказать, это вырожденный случай шаблонной диагностики :). Однако, чтобы понять, найдена ошибка или нет, нужно знать тип операнда ptr.


Построение полной и корректной семантической модели требует непротиворечивости и, соответственно, собираемости (компилируемости) проверяемого кода. Компилируемость исходного кода является обязательным условием для полноценной и корректной работы всех анализаторов PVS-Studio. И хотя в них заложены механизмы отказоустойчивости при работе с некомпилируемым кодом, такой код может ухудшать точность работы диагностических правил.


Препроцессирование C и C++ исходного кода


Препроцессирование C и C++ кода — это процесс раскрытия в исходном коде директив компилятора и подстановка значений макросов. В частности, в результате работы препроцессора на месте директив #include происходит подстановка содержимого заголовочных файлов, пути до которых записаны в данной директиве. Директивы и макросы раскрываются в случае такой подстановки последовательно – так же, как и во всех раскрываемых директивой #include заголовочных файлах.


Препроцессирование является первым этапом работы компилятора — подготовкой единицы компиляции и её зависимостей к непосредственной трансляции исходного кода во внутреннее представление компилятора.


0908_Technology_ru/image4.png


Раскрытие директив #include приводит к объединению исходного файла и всех используемых в нём заголовочных файлов в единый файл, часто называемый промежуточным (intermediate). По аналогии с компилятором, С и C++ анализатор PVS-Studio использует препроцессирование перед началом анализа.


Для препроцессирования проверяемого кода PVS-Studio пользуется тем же компилятором, который использовался для сборки (но в режиме препроцессора). PVS-Studio поддерживает работу с большим количеством препроцессоров, которые перечислены на странице продукта. Для корректной работы анализатора требуется использовать правильный препроцессор – такой же, который применяется при компиляции проверяемого кода. Это обусловлено тем, что выходной формат препроцессоров различных компиляторов отличается.


Перед началом анализа C и C++ кода PVS-Studio осуществляет запуск препроцессора для каждой единицы трансляции проверяемого кода. Помимо содержимого исходных файлов, на работу препроцессора также влияют параметры компиляции. PVS-Studio использует для препроцессирования параметры сборки, применяемые при компиляции проверяемого кода. Информацию о списке единиц трансляции, а также параметры компиляции PVS-Studio получает либо из сборочной системы проверяемого проекта, либо путём отслеживания (перехвата) вызовов компиляторов в процессе сборки проекта.


Работа анализатора PVS-Studio C и С++ основана на результате работы соответствующего препроцессора — исходный код не анализируется напрямую. Препроцессирование C и C++ кода за счёт раскрытия директив компилятора позволяет анализатору построить полную семантическую модель проверяемого кода.


Без тонкостей опять не обходится. Фразу "исходный код не анализируется напрямую" следует читать как "почти всегда исходный код не анализируется напрямую". Есть несколько диагностик, такие как V1040, которые обращаются напрямую к файлам с исходным кодом. Им нужна информация про директивы #include и макросы, которая теряется после препроцессирования.


Отслеживание компиляции C и C++ исходного кода


Технология отслеживания (мониторинга) PVS-Studio позволяет перехватывать запуск процессов на уровне API операционной системы. Перехват запуска позволяет извлекать у процесса полную информацию о его работе, параметрах его запуска и рабочем окружении. Отслеживание запуска процессов PVS-Studio поддерживается на платформах Windows (реализовано через прямую работу с WinAPI) и Linux (реализовано с помощью стандартной системной утилиты strace).


0908_Technology_ru/image5.png


C и C++ анализаторы PVS-Studio могут использовать отслеживание процессов компиляции как один из вариантов проведения анализа С++ кода. Как уже говорилось ранее, PVS-Studio имеет средства прямой интеграции с наиболее распространёнными сборочными системами для C и C++ проектов. Но экосистема данных языков весьма многообразна, и в ней существует большое количество сборочных систем (например, в embedded сегменте), интеграцию с которыми анализатор не поддерживает.


Хотя прямая низкоуровневая интеграция C++ анализатора PVS-Studio в такие системы возможна, она является достаточно трудоёмкой, так как требует передачи в анализатор параметров компиляции для каждой единицы трансляции — исходного C или C++ файла.


Использование системы отслеживания процессов компиляции PVS-Studio позволяет упростить и автоматизировать процесс передачи анализатору всей необходимой информации для проведения анализа. Система отслеживания собирает параметры компиляции у процессов, анализирует, модифицирует их (например, активируя режим препроцессирования у компилятора — анализатору нужно проведение только этого этапа) и передаёт непосредственно в C++ анализатор PVS-Studio.


Таким образом, система отслеживания запуска процессов позволяет реализовать в PVS-Studio универсальную систему проверки C и C++ проектов, не зависящую от используемой сборочной системы, легко настраиваемую и полностью учитывающую оригинальные параметры компиляции проверяемого кода.


См. также раздел документации "Система мониторинга компиляции в PVS-Studio".


Анализ потоков данных (data-flow анализ) и символьное выполнение (symbolic execution)


Анализ потоков данных — это методика, позволяющая статическому анализатору делать предположения о значениях переменных и выражений в различных частях исходного кода. Под предполагаемыми значениями понимаются какие-то конкретные значения, диапазоны значений или множество возможных значений. Дополнительно собирается и обрабатывается информация о том, освобождена память по указателю или нет, каковы размеры массивов и так далее.


0908_Technology_ru/image6.png


Предположения о значениях строятся на основе анализа движения значений переменных по графу выполнения программы (control-flow graph). В большинстве случаев анализатор не может знать точного значения переменной или выражения. Но он может делать предположения о диапазонах или множествах значений, которые данные выражения могут принимать в различных точках этого графа. Для этого учитываются прямые и косвенные ограничения, накладываемые на рассматриваемые выражения по ходу обхода графа выполнения.


Примечание. В некоторых наших статьях и докладах мы называем предполагаемые значения переменных "виртуальными".


Все анализаторы PVS-Studio используют data-flow анализ для уточнения работы своих диагностических правил. Это требуется в случаях, когда для принятия решения об опасности рассматриваемого кода недостаточно информации, доступной только из AST или семантической структуры этого кода.


Анализаторы PVS-Studio используют собственную внутреннюю реализацию методики анализа потоков данных. Анализаторы PVS-Studio для С, C++ и Java используют общую внутреннюю C++ библиотеку для работы data-flow анализа. Анализатор для C# имеет реализацию data-flow алгоритмов, написанную на языке C#.


Рассмотрим пример data-flow анализа на примере реального Java кода (источник):


private static byte char64(char x) {
  if ((int)x < 0 || (int)x > index_64.length)
    return -1;
  return index_64[(int)x];
}

После выполнения условного оператора становится известно, что значение переменной x лежит в диапазоне [0..128]. В противном случае функция досрочно закончит свою работу. Размер массива составляет 128 элементов, а значит, перед нами off-by-one error. Для правильной проверки следовало использовать оператор >=.


В случаях, когда у анализатора при разборе кода отсутствует возможность напрямую рассчитать диапазон значений выражения, применяется методика символьного выполнения (symbolic execution). Символьное выполнение представляет возможные значения переменных и выражений в символическом виде с помощью формул. В таком представлении анализатор оперирует не конкретными значениями переменных, а символами, абстрагирующими данные переменные.


Рассмотрим пример на языке C++:


int F(std::vector<int> &v, int x)
{
  int denominator = v[x] - v[x];
  return x / denominator;
}

Даже если ничего неизвестно о том, с какими значениями вызывается функция, анализатор PVS-Studio обнаружит здесь деление на ноль.


При обходе графа выполнения анализатор строит формулы для встречаемых выражений. В дальнейшем он может вычислять ограничения значений для этих выражений. Это достигается за счёт того, что в упомянутые выше формулы подставляются известные ограничения для тех символов, от которых зависит формула. Благодаря решению созданных при обходе графа формул, алгоритмы символьного выполнения позволяют вычислять ограничения значений одних выражений или переменных в зависимости от значений других выражений или переменных.


Можно сказать, что вычисление конечного значения выражения откладывается до момента, когда оно понадобится. Например, при работе диагностического правила, где оно будет вычислено по заранее сформулированной формуле.


Анализатор PVS-Studio для C, C++ и анализатор Java используют в рамках работы своих data-flow алгоритмов методику символьного выполнения.


Межпроцедурный анализ


Межпроцедурным анализом называют способность статического анализатора раскрывать точки вызова функций и учитывать влияние таких вызовов на состояние программы и переменных в локальном проверяемом контексте. Анализаторы PVS-Studio используют межпроцедурный анализ для уточнения ограничений и диапазонов значений переменных и выражений, рассчитываемых с помощью data-flow механизмов.


0908_Technology_ru/image7.png


Межпроцедурный анализ в PVS-Studio позволяет учитывать при анализе потока данных значения, которые возвращают вызываемые функции. Рассмотрим пример ошибки в коде на языке С++:


int *my_alloc() { return new int; }

void foo(bool x, bool y)
{
  int *a = my_alloc();
  std::free(a);
}

Ошибка несовместимого способа выделения и освобождения памяти будет обнаружена как раз благодаря информации о том, что может вернуть вызываемая функция.


Также отслеживаются состояния передаваемых в функции переменных и выражений. Это позволяет определять их потенциально опасное использование внутри тел функций. Таким образом, возможно как обнаружение потенциальных дефектов внутри тел самих вызываемых функций, так и определение взаимосвязи ограничений, которые накладываются на возвращаемые значения функций их входными значениями.


Рассмотрим вариацию ошибки, которую мы уже приводили до этого:


void my_free(void *p) { free(p); }

void foo(bool x, bool y)
{
    int *a = new int;
    my_free(a);
}

Как и в предыдущем случае, анализатор предупредит, что память выделена с помощью оператора new, а освобождается с помощью функции free.


Работа межпроцедурного анализа ограничена доступностью исходного кода функций, которые анализатору требуется раскрыть. Прямой учёт при анализе функций, описанных в сторонних библиотеках, невозможен (из-за отсутствия при проверке их исходного кода). Однако анализаторы PVS-Studio способны учитывать их возвращаемые значения с помощью механизма аннотаций.


Межмодульный анализ и аннотирование функций


Помимо межпроцедурного, анализаторы PVS-Studio также поддерживают межмодульный анализ. Под межмодульным анализом в PVS-Studio понимается расширение возможностей межпроцедурного анализа на функции, объявленные в программных модулях, отличных от того, к которому относится текущий проверяемый файл.


0908_Technology_ru/image8.png


В различных языках программирования под модулями могут пониматься различные сущности. В данном контексте под модулем мы подразумеваем единицу компиляции. Для языков C и C++ — это отдельный файл с исходным кодом (файл с расширением .c или .cpp). Для языка C# — это проект. Для языка Java — это исходный файл (файл с расширением .java) с объявленным в нём классом.


Анализаторы PVS-Studio для Java и C# способны получать исходный код функций, находящихся как в том же исходном файле, так и в других исходных файлах, относящихся к проверяемому проекту. Анализатор PVS-Studio для C# может также получать и анализировать исходный код функций, объявленных в других проектах, если эти проекты также были переданы в анализатор для проверки.


Анализатор PVS-Studio для C++ может получать тела функций, объявленных в проверяемой единице компиляции (препроцессированном исходном файле с раскрытыми включениями заголовочных файлов). Межмодульный режим работы C++ анализатора позволяет также получать data-flow информацию из других единиц компиляции. Для этого анализатор использует двухпроходный режим анализа. Первый проход собирает межпроцедурную data-flow информацию для всех проверяемых исходных файлов. На втором проходе эта информация используется при непосредственном анализе исходных файлов.


Если при разборе кода анализатор встречает функцию, тело которой невозможно раскрыть для анализа, он может воспользоваться механизмом аннотирования функций. Аннотирование функций позволяет задавать в декларативном виде информацию как об ограничениях в возвращаемых функцией значениях, так и об ограничениях, накладываемых на её входные параметры.


Аннотирование в анализаторах PVS-Studio можно разделить на 2 категории: библиотечные функции и пользовательские функции. Все анализаторы PVS-Studio содержат в себе аннотации на множество функций стандартных и распространённых библиотек. C++ анализатор PVS-Studio также предоставляет возможность задавать аннотации в виде специального декларативного синтаксиса для пользовательских функций, специфичных для конкретного проверяемого проекта.


На данный момент, например, в анализаторе для C и C++ проаннотировано около 7400 функций. Ручной разметке подвергаются:


  • WinAPI,
  • стандартная библиотека C,
  • стандартная библиотека шаблонов (STL),
  • стандартные библиотеки .NET,
  • Unreal Engine,
  • Unity,
  • glibc (GNU C Library),
  • Qt,
  • MFC,
  • zlib,
  • libpng,
  • OpenSSL,
  • и т.д.

Это позволяет задавать информацию по поведению функций, тела которых недоступны анализатору, и он не может самостоятельно понять, правильно они используются или нет. Рассмотрим для примера, как проаннотирована функция fread:


C_"size_t fread"
  "(void * _DstBuf, size_t _ElementSize, size_t _Count, FILE * _File);"
ADD(HAVE_STATE | RET_SKIP | F_MODIFY_PTR_1,
    nullptr, nullptr, "fread", POINTER_1, BYTE_COUNT, COUNT, POINTER_2)
  .Add_Read(from_2_3, to_return, buf_1)
  .Add_DataSafetyStatusRelations(0, 3)
  .Add_FileAccessMode(4, AccessModeTypes::Read)
  .Out(Modification::BoxedValue, Arg1)
  .Out(Modification::BoxedValue, Arg4)
  .Returns(Arg3, [](const IntegerVirtualValue &v)
    { return IntegerVirtualValue { 0, v.Max(), true }; });

Аннотация позволяет указать, что считается корректными аргументами. Но намного более интересно, что аннотация задаёт взаимосвязь между входными аргументами и возвращаемым значением. Это позволяет обнаружить ошибку следующего вида:


unsigned foo(FILE *f)
{
    unsigned char buf[10];
    size_t n = fread(buf, sizeof(unsigned char), 10, f);
    unsigned sum = 0;
    for (size_t i = 0; i <= n; ++i)
       sum += buf[i];
    return sum;
}

Анализатор знает, что может быть прочитано 10 байт и следовательно переменная n может принять значение в диапазоне [0..10]. Поскольку условие цикла написано с ошибкой, то и значение переменной i также может принять значение в диапазоне [0..10].


Благодаря взаимодействию механизма аннотирования и data-flow анализа PVS-Studio выдаст сообщение о потенциальном выходе за границу массива, если функция fread прочитает 10 байт: "V557 Array overrun is possible. The value of 'i' index could reach 10".


Taint-анализ (taint checking)


Taint-анализ — это методика, позволяющая отследить распространение по программе внешних непроверенных, "загрязнённых" (отсюда и название taint) данных. Попадание в определённые уязвимые приёмники (sinks) таких данных может привести к возникновению целого ряда дефектов безопасности, таких как SQL-инъекции, межсайтовый скриптинг (XSS, cross-site scripting) и многих других.


Потенциальные уязвимости ПО, связанные с распространением заражённых данных, описаны в стандартах безопасной разработки, таких как OWASP ASVS (Application Security Verification Standard).


0908_Technology_ru/image9.png


Обычно программу невозможно полностью защитить от ввода в неё потенциально опасных данных. Поэтому основным способом борьбы с внешними taint-данными является их проверка перед использованием или попаданием в уязвимые приёмники — так называемая очистка (санитизация) данных.


Анализатор PVS-Studio для C и C++, а также анализатор PVS-Studio для C# могут с помощью технологий межпроцедурного data-flow анализа отслеживать распространение по программе taint-данных. На механизме отслеживания taint-данных основана целая группа диагностических правил PVS-Studio.


Анализаторы PVS-Studio контролируют всю трассу распространения заражённых данных с учётом их передачи между программными модулями, а также их проверки (очистки). PVS-Studio сгенерирует предупреждение о потенциальной угрозе безопасности кода только в случае, если анализатор отследит полный путь прохождения taint-данных от источника до приёмника без проверки. Таким образом, PVS-Studio контролирует не только попадание опасных данных в программу, но и их использование без проверки — опасным анализатор посчитает именно использование данных, а не только их ввод.


Рассмотрим пример на языке C#:


void ProcessUserInfo()
{
  using (SqlConnection connection = new SqlConnection(_connectionString))
  {
    ....
    String userName = Request.Form["userName"];

    using (var command = new SqlCommand()
    {
      Connection = connection,
      CommandText = "SELECT * FROM Users WHERE UserName = '" + userName + "'",
      CommandType = System.Data.CommandType.Text
    })
    {            
      using (var reader = command.ExecuteReader())
        ....
    }
  } 
}

При формировании SQL-команды используется значение переменной userName, полученное из внешнего источника – Request.Form. Если в качестве значения используется скомпрометированная строка (например, ' OR '1'='1), это исказит логику запроса.


В данном случае анализатор отследит распространение данных от внешнего источника (Request.Form) до приёмника (свойство CommandText SQL-команды) без надлежащей проверки и выдаст предупреждение.


Заключение


В конце хочется отметить, что для инструментов статического анализа важно не только количество диагностик, но и то, на каких технологиях они основаны. Важно, как реализованы диагностики и как много внимания уделяется таким темам, как ложные срабатывания и интеграция в процесс разработки.


0908_Technology_ru/image10.png


Ложно-положительные срабатывания при статическом анализе неизбежны. Поэтому, во-первых, наша команда много работает над тем, чтобы сократить их количество. Редко диагностики PVS-Studio хочется отключить, в отличие от многих предупреждений компиляторов. Это сильная сторона нашего анализатора. Во-вторых, у нас продумано, что делать с оставшимися предупреждениями и как интегрировать анализатор в большой проект.


Вы можете попробовать статический анализатор PVS-Studio, запросив бесплатную пробную лицензию.


Дополнительные ссылки


  1. Доклад на YappiDays 2018. Андрей Карпов. Опыт разработки статического анализатора кода.
  2. Доклад на CoreHard 2018. Павел Беликов. Как работает анализ Data Flow в статическом анализаторе кода.
  3. Доклад на Italian Cpp Community 2021 (itCppCon21). Yuri Minaev. Inside a static analyzer: type system.
  4. Олег Лысый, Сергей Ларин. Межмодульный анализ C++ проектов в PVS-Studio.
  5. Андрей Карпов. Зачем PVS-Studio использует анализ потока данных.
  6. Сергей Васильев. OWASP, уязвимости и taint анализ в PVS-Studio C#.
  7. Андрей Карпов, Виктория Ханиева. Использование машинного обучения в статическом анализе исходного кода программ.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Paul Eremeev, Andrey Karpov. PVS-Studio: static code analysis technology.

Комментарии (14)


  1. Korobei
    11.01.2022 22:55
    +2

    Интересно, на сколько пересекаются типы ошибок из разных языков? Понятно что для C++ много специфичного, но например для C# vs Java, если добавляется новый анализатор в C#, можно ли фактически беcплатно его прикрутить в Java?


    1. Paull Автор
      12.01.2022 10:02
      +1

      Смотрите - именно типы ошибок, если мы говорим об анализе общего назначения (т.к. например диагностики по стандарту MISRA С точно никогда не пересекуться с другими языками), между данными 3-мя языками (C++, C#, Java) пересекаются достаточно сильно - речь идёт о десятках процентов таких общих ошибок. Это, однако, не означает, что в одном из анализаторов мы получаем "бесплатные" ошибки, если они уже есть в другом - по крайней мере, в случае с PVS-Studio, нам всё-равно приходится реализовывать такие общие диагностики в каждом анализаторе отдельно.

      Конечно, мы переиспользуем уже имеющуюся экспертизу по такой диагностике, что сильно помогает, но это всё-таки не "бесплатно". Теоретически, такай подхват общих диагностик анализаторами для разных языков возможен (и мы знаем, что такие анализаторы есть) - обычно это делается на уровне некоего общего промежуточного представления, в котором работают сразу несколько анализаторов, однако в случае с PVS-Studio исторически сложилось так, что каждый анализатор уникален.


  1. Metotaxin
    12.01.2022 11:48
    +2

    @PaullСпасибо за интересные статьи. Подскажите, где-нибудь можно найти сравнение вашего анализатора с другими? Например с Solar appScreener?


    1. Paull Автор
      12.01.2022 11:53
      +3

      Рад, что вам понравилось!

      По поводу сравнения с другими решениями - мы пробовали такой формат, уже достаточно давно, лет 6-7 назад, и пришли к выводу, что это, скорее, достаточно неблагодарное занятие, если мы занимаемся этим сами. К сожалению, у читателя всегда остаётся ощущение предвзятости. Сравнение анализаторов вообще достаточно сложная задача, т.к. оно затрагивает очень много факторов, и, в большлинстве случаев, результатом получается некое пересечение множеств - каждый анализатор умеет что-то своё, что не умеет другой.

      Поэтому сравнивать, как мне кажется, имеет смысл по какой-то конкретной характерристике, а не в общем. Если вас интересует что-то такое конкретное именно в плане сравнения с Solar appScreener - если вы её уточните, я попробую что-нибудь найти для вас.



  1. MadHacker
    12.01.2022 12:45
    +1

    Вот подскажите про межмодульный анализ в вашем анализаторе.
    Вариант первый. Есть модули A и B допустим в одном Cmake проекте. технически пусть это будет библиотека и исполняемый файл его использующий. Допустим анализ делается через Clion. В таком варианте при межмодульном анализе при проверке исполняемого файла будут учитываться вычисленные виртуальные значения библиотеки B?
    Если ответ да — более сложный вопрос.
    Есть у нас проект A. В проект A входит библиотека B.
    Компилируются они по отдельности.
    Можно ли как то сделать файл метаданных или чего нибудь ещё по библиотеке B, чтоб при анализе проекта A использовались выведенные для B ограничения значений и вот это всё?
    Ну то есть для основной компиляции есть хидеры и скомпилированная либа, а для анализа хидеры и какая то скомпилированная метаинформация?


    1. Minatych
      12.01.2022 15:39
      +2

      PVS-Studio сохраняет информацию, собранную с помощью семантического анализа в файлы в специальном формате для каждой единицы трансляции соответственно. После чего, они объединяются в один для дальнейшего использования.

      Если в вашем cmake проекте есть цели для сборки нужных библиотек, то они все будут учитываться при анализе. В текущей реализации, в ситуации, когда библиотека только линкуется, но её файлы с исходным кодом не присутствуют, её анализ не будет осуществляться, и, соответственно межмодульная информация не будет собрана.

      В теории, нет ничего сложного в том, чтобы объединить межмодульную информацию для вашего проекта и стороннего, если они в реальности не нарушают ODR. Это хорошая идея для доработки интерфейсной части анализатора, но, пока что это придётся делать вручную (через ядро pvs-studio).

      Я сейчас пишу статью о том, как у нас реализован межмодульный анализ. Хорошая идея будет там описать этот нюанс.


      1. MadHacker
        13.01.2022 12:44
        +2

        Ну он не сторонний. Он просто в отдельном Cmake со своими заморочками.
        Большой проект в котором много модулей. Которые отдельно в том числе собираются на CI. В идеале вообще доделать Cmake скрипты, чтоб они вызывали анализ и формировали отчёт по таргету и как раз мета файл, который так же инсталился бы потом с либой для дальнейшего использования при проекте следующих проектов цепочки.

        но, пока что это придётся делать вручную (через ядро pvs-studio).

        Всмысле есть возможность самостоятельно написать плагин для этого или вызвать что-то из инструментария pvs, или речь о том что потенциально это есть, но никак недоступно пользователям?

        И ещё немного не по теме… PVS пока не умеет анализировать конфигурации в WSL совсем из Clion, или требуется донастройка, установка самого PVS в WSL или что-то такое?


        1. Minatych
          14.01.2022 15:29
          +1

          Всё зависит от структуры ваших проектов. Если они связаны одним CMake-скриптом на верхнем уровне, то межмодульный анализ и так отработает нормально.

          Если у вас 2 отдельных, несвязанных друг с другом CMake-проекта, то всё сложнее. По умолчанию мы не разбираем CMake скрипты. Вместо этого используется сгенерированные последними файлы compile_commands.json, в которых содержатся все команды компиляции. Для каждой такой команды генерируется модуль результатов семантического анализа (DFO). Но, в compile_commands.json нет команд для компоновщика. Поэтому отследить внешние для вашего проекта библиотеки оттуда не получится.

          По поводу скриптов. У нас есть CMake-модуль, который создаёт таргет для запуска анализа. На данный момент в нём не поддерживается межмодульный анализ. В теории, можно для таргетов, собираемых как библиотека, складывать рядом соответствующий DFO-файл. А потом разобрать у всех таргетов команды компоновки, и подключить его при необходимости. Но эту возможность нужно ещё исследовать, заранее сложно увидеть все подводные камни.

          Что я имел в виду под ручным способом. C и C++ анализаторы PVS-Studio представлены ядром и набором вспомогательных утилит командной строки. Ядро - основная программа (PVS-Studio.exe для Windows и pvs-studio для Linux/macOS). Оно запускается в разных режимах, которые настраиваются через конфигурационные файлы. Для каждой единицы трансляции нужно протолкнуть соответствующий конфигурационный файл. Чтобы активировать межмодульный анализ, нужно в этих файлах прописать шаг, который вы выполняете. Всего их 3:

          • сбор семантической информации

          • слияние DFO модулей

          • запуск анализа с использованием объединённого DFO-файла

          Помимо этого, в конфигурационных файлах прописывается и другая важная информация, например версия стандарта, платформа, препроцессор и т. д. Вручную писать их неудобно. Для этого мы и создали утилиты pvs-studio-anayzer (для Linux), или CompilerCommandsAnalyzer.exe для Windows. Их, к слову, использует плагин для CLion. А для анализа .sln используется PVS-Studio_Cmd.exe.

          В этих утилитах на текущий момент не поддерживается функциональность, которую вы хотите. Однако само ядро может справиться с этой задачей, если правильно сгенерировать конфигурационные файлы. Но это, естественно, сложно и неудобно.

           


  1. Metotaxin
    12.01.2022 14:03

    @Paull Постарался описать кейс использования.

    Наша основная цель: поиск уязвимостей и НДВ в наших продуктах и сторонних продуктах, которые мы используем в работе.
    Пользователи анализатора в первую очередь сотрудники ИБ с минимальным опытом разработки, а во вторую разработчики.


    Мы используем довольно много различных open source решений и хотелось бы анализировать:
    Языки:

    • java;

    • groovy;

    • python;

    • php;

    • javascript;

    • pl/sql.

      Минимум:

    • рекурсивно .jar файлы;

    • .class файлы;

    • структуру jsp страниц.

      Критичные для нас моменты:

    • возможность анализа бинарных файлов (с использованием декомпиляции и деобфускации);

    • сравнение результатов сканирований и ретроспектива уязвимостей от версии к версии ПО с отображением сохранившихся, устранённых и новых уязвимостей;

    • ручное управление уровнем критичности уязвимостей;

    • рекомендации по настройке средств защиты (для веб-приложений), рекомендации по настройке WAF;

    • классификация уязвимостей по OWASP/PCI DSS/HIPAA/CWE;

    • наборы готовых правил сканирования с возможностью редактирования, создание своих пресетов из правил;

    • пополняемая база уязвимостей и НДВ;

    • интеграция с jira, git, gitlab, github, jenkins в идеале с Intellij IDEA и teamcity;

    • выгрузка результатов сканирования в json/csv/pdf.


    1. Paull Автор
      12.01.2022 15:56
      +2

      PVS-Studio поддерживает языки C, C++, Java и C#. Соответственно, из перечисленных пунктов, для поддерживаемых нами языков, часть из данных требований мы поддерживаем (например, классификации по OWASP и CWE), часть не поддерживаем (например, HIPAA и анализ бинарных файлов). У нас есть поиск потенциальных уязвимостей, но нет поиска НДВ.

      Т.е. именно по критичным для вас требованиям у нас сейчас есть не все возможности и не все языки.

      Если вам интересно подробнее обсудить соответсвие данным специфичным требованиям по тем языкам, что мы поддерживаем, я думаю, нам лучше было бы перейти в формат более личной беседы - если бы вы отправили нам этот список в поддержку, мы бы ответили подробнее по каждому конкретному пункту.


  1. Metotaxin
    12.01.2022 16:27
    +1

    @Paull, мы с коллегами дополним список и напишем обращение в саппорт. Спасибо.


  1. Mingun
    13.01.2022 18:18
    +2

    Кстати, вот вам идея на заметку: SimpleDateFormat в Java может подготовить сюрприз к Новому году: год, написанный маленькими буквами совсем не то же самое, что год, написанный большими. В последнюю неделю старого года вы можете вдруг оказаться в будущем, потому что "YYYY" — это 2022 для дат 27.12.2021 — 31.12.2021, а не 2021, как кто-то может ожидать. Нужно использовать "yyyy"


    1. Paull Автор
      14.01.2022 09:46

      Спасибо, обязательно глянем.