Protocol Buffers — это очень популярный, крутой и качественный проект, развиваемый в основном компанией Google. Это хороший вызов для статического анализатора кода PVS-Studio. Найти хоть что-то — это уже достижение. Попробуем.
Продолжая наш многолетний цикл публикаций про проверку открытых проектов, я обратил внимание на проект Protocol Buffers (protobuf). Библиотека реализует протокол сериализации (передачи) структурированных данных. Это эффективная бинарная альтернатива текстовому формату XML.
Проект показался мне интересным вызовом для анализатора PVS-Studio, ведь компания Google весьма основательно подходит к качеству разрабатываемого C++ кода. Взять хотя бы документ "Безопасное использование C++", который недавно активно обсуждался. Дополнительно protobuf используется большим количеством разработчиков в своих проектах и потому хорошо ими протестирован. Найти хотя бы пару ошибок в этом проекте является достижением, которое будет лестно нашей команде. Так чего мы ждём, вперёд!
Раньше мы никогда специально не проверяли этот проект. Однажды, три года назад, мы заглянули в него, когда писали цикл статей про проверку проекта Chromium. Мы заметили интересную ошибку в функции проверки даты, которую описали в отдельной заметке "31 февраля".
Признаюсь, у меня была задумка, когда я писал эту статью. Я хотел показать возможности нового механизма межмодульного анализа С++ проектов. К сожалению, в этот раз межмодульный анализ ничего нового не привнёс. Что с ним, что без него — находятся одни и те же интересные места. Впрочем, это неудивительно. В этом проекте вообще сложно что-то найти :).
Итак давайте посмотрим, какие ляпы скрылись от внимания разработчиков и других инструментов.
Copy-Paste
void SetPrimitiveVariables(....) {
....
if (HasHasbit(descriptor)) {
(*variables)["get_has_field_bit_message"] = ....;
(*variables)["set_has_field_bit_message"] = ....;
(*variables)["clear_has_field_bit_message"] = ....;
....
} else {
(*variables)["set_has_field_bit_message"] = ""; // <=
(*variables)["set_has_field_bit_message"] = ""; // <=
(*variables)["clear_has_field_bit_message"] = "";
....
}
Предупреждение PVS-Studio: V519 [CWE-563] The variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 163, 164. java_primitive_field_lite.cc 164
Классическая ошибка, возникшая в процессе копирования строк. Где-то фрагмент строки заменили, где-то нет. В результате ключом два раза является "set_has_field_bit_message".
Если посмотреть код выше, то становится понятно, что на самом деле в else-ветке планировалось написать:
(*variables)["get_has_field_bit_message"] = "";
(*variables)["set_has_field_bit_message"] = "";
(*variables)["clear_has_field_bit_message"] = "";
Утечка файлового дескриптора
ExpandWildcardsResult ExpandWildcards(
const string& path, std::function<void(const string&)> consume) {
....
HANDLE handle = ::FindFirstFileW(wpath.c_str(), &metadata);
....
do {
// Ignore ".", "..", and directories.
if ((metadata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 &&
kDot != metadata.cFileName && kDotDot != metadata.cFileName) {
matched = ExpandWildcardsResult::kSuccess;
string filename;
if (!strings::wcs_to_utf8(metadata.cFileName, &filename)) {
return ExpandWildcardsResult::kErrorOutputPathConversion; // <=
}
....
} while (::FindNextFileW(handle, &metadata));
FindClose(handle);
return matched;
}
Предупреждение PVS-Studio: V773 [CWE-401] The function was exited without releasing the 'handle' handle. A resource leak is possible. io_win32.cc 400
Перед выходом из функции файловый дескриптор handle должен быть закрыт с помощью вызова FindClose(handle). Однако этого не произойдёт, если не удастся сконвертировать текст из формата UTF-8-encoded в UTF-8. Функция просто завершит работу и вернёт статус ошибки.
Потенциальное переполнение
uint32_t GetFieldOffset(const FieldDescriptor* field) const {
if (InRealOneof(field)) {
size_t offset =
static_cast<size_t>(field->containing_type()->field_count() +
field->containing_oneof()->index());
return OffsetValue(offsets_[offset], field->type());
} else {
return GetFieldOffsetNonOneof(field);
}
}
Предупреждение PVS-Studio: V1028 [CWE-190] Possible overflow. Consider casting operands, not the result. generated_message_reflection.h 140
Складываются два значение типа int и помещаются в переменную типа size_t:
size_t offset = static_cast<size_t>(int_var_1 + int_var_2);
Предполагается, что в случае 64-битной сборки сумма значений двух 32-битных переменных может превысить значение INT_MAX. Поэтому результат помещается в переменную типа size_t, которая будет 64-битной в 64-битной программе. Более того, понимая, что при сложении двух int может произойти переполнение, программистом используется явное приведение типа.
Вот только используется это явное приведение типа неправильно. Оно ни от чего не защищает. И без него сработало бы неявное расширение типа от int к size_t. Так что написанный код ничем не отличается от варианта:
size_t offset = int_var_1 + int_var_2;
Скорее всего, по невнимательности скобку поставили не там, где нужно. Правильным вариантом является:
size_t offset = static_cast<size_t>(int_var_1) + int_var_2;
Разыменование нулевого указателя
bool KotlinGenerator::Generate(....)
{
....
std::unique_ptr<FileGenerator> file_generator;
if (file_options.generate_immutable_code) {
file_generator.reset(
new FileGenerator(file, file_options, /* immutable_api = */ true));
}
if (!file_generator->Validate(error)) {
return false;
}
....
}
Предупреждение PVS-Studio: V614 [CWE-457] Potentially null smart pointer 'file_generator' used. java_kotlin_generator.cc 100
Если переменная generate_immutable_code вдруг окажется равна fasle, то умный указатель file_generator останется равен nullptr. Как следствие, произойдёт разыменование нулевого указателя.
Видимо, пока переменная generate_immutable_code всегда истинна, раз эта ошибка ещё не обнаружена. Её можно счесть несущественной. Как только в процессе редактирования кода логика работы изменится, то произойдёт разыменование нулевого указателя, что будет сразу замечено и исправлено. А с другой стороны, в этом коде находится мина, которую лучше найти и исправить заранее, чем ждать, когда кто-то на ней подорвётся в будущем. Суть статического анализа как раз том, чтобы найти ошибки заранее.
Там ли скобка?
AlphaNum::AlphaNum(strings::Hex hex) {
char *const end = &digits[kFastToBufferSize];
char *writer = end;
uint64 value = hex.value;
uint64 width = hex.spec;
// We accomplish minimum width by OR'ing in 0x10000 to the user's value,
// where 0x10000 is the smallest hex number that is as wide as the user
// asked for.
uint64 mask = ((static_cast<uint64>(1) << (width - 1) * 4)) | value;
....
}
Нас интересует это подвыражение:
((static_cast<uint64>(1) << (width - 1) * 4))
Код не нравится анализатору сразу по 2 причинам:
- V634 [CWE-783] The priority of the '*' operation is higher than that of the '<<' operation. It's possible that parentheses should be used in the expression. strutil.cc 1408
- V592 The expression was enclosed by parentheses twice: ((expression)). One pair of parentheses is unnecessary or misprint is present. strutil.cc 1408
Согласитесь, предупреждения дополняют друг друга. Есть совместное использование оператора сдвига и умножения. Легко забыть, какой из этих операторов более приоритетен. А повторяющиеся скобочки намекают на то, что про эту неоднозначность знали и хотели её избежать. Но не получилось.
Есть два варианта. Первый: код корректен. В этом случае дополнительные скобки просто должны упрощать чтение кода, но ни на что не влияют:
uint64 mask = (static_cast<uint64>(1) << ((width - 1) * 4)) | value;
Второй: выражение написано с ошибкой. Тогда дополнительные скобки должны поменять последовательность выполняемых операций:
uint64 mask = ((static_cast<uint64>(1) << (width - 1)) * 4) | value;
Заключение
Приятно, что удалось найти недоработки в таком известном и качественном проекте. С другой стороны, это плохой проект для демонстрации возможностей статических анализаторов кода :). Сложно показать пользу от инструмента, если находится всего пара ошибок :).
Следует помнить, что пользу статический анализатор приносит при регулярном использовании для проверки свежего кода, а не при разовых проверках уже оттестированных проектов.
Тем не менее, нужно с чего-то начать. Поэтому предлагаю скачать PVS-Studio, проверить проект и взглянуть на самые лучшие предупреждения. Скорее всего, вы увидите много для себя интересного :).
Если же ваш код столь же качественный, как protobuf, то предлагаем сразу перейти к полноценному сценарию использования инструмента. Попробуйте внедрить PVS-Studio в процесс разработки и посмотреть, что интересного будет находиться каждый день. Как это сделать в случае, если у вас большой проект, описано здесь.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Protocol Buffers, a brutal protocol from Google, vs. PVS-Studio, a static code analyzer.
Комментарии (23)
qw1
06.11.2021 13:47+1Последняя не ошибка. То, что анализатор будет ругаться на любое выражение типа
1 << 4*x
это странно.datacompboy
06.11.2021 14:45Вне зависимости от того ошибка это или нет, это код который нетривиально парзить человеку.
qw1
06.11.2021 15:20+1Иногда это позволит найти какую-то ошибку.
С другой стороны, разбирать 100500 false positive тоже сомнительное удовольствие.Andrey2008 Автор
06.11.2021 15:44Не будет из 100500. Не верится, что такие выражения часто требуются. А если это особый проект, где их много, то можно отключить диагностику.
Andrey2008 Автор
06.11.2021 15:43И правильно что ругается, ибо каждый раз нужно вспоминать (или подглядывать) приоритет операций. Если лишнее — эту диагностику можно отключить. В данном случае ситуация ещё усугубляется повторяющимися скобками.
qw1
06.11.2021 19:37+1А на это будет ругаться:
if (a && b || c && d) { ... }
А на это:x = 1+3*y;
А почему такая разница?Andrey2008 Автор
06.11.2021 20:49if (a && b || c && d) // нет if (a || b && c || d) // да x = 1 + 3 * y; // нет
- Первая строка. Тот, кто писал, скорее всего понимает, что хочет объединить операцией || два подвыражения. Типовой код. Ошибки скорее всего нет.
- Вторая строка. Возможно, код работает не так, как ожидает программист. Возможно, ошибки нет, но код лучше перепроверить.
- Третья строка. Невероятно, что кто-то спутает приоритет операций + и *. Ошибки скорее всего нет.
qw1
07.11.2021 01:02А если
if (a || b && c)
?
Я просто правило хочу понять )))Andrey2008 Автор
07.11.2021 11:38Просто не получится :). Это ведь ещё и разные диагностики :). Изначально речь шла про V634. Она именно оценивает выражения со сдвигами.
Паттерн вида x << y + z.
Забывают, что у сложения/вычитания/умножения/деления более высокий приоритет, чем у сдвига. Лучше всегда явно ставить скобки.
Пример:int x = a<<4 + b;
Исключения:- Операция с высоким приоритетом находится в макросе, а сдвиг нет.
- Операция с высоким приоритетом ('-','/','%') выполняется над числом 64(int64), 32(int), 16(short), 8(char).
- Разделение на строки выражения свидетельствует о преднамеренном использовании:
a = 1 << 2 * 8; //ok b = 2 << 2 * 8 //ok c = 3 << 2 * 8 //err
- Нет смысл сдвигать вправо константу на константу или влево константу (кроме 1) на константу.
- Сдвиг является условием. В этом случае нет смысла результат сдвига умножать. Пример: if (value >> (i — 1) * 8)
А если речь заходит про && и ||, то это уже V648.
Странно, когда логические операции '&&' и '||' используются вместе и не разделены скобками. Часто их приоритеты путают. Приоритет оператора && выше, чем у ||.
Следует предупреждать в том случае, если написано так: A || B && C.
Исключение:- Если имеется выражение вида A || !A && B, т.е. оператор || разделяет два противоположных по смыслу высказывания.
- Если выражение вида… || p != nullptr && f(p) ...
- Имеется выражение вида x == 0 || A != 123 && A != 321, т.е. оператор &&
разделяет сравнение одной и той же переменной с разными константами.
При этом, с одной стороны от последовательности || && ничего не должно быть.Т.е. здесь не ругаемся: b || A != 0 && A != 1 а здесь ругаемся: b || A != 0 && A != 1 || c и здесь ругаемся: b || A != 0 && (A != 1 || c)
qw1
07.11.2021 16:22+2Понятно. Всякие очевидные false positive прописываются в исключения, поэтому в результате их будет немного. Плюс прогоны инструмента на гигабайтах реальных исходников, и что там будет часто встречаться (при этом не являясь ошибкой), тоже будет загнано в исключения.
datacompboy
06.11.2021 15:40-1size_t offset = static_cast<size_t>(int_var_1 + int_var_2);
Мну тут таки не согласно. Проблема тут не в int32/unit64, а скорее в signed / unsigned операциях. В зависимости от компилятора int может быть и 32 и 64, но size_t точно не меньше их размеров и точно unsigned. Так как int_var_1 и int_var_2 тут это количество и смещение (всегда неотрицательные), их сумма точно всегда влезет в size_t -- но не факт что всегда влезет в int.
Andrey2008 Автор
06.11.2021 15:46Анализатор отталкивается не от теоретических, а практических типов :). В данном режиме сборки int 32-битный. А значит может быть переполнение.
datacompboy
06.11.2021 16:11-1Я к тому что неважно 32 или 64 -- переполнение возможно всё равно так как int+int не обязательно влезает в int. (К слову у меня сборка на линупсе так что всё 64бита -- и всё равно возможно! впрочем тогда надо СЛИШКОМ большие структуры... :D)
demp
07.11.2021 14:46+3Похоже здесь
static_cast<size_t>
был добавлен просто для того, чтобы задушить предупреждение компилятора о преобразовании знакового int в беззнаковый size_t.Любовь Гугла к int для индексов и количества элементов в контейнере все время наталкивается на реальность стандартной библиотеки, где для этого используется size_t.
Но больше всего меня, как пользователя protobuf, раздражает их использование int для размера буфера. Ведь в Гугле уверены, что
640 кБ2 ГБ хватит всем. Поэтому я останавливаюсь, вздыхаю и мысленно ругаюсь, когда приходится писать или читать кода видаconst std::string buf = protobufMessage.SerialzeAsString(); // ага, авторы protobuf решили, что std::string это самый подходящий тип для буфера с бинарными данными
protobufMessage.ParseFromArray(buf.data(), static_cast<int>(buf.size())); // избавляемся от предупреждения и верим что тут всегда будет меньше 2 ГБ
qw1
07.11.2021 16:23Жаль, что после релиза уже нельзя исправить сигнатуры — у всех пользователей библиотеки, которые глушили предупреждения cast-ами, снова полезут warning-и.
demp
08.11.2021 11:35+3Со сменой типа параметров это может быть чуть проще. При желании можно добавить перегруженную версию
ParseFromArray(const void* data, size_t size)
, опционально пометитьParseFromArray(const void* data, int size)
как deprecated.При смене возвращаемого типа тоже все решаемо, например как
ByteSize()
->ByteSizeLong()
.Проблема в точке зрения авторов protobuf, что int для размеров это нормально.
qw1
08.11.2021 11:52Да, не подумал. В принципе, int-размеров достаточно для всех моих применений, но неидеоматичность современному C++ и постоянные касты неприятны.
datacompboy
Эмм.... А откуда исходники, версия? Я тут смотрю в поисках вот этого куска кода:
И не вижу его ни в одной версии исходников O_O
datacompboy
Ох. Мои глаза :D не в тот файл смотрю =) Просто строка-дубликат, не ошибка, пьфью.
marshinov
Кажется там должно быть
(*variables)["get_has_field_bit_message"]
datacompboy
Нет, там не предполагалось второй. Код вообще получен не копипастой, а редактированием - в первом случае дубликат убрали, во втором проглядели.