Механизм пользовательских аннотаций — это способ разметки типов и функций в формате JSON с целью дать анализатору дополнительную информацию. Благодаря этой информации анализатор сможет находить больше ошибок в коде. Механизм работает только для языков С и С++.
Допустим, что в проекте требуется запретить вызов некоторой функции, т.к. она нежелательна к использованию:
void DeprecatedFunction(); // should not be used
void foo()
{
DeprecatedFunction(); // unwanted call site
}
Для того, чтобы анализатор сгенерировал предупреждение V2016 в месте вызова этой функции, необходимо создать специальный файл формата JSON со следующим содержимым:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "DeprecatedFunction",
"attributes": [ "dangerous" ]
}
]
}
После этого достаточно подключить файл (все способы подключения рассмотрены здесь):
//V_PVS_ANNOTATIONS, language: cpp, path: %path/to/annotations.json%
void DeprecatedFunction();
void foo()
{
DeprecatedFunction(); // <= V2016 will be issued here
}
Примечание. По умолчанию диагностическое правило V2016 отключена. Для выдачи предупреждений включите диагностику в настройках.
Подробнее о способе подключения файла аннотаций вы можете узнать в этой документации.
Содержимое файла — JSON-объект, состоящий из двух обязательных полей: version и annotations.
Поле version принимает значение целого типа и задаёт версию механизма. В зависимости от значения файл с разметкой может обрабатываться по-разному. На данный момент поддерживается только одно значение — 1.
Поле annotations — массив объектов "аннотация":
{
"version": 1,
"annotations":
[
{
...
},
{
...
}
]
}
Аннотации могут быть двух видов:
Если аннотация объявляется непосредственно в массиве annotations, то она считается аннотацией верхнего уровня. В ином случае считается, что это вложенная аннотация.
Объект аннотации типа состоит из следующих полей:
Обязательное поле. Принимает строку с одним из значений: "record", "class", "struct", "union". Последние три варианта являются псевдонимами "record" и добавлены для удобства.
Обязательное поле. Принимает строку с квалифицированным (полным) именем сущности. Анализатор будет искать эту сущность, начиная с глобальной области видимости. Если сущность находится в глобальной области, то "::" в начале имени может быть опущено.
Опциональное поле. Массив объектов вложенных аннотаций.
Опциональное поле. Массив строк, который задаёт свойства сущности. Для аннотаций типов доступны следующие атрибуты:
Объект аннотации функции состоит из следующих полей:
Обязательное поле. Принимает строку со значением function. Для вложенных аннотаций функций (в поле members аннотаций типов) также становится доступным значение ctor. Оно показывает, что аннотируется конструктор пользовательского типа.
Принимает строку с именем функции. Поле обязательно, если type имеет значение "function", иначе должно быть опущено. По этому имени анализатор будет искать аннотируемую сущность, начиная с глобальной области видимости.
Для аннотаций верхнего уровня указывается квалифицированное (полное) имя, для вложенных — неквалифицированное.
Если функция находится в глобальной области видимости, то '::' в начале имени может быть опущено.
Опциональное поле. Массив объектов, описывающих формальные параметры. Данное поле совместно с name задаёт сигнатуру функции, по которой анализатор будет сравнивать аннотацию с её объявлением в коде программы. В случае с функциями-членами анализатор также рассматривает поле qualifiers.
Каждый объект содержит следующие поля:
Если аннотацию нужно применить для всех перегрузок вне зависимости от параметров, то поле можно опустить:
// Code
void foo(); // dangerous
void foo(int); // dangerous
void foo(float); // dangerous
// Annotation
{
....
"type": "function",
"name": "foo",
"attributes": [ "dangerous" ]
....
}
Если же интересует перегрузка, не принимающая параметров, следует явно указать пустой массив:
// Code
void foo(); // dangerous
void foo(int); // ok
void foo(float); // ok
// Annotation
{
....
"type": "function",
"name": "foo",
"attributes": [ "dangerous" ],
"params": []
....
}
# |
Название атрибута |
Описание атрибута |
---|---|---|
1 |
immutable |
Подсказывает анализатору, что после вызова функции переданный аргумент не был модифицирован. Например, функция printf имеет побочные эффекты (печать в stdout), однако не производит модификации переданных аргументов. |
2 |
not_null |
Действует только для параметров nullable-типа. В функцию необходимо передавать аргумент в состоянии "валидный". |
3 |
unique_arg |
Передаваемые аргументы должны отличаться. Например, нет смысла передавать в std::swap два одинаковых аргумента. |
4 |
format_arg |
Параметр обозначает форматную строку. Анализатор будет проверять аргументы согласно форматной спецификации printf. |
5 |
pointer_to_free |
Указатель, по которому в функции будет освобождена память с помощью free. Указатель может быть нулевым. |
6 |
pointer_to_gfree |
Указатель, по которому в функции будет освобождена память с помощью g_free. Указатель может быть нулевым. |
7 |
pointer_to_delete |
Указатель, по которому в функции будет освобождена память с помощью 'operator delete'. Указатель может быть нулевым. |
8 |
pointer_to_delete[] |
Указатель, по которому в функции будет освобождена память с помощью 'operator delete[]'. Указатель может быть нулевым. |
9 |
pointer_to_unmap |
Указатель, по которому в функции будет освобождена память с помощью 'munmap'. Указатель может быть нулевым. |
10 |
taint_source |
Данные, возвращающиеся через параметр, получены из недостоверного источника. |
11 |
taint_sink |
Данные, передаваемые через параметр, могут привести к эксплуатации уязвимости, если они получены из недостоверного источника. |
12 |
non_empty_string |
Параметр должен принимать любую непустую строку. |
Все поля ограничений — опциональные. Далее приведён список полей, которые задают условия ограничения.
Поля, задающие список разрешённых и запрещённых значений параметра:
Каждая строка внутри массива — это интервал от минимальной до максимальной границы включительно. Строка с интервалами указывается в формате "x..y", где 'x' и 'y' — это левая и правая границы соответственно. Одну из границ можно опустить. Тогда строка будет иметь вид "x.." или "..y". В таком случае интервал будет от 'x' до плюс бесконечности и от минус бесконечности до 'y' соответственно.
Примеры интервалов:
Массив может содержать несколько интервалов. При их чтении анализатор нормализует все интервалы в массиве, т.е. объединяет пересекающиеся и рядом стоящие интервалы, располагая их в порядке возрастания.
Если поля allowed и disallowed указаны одновременно, то анализатор вычитает "disallowed" интервалы из "allowed", чтобы получить множество разрешённых значений. Если значения из поля disallowed полностью перекрывают значения из allowed, то пользователю будет выдано предупреждение V019.
Опциональное поле. Объект, внутри которого возможно использование только поля attributes — массива строк, который позволяет задать атрибуты возвращаемого значения.
# |
Название атрибута |
Описание атрибута |
---|---|---|
1 |
not_null |
Функция всегда возвращает объект nullable-типа в состоянии "валидный". |
2 |
maybe_null |
Функция может вернуть объект nullable-типа в состоянии "невалидный", и его стоит проверить перед разыменованием. |
3 |
taint_source |
Функция может вернуть данные из недостоверного источника. |
Опциональное поле. Массив строк, позволяющий задать шаблонные параметры функции. Поле необходимо, когда шаблонные параметры используются в сигнатуре функции:
// Code
template <typename T1, class T2>
void MySwap(T1 &lhs, T2 &rhs);
// Annotation
{
....
"template_params": [ "typename T1", "class T2" ],
"name": "MySwap",
"params": [
{ "type": "T1 &", attributes: [ "unique_arg" ] },
{ "type": "T2 &", attributes: [ "unique_arg" ] }
]
....
}
Опциональное поле. Позволяет применить аннотацию только к функции-члену с определённым набором cvref-квалификаторов. Доступно только для вложенных аннотаций, у которых поле type имеет значение "function". Данное поле совместно с name и params задаёт сигнатуру нестатической функции-члена, по которой анализатор будет сравнивать аннотацию с её объявлением в коде программы. Принимает массив строк со следующими возможными значениями: "const", "volatile", "&" или "&&".
Пример:
// Code
struct Foo
{
void Bar(); // don't need to annotate this overload
void Bar() const; // want to annotate this overload
void Bar() const volatile; // and this one
};
// Annotation
{
....
"type": "record",
"name": "Foo",
"members": [
{
"type": "function",
"name": "Bar",
"qualifiers": [ "const" ]
},
{
"type": "function",
"name": "Bar",
"qualifiers": [ "const", "volatile" ]
}
]
....
}
Если аннотацию надо применить ко всем квалифицированным и неквалифицированным версиям, то нужно опустить поле:
// Code
struct Foo
{
void Bar(); // want to annotate this overload
void Bar() const; // and this one
};
// Annotation
{
....
"type": "record",
"name": "Foo",
"members": [
{
"type": "function",
"name": "Bar",
}
]
....
}
Если надо применить аннотацию только к неквалифицированной версии, то значением поля должен быть пустой массив:
// Code
struct Foo
{
void Bar(); // want to annotate this overload
void Bar() const; // but NOT this one
};
// Annotation
{
....
"type": "record",
"name": "Foo",
"members": [
{
"type": "function",
"name": "Bar",
"qualifiers": []
}
]
....
}
Опциональное поле. Массив строк, который задаёт свойства сущности.
# |
Название атрибута |
Описание атрибута |
Примечание |
---|---|---|---|
1 |
pure |
Функция считается чистой. |
Функция чистая, когда она не имеет побочных эффектов, не модифицирует переданные аргументы, и результат функции одинаков при исполнении на одном и том же наборе аргументов. |
2 |
noreturn |
Функция не возвращает управление вызывающей функции. |
|
3 |
nodiscard |
Результат функции должен использоваться. |
|
4 |
nullable_uninitialized |
Функция-член пользовательского nullable-типа переводит объект в состоянии "невалидный". |
|
5 |
nullable_initialized |
Функция-член пользовательского nullable-типа переводит объект в состоянии "валидный". |
|
6 |
nullable_checker |
Функция проверяет состояние пользовательского nullable-типа. Если функция возвращает true, то объект считается в состоянии "валидный", не возвращает — "невалидный". Результат функции должен неявно конвертироваться к типу bool. |
|
7 |
nullable_getter |
Функция производит доступ к внутренним данным пользовательского nullable-типа. Объект при этом должен быть в состоянии "валидный". |
|
8 |
dangerous |
Функция помечена как опасная, и код программы не должен содержать её вызова. |
Можно использовать в качестве пометки функции как устаревшей (deprecated). Для выдачи предупреждений требуется включить диагностическое правило V2016 в настройках. |
Ниже приведена таблица применимости различных атрибутов с аннотациями функций:
# |
Атрибут |
Свободная функция |
Конструктор |
Функция-член |
---|---|---|---|---|
1 |
pure |
✓ |
✕ |
✓ |
2 |
noreturn |
✓ |
✕ |
✓ |
3 |
nodiscard |
✓ |
✓ |
✓ |
4 |
nullable_uninitialized |
✕ |
✓ |
✓ |
5 |
nullable_initialized |
✕ |
✓ |
✓ |
6 |
nullable_checker |
✕ |
✕ |
✓ |
7 |
nullable_getter |
✕ |
✕ |
✓ |
8 |
dangerous |
✓ |
✕ |
✓ |
JSON схемы поставляется в дистрибутиве, а также доступны по ссылкам ниже:
Допустим, есть следующий пользовательский nullable-тип:
constexpr struct MyNullopt { /* .... */ } my_nullopt;
template <typename T>
class MyOptional
{
public:
MyOptional();
MyOptional(MyNullopt);
template <typename U>
MyOptional(U &&val);
public:
bool HasValue() const;
T& Value();
const T& Value() const;
private:
/* implementation */
};
Примечания по коду:
Тогда аннотация класса и его функций-членов будет выглядеть следующим образом:
{
"version": 1,
"annotations": [
{
"type": "class",
"name": "MyOptional",
"attributes": [ "nullable" ],
"members": [
{
"type": "ctor",
"attributes": [ "nullable_uninitialized" ]
},
{
"type": "ctor",
"attributes": [ "nullable_uninitialized" ],
"params": [
{
"type": "MyNullopt"
}
]
},
{
"type": "ctor",
"template_params": [ "typename U" ],
"attributes": [ "nullable_initialized" ],
"params": [
{
"type": "U &&val"
}
]
},
{
"type": "function",
"name": "HasValue",
"attributes": [ "nullable_checker", "pure", "nodiscard" ]
},
{
"type": "function",
"name": "Value",
"attributes": [ "nullable_getter", "nodiscard" ]
}
]
}
]
}
Допустим, есть следующий код:
namespace Foo
{
template <typaname CharT>
size_t my_strlen(const CharT *ptr);
}
Функция Foo::my_strlen имеет следующие свойства:
Тогда аннотация функции будет выглядеть следующим образом:
{
"version": 1,
"annotations":
[
{
"type": "function",
"name": "Foo::my_strlen",
"attributes": [ "pure" ],
"template_params": [ "typename CharT" ],
"params": [
{
"type": "const CharT *",
"attributes": [ "not_null" ]
}
]
}
]
}
Допустим, есть следующая функция Foo::LogAtError:
namespace Foo
{
void LogAtError(const char *, ...);
}
О ней известны следующие факты:
Анализатор может проверять переданные аргументы согласно форматной строке, а также понимать, что после вызова этой функции код недостижим. Для этого надо разметить функцию следующим образом:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "Foo::LogAtError",
"attributes": [ "noreturn" ],
"params": [
{
"type": "const char *",
"attributes" : [ "format_arg", "not_null", "immutable" ]
},
{
"type": "...",
"attributes": [ "immutable" ]
}
]
}
]
}
Допустим, что в предыдущем примере программист добавил несколько перегрузок функции Foo::LogAtExit:
namespace Foo
{
void LogAtExit(const char *fmt, ...);
void LogAtExit(const char8_t *fmt, ...);
void LogAtExit(const wchar_t *fmt, ...);
void LogAtExit(const char16_t *fmt, ...);
void LogAtExit(const char32_t *fmt, ...);
}
В этом случае можно не писать аннотации для всех перегрузок, а лишь для одной, воспользовавшись символом подстановки:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "Foo::LogAtExit",
"attributes": [ "noreturn" ],
"params": [
{
"type": "?",
"attributes" : [ "format_arg", "not_null", "immutable" ]
},
{
"type": "...",
"attributes": [ "immutable" ]
}
]
}
]
}
Допустим, есть две перегрузки функции Foo::Bar:
namespace Foo
{
void Bar(int i);
void Bar(double d);
}
Требуется запретить использование первой перегрузки. Для этого надо разметить функцию следующим образом:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "Foo::Bar",
"attributes": [ "dangerous" ],
"params": [
{
"type": "int"
}
]
}
]
}
Допустим, что есть функция, которая возвращает внешние данные через out-параметр и возвращаемое значение.
std::string ReadStrFromStream(std::istream &input, std::string &str)
{
....
input >> str;
return str;
....
}
Для того, чтобы пометить эту функцию как источник недостоверных данных, необходимо её разметить следующим образом:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "ReadStrFromStream",
"params": [
{
"type": "std::istream &input"
},
{
"type": "std::string &str",
"attributes": [ "taint_source" ]
}
],
"returns": { "attributes": [ "taint_source" ] }
}
]
}
Предположим, что существует функция, при попадании в которую недостоверных данных может быть эксплуатирована какая-либо уязвимость.
void DoSomethingWithData(std::string &str)
{
.... // Some vulnerability
}
Чтобы пометить такую функцию как приемник недостоверных данных (функцию-сток), необходимо написать следующую аннотацию:
{
"version": 1,
"annotations": [
{
{
"type": "function",
"name": "DoSomethingWithData",
"params": [ { "type": "std::string &str",
"attributes": [ "taint_sink" ] }]
}
}
]
}
В C++ анализаторе есть диагностическое правило V1116, которое срабатывает при обнаружении создания объекта исключения с пустым сообщением. По умолчанию данное правило работает только с типами исключений из стандартной библиотеки C++. Для поддержки пользовательских типов исключений необходимо добавить специальную аннотацию.
Предположим, есть класс, определяющий объекты, которые будут использованы для выброса исключения:
class MyException: public std::runtime_error
{
public:
MyException(const std::string& what_arg )
: std::runtime_error(what_arg){}
MyException(const char *what_arg)
: std::runtime_error(what_arg){}
};
В аннотации необходимо определить тип исключения с атрибутом 'exception' и затем конструкторы, способные принимать поясняющие сообщения. Для каждого параметра конструктора, который должен принимать сообщение, следует указать атрибут 'non_empty_string'.
Например, аннотация класса 'MyException' может выглядеть так:
{
"version": 2,
"language": "cpp",
"annotations": [
{
"type": "class",
"name": "MyException",
"attributes": ["exception"],
"members": [
{
"type": "ctor",
"params": [{ "type": "const char*",
"attributes": ["non_empty_string"]}]
},
{
"type": "ctor",
"params": [{ "type": "const std::string&",
"attributes": ["non_empty_string"]}]
}
]
}
]
}