Protocol Buffers — очень популярный, крутой и качественный продукт, который в основном разрабатывается Google. Это хороший вызов для статического анализатора кода PVS-Studio. Найти хоть что-то — уже достижение. Давайте попробуем.

Пишу о Protocol Buffers (protobuf) в рамках многолетней серии статей о проверке open-source проектов. Библиотека реализует протокол сериализации структурированных данных. Это эффективная бинарная альтернатива текстовому формату XML.

Проект показался интригующим вызовом для анализатора PVS-Studio, поскольку Google очень серьезно подходит к качеству производимого кода на C++. Возьмем, к примеру, активно обсуждаемый в последнее время документ Безопасное использование C++. Кроме того, многие разработчики используют protobuf в своих проектах, а это означает, что продукт protobuf хорошо протестирован. Найти хотя бы пару ошибок в этом проекте — вызов, который мы взяли на себя. Так чего же мы ждем? Пришло время узнать, на что способен PVS-Studio!

Мы никогда раньше специально не проверяли этот проект. Когда-то, года три назад, мы его рассматривали, когда писали цикл статей о проверке Chromium. Мы нашли интересную ошибку в функции проверки данных и описали ее в отдельной статье — 31 февраля.

Честно говоря, когда я писал свою статью в этот раз, у меня был определенный план. Я хотел продемонстрировать новую возможность анализатора — механизм межмодульного анализа C++ проектов — и что он умеет. К сожалению, на этот раз межмодульный анализ не дал новых интересных результатов. С ним или без — было все равно, никаких новых интересных триггеров анализатора в коде. Хотя в этом не было ничего удивительного. В этом проекте вообще сложно что-то найти :).

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

Копировать вставить

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] Переменной дважды подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 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] Произошел выход из функции без отпускания ручки ручка. Возможна утечка ресурсов. io_win32.cc 400

Перед выходом из функции вызов метода FindClose(handle) должен закрыть файловый дескриптор handle. Однако этого не происходит, если текст в кодировке UTF-8 не может быть преобразован в 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] Возможно переполнение. Рассмотрим приведение операндов, а не результата. сгенерированный_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] Используется потенциально нулевой смарт-указатель ‘file_generator’. java_kotlin_generator.cc 100

Если переменная generate_immutable_code равна false, то интеллектуальный указатель file_generator остается равным nullptr. Следовательно, нулевой указатель будет разыменован.

По-видимому, переменная generate_immutable_code всегда имеет значение true — иначе ошибка была бы уже обнаружена. Его можно назвать незначительным. Как только кто-то отредактирует код и его логику, нулевой указатель будет разыменован, кто-то заметит и устранит проблему. С другой стороны, этот код содержит, так сказать, мину. И лучше найти его пораньше, чем сидеть и ждать, пока кто-нибудь взорвется на нем в будущем. Цель статического анализа — найти ошибки до того, как они станут опасными.

Скобка стоит на своем месте?

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))

Анализатору не нравится этот код по двум причинам:

  • V634 [CWE-783] Приоритет операции * выше, чем у операции ‹‹. Возможно, в выражении следует использовать скобки. strutil.cc 1408
  • V592 Выражение было дважды заключено в круглые скобки: ((выражение)). Одна пара скобок лишняя или присутствует опечатка. strutil.cc 1408

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

Есть два способа понять этот код. Версия первая: код правильный. В этом случае дополнительные скобки только облегчают чтение кода и ни на что не влияют:

uint64 mask = (static_cast<uint64>(1) << ((width - 1) * 4)) | value;

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

uint64 mask = ((static_cast<uint64>(1) << (width - 1)) * 4) | value;

Заключение

Приятно уметь находить недостатки в таком известном и качественном продукте, как protobuf. С другой стороны, наверное, не лучшая идея использовать protobuf для демонстрации возможностей статического анализа кода :). Трудно хвастаться возможностями инструмента, если инструмент может найти только пару ошибок :).

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

Однако нужно с чего-то начинать. Так что рекомендую скачать PVS-Studio, проверить свой проект и заглянуть в Лучшие предупреждения. Скорее всего, вы увидите много вещей, требующих вашего внимания :).

Если у вас самый качественный код — как у protobuf — рекомендую начать использовать анализатор по назначению. Попробуйте интегрировать PVS-Studio в процесс разработки и посмотрите, что он может найти каждый день. Хотите знать, как вы можете сделать это, если у вас большой проект? Кликните сюда".