В релиз 4.0 мы добавляем возможность кастомизации анализа с помощью атрибутов для C/C++.
Анализатор Svace не требует предварительной подготовки проекта для анализа. Достаточно, чтобы проект компилировался. Дополнительные действия по подготовке, как, например, написание аннотаций, описывающих семантику пользовательских функций, затрудняют использование анализатора. Тем не менее в ряде случаев требуется предоставить информацию для анализатора, которую сложно вывести автоматически.
В Svace для кастомизации анализа существует механизм спецификаций. Спецификации являются заглушками моделируемых функций с указанием дополнительной информации. После добавления спецификаций анализатор лучше понимает семантику программы, благодаря чему уменьшается количество ложных срабатываний, а также увеличивается количество потенциально находимых ошибок.
Спецификации отделены от кода, что имеет как плюсы, так и минусы. Плюс заключается в том, что не надо менять кодовую базу для кастомизации анализатора. Минусом же является слабая связь между кодом и спецификациями. Если оригинальная функция будет переименована, либо изменена (например, добавится ещё один параметр), то спецификация перестанет применяться.
В ближайшем релизе для языков C/C++ мы добавляем возможность кастомизации пользовательского кода с помощью атрибутов компиляторов C23 и C++11. Пример синтаксиса стандартного атрибута (noreturn):
[[noreturn]] void f() {
throw "error";
}
Также поддержан синтаксис атрибутов gcc:
void f() __attribute__ ((noreturn)) {
throw "error";
}
Атрибуты имеют привязку к коду и поэтому не будут потеряны при изменении кода. Механизм атрибутов не является заменой спецификациям. Мы будем поддерживать оба способа.
Были добавлены следующие атрибуты:
Все эти атрибуты добавляют информацию о возвращаемом значении функции. Их необходимо добавлять к определениям функций:
__attribute__((svace_possible_negative)) int getNeg(int a) {
//code
}
[[svace::possible_null]] int* alloc_buffer(int size) {
//code
}
Замечу, что Svace учитывал атрибут noreturn
и раньше, т.е. функции, помеченные этим атрибутом, рассматривались как завершающие программу.
Атрибут possible_negative
сообщает анализатору, что возвращаемое значение может быть отрицательным. Анализатор выдаст ошибку, если такое значение будет использовано как индекс массива, либо в вызовах функций, которые не допускают отрицательные значения.
Пример использования:
int foo(int c);
__attribute__((svace_possible_negative)) int getNeg(int a) {
return foo(a);
}
void example(int b){
char buf[10];
int a = getNeg(b);
buf[a]=0;//NEGATIVE_CODE_ERROR
}
Атрибут possible_null
, аналогично, сообщает, что возвращаемое значение может быть нулёвым указателем.
С помощью атрибута value_interval
можно установить возможный диапазон значений:
__attribute__((svace_value_interval(0, 100))) int foo1(int count) {...}
Анализатор будет предполагать, что функция не может вернуть значение за пределами указанного отрезка.
Атрибут taint
означает, что функция может вернуть адрес памяти, содержимое которой может контролироваться потенциальным злоумышленником.
Такие строки должны быть проверены перед использованием внутри программы.
Атрибут taint_int
означает, что целое число может контролироваться злоумышленником. Этот атрибут имеет две формы. Обычную, означающую, что целое число может иметь любые значения, и с интервалом, аналогичным value_interval
, в котором указывается диапазон:
__attribute__((svace_taint_int(0, 100))) int readIntFromFile(int offset) {...}
Ещё один атрибут target_dependent
предназначен специально для подавления предупреждений функций, реализация которых зависит от платформы.
Рассмотрим функцию get_size
, которая в зависимости от компилируемой платформы, возвращает разные значения:
int get_size() {
#ifdef __x86_64__
return 64;
#else
return 32;
#endif
}
Этот код может быть собран для разных платформ. Анализатор Svace
будет проверять результаты для каждой из платформ по отдельности.
Например, если определён макрос __x86_64__
, то такая функция всегда будет возвращать значение 64. Тогда для следующего кода:
BOOL is64() {
if (get_size() == 64)
return TRUE;
return FALSE; //unreachable code
}
анализатор будет выдавать предупреждение о недостижимом коде, т.к. get_size() == 64
для рассматриваемой платформы всегда истинно.
Данный атрибут позволяет подавлять подобные срабатывания. Пример использования:
__attribute__((svace_target_dependent))
int get_size() {
#ifdef __x86_64__
return 64;
#else
return 32;
#endif
}
Помимо функций следующие атрибуты можно использовать для полей структур:
Они имеют аналогичную семантику, но их действие распространяется не на вызов функции, а на чтение из поля структуры. Например, для следующего кода
struct struct1 {
char* a;
int b __attribute__((svace_possible_negative));
};
void test(struct struct1*p, int b){
char buf[10];
buf[p->b]=0;//NEGATIVE_CODE_ERROR
}
будет выдана ошибка NEGATIVE_CODE_ERROR
также, как и для примера выше с вызовом функции getNeg
.
Атрибуты предоставляют удобный способ кастомизации поведения функций для C/C++. Если у вас есть идеи какие ещё атрибуты можно добавить – пишите нам и мы рассмотрим возможность их добавления.
Также мы планируем добавлять аннотации для языка Java, которые будут иметь похожее поведение с механизмом атрибутов.