Русский
Русский
English
Статистика
Реклама

Статический анализатор кода

Как внедрить статический анализатор кода в legacy проект и не демотивировать команду

20.06.2020 18:10:45 | Автор: admin
PVS-Studio охраняет сон программиста

Попробовать статический анализатор кода легко. А вот, чтобы внедрить его, особенно в разработку большого старого проекта, потребуется умение. При неправильном подходе анализатор может добавить работы, замедлить разработку и демотивировать команду. Давайте кратко поговорим, как правильно подойти к интеграции статического анализа в процесс разработки и начать его использовать как часть CI/CD.


Введение


Недавно моё внимание привлекла публикация "Getting Started With Static Analysis Without Overwhelming the Team". С одной стороны, это хорошая статья, с которой стоит познакомиться. С другой стороны, как мне кажется, в ней так и не прозвучало полного ответа, как безболезненно внедрить статический анализ в проект с большим количеством legacy кода. В статье говорится, что можно смириться с техническим долгом и работать только с новым кодом, но нет ответа на то, что делать с этим техническим долгом потом.

Наша команда PVS-Studio предлагает свой взгляд на эту тему. Давайте рассмотрим, как вообще возникает проблема внедрения статического анализатора кода, как преодолеть эту проблему и как безболезненно постепенно устранить технический долг.

Проблематика


Запустить и посмотреть, как работает статический анализатор, как правило, несложно [1]. Можно увидеть в коде интересные ошибки или даже страшные потенциальные уязвимости. Можно даже что-то поправить, но вот дальше у многих программистов опускаются руки.

Все статические анализаторы выдают ложные срабатывания. Такова особенность методологии статического анализа кода, и с ней ничего нельзя сделать. В общем случае это нерешаемая задача, что подтверждается теоремой Райса [2]. Не помогут и алгоритмы машинного обучения [3]. Если даже человек не всегда может сказать, ошибочен тот или иной код, то не стоит ждать этого от программы :).

Ложные срабатывания не являются проблемой, если статический анализатор уже настроен:
  • Отключены неактуальные наборы правил;
  • Отключены отдельные неактуальные диагностики;
  • Если мы говорим о C или C++, то размечены макросы, содержащие специфические конструкции, из-за которых появляются бесполезные предупреждению в каждом месте, где такие макросы используются;
  • Размечены собственные функции, выполняющие действия аналогичные системным функциям (свой аналог memcpy или printf) [4];
  • Точечно с помощью комментариев отключены ложные срабатывания;
  • И так далее.

В таком случае, можно ожидать низкий уровень ложных срабатываний на уровне порядка 10-15% [5]. Другим словами, 9 из 10 предупреждений анализатора будут указывать на реальную проблему в коде или, по крайней мере, на код с сильным запахом. Согласитесь, такой сценарий крайне приятен, и анализатор является настоящим другом программиста.
Предупреждения

В реальности в большом проекте первоначальная картина будет совсем иная. Анализатор выдаёт сотни или тысячи предупреждений на legacy код. Что из этих предупреждений по делу, а что нет, быстро понять невозможно. Сесть и начать разбираться со всеми этими предупреждениями нерационально, так как основная работа в этом случае остановится на дни или недели. Как правило, команда не может позволить себе такой сценарий. Также возникнет огромное количество diff-ов, которые портят историю изменений. А быстрая массовая правка такого количества фрагментов в коде неизбежно выльется в новые опечатки и ошибки.

И самое главное, подобный подвиг в борьбе с предупреждениями имеет мало смысла. Согласитесь, что раз проект много лет успешно работает, то и большинство критических ошибок в нём уже исправлено. Да, эти исправления были очень дорогими, приходилось отлаживаться, получать негативные отзывы пользователей о багах и так далее. Статический анализатор помог бы исправить многие из этих ошибок ещё на этапе написания кода, быстро и дешево. Но в данный момент, так или иначе эти ошибки исправлены, и анализатор в основном обнаруживает некритические ошибки в старом коде. Этот код может не использоваться, использоваться очень редко, и ошибка в нём может не приводить к заметным последствиям. Возможно, где-то тень от кнопки не того цвета, но пользоваться продуктом это никому не мешает.

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

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

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

Здесь та же аналогия, что и с предупреждениями компилятора. Неспроста рекомендуют держать количество предупреждений компилятора на уровне 0. Если предупреждений 1000, то, когда их станет 1001, на это никто не обратит внимание, да и не понятно, где искать это самое новое предупреждение.
Неправильное внедрение статического анализа

Самое худшее в этой истории, если кто-то сверху в этот момент заставит использовать статический анализ кода. Это только демотивирует команду, так как с их точки зрения появится дополнительная бюрократическая сложность, которая только мешает. Отчёты анализатора никто не будет смотреть, и всё использование будет только на бумаге. Т.е. формально анализ встроен в DevOps процесс, но на практике от этого никому никакой пользы нет. Мы слышали подробные истории при общении на стендах от посетителей конференций. Подобный опыт может надолго, если не на всегда, отбить желание у программистов связываться с инструментами статического анализа.

Внедрение и устранение технического долга


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

CI/CD


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

Фиксация существующего технического долга и работа с новыми предупреждениями


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

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

Можно указать PVS-Studio считать эти предупреждения пока неактуальными (отложить технический долг на потом), и он больше не будет их показывать. Анализатор создаёт специальный файл, где сохраняет информацию о пока неинтересных ошибках. И теперь PVS-Studio будет выдавать предупреждения только на новый или измененный код. Причем, всё это реализовано умно. Если, например, в начало файла с исходным кодом будет добавлена пустая строка, то анализатор понимает, что, по сути, ничего не изменилось, и по-прежнему будет молчать. Этот файл разметки можно заложить в систему контроля версий. Файл большой, но это не страшно, так как часто его закладывать смысла нет.

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

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

Исправление ошибок и рефакторинг


Самое простое и естественное это выделить некоторое время на разбор массово подавленных предупреждений анализатора и постепенно разбираться с ними. Где-то следует исправить ошибки в коде, где-то провести рефакторинг, чтобы подсказать анализатору, что код не является проблемным. Простой пример:
if (a = b)

На подобный код ругается большинство С++ компиляторов и анализаторов, так как высока вероятность, что на самом деле хотели написать (a == b). Но существует негласное соглашение, и это обычно отмечено в документации, что если стоят дополнительные скобки, то считается, что программист сознательно написал такой код, и ругаться не надо. Например, в документации PVS-Studio к диагностике V559 (CWE-481) явно написано, что следующая строчка будет считаться корректной и безопасной:
if ((a = b))

Другой пример. Забыт ли в этом C++ коде break или нет?
case A:  foo();case B:  bar();  break;

Анализатор PVS-Studio выдаст здесь предупреждение V796 (CWE-484). Возможно, это не ошибка, и тогда следует дать анализатору подсказку, добавив атрибут [[fallthrough]] или, например, __attribute__((fallthrough)):
case A:  foo();  [[fallthrough]];case B:  bar();  break;

Можно сказать, что подобные изменения кода не исправляют ошибки. Да, это так, но выполняется два полезных действия. Во-первых, отчёт анализатора избавляется от ложных срабатываний. Во-вторых, код становится более понятным для людей, занимающихся его сопровождением. И это очень важно! Ради одного этого уже стоит проводить мелкий рефакторинг, чтобы код стал понятней и легче в сопровождении. Раз анализатору непонятно, нужен break или нет, это будет непонятно и коллегам-программистам.

Помимо исправления ошибок и рефакторинга, можно точечно подавлять явно ложные предупреждения анализатора. Какие-то неактуальные диагностики можно отключить. Например, кто-то считает бессмысленными предупреждения V550 о сравнении значений типа float/double. А кто-то относит их к важным и заслуживающим изучения [7]. Какие предупреждения считать актуальными, а каким нет, решать только команде разработчиков.

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

Также, мы всегда помогаем нашим клиентам настроить PVS-Studio, если возникают какие-то сложности. Более того, были случае, когда мы сами устраняли ложные предупреждения и исправляли ошибки [8]. На всякий случай решил упомянуть, что возможен и такой вариант расширенного сотрудничества :).

Метод храповика


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

Храповик


Фиксируется количество предупреждений, которое выдаёт статический анализатор. Quality gate настраивается таким образом, что теперь можно заложить только такой код, который не увеличивает количество срабатываний. В результате запускается процесс постепенного уменьшения количества срабатываний за счёт настройки анализатора и правки ошибок.

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

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

У автора статьи есть и доклад на эту тему: "Непрерывный статический анализ".

Заключение


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

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

Спасибо за внимание, и приходите скачать и попробовать анализатор PVS-Studio.

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


  1. Андрей Карпов. Как быстро посмотреть интересные предупреждения, которые выдает анализатор PVS-Studio для C и C++ кода?
  2. Wikipedia. Теорема Райса.
  3. Андрей Карпов, Виктория Ханиева. Использование машинного обучения в статическом анализе исходного кода программ.
  4. PVS-Studio. Документация. Дополнительная настройка диагностик.
  5. Андрей Карпов. Характеристики анализатора PVS-Studio на примере EFL Core Libraries, 10-15% ложных срабатываний.
  6. PVS-Studio. Документация. Массовое подавление сообщений анализатора.
  7. Иван Андряшин. О том, как мы опробовали статический анализ на своем проекте учебного симулятора рентгенэндоваскулярной хирургии.
  8. Павел Еремеев, Святослав Размыслов. Как команда PVS-Studio улучшила код Unreal Engine.
  9. Андрей Карпов. Причины внедрить в процесс разработки статический анализатор кода PVS-Studio.



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. How to introduce a static code analyzer in a legacy project and not to discourage the team.
Подробнее..

Проверка коллекции header-only C библиотек (awesome-hpp)

22.10.2020 12:07:47 | Автор: admin
PVS-Studio и Awesome hpp

Волею судьбы мы проверили большинство библиотек, входящих в коллекцию под названием "Awesome hpp". Это небольшие проекты на языке C++, состоящие только из заголовочных файлов. Надеемся, найденные ошибки помогут сделать эти библиотеки немного лучше. Также мы будем рады, если их авторы начнут бесплатно использовать анализатор PVS-Studio на регулярной основе.

Предлагаю вашему вниманию обзор результатов проверки различных библиотек, перечисленных в списке awesome-hpp (A curated list of awesome header-only C++ libraries).

Впервые про этот список я узнал из подкаста "Cross Platform Mobile Telephony". Пользуясь случаем, рекомендую всем C++ программистам познакомиться с CppCast. CppCast is the first podcast for C++ developers by C++ developers!

Несмотря на большое количество проектов в списке, ошибок нашлось совсем немного. Тому есть три причины:

  • Это очень маленькие проекты. Многие буквально состоят из одного заголовочного файла.
  • Мы проверили не все проекты. С компиляцией некоторых из них возникли проблемы, и мы решили их пропустить.
  • Часто, чтобы понять, есть ли ошибки в шаблонных классах/функциях или нет, они должны инстанцироваться. Соответственно многие ошибки смогут быть выявлены анализатором только в настоящем проекте, когда библиотека активно используется. Мы же просто включали заголовочные файлы в пустой .cpp файл и проверяли, что делает проверку малоэффективной.

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

Примечание для моих коллег
Я люблю совмещать и достигать несколько полезных результатов, занимаясь каким-то делом. Призываю брать пример. Узнав про существование коллекции awesome-hpp, я смог реализовать следующие полезные дела:



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

Теперь давайте наконец посмотрим, что нашлось в некоторых библиотеках.

Найденные ошибки


Библиотека iutest


Краткое описание библиотеки iutest:
iutest is framework for writing C++ tests.
template<typename Event>pool_handler<Event> & assure() {  ....  return static_cast<pool_handler<Event> &>(it == pools.cend() ?    *pools.emplace_back(new pool_handler<Event>{}) : **it);  ....}

Предупреждение PVS-Studio: V1023 A pointer without owner is added to the 'pools' container by the 'emplace_back' method. A memory leak will occur in case of an exception. entt.hpp 17114

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

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

Правильный вариант:

pools.emplace_back(std::make_unique<pool_handler<Event>>{})

Ещё одно такое же место: V1023 A pointer without owner is added to the 'pools' container by the 'emplace_back' method. A memory leak will occur in case of an exception. entt.hpp 17407

Библиотека jsoncons


Краткое описание библиотеки jsoncons:
A C++, header-only library for constructing JSON and JSON-like data formats, with JSON Pointer, JSON Patch, JSONPath, JMESPath, CSV, MessagePack, CBOR, BSON, UBJSON.
Первая ошибка

static constexpr uint64_t basic_type_bits = sizeof(uint64_t) * 8;uint64_t* data() {  return is_dynamic() ? dynamic_stor_.data_ : short_stor_.values_;}basic_bigint& operator<<=( uint64_t k ){  size_type q = (size_type)(k / basic_type_bits);  ....  if ( k )  // 0 < k < basic_type_bits:  {    uint64_t k1 = basic_type_bits - k;    uint64_t mask = (1 << k) - 1;             // <=    ....    data()[i] |= (data()[i-1] >> k1) & mask;    ....  }  reduce();  return *this;}

Предупреждение PVS-Studio: V629 Consider inspecting the '1 << k' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. bigint.hpp 744

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

uint64_t mask = (static_cast<uint64_t>(1) << k) - 1;

Или так:

uint64_t mask = (1ull << k) - 1;

Точно такую же ошибку, как первая, можно увидеть здесь: V629 Consider inspecting the '1 << k' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. bigint.hpp 779

Вторая ошибка

template <class CharT = typename std::iterator_traits<Iterator>::value_type>typename std::enable_if<sizeof(CharT) == sizeof(uint16_t)>::type next() UNICONS_NOEXCEPT{    begin_ += length_;    if (begin_ != last_)    {        if (begin_ != last_)        {  ....}

Предупреждение PVS-Studio: V571 Recurring check. The 'if (begin_ != last_)' condition was already verified in line 1138. unicode_traits.hpp 1140

Странная повторная проверка. Есть подозрение, что здесь какая-то опечатка и второе условие должно выглядеть как-то иначе.

Библиотека clipp


Краткое описание библиотеки clipp:
clipp command line interfaces for modern C++. Easy to use, powerful and expressive command line argument handling for C++11/14/17 contained in a single header file.
inline boolfwd_to_unsigned_int(const char*& s){  if(!s) return false;  for(; std::isspace(*s); ++s);  if(!s[0] || s[0] == '-') return false;  if(s[0] == '-') return false;  return true;}

Предупреждение PVS-Studio: V547 Expression 's[0] == '-'' is always false. clipp.h 303

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

Библиотека SimpleIni


Краткое описание библиотеки SimpleIni:
A cross-platform library that provides a simple API to read and write INI-style configuration files. It supports data files in ASCII, MBCS and Unicode.
#if defined(SI_NO_MBSTOWCS_NULL) || (!defined(_MSC_VER) && !defined(_linux))

Предупреждение PVS-Studio: V1040 Possible typo in the spelling of a pre-defined macro name. The '_linux' macro is similar to '__linux'. SimpleIni.h 2923

Скорее всего, в имени макроса _linux не хватает одного подчёркивания и должно использоваться имя __linux. Впрочем, в POSIX этот макрос объявлен устаревшим и лучше использовать __linux__.

Библиотека CSV Parser


Краткое описание библиотеки CSV Parser:
A modern C++ library for reading, writing, and analyzing CSV (and similar) files.
CSV_INLINE void CSVReader::read_csv(const size_t& bytes) {  const size_t BUFFER_UPPER_LIMIT = std::min(bytes, (size_t)1000000);  std::unique_ptr<char[]> buffer(new char[BUFFER_UPPER_LIMIT]);  auto * HEDLEY_RESTRICT line_buffer = buffer.get();  line_buffer[0] = '\0';  ....  this->feed_state->feed_buffer.push_back(    std::make_pair<>(std::move(buffer), line_buffer - buffer.get())); // <=  ....}

Предупреждение PVS-Studio: V769 The 'buffer.get()' pointer in the 'line_buffer buffer.get()' expression equals nullptr. The resulting value is senseless and it should not be used. csv.hpp 4957

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

Если совсем кратко, работает этот код или нет, зависит от порядка вычисления аргументов. А в каком порядке вычисляются аргументы, зависит от компилятора.

Библиотека PPrint


Краткое описание библиотеки PPrint:.
Pretty Printer for Modern C++.
template <typename Container>typename std::enable_if<......>::type print_internal(......) {  ....  for (size_t i = 1; i < value.size() - 1; i++) {    print_internal(value[i], indent + indent_, "", level + 1);    if (is_container<T>::value == false)      print_internal_without_quotes(", ", 0, "\n");    else      print_internal_without_quotes(", ", 0, "\n");  }  ....}

Предупреждение PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 715

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

Аналогичные предупреждения:

  • V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 780
  • V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 851
  • V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 927
  • V523 The 'then' statement is equivalent to the 'else' statement. pprint.hpp 1012

Библиотека Strf


Краткое описание библиотеки Strf:
A fast C++ formatting library that supports encoding conversion.
Первая ошибка

template <int Base>class numpunct: private strf::digits_grouping{  ....  constexpr STRF_HD numpunct& operator=(const numpunct& other) noexcept  {    strf::digits_grouping::operator=(other);    decimal_point_ = other.decimal_point_;    thousands_sep_ = other.thousands_sep_;  }  ....};

Предупреждение PVS-Studio: V591 Non-void function should return a value. numpunct.hpp 402

В конце функции забыли написать "return *this;".

Вторая аналогичная ошибка

template <int Base>class no_grouping final{  constexpr STRF_HD no_grouping& operator=(const no_grouping& other) noexcept  {    decimal_point_ = other.decimal_point_;  }  ....}

Предупреждение PVS-Studio: V591 Non-void function should return a value. numpunct.hpp 528.

Библиотека Indicators


Краткое описание библиотеки Indicators:
Activity Indicators for Modern C++.
static inline void move_up(int lines) { move(0, -lines); }static inline void move_down(int lines) { move(0, -lines); }   // <=static inline void move_right(int cols) { move(cols, 0); }static inline void move_left(int cols) { move(-cols, 0); }

Предупреждение PVS-Studio: V524 It is odd that the body of 'move_down' function is fully equivalent to the body of 'move_up' function. indicators.hpp 983

Я не уверен, что это ошибка. Но код очень подозрительный. Высока вероятность, что была скопирована функция move_up и заменено её имя на move_down. А вот минус удалить забыли. Стоит проверить этот код.

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

Библиотека manif


Краткое описание библиотеки manif:
manif is a header-only C++11 Lie theory library for state-estimation targeted at robotics applications.
template <typename _Derived>typename LieGroupBase<_Derived>::Scalar*LieGroupBase<_Derived>::data(){  return derived().coeffs().data();}template <typename _Derived>const typename LieGroupBase<_Derived>::Scalar*LieGroupBase<_Derived>::data() const{  derived().coeffs().data(); // <=}

Предупреждение PVS-Studio: V591 Non-void function should return a value. lie_group_base.h 347

Неконстантная функция реализована правильно, а константная нет. Интересно даже, как так получилось

Библиотека FakeIt


Краткое описание библиотеки FakeIt:
FakeIt is a simple mocking framework for C++. It supports GCC, Clang and MS Visual C++. FakeIt is written in C++11 and can be used for testing both C++11 and C++ projects.
template<typename ... arglist>struct ArgumentsMatcherInvocationMatcher :         public ActualInvocation<arglist...>::Matcher {  ....  template<typename A>  void operator()(int index, A &actualArg) {      TypedMatcher<typename naked_type<A>::type> *matcher =        dynamic_cast<TypedMatcher<typename naked_type<A>::type> *>(          _matchers[index]);      if (_matching)        _matching = matcher->matches(actualArg);  }  ....  const std::vector<Destructible *> _matchers;};

Предупреждение PVS-Studio: V522 There might be dereferencing of a potential null pointer 'matcher'. fakeit.hpp 6720

Указатель matcher инициализируется значением, которое возвращает оператор dynamic_cast. А этот оператор может возвращать nullptr, и это весьма вероятный сценарий. Иначе вместо dynamic_cast эффективнее использовать static_cast.

Есть подозрение, что в условии допущена опечатка и на самом деле должно быть написано:

if (matcher)  _matching = matcher->matches(actualArg);

Библиотека GuiLite


Краткое описание библиотеки GuiLite:
The smallest header-only GUI library(4 KLOC) for all platforms.
#define CORRECT(x, high_limit, low_limit)  {\  x = (x > high_limit) ? high_limit : x;\  x = (x < low_limit) ? low_limit : x;\}while(0)void refresh_wave(unsigned char frame){  ....  CORRECT(y_min, m_wave_bottom, m_wave_top);  ....}

Предупреждение PVS-Studio: V529 Odd semicolon ';' after 'while' operator. GuiLite.h 3413

К какой-то проблеме ошибка в макросе не приводит. Но всё равно это ошибка, поэтому я решил описать её в статье.

В макросе планировалось использовать классический паттерн do { } while(....). Это позволяет выполнить несколько действий в одном блоке и при этом иметь возможность для красоты после макроса писать точку с запятой ';', как будто это вызов функции.

Но в рассмотренном макросе случайно забыли написать ключевое слово do. В результате макрос как-бы разделился на две части. Первая это блок. Вторая пустой не выполняющийся цикл: while (0);.

А в чём, собственно, проблема?

Например, такой макрос нельзя использовать в конструкции вида:

if (A)  CORRECT(y_min, m_wave_bottom, m_wave_top);else  Foo();

Этот код не скомпилируется, так как он будет раскрыт в:

if (A)  { ..... }while(0);else  Foo();

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


Библиотека PpluX


Краткое описание библиотеки PpluX:
Single header C++ Libraries for Thread Scheduling, Rendering, and so on...
struct DisplayList {  DisplayList& operator=(DisplayList &&d) {    data_ = d.data_;    d.data_ = nullptr;  }  ....}

Предупреждение PVS-Studio: V591 Non-void function should return a value. px_render.h 398

Библиотека Universal


Краткое описание библиотеки Universal:
The goal of Universal Numbers, or unums, is to replace IEEE floating-point with a number system that is more efficient and mathematically consistent in concurrent execution environments.
Первая ошибка

template<typename Scalar>vector<Scalar> operator*(double scalar, const vector<Scalar>& v) {  vector<Scalar> scaledVector(v);  scaledVector *= scalar;  return v;}

Предупреждение PVS-Studio: V1001 The 'scaledVector' variable is assigned but is not used by the end of the function. vector.hpp 124

Опечатка. Вместо исходного вектора v из функции нужно вернуть новый вектор scaledVector.

Аналогичную опечатку можно увидеть здесь: V1001 The 'normalizedVector' variable is assigned but is not used by the end of the function. vector.hpp 131

Вторая ошибка

template<typename Scalar>class matrix {  ....  matrix& diagonal() {  }  ....};

Предупреждение PVS-Studio: V591 Non-void function should return a value. matrix.hpp 109

Третья ошибка

template<size_t fbits, size_t abits>void module_subtract_BROKEN(  const value<fbits>& lhs, const value<fbits>& rhs, value<abits + 1>& result){  if (lhs.isinf() || rhs.isinf()) {    result.setinf();    return;  }  int lhs_scale = lhs.scale(),      rhs_scale = rhs.scale(),      scale_of_result = std::max(lhs_scale, rhs_scale);  // align the fractions  bitblock<abits> r1 =    lhs.template nshift<abits>(lhs_scale - scale_of_result + 3);  bitblock<abits> r2 =    rhs.template nshift<abits>(rhs_scale - scale_of_result + 3);  bool r1_sign = lhs.sign(), r2_sign = rhs.sign();  //bool signs_are_equal = r1_sign == r2_sign;  if (r1_sign) r1 = twos_complement(r1);  if (r1_sign) r2 = twos_complement(r2);  // <=  ....}

Предупреждение PVS-Studio: V581 The conditional expressions of the 'if' statements situated alongside each other are identical. Check lines: 789, 790. value.hpp 790

Классическая ошибка, возникшая из-за Copy-Paste. Взяли и размножили строчку:

if (r1_sign) r1 = twos_complement(r1);

Поменяли в ней r1 на r2:

if (r1_sign) r2 = twos_complement(r2);

А поменять r1_sign забыли. Правильный вариант:

if (r2_sign) r2 = twos_complement(r2);

Библиотека Chobo Single-Header Libraries


Краткое описание библиотеки Chobo Single-Header Libraries:
A collection of small single-header C++11 libraries by Chobolabs.
Первая ошибка

template <typename T, typename U, typename Alloc = std::allocator<T>>class vector_view{  ....  vector_view& operator=(vector_view&& other)  {    m_vector = std::move(other.m_vector);  }  ....}

Предупреждение PVS-Studio: V591 Non-void function should return a value. vector_view.hpp 163

Вторая ошибка

template <typename UAlloc>vector_view& operator=(const std::vector<U, UAlloc>& other){  size_type n = other.size();  resize(n);  for (size_type i = 0; i < n; ++i)  {    this->at(i) = other[i];  }}

Предупреждение PVS-Studio: V591 Non-void function should return a value. vector_view.hpp 184

Библиотека PGM-index


Краткое описание библиотеки PGM-index:
The Piecewise Geometric Model index (PGM-index) is a data structure that enables fast lookup, predecessor, range searches and updates in arrays of billions of items using orders of magnitude less space than traditional indexes while providing the same worst-case query time guarantees.
Первая ошибка

char* str_from_errno(){#ifdef MSVC_COMPILER  #pragma warning(disable:4996)  return strerror(errno);#pragma warning(default:4996)#else  return strerror(errno);#endif}

Предупреждение PVS-Studio: V665 Possibly, the usage of '#pragma warning(default: X)' is incorrect in this context. The '#pragma warning(push/pop)' should be used instead. Check lines: 9170, 9172. sdsl.hpp 9172

Неправильное временное отключение предупреждения компилятора. Подобные неаккуратности ещё как-то простительны пользовательскому коду. Но это точно недопустимо в header-only библиотеках.

Вторая ошибка

template<class t_int_vec>t_int_vec rnd_positions(uint8_t log_s, uint64_t& mask,                        uint64_t mod=0, uint64_t seed=17){  mask = (1<<log_s)-1;         // <=  t_int_vec rands(1<<log_s ,0);  set_random_bits(rands, seed);  if (mod > 0) {    util::mod(rands, mod);  }  return rands;}

Предупреждение PVS-Studio: V629 Consider inspecting the '1 << log_s' expression. Bit shifting of the 32-bit value with a subsequent expansion to the 64-bit type. sdsl.hpp 1350

Один из правильных вариантов:

mask = ((uint64_t)(1)<<log_s)-1;

Библиотека Hnswlib


Краткое описание библиотеки Hnswlib:
Header-only C++ HNSW implementation with python bindings. Paper's code for the HNSW 200M SIFT experiment.
template<typename dist_t>class BruteforceSearch : public AlgorithmInterface<dist_t> {public:  BruteforceSearch(SpaceInterface <dist_t> *s, size_t maxElements) {    maxelements_ = maxElements;    data_size_ = s->get_data_size();    fstdistfunc_ = s->get_dist_func();    dist_func_param_ = s->get_dist_func_param();    size_per_element_ = data_size_ + sizeof(labeltype);    data_ = (char *) malloc(maxElements * size_per_element_);    if (data_ == nullptr)      std::runtime_error(        "Not enough memory: BruteforceSearch failed to allocate data");    cur_element_count = 0;  }  ....}

Предупреждение PVS-Studio: V596 The object was created but it is not being used. The 'throw' keyword could be missing: throw runtime_error(FOO); bruteforce.h 26

Забыли перед std::runtime_error написать оператор throw.

Ещё одна такая ошибка: V596 The object was created but it is not being used. The 'throw' keyword could be missing: throw runtime_error(FOO); bruteforce.h 161

Библиотека tiny-dnn


Краткое описание библиотеки tiny-dnn:
tiny-dnn is a C++14 implementation of deep learning. It is suitable for deep learning on limited computational resource, embedded systems and IoT devices.
Первая ошибка

class nn_error : public std::exception { public:  explicit nn_error(const std::string &msg) : msg_(msg) {}  const char *what() const throw() override { return msg_.c_str(); } private:  std::string msg_;};inline Device::Device(device_t type, const int platform_id, const int device_id)  : type_(type),    has_clcuda_api_(true),    platform_id_(platform_id),    device_id_(device_id) {  ....#else  nn_error("TinyDNN has not been compiled with OpenCL or CUDA support.");#endif}

Предупреждение PVS-Studio: V596 The object was created but it is not being used. The 'throw' keyword could be missing: throw nn_error(FOO); device.h 68

nn_error это не функция, генерирующая исключение, а просто класс. Поэтому правильно его использовать так:

throw nn_error("TinyDNN has not been compiled with OpenCL or CUDA support.");

Ещё одно неправильное использование этого класса: V596 The object was created but it is not being used. The 'throw' keyword could be missing: throw nn_error(FOO); conv2d_op_opencl.h 136

Вторая ошибка

inline std::string format_str(const char *fmt, ...) {  static char buf[2048];#ifdef _MSC_VER#pragma warning(disable : 4996)#endif  va_list args;  va_start(args, fmt);  vsnprintf(buf, sizeof(buf), fmt, args);  va_end(args);#ifdef _MSC_VER#pragma warning(default : 4996)#endif  return std::string(buf);}

Предупреждение PVS-Studio: V665 Possibly, the usage of '#pragma warning(default: X)' is incorrect in this context. The '#pragma warning(push/pop)' should be used instead. Check lines: 139, 146. util.h 146

Библиотека Dlib


Краткое описание библиотеки Dlib:
TDLib (Telegram Database library) is a cross-platform library for building Telegram clients. It can be easily used from almost any programming language.
Первая ошибка

Ради интереса попробуйте найти эту ошибку самостоятельно.

class bdf_parser{public:  enum bdf_enums  {    NO_KEYWORD = 0,    STARTFONT = 1,    FONTBOUNDINGBOX = 2,    DWIDTH = 4,    DEFAULT_CHAR = 8,    CHARS = 16,    STARTCHAR = 32,    ENCODING = 64,    BBX = 128,    BITMAP = 256,    ENDCHAR = 512,    ENDFONT = 1024  };  ....  bool parse_header( header_info& info )  {    ....    while ( 1 )    {      res = find_keywords( find | stop );      if ( res & FONTBOUNDINGBOX )      {          in_ >> info.FBBx >> info.FBBy >> info.Xoff >> info.Yoff;          if ( in_.fail() )              return false;    // parse_error          find &= ~FONTBOUNDINGBOX;          continue;      }      if ( res & DWIDTH )      {          in_ >> info.dwx0 >> info.dwy0;          if ( in_.fail() )              return false;    // parse_error          find &= ~DWIDTH;          info.has_global_dw = true;          continue;      }      if ( res & DEFAULT_CHAR )      {          in_ >> info.default_char;          if ( in_.fail() )              return false;    // parse_error          find &= ~DEFAULT_CHAR;          continue;      }      if ( res & NO_KEYWORD )          return false;    // parse_error: unexpected EOF      break;    }  ....};

Нашли?

Растерянный Траволта-Единорог

Она здесь:

if ( res & NO_KEYWORD )

Предупреждение PVS-Studio: V616 The 'NO_KEYWORD' named constant with the value of 0 is used in the bitwise operation. fonts.cpp 288

Именованная константа NO_KEYWORD имеет значение 0. А следовательно условие не имеет смысла. Правильно было бы написать:

if ( res == NO_KEYWORD )

Ещё одна неправильная проверка находится здесь: V616 The 'NO_KEYWORD' named constant with the value of 0 is used in the bitwise operation. fonts.cpp 334

Вторая ошибка

void set(std::vector<tensor*> items){  ....  epa.emplace_back(new enable_peer_access(*g[0], *g[i]));  ....}

Предупреждение PVS-Studio: V1023 A pointer without owner is added to the 'epa' container by the 'emplace_back' method. A memory leak will occur in case of an exception. tensor_tools.h 1665

Чтобы понять, в чём тут заковыка, предлагаю познакомиться с документацией на диагностику V1023.

Третья ошибка

template <    typename detection_type,     typename label_type     >bool is_track_association_problem (  const std::vector<    std::vector<labeled_detection<detection_type,label_type> > >& samples){  if (samples.size() == 0)    return false;  unsigned long num_nonzero_elements = 0;  for (unsigned long i = 0; i < samples.size(); ++i)  {    if (samples.size() > 0)      ++num_nonzero_elements;  }  if (num_nonzero_elements < 2)    return false;  ....}

Предупреждение PVS-Studio: V547 Expression 'samples.size() > 0' is always true. svm.h 360

Это очень, очень странный код! Если запускается цикл, то значит условие (samples.size() > 0) всегда истинно. Следовательно, цикл можно упростить:

for (unsigned long i = 0; i < samples.size(); ++i){  ++num_nonzero_elements;}

После этого становится понятно, что цикл вообще не нужен. Можно написать гораздо проще и эффективнее:

unsigned long num_nonzero_elements = samples.size();

Но это ли планировалось сделать? Код явно заслуживает внимательного изучения программистом.

Четвёртая ошибка

class console_progress_indicator{  ....  double seen_first_val;  ....};bool console_progress_indicator::print_status (  double cur, bool always_print){  ....  if (!seen_first_val)  {    start_time = cur_time;    last_time = cur_time;    first_val = cur;    seen_first_val = true;  // <=    return false;  }  ....}

Предупреждение PVS-Studio: V601 The bool type is implicitly cast to the double type. console_progress_indicator.h 136

В член класса, имеющий тип double, записывают значение true. Хм

Пятая ошибка

void file::init(const std::string& name){  ....  WIN32_FIND_DATAA data;  HANDLE ffind = FindFirstFileA(state.full_name.c_str(), &data);  if (ffind == INVALID_HANDLE_VALUE ||      (data.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY) != 0)  {    throw file_not_found("Unable to find file " + name);                  }  else  {    ....  } }

Предупреждение PVS-Studio: V773 The exception was thrown without closing the file referenced by the 'ffind' handle. A resource leak is possible. dir_nav_kernel_1.cpp 60

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

Шестая ошибка

Ещё одно очень странное место.

inline double poly_min_extrap(double f0, double d0,                              double x1, double f_x1,                              double x2, double f_x2){  ....  matrix<double,2,2> m;  matrix<double,2,1> v;  const double aa2 = x2*x2;  const double aa1 = x1*x1;  m =  aa2,       -aa1,      -aa2*x2, aa1*x1;     v = f_x1 - f0 - d0*x1,      f_x2 - f0 - d0*x2;  ....}

Предупреждение PVS-Studio: V521 Such expressions using the ',' operator are dangerous. Make sure the expression is correct. optimization_line_search.h 211

Планируется инициализировать матрицы. Но ведь все эти aa2, f_x1, d0 и так далее это просто переменные типа double. Значит, запятые не разделяют аргументы, предназначенные для создания матриц, а являются обыкновенными comma operator, которые возвращают значение правой части.

Заключение


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

  • Повышение квалификации. Изучая предупреждения анализатора можно узнать много нового и полезного. Примеры: memset, #pragma warning, emplace_back, strictly aligned.
  • Выявление опечаток, ошибок и потенциальных уязвимостей на ранних этапах.
  • Код постепенно становится качественнее, проще, понятней.
  • Вы можете гордиться и всем рассказывать, что используете современные технологии при разработке проектов :). И это юмор только отчасти. Это настоящее конкурентное преимущество.

Вопрос только в том, как начать, как безболезненно внедрить и как правильно использовать? С этим вам помогут следующие статьи:



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Checking a Header-Only C++ Library Collection (awesome-hpp).
Подробнее..

CodeQL SAST своими руками (и головой). Часть 1

09.02.2021 16:13:11 | Автор: admin

Привет Хабр!

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

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

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

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

Содержание

1. Что такое CodeQL
2. Сценарии использования CodeQL
3. Демонстрация работы
4. Консоль LGTM
5. Установка CodeQL
6. Кодовая база
7. Как выглядит простой CodeQL запрос
8. Что дальше?
9. Дополнительные материалы

Что такое CodeQL

CodeQL это open-source инструмент и язык запросов, немного напоминающий SQL и позволяющий программным образом обращаться к тем или иным участкам кода и выполнять заданные аналитиками проверки графа потоков данных/управления и структуры исходного кода в целом. Аналогом этому подходу являются конфигурируемые правила поиска уязвимостей в инструментах SAST (Static Application Security Testing).

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

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

Изначально CodeQL это разработка компании Semmle, которая в сентябре 2020 была куплена GitHub и внедрена в их Security Lab. С этого момента развитием продукта занимается сам GitHub и небольшое сообщество энтузиастов. На данный момент официальный репозиторий содержит суммарно свыше 2000 QL-запросов, которые покрывают большое количество разнообразных проблем с кодом, начиная от поиска некорректных регулярных выражений в JavaScript и заканчивая обнаружением использования небезопасных криптографических алгоритмов в коде на C++.

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

На данный момент с разной степенью полноты поддерживаются следующие языки: C/C++, C#, Java, Go, Python, JavaScript/TypeScript. Помимо этого для каждого языка есть набор поддерживаемых фреймворков, упрощающий написание запросов.

CodeQL предоставляется в нескольких вариантах:

  1. Консольная утилита, позволяющая встроить проверки в CI/CD цикл и осуществлять сканирование кода из командной строки.

  2. Расширение для Visual Studio Code для удобного написания и ad-hoc исполнения запросов.

  3. Онлайн-консоль LGTM, позволяющая писать запросы и проводить тестовое сканирование приложения из заданного GitHub-репозитория.

Кроме этого можно подключить сканирование своих репозиториев непосредственно в CI/CD на GitHub.

Всё ещё не убеждены попробовать? Тогда подкину дополнительную мотивацию. GitHub проводит соревнования CTF и вознаграждаемые bug bounty программы для энтузиастов, которые предлагают новые запросы, помогающие обнаруживать как уже известные и документированные уязвимости (CVE), так и 0-day уязвимости.

Сценарии использования CodeQL

Давайте посмотрим, какие есть потенциальные варианты использования CodeQL в нашем проекте:

  1. Самый простой сценарий состоит в том, что мы просто запускаем сканер со всем набором стандартных запросов и разбираем результаты, среди которых будут и проблемы с качеством кода, и проблемы с безопасностью.

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

  3. Самый продуктивный сценарий включает в себя модификацию базовых запросов и/или написание собственных новых запросов, исходя из специфики конкретного приложения и появляющихся угроз.
    Например при выходе очередной 0-day уязвимости аналитик безопасности может составить запрос, который будет проверять все проекты на ее наличие. Или при анализе дефектов, найденных в процессе внутреннего аудита, каждый такой дефект может быть переписан на языке QL, чтобы не допустить проблем в других проектах.

  4. Также CodeQL может быть использован для исследования кода в целом (например определить все точки входа в приложение, чтобы впоследствии эту информацию передать аналитикам, занимающимся динамическим анализом приложения).

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

Демонстрация работы

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

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

Простой запрос CodeQL по поиску пустых блоковПростой запрос CodeQL по поиску пустых блоковОбнаруженный пустой участок кодаОбнаруженный пустой участок кода

Или более сложный случай, когда мы ищем проблемы с Cross-Site Scripting:

Запрос CodeQL, обнаруживающий XSS путём отслеживания путей недоверенных данныхЗапрос CodeQL, обнаруживающий XSS путём отслеживания путей недоверенных данных

Результат включает в себя все промежуточные шаги (в данном примере только стартовый и конечный), которые привели данные от места ввода до вывода и участки кода, в которых заключена проблема:

А вот так тот же результат выглядит в расширении для VSCode:

На приведенном скриншоте мы видим инструкции CodeQL (верхняя панель), которые отслеживают путь данных от удалённых точек ввода пользовательских данных (например параметры GET-запросов) до конструкций кода, способных отобразить эти недоверенные данные пользователю. При этом отдельной инструкцией isSanitizer указывается, что в коде присутствует санитизирующая функция и соответственно если поток подозрительных данных проходит через эту функцию, он не должен дальше восприниматься как недоверенный. Это один из нескольких способов, которыми мы можем уменьшить количество заведомо ложных срабатываний.

В свою очередь, в результатах выполнения запроса (нижняя панель) мы можем посмотреть участок кода, где в приложении появляются недоверенные данные (т. н. source) и участок кода, где они выводятся (т. н. sink).

Консоль LGTM

Чтобы поэкспериментировать с языком без локальной установки пакета CodeQL можно воспользоваться онлайн-консолью LGTM (Looks Good To Me). Она включает в себя простой редактор запросов и возможность выполнить этот запрос на уже предустановленных кодовых базах нескольких open-source проектов, либо на своем GitHub-проекте.

Давайте сразу попробуем исполнить простейшие запросы и начать практическое знакомство с CodeQL:

  1. Переходим в онлайн-консоль: https://lgtm.com/query/.

  2. Выбираем в качестве языка JavaScript, а в качестве проекта meteor/meteor.

  3. Копируем нижеприведенные запросы.

  4. Нажимаем Run и смотрим результаты в панели под полем ввода кода.

Простой запрос, отображающий все места в анализируемом исходном коде, где объявляются классы выглядит так:

import javascriptfrom ClassExpr ceselect ce

Более сложный запрос, который покажет нам все места в файле client.js, где происходит вызов функции eval(), а также аргументы этой функции:

import javascriptfrom CallExpr callwhere call.getCalleeName() = "eval" and call.getLocation().getFile().getRelativePath().matches("%client.js")select call, call.getAnArgument()

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

Установка CodeQL

Для использования в своих проектах на постоянной основе консоль LGTM не очень удобна, поэтому есть смысл установить CodeQL CLI и библиотеки локально.

Процесс установки всего пакета в целом несложен, но требует понимания ряда нюансов.

Простой вариант, с которого можно начать, выглядит так:

  1. Установить VSCode и CodeQL extension.

  2. Скачать и распаковать CodeQL CLI в директорию, например, codeql.

  3. Прописать путь до директории codeql в %PATH%.

  4. Скачать стартовый воркспейс VSCode для работы с CodeQL (впоследствии можно будет сделать свой, но для начала работы можно использовать готовый):
    git clone https://github.com/github/vscode-codeql-starter/

    git submodule update --init --remote

    В нем мы будем работать (то есть писать наши запросы) в папке, соответствующей интересующему нас языку (например для JS это codeql-custom-queries-javascript).

  5. Скачиваем тестовую кодовую базу (то есть базу, в которой определенным образом хранятся все необходимые данные о коде и внутренних взаимосвязях в нем, о чем подробнее будет рассказано ниже), например (для JS) https://github.com/githubsatelliteworkshops/codeql/releases/download/v1.0/esbenabootstrap-pre-27047javascript.zip
    Чуть ниже мы посмотрим как создавать свои собственные кодовые базы для наших проектов.

  6. Опционально распаковываем архив с кодовой базой.

  7. В VSCode выбираем Open workspace и открываем файл стартового воркспейса.

  8. В VSCode на закладке CodeQL добавляем папку (или архив) с кодовой базой, против которой будет запускаться анализ кода.

  9. Готово. Теперь в соответствующей папке воркспейса (см. шаг 4) можно открыть файл example.ql и в нем начать писать свои запросы.

  10. Исполняем тестовый запрос и убеждаемся, что всё работает

import javascriptfrom Expr eselect Wazzup!

Кодовая база

В CodeQL весь анализируемый код должен быть специальным образом организован в т. н. кодовую базу, к которой мы будем впоследствии выполнять запросы. В ней содержится полное иерархическое представление этого кода, включая абстрактное синтаксическое дерево (AST), граф потока данных и граф потока управления. Языковые библиотеки CodeQL задают классы, которые добавляют уровень абстракции относительно таблиц в этой базе. Другими словами у нас появляется возможность писать запросы к кодовой базе, используя принципы ООП. Это как раз та особенность, которая отличает CodeQL от инструментов, которые ищут проблемы в коде при помощи заранее заданных шаблонов и regex'ов.

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

Для разных языков процесс создания базы немного отличается. Например создание кодовой базы для JS в папке my-js-codebase выполняется следующей командой в директории, которая содержит исходный код:

codeql database create my-js-codebase --language=javascript

Для компилируемых языков требуется, чтобы в системе был соответствующий компилятор и сборщик (например Maven для Java)

Следующий шаг загрузить информацию о базе в VSCode. Это делается в редакторе на соответствующей вкладке CodeQL Choose Database from Folder

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

Как выглядит простой CodeQL запрос

Давайте разберем, из чего вообще состоит типичный запрос в CodeQL на примере кодовой базы на языке JavaScript.

Самый базовый запрос, который выводит все jQuery-функции с именем $ (типа $(arg1, arg2)) и их первый аргумент, выглядит так, как показано ниже. Вы можете самостоятельно выполнить его для любой кодовой базы с jQuery:

/*** @name QueryName* @kind problem* @id my_id_1*/// метаданныеimport javascript // Выражение для подключения библиотеки CodeQL для работы с конструкциями JavaScript.// Также есть возможность подключения других библиотек для работы с различными фреймворками и технологиями.// Например semmle.javascript.NodeJS или semmle.javascript.frameworks.HTTP.from CallExpr dollarCall, Expr dollarArg // Объявление переменной dollarCall типа CallExpr и переменной dollarArg типа Expr.// CallExpr - это класс из стандартной библиотеки, представляющий коллекцию всех вызовов функций в интересующем нас коде.// Expr - класс, представляющий коллекцию всех выражений. Например в Object.entries = function(obj) выражениями являются //   вся строка целиком, Object, Object.entries, entries, function(obj), obj.where dollarCall.getCalleeName() = "$"// Логические формулы, которые мы применяем к объявленным переменным.// Мы проверяем, что результат выполнения предиката (т.е. логической инструкции) getCaleeName() (который возвращает название // вызываемой функции) нашего объекта dollarCall (который содержит все вызовы функций) равен "$"and dollarArg = dollarCall.getArgument(0)// Эта логическая формула операцией AND соединяется с предыдущей и уточняет условие, применяемое в запросе.// В итоге мы из всех вызовов функций, в названии которых есть $ выбираем в переменную //  dollarArg первые аргументы (как сущности, а не как конкретные значения аргументов).select dollarCall, dollarArg // указание на то, какие выражения (значение каких переменных или предикатов) мы хотим вывести в результате.

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

Что дальше?

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

Рекомендую поэкспериментировать с запросами на разных приложениях (либо собственных, либо open-source) хотя бы в онлайн-консоли LGTM.

Помимо это есть два неплохих обучающих мини-курса от самих разработчиков, которые помогут вам лучше понять основы и базовые механики CodeQL. Они не требуют знания соответствующих языков, но раскрывают возможности CodeQL для анализа кода. Советую их посмотреть и при желании пройти до конца:

https://lab.github.com/githubtraining/codeql-u-boot-challenge-(cc++) интерактивный курс по работе с CodeQL на примере C/C++

https://lab.github.com/githubtraining/codeql-for-javascript:-unsafe-jquery-plugin интерактивный курс по анализу JavaScript Bootstrap с помощью CodeQL.

Плюс к этому для начала подойдет очень полезный двухчасовой мастер-класс от GitHub, на котором рассматривается база CodeQL и где при помощи лектора зритель учится шаг за шагом писать запрос для поиска небезопасной десериализации в коде Java-приложения (фреймворк XStream):

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

It is dangerous to go alone! CodeQL инструмент достаточно сложный, с большим количеством нюансов и, к сожалению, с пока еще не очень большой экспертизой в мире. Поэтому мы бы хотели уже сейчас заняться развитием русскоязычного сообщества экспертов в CodeQL для обмена опытом и совместного решения проблем (которые, разумеется, тоже существуют). Для этой цели мы создали отдельный канал в Telegram, посвященный обсуждениям нюансов этого инструмента и расширению круга экспертизы. Там же мы публикуем новости, обучающие материалы и другую информацию по CodeQL.

Присоединяйтесь - https://t.me/codeql !

Дополнительные материалы

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

https://help.semmle.com/codeql/ общая помощь по CodeQL от изначальных разработчиков.
https://help.semmle.com/QL/ql-handbook/ референс по синтаксису языка.
https://help.semmle.com/QL/learn-ql/ детальная помощь по CodeQL для разных языков.
https://securitylab.github.com/get-involved информация по тому, как можно узнать больше про CodeQL, помочь его развитию, а также по тому, как получить инвайт в Slack-канал (англоязычный) с ведущими экспертами со всего мира и разработчиками самого CodeQL.

Подробнее..

Обработка дат притягивает ошибки или 77 дефектов в Qt 6

16.02.2021 22:10:00 | Автор: admin

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


Это классическая статья о проверке открытого проекта, которая пополнит нашу "доказательную базу" полезности и эффективности использования PVS-Studio для контроля качества кода. Хотя мы уже писали про поверку проекта Qt (в 2011, в 2014 и в 2018), очень полезно сделать это вновь. Так, мы на практике демонстрируем простую, но очень важную мысль: статический анализ должен применяться регулярно!


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


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


Даты


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


Встретились ошибки обработки дат и в Qt. Давайте с них и начнём.


Фрагмент N1: неправильная интерпретация статуса ошибки


Для начала нам следует посмотреть, как устроена функция, возвращающая номер месяца по его сокращённому названию.


static const char qt_shortMonthNames[][4] = {    "Jan", "Feb", "Mar", "Apr", "May", "Jun",    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};static int fromShortMonthName(QStringView monthName){  for (unsigned int i = 0;       i < sizeof(qt_shortMonthNames) / sizeof(qt_shortMonthNames[0]); ++i)  {    if (monthName == QLatin1String(qt_shortMonthNames[i], 3))      return i + 1;  }  return -1;}

В случае успеха функция возвращает номер месяца (значение от 1 до 12). Если имя месяца некорректно, то функция возвращает отрицательное значение (-1). Обратите внимание, что функция не может вернуть значение 0.


Но там, где только что рассмотренная функция используется, программист как раз рассчитывает, что в качестве статуса ошибки ему будет возвращено нулевое значение. Фрагмент кода, некорректно использующий функцию fromShortMonthName:


QDateTime QDateTime::fromString(QStringView string, Qt::DateFormat format){  ....  month = fromShortMonthName(parts.at(1));  if (month)    day = parts.at(2).toInt(&ok);  // If failed, try day then month  if (!ok || !month || !day) {    month = fromShortMonthName(parts.at(2));    if (month) {      QStringView dayPart = parts.at(1);      if (dayPart.endsWith(u'.'))        day = dayPart.chopped(1).toInt(&ok);    }  }  ....}

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


  • V547 [CWE-571] Expression 'month' is always true. qdatetime.cpp 4907
  • V560 [CWE-570] A part of conditional expression is always false: !month. qdatetime.cpp 4911
  • V547 [CWE-571] Expression 'month' is always true. qdatetime.cpp 4913
  • V560 [CWE-570] A part of conditional expression is always false: !month. qdatetime.cpp 4921

Фрагмент N2: ошибка логики обработки даты


Для начала посмотрим на реализацию функции, возвращающей количество секунд.


enum {  ....  MSECS_PER_DAY = 86400000,  ....  SECS_PER_MIN = 60,};int QTime::second() const{    if (!isValid())        return -1;    return (ds() / 1000)%SECS_PER_MIN;}

Рассмотренная функция может вернуть значение в диапазоне [0..59] или статус ошибки -1.


В одном месте эта функция используется очень странным образом:


static qint64 qt_mktime(QDate *date, QTime *time, ....){  ....  } else if (yy == 1969 && mm == 12 && dd == 31             && time->second() == MSECS_PER_DAY - 1) {      // There was, of course, a last second in 1969, at time_t(-1); we won't      // rescue it if it's not in normalised form, and we don't know its DST      // status (unless we did already), but let's not wantonly declare it      // invalid.  } else {  ....}

Предупреждение PVS-Studio: V560 [CWE-570] A part of conditional expression is always false: time->second() == MSECS_PER_DAY 1. qdatetime.cpp 2488


Согласно комментарию, если что-то пошло не так, то лучше ничего не делать. Однако, условие всегда будет ложным, поэтому всегда выполняется else-ветка.


Ошибочно вот это сравнение:


time->second() == MSECS_PER_DAY - 1

MSECS_PER_DAY 1 это 86399999. Функция second, как мы уже знаем, никак не может вернуть такое значение. Таким образом, здесь какая-то логическая ошибка и код заслуживает пристального внимания разработчиков.


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


Опечатки


Фрагмент N3: неожиданно, мы поговорим о HTML!


QString QPixelTool::aboutText() const{  const QList<QScreen *> screens = QGuiApplication::screens();  const QScreen *windowScreen = windowHandle()->screen();  QString result;  QTextStream str(&result);  str << "<html></head><body><h2>Qt Pixeltool</h2><p>Qt " << QT_VERSION_STR    << "</p><p>Copyright (C) 2017 The Qt Company Ltd.</p><h3>Screens</h3><ul>";  for (const QScreen *screen : screens)    str << "<li>" << (screen == windowScreen ? "* " : "  ")        << screen << "</li>";  str << "<ul></body></html>";  return result;}

Предупреждение PVS-Studio: V735 Possibly an incorrect HTML. The "</ body>" closing tag was encountered, while the "</ ul>" tag was expected. qpixeltool.cpp 707


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


Дважды используется тег открытия списка. Это явная опечатка. Первый тег, должен открывать список, а второй закрывать. Правильный код:


str << "</ul></body></html>";

Фрагмент N4: повторная проверка в условии


class Node{  ....  bool isGroup() const { return m_nodeType == Group; }  ....};void DocBookGenerator::generateDocBookSynopsis(const Node *node){  ....  if (node->isGroup() || node->isGroup()      || node->isSharedCommentNode() || node->isModule()      || node->isJsModule() || node->isQmlModule() || node->isPageNode())    return;  ....}

Предупреждение PVS-Studio: V501 [CWE-570] There are identical sub-expressions to the left and to the right of the '||' operator: node->isGroup() || node->isGroup() docbookgenerator.cpp 2599


Простая опечатка, но её исправление зависит от того, чего на самом деле хотели достичь в этом коде. Если проверка просто дублируется, то её стоит просто удалить. Но возможен и другой сценарий: не проверено какое-то другое нужное условие.


Фрагмент N5: создание лишней локальной переменной


void MainWindow::addToPhraseBook(){  ....  QString selectedPhraseBook;  if (phraseBookList.size() == 1) {    selectedPhraseBook = phraseBookList.at(0);    if (QMessageBox::information(this, tr("Add to phrase book"),          tr("Adding entry to phrasebook %1").arg(selectedPhraseBook),           QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok)                          != QMessageBox::Ok)      return;  } else {    bool okPressed = false;    QString selectedPhraseBook =       QInputDialog::getItem(this, tr("Add to phrase book"),                            tr("Select phrase book to add to"),                            phraseBookList, 0, false, &okPressed);    if (!okPressed)      return;  }  MessageItem *currentMessage = m_dataModel->messageItem(m_currentIndex);  Phrase *phrase = new Phrase(currentMessage->text(),                              currentMessage->translation(),                              QString(), nullptr);  phraseBookHash.value(selectedPhraseBook)->append(phrase);}

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


Единорог из старой коллекции


Предупреждение PVS-Studio: V561 [CWE-563] It's probably better to assign value to 'selectedPhraseBook' variable than to declare it anew. Previous declaration: mainwindow.cpp, line 1303. mainwindow.cpp 1313


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


QString selectedPhraseBook =

В результате в else-блоке появилась ещё одна локальная строковая переменная, которая инициализируется, но не используется. Тем временем переменная с таким же именем во внешнем блоке останется пустой.


Фрагмент N6: приоритет операций


Классический паттерн ошибки, который встречается весьма часто.


bool QQmlImportInstance::resolveType(....){  ....  if (int icID = containingType.lookupInlineComponentIdByName(typeStr) != -1)  {    *type_return = containingType.lookupInlineComponentById(icID);  } else {    auto icType = createICType();    ....  }  ....}

Предупреждение PVS-Studio: V593 [CWE-783] Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. qqmlimport.cpp 754


Значение переменной icID всегда будет иметь значение 0 или 1. Это явно не то, что задумывалось. Причина: в начале происходит сравнение с -1, а только затем инициализация переменной icID.


Современный синтаксис C++ позволяет корректно записать это условие следующим образом:


if (int icID = containingType.lookupInlineComponentIdByName(typeStr);    icID != -1)

Кстати, очень похожую ошибку мы уже обнаруживали в Qt:


char ch;while (i < dataLen && ((ch = data.at(i) != '\n') && ch != '\r'))  ++i;

Но пока на вооружение не будет взят такой анализатор кода, как PVS-Studio, программисты вновь и вновь будут допускать такие ошибки. Никто не совершенен. И да, это тонкий намёк внедрить PVS-Studio :).


Фрагмент N7: коварное деление по модулю


Часто бывает необходимо определить, делится число без остатка на 2 или нет. Правильный вариант это поделить по модулю два и проверить результат:


if (A % 2 == 1)

Но программисты вновь и вновь ошибаются и пишут что-то типа этого:


if (A % 1 == 1)

Это естественно неправильно, так как остаток от деления по модулю на один это всегда ноль. Не обошлось без этой ошибки и в Qt:


bool loadQM(Translator &translator, QIODevice &dev, ConversionData &cd){  ....  case Tag_Translation: {    int len = read32(m);    if (len % 1) {                                             // <=      cd.appendError(QLatin1String("QM-Format error"));      return false;    }    m += 4;    QString str = QString((const QChar *)m, len/2);  ....}

Предупреждение PVS-Studio: V1063 The modulo by 1 operation is meaningless. The result will always be zero. qm.cpp 549


Фрагмент N8: перезаписывание значения


QString Node::qualifyQmlName(){  QString qualifiedName = m_name;  if (m_name.startsWith(QLatin1String("QML:")))    qualifiedName = m_name.mid(4);  qualifiedName = logicalModuleName() + "::" + m_name;  return qualifiedName;}

Предупреждение PVS-Studio: V519 [CWE-563] The 'qualifiedName' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1227, 1228. node.cpp 1228


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


QString qualifiedName = m_name;if (m_name.startsWith(QLatin1String("QML:")))  qualifiedName = m_name.mid(4);qualifiedName = logicalModuleName() + "::" + qualifiedName;return qualifiedName;

Фрагмент N9: copy-paste


class Q_CORE_EXPORT QJsonObject{  ....  bool operator<(const iterator& other) const  { Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }  bool operator<=(const iterator& other) const  { Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }  ....}

Прудпреждение PVS-Studio: V524 It is odd that the body of '<=' function is fully equivalent to the body of '<' function. qjsonobject.h 155


Такие скучные функции, как операторы сравнения, никто не проверяет. На них обычно не пишут тесты. Их не просматривают или делают это очень быстро на code review. А зря. Статический анализ кода здесь как нельзя кстати. Анализатор не устаёт и с удовольствием проверяет даже такой скучный код.


Реализация операторов < и <= совпадают. Это явно неправильно. Скорее всего, код писался методом Copy-Paste, и затем забыли изменить всё что нужно в скопированном коде. Правильно:


bool operator<(const iterator& other) const{ Q_ASSERT(item.o == other.item.o); return item.index < other.item.index; }bool operator<=(const iterator& other) const{ Q_ASSERT(item.o == other.item.o); return item.index <= other.item.index; }

Фрагмент N10: static_cast / dynamic_cast


void QSGSoftwareRenderThread::syncAndRender(){  ....  bool canRender = wd->renderer != nullptr;  if (canRender) {     auto softwareRenderer = static_cast<QSGSoftwareRenderer*>(wd->renderer);     if (softwareRenderer)       softwareRenderer->setBackingStore(backingStore);  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'softwareRenderer' is always true. qsgsoftwarethreadedrenderloop.cpp 510


В начале рассмотрим вот эту проверку:


bool canRender = wd->renderer != nullptr;if (canRender) {

Благодаря ей можно быть уверенным, что внутри тела условного оператора значение указателя wd->renderer всегда точно ненулевое. Тогда непонятно, что же хотят проверить следующим кодом?


auto softwareRenderer = static_cast<QSGSoftwareRenderer*>(wd->renderer);if (softwareRenderer)

Если указатель wd->renderer ненулевой, то и указатель softwareRenderer точно ненулевой. Есть подозрение, что здесь опечатка, которая состоит в том, что на самом деле следовало использовать dynamic_cast. В этом случае код начинает приобретать смысл. Если преобразование типа невозможно, оператор dynamic_cast возвращает nullptr, и это возвращенное значение, естественно, следует проверять. Впрочем, возможно, я неправильно интерпретировал ситуацию и код нужно исправлять другим способом.


Фрагмент N11: скопировали блок кода и забыли изменить


void *QQuickPath::qt_metacast(const char *_clname){  if (!_clname) return nullptr;  if (!strcmp(_clname, qt_meta_stringdata_QQuickPath.stringdata0))    return static_cast<void*>(this);  if (!strcmp(_clname, "QQmlParserStatus"))    return static_cast< QQmlParserStatus*>(this);  if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))   // <=    return static_cast< QQmlParserStatus*>(this);  if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))   // <=    return static_cast< QQmlParserStatus*>(this);  return QObject::qt_metacast(_clname);}

Предупреждение PVS-Studio: V581 [CWE-670] The conditional expressions of the 'if' statements situated alongside each other are identical. Check lines: 2719, 2721. moc_qquickpath_p.cpp 2721


Эти две строчки:


if (!strcmp(_clname, "org.qt-project.Qt.QQmlParserStatus"))  return static_cast< QQmlParserStatus*>(this);

Были размножены с помощью Copy-Paste. После чего они не были модифицированы и не имеют смысла.


Фрагмент N12: переполнение из-за не там поставленной скобки


int m_offsetFromUtc;....void QDateTime::setMSecsSinceEpoch(qint64 msecs){  ....  if (!add_overflow(msecs, qint64(d->m_offsetFromUtc * 1000), &msecs))    status |= QDateTimePrivate::ValidWhenMask;  ....}

Предупреждение PVS-Studio: V1028 [CWE-190] Possible overflow. Consider casting operands of the 'd->m_offsetFromUtc * 1000' operator to the 'qint64' type, not the result. qdatetime.cpp 3922


Программист предвидит ситуацию, что при умножении переменной типа int на 1000 может произойти переполнение. Чтобы этого избежать, он планирует использовать при умножении 64-битный тип qint64. И использует явное приведение типа.


Вот только толку от этого приведения типа никакого нет. В начале всё равно произойдёт переполнение. И только затем выполнится приведение типа. Правильный вариант:


add_overflow(msecs, qint64(d->m_offsetFromUtc) * 1000, &msecs)

Фрагмент N13: не полностью инициализированный массив


class QPathEdge{  ....private:  int m_next[2][2];  ....};inline QPathEdge::QPathEdge(int a, int b)    : flag(0)    , windingA(0)    , windingB(0)    , first(a)    , second(b)    , angle(0)    , invAngle(0){    m_next[0][0] = -1;    m_next[1][0] = -1;    m_next[0][0] = -1;    m_next[1][0] = -1;}

Предупреждения PVS-Studio:


  • V1048 [CWE-1164] The 'm_next[0][0]' variable was assigned the same value. qpathclipper_p.h 301
  • V1048 [CWE-1164] The 'm_next[1][0]' variable was assigned the same value. qpathclipper_p.h 302

Перед нами неудачная попытка инициализировать массив размером 2x2. Два элемента инициализируются повторно, и два остаются неинициализированными. Правильный вариант:


m_next[0][0] = -1;m_next[0][1] = -1;m_next[1][0] = -1;m_next[1][1] = -1;

Я очень люблю такие примеры ошибок, которые встречаются в коде профессиональных разработчиков. Это как раз такой случай. Он показывает, что любой может опечататься, и поэтому статический анализ является вашим другом. Дело в том, что я уже десяток лет веду бой со скептиками, которые уверены, что такие ошибки можно встретить только в лабораторных работах студентов и что они такие ошибки никогда не делают :). Ещё 10 лет назад я написал заметку "Миф второй профессиональные разработчики не допускают глупых ошибок", и с тех пор, естественно, ничего не изменилось. Люди всё также делают такие ошибки и всё также утверждают, что это не так :).


Будь мудр - используй статический анализатор кода


Ошибки в логике


Фрагмент N14: недостижимый код


void QmlProfilerApplication::tryToConnect(){  Q_ASSERT(!m_connection->isConnected());  ++ m_connectionAttempts;  if (!m_verbose && !(m_connectionAttempts % 5)) {// print every 5 seconds    if (m_verbose) {      if (m_socketFile.isEmpty())        logError(          QString::fromLatin1("Could not connect to %1:%2 for %3 seconds ...")          .arg(m_hostName).arg(m_port).arg(m_connectionAttempts));      else        logError(          QString::fromLatin1("No connection received on %1 for %2 seconds ...")          .arg(m_socketFile).arg(m_connectionAttempts));    }  }  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'm_verbose' is always false. qmlprofilerapplication.cpp 495


Этот код никогда ничего не запишет в лог. Причиной являются противоположные условия:


if (!m_verbose && ....) {  if (m_verbose) {

Фрагмент N15: перетирание значения переменной


void QRollEffect::scroll(){  ....  if (currentHeight != totalHeight) {      currentHeight = totalHeight * (elapsed/duration)          + (2 * totalHeight * (elapsed%duration) + duration)          / (2 * duration);      // equiv. to int((totalHeight*elapsed) / duration + 0.5)      done = (currentHeight >= totalHeight);  }  done = (currentHeight >= totalHeight) &&         (currentWidth >= totalWidth);  ....}

Предупреждение PVS-Studio: V519 [CWE-563] The 'done' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 509, 511. qeffects.cpp 511


Весь условный оператор не имеет смысла, так как значение переменной done всё равно тут же перезаписывается. Возможно, здесь не хватает ключевого слова else.


Фрагмент N16-N20: перетирание значения переменной


Альтернативный вариант перетирания значения переменной.


bool QXmlStreamWriterPrivate::finishStartElement(bool contents){  ....  if (inEmptyElement) {    ....    lastNamespaceDeclaration = tag.namespaceDeclarationsSize;   // <=    lastWasStartElement = false;  } else {    write(">");  }  inStartElement = inEmptyElement = false;  lastNamespaceDeclaration = namespaceDeclarations.size();      // <=  return hadSomethingWritten;}

Предупреждение PVS-Studio: V519 [CWE-563] The 'lastNamespaceDeclaration' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 3030, 3036. qxmlstream.cpp 3036


Возможно, первая запись значения в переменную lastNamespaceDeclaration является лишней и её можно удалить. А возможно, перед нами серьезная логическая ошибка.


Есть ещё 4 предупреждения, указывающих на такой же паттерн ошибки:


  • V519 [CWE-563] The 'last' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 609, 637. qtextengine.cpp 637
  • V519 [CWE-563] The 'm_dirty' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 1014, 1017. qquickshadereffect.cpp 1017
  • V519 [CWE-563] The 'changed' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 122, 128. qsgdefaultspritenode.cpp 128
  • V519 [CWE-563] The 'eaten' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 299, 301. qdesigner.cpp 301

Фрагмент N21: путаница между нулевым указателем и пустой строкой


// this could become a list of all languages used for each writing// system, instead of using the single most common language.static const char languageForWritingSystem[][6] = {    "",     // Any    "en",  // Latin    "el",  // Greek    "ru",  // Cyrillic    ...... // Нулевых указателей нет. Используются пустые строковые литералы.    "", // Symbol    "sga", // Ogham    "non", // Runic    "man" // N'Ko};static void populateFromPattern(....){  ....  for (int j = 1; j < QFontDatabase::WritingSystemsCount; ++j) {    const FcChar8 *lang = (const FcChar8*) languageForWritingSystem[j];    if (lang) {  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'lang' is always true. qfontconfigdatabase.cpp 462


В массиве languageForWritingSystem нет нулевых указателей. Поэтому проверка if(lang) не имеет смысла. Зато в массиве есть пустые строки. Быть может, хотелось сделать проверку именно на пустую строку? Если да, тогда корректный код должен выглядеть так:


if (strlen(lang) != 0) {

Или можно ещё проще написать:


if (lang[0] != '\0') {

Фрагмент N22: странная проверка


bool QNativeSocketEnginePrivate::createNewSocket(....){  ....  int socket = qt_safe_socket(domain, type, protocol, O_NONBLOCK);  ....  if (socket < 0) {    ....    return false;  }  socketDescriptor = socket;  if (socket != -1) {    this->socketProtocol = socketProtocol;    this->socketType = socketType;  }  return true;}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'socket != 1' is always true. qnativesocketengine_unix.cpp 315


Условие socket != -1 всегда истинно, так как выше происходит выход из функции, если значение переменной socket отрицательное.


Фрагмент N23: так что же всё-таки должна вернуть функция?


bool QSqlTableModel::removeRows(int row, int count, const QModelIndex &parent){  Q_D(QSqlTableModel);  if (parent.isValid() || row < 0 || count <= 0)    return false;  else if (row + count > rowCount())    return false;  else if (!count)    return true;  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression '!count' is always false. qsqltablemodel.cpp 1110


Для упрощения выделю самое главное:


if (.... || count <= 0)  return false;....else if (!count)  return true;

Первая проверка говорит нам, что если значение count меньше или равно 0, то это ошибочное состояние и функция должна вернуть false. Однако ниже мы видим точное сравнение этой переменной с нулём, и этот случай уже интерпретируется по-другому: функция должна вернуть true.


Здесь явно что-то не так. Я подозреваю, что на самом деле проверка должна быть не <=, а просто <. Тогда код обретает смысл:


bool QSqlTableModel::removeRows(int row, int count, const QModelIndex &parent){  Q_D(QSqlTableModel);  if (parent.isValid() || row < 0 || count < 0)    return false;  else if (row + count > rowCount())    return false;  else if (!count)    return true;  ....}

Фрагмент N24: лишний статус?


В следующем коде переменная identifierWithEscapeChars выглядит просто как лишняя сущность. Или это логическая ошибка? Или код не дописан? К моменту второй проверки эта переменная в любом случае всегда будет равна true.


int Lexer::scanToken(){  ....  bool identifierWithEscapeChars = false;  ....  if (!identifierWithEscapeChars) {    identifierWithEscapeChars = true;    ....  }  ....  if (identifierWithEscapeChars) {    // <=    ....  }  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'identifierWithEscapeChars' is always true. qqmljslexer.cpp 817


Фрагмент N25: что делать с девятью объектами?


bool QFont::fromString(const QString &descrip){  ....  const int count = l.count();  if (!count || (count > 2 && count < 9) || count == 9 || count > 17 ||      l.first().isEmpty()) {    qWarning("QFont::fromString: Invalid description '%s'",             descrip.isEmpty() ? "(empty)" : descrip.toLatin1().data());    return false;  }  setFamily(l[0].toString());  if (count > 1 && l[1].toDouble() > 0.0)    setPointSizeF(l[1].toDouble());  if (count == 9) {                           // <=    setStyleHint((StyleHint) l[2].toInt());    setWeight(QFont::Weight(l[3].toInt()));    setItalic(l[4].toInt());    setUnderline(l[5].toInt());    setStrikeOut(l[6].toInt());    setFixedPitch(l[7].toInt());  } else if (count >= 10) {  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'count == 9' is always false. qfont.cpp 2142


Как должна себя вести функция, если переменная count равна 9? С одной стороны, функция должна выдать предупреждение и завершить свою работу. Ведь явно написано:


if (.... || count == 9 || ....) {  qWarning(....);  return false;}

С другой стороны, для 9 объектов предусмотрено выполнение специального кода:


if (count == 9) {  setStyleHint((StyleHint) l[2].toInt());  setWeight(QFont::Weight(l[3].toInt()));  setItalic(l[4].toInt());  ....}

Этот код, конечно, никогда не выполняется. Код ждёт, чтобы его пришли и исправили :).


Нулевые указатели


Фрагмент N26-N42: использование указателя до его проверки


class __attribute__((visibility("default"))) QMetaType {  ....  const QtPrivate::QMetaTypeInterface *d_ptr = nullptr;};QPartialOrdering QMetaType::compare(const void *lhs, const void *rhs) const{    if (!lhs || !rhs)        return QPartialOrdering::Unordered;    if (d_ptr->flags & QMetaType::IsPointer)        return threeWayCompare(*reinterpret_cast<const void * const *>(lhs),                               *reinterpret_cast<const void * const *>(rhs));    if (d_ptr && d_ptr->lessThan) {        if (d_ptr->equals && d_ptr->equals(d_ptr, lhs, rhs))            return QPartialOrdering::Equivalent;        if (d_ptr->lessThan(d_ptr, lhs, rhs))            return QPartialOrdering::Less;        if (d_ptr->lessThan(d_ptr, rhs, lhs))            return QPartialOrdering::Greater;        if (!d_ptr->equals)            return QPartialOrdering::Equivalent;    }    return QPartialOrdering::Unordered;}

Предупреждение PVS-Studio: V595 [CWE-476] The 'd_ptr' pointer was utilized before it was verified against nullptr. Check lines: 710, 713. qmetatype.cpp 710


Ошибка на первый взгляд может быть не заметна. Но на самом деле всё просто. Проследим, как работают с указателем d_ptr:


if (d_ptr->flags & ....)if (d_ptr && ....)

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


Это один из самых распространённых паттернов ошибки в языке C и С++. Пруфы. В исходных Qt кодах тоже встречается немало ошибок этой разновидности:


  • V595 [CWE-476] The 'self' pointer was utilized before it was verified against nullptr. Check lines: 1346, 1351. qcoreapplication.cpp 1346
  • V595 [CWE-476] The 'currentTimerInfo' pointer was utilized before it was verified against nullptr. Check lines: 636, 641. qtimerinfo_unix.cpp 636
  • V595 [CWE-476] The 'lib' pointer was utilized before it was verified against nullptr. Check lines: 325, 333. qlibrary.cpp 325
  • V595 [CWE-476] The 'fragment.d' pointer was utilized before it was verified against nullptr. Check lines: 2262, 2266. qtextcursor.cpp 2262
  • V595 [CWE-476] The 'window' pointer was utilized before it was verified against nullptr. Check lines: 1581, 1583. qapplication.cpp 1581
  • V595 [CWE-476] The 'window' pointer was utilized before it was verified against nullptr. Check lines: 1593, 1595. qapplication.cpp 1593
  • V595 [CWE-476] The 'newHandle' pointer was utilized before it was verified against nullptr. Check lines: 873, 879. qsplitter.cpp 873
  • V595 [CWE-476] The 'targetModel' pointer was utilized before it was verified against nullptr. Check lines: 454, 455. qqmllistmodel.cpp 454
  • V595 [CWE-476] The 'childIface' pointer was utilized before it was verified against nullptr. Check lines: 102, 104. qaccessiblequickitem.cpp 102
  • V595 [CWE-476] The 'e' pointer was utilized before it was verified against nullptr. Check lines: 94, 98. qquickwindowmodule.cpp 94
  • V595 [CWE-476] The 'm_texture' pointer was utilized before it was verified against nullptr. Check lines: 235, 239. qsgplaintexture.cpp 235
  • V595 [CWE-476] The 'm_unreferencedPixmaps' pointer was utilized before it was verified against nullptr. Check lines: 1140, 1148. qquickpixmapcache.cpp 1140
  • V595 [CWE-476] The 'camera' pointer was utilized before it was verified against nullptr. Check lines: 263, 264. assimpimporter.cpp 263
  • V595 [CWE-476] The 'light' pointer was utilized before it was verified against nullptr. Check lines: 273, 274. assimpimporter.cpp 273
  • V595 [CWE-476] The 'channel' pointer was utilized before it was verified against nullptr. Check lines: 337, 338. assimpimporter.cpp 337
  • V595 [CWE-476] The 'm_fwb' pointer was utilized before it was verified against nullptr. Check lines: 2492, 2500. designerpropertymanager.cpp 2492

Фрагмент N43: использование указателя до его проверки в рамках одного выражения


Та же самая ситуация, что и выше. Однако в этот раз разыменование и проверка указателя находятся в одном выражении. Классическая ошибка, возникающая из-за невнимательности при написании кода и при последующем code-review.


void QFormLayoutPrivate::updateSizes(){  ....  QFormLayoutItem *field = m_matrix(i, 1);  ....  if (userHSpacing < 0 && !wrapAllRows && (label || !field->fullRow) && field)  ....}

Предупреждение PVS-Studio: V713 [CWE-476] The pointer 'field' was utilized in the logical expression before it was verified against nullptr in the same logical expression. qformlayout.cpp 405


Минутка отдыха


Я устал. Думаю, устали и читатели. Тут устанешь, даже если просто бегло просматривать текст статьи :). Поэтому я пошёл за второй чашечкой кофе. Первую я выпил где-то на рубеже 12-ого фрагмента. Приглашаю и читателей сходить за любимым напитком.


И пока все ушли на кухню, рекламная пауза. Приглашаю команду, занимающуюся разработкой проекта Qt рассмотреть вопрос приобретения лицензии на анализатор кода PVS-Studio. Запросить прайс можно здесь. С нашей стороны поддержка и помощь в настройке. Да, согласен, я сегодня более навязчив, чем обычно. Это эксперимент :).


Минутка отдыха с единорогом


Фрагмент N44-N72: нет проверки, что вернула функция malloc


void assignData(const QQmlProfilerEvent &other){  if (m_dataType & External) {    uint length = m_dataLength * (other.m_dataType / 8);    m_data.external = malloc(length);                          // <=    memcpy(m_data.external, other.m_data.external, length);    // <=  } else {    memcpy(&m_data, &other.m_data, sizeof(m_data));  }}

Предупреждение PVS-Studio: V575 [CWE-628] The potential null pointer is passed into 'memcpy' function. Inspect the first argument. Check lines: 277, 276. qqmlprofilerevent_p.h 277


Нельзя просто взять и использовать указатель, который вернула функция malloc. Нужно обязательно проверить, не является ли этот указатель нулевым, даже если очень лень всем этим заниматься. Для этого есть 4 причины, которые описаны в статье "Почему важно проверять, что вернула функция malloc".


Перед нами один из случаев, где отсутствует необходимая проверка. Есть и другие предупреждения, но из-за их количества включать весь список в статью не хочется. На всякий случай, я выписал 28 предупреждений в файл: qt6-malloc.txt. Но на самом деле разработчикам, конечно, лучше самим перепроверить проект и самостоятельно изучить предупреждения. У меня не было задачи выявить как можно больше ошибок.


Что интересно, на фоне важных забытых проверок есть совершенно ненужные. Речь идёт о вызове оператора new, который в случае ошибки выделения памяти сгенерирует исключение std::bad_alloc. Вот один из примеров такой избыточной проверки:


static QImageScaleInfo* QImageScale::qimageCalcScaleInfo(....){  ....  QImageScaleInfo *isi;  ....  isi = new QImageScaleInfo;  if (!isi)    return nullptr;  ....}

Предупреждение PVS-Studio: V668 [CWE-570] There is no sense in testing the 'isi' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. qimagescale.cpp 245


P.S. Здесь читатели всегда задают вопрос, учитывает ли анализатор placement new или "new (std::nothrow) T"? Да, учитывает и не выдаёт для них ложные срабатывания.


Избыточный код ("код с запахом")


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


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


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


Итак, взглянем на несколько показательных случаев.


Фрагмент N73: "код с запахом" обратная проверка


void QQuick3DSceneManager::setWindow(QQuickWindow *window){  if (window == m_window)    return;  if (window != m_window) {    if (m_window)      disconnect(....);    m_window = window;    connect(....);    emit windowChanged();  }}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'window != m_window' is always true. qquick3dscenemanager.cpp 60


Если window==m_window, то функция завершает работу. Последующая обратная проверка не имеет никакого смысла и только загромождает код.


Фрагмент N74: "код с запахом" странная инициализация


QModelIndex QTreeView::moveCursor(....){  ....  int vi = -1;  if (vi < 0)    vi = qMax(0, d->viewIndex(current));  ....}

Предупреждение PVS-Stduio: V547 [CWE-571] Expression 'vi < 0' is always true. qtreeview.cpp 2219


Что это? Зачем так писать?


Что это? Зачем так писать? Код можно упростить до одной строки:


int vi = qMax(0, d->viewIndex(current));

Фрагмент N75: "код с запахом" недостижимый код


bool copyQtFiles(Options *options){  ....  if (unmetDependencies.isEmpty()) {    if (options->verbose) {      fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",              qPrintable(sourceFileName));    }  } else {    if (unmetDependencies.isEmpty()) {      if (options->verbose) {        fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",                  qPrintable(sourceFileName));      }    } else {      fprintf(stdout, "  -- Skipping %s. It has unmet dependencies: %s.\n",              qPrintable(sourceFileName),              qPrintable(unmetDependencies.join(QLatin1Char(','))));    }  }  ....}

Предупреждение PVS-Studio: V571 [CWE-571] Recurring check. The 'if (unmetDependencies.isEmpty())' condition was already verified in line 2203. main.cpp 2209


На первый взгляд перед нами респектабельный код, формирующий подсказку. Но давайте приглядимся. Если первый раз условие unmetDependencies.isEmpty() выполнилось, то второй раз этого уже не произойдёт. Это нестрашно, так как автор планировал вывести то же самое сообщение. Настоящей ошибки нет, но код переусложнён. Он может быть упрощен до следующего варианта:


bool copyQtFiles(Options *options){  ....  if (unmetDependencies.isEmpty()) {    if (options->verbose) {      fprintf(stdout, "  -- Skipping %s, architecture mismatch.\n",              qPrintable(sourceFileName));    }  } else {    fprintf(stdout, "  -- Skipping %s. It has unmet dependencies: %s.\n",            qPrintable(sourceFileName),            qPrintable(unmetDependencies.join(QLatin1Char(','))));  }  ....}

Фрагмент N76: "код с запахом" сложный тернарный оператор


bool QDockAreaLayoutInfo::insertGap(....){  ....  QDockAreaLayoutItem new_item    = widgetItem == nullptr      ? QDockAreaLayoutItem(subinfo)      : widgetItem ? QDockAreaLayoutItem(widgetItem)                    : QDockAreaLayoutItem(placeHolderItem);  ....}

Предупреждение PVS-Studio: V547 [CWE-571] Expression 'widgetItem' is always true. qdockarealayout.cpp 1167


Возможно, здесь мы имеем дело с настоящей ошибкой. Но я больше склоняюсь к тому, что этот код несколько раз менялся и в результате получился бессмысленно сложным (избыточным). Его можно сократить до:


  QDockAreaLayoutItem new_item    = widgetItem == nullptr      ? QDockAreaLayoutItem(subinfo) : QDockAreaLayoutItem(widgetItem);

Фрагмент N77: "код с запахом" избыточная защита


typedef unsigned int uint;ReturnedValue TypedArrayCtor::virtualCallAsConstructor(....){  ....  qint64 l = argc ? argv[0].toIndex() : 0;  if (scope.engine->hasException)    return Encode::undefined();  // ### lift UINT_MAX restriction  if (l < 0 || l > UINT_MAX)    return scope.engine->throwRangeError(QLatin1String("Index out of range."));  uint len = (uint)l;  if (l != len)    scope.engine->throwRangeError(      QStringLiteral("Non integer length for typed array."));  ....}

Предупреждение PVS-Studio: V547 [CWE-570] Expression 'l != len' is always false. qv4typedarray.cpp 306


Кто-то очень переживает, что значение 64-битной переменной не вмещается в 32-битную переменную unsigned. И использует сразу две проверки корректности. При этом вторая проверка избыточна.


Вот этого условия более чем достаточно:


if (l < 0 || l > UINT_MAX)

Приведенный ниже фрагмент можно смело удалить, и программа менее надёжной не станет:


uint len = (uint)l;if (l != len)  scope.engine->throwRangeError(    QStringLiteral("Non integer length for typed array."));

Дальше продолжать не буду. Думаю, идею вы поняли.


Здесь можно сделать маленький вывод: результатом использования анализатора PVS-Studio будет не только устранение ошибок, но и упрощение кода.


Другие ошибки


Я остановился после того, как описал 77 дефектов. Это красивое число и выписанного более чем достаточно, чтобы написать статью. Однако это не значит, что нет других ошибок, которые способен выявить PVS-Studio. При изучении лога я был весьма поверхностен и пропускал всё, где нужно было разбираться более пары минут, ошибка это или нет :).


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


Заключение


Статический анализ это круто! После внедрения PVS-Studio будет экономить время и нервы, выявляя множество ошибок сразу после написания кода. Намного лучше искать на code review с коллегами не опечатки, а высокоуровневые ошибки и обсуждать эффективность реализованного алгоритма. Тем более, как показывает практика, эти дурацкие опечатки всё равно отлично прячутся при просмотре кода глазами. Так что пусть их лучше ищет программа, а не человек.


Если у вас ещё остались вопросы или возражения, приглашаю познакомиться со статьёй "Причины внедрить в процесс разработки статический анализатор кода PVS-Studio". С вероятность 90 % вы найдете в ней ответ на ваши вопросы :). В оставшихся 10 % случаев напишите нам, пообщаемся :).


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Date Processing Attracts Bugs or 77 Defects in Qt 6.

Подробнее..

Теперь PVS-Studio ещё лучше знает, что за зверь такой strlen

27.04.2021 16:10:26 | Автор: admin

0824_DataFlow_And_Strlen_ru/image1.png
Как-то так несправедливо сложилось, что мы почти не уделяем в наших заметках внимание усовершенствованию внутренних механизмов анализатора, в отличие от новых диагностик. Поэтому давайте для разнообразия познакомимся с новым полезным усовершенствованием, коснувшимся анализа потока данных.


Всё началось с твита от JetBrains CLion IDE


На днях я увидел в Twitter пост от JetBrains про новые возможности статического анализатора, встроенного в CLion.


0824_DataFlow_And_Strlen_ru/image2.png


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


0824_DataFlow_And_Strlen_ru/image3.png


Ну и ещё немного с ними мило попереписывался:



После всего этого я подумал. А ведь они молодцы! Улучшили анализ потока данных и рассказывают миру. А мы чем хуже? Мы ведь тоже постоянно что-то улучшаем внутри анализатора, в том числе и тот же механизм анализа потока данных. И вот я уже пишу эту заметку.


А что у нас интересного с Data Flow


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


bool foo(){  unsigned N = 2;  for (unsigned i = 0; i < N; ++i)  {    bool stop = (i - 1 == N);    if (stop)      return true;  }  return false;}

Анализатор не мог понять, что переменной stop всегда присваивается значение false.


Почему false? Давайте быстро посчитаем:


  • диапазон значения переменной i = [0; 1];
  • возможные значения выражения i-1 = [0; 0] U [UINT_MAX; UINT_MAX];
  • значение переменной N, равное двойке, не входит в множество { 0, UINT_MAX };
  • условие всегда ложно.

Примечание. Неопределённого поведения здесь нет, так как происходит переполнение (wrap) при работе с беззнаковым типом.


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


Например, возникли ложные срабатывания, связанные с обработкой длины строк. Борьба с ними привела к новым улучшениям и обучению анализатора лучше понимать, как и зачем используют такие функции, как strlen. Сейчас мы на практике покажем, о каких улучшениях идёт речь.


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


int Assemble(unsigned char *output, int addr, char *str) {  output[0] = output[1] = output[2] = 0;  char astr[128],ins[4];  if ((!strlen(str)) || (strlen(str) > 0x127)) return 1;  strcpy(astr,str);  ....}

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


Предупреждение PVS-Studio: V512 A call of the 'strcpy' function will lead to overflow of the buffer 'astr'. asm.cpp 21


Всё равно не видите ошибку? Давайте внимательно разберём код. Для начала уберём всё не относящееся к делу:


int Assemble(char *str) {  char astr[128];  if ((!strlen(str)) || (strlen(str) > 0x127)) return 1;  strcpy(astr,str);  ....}

Есть локальный массив из 128 байт, в который планируется скопировать строчку, переданную в качестве аргумента. Копирование не должно выполняться, если строка пустая или содержит более 127 символов (не считая терминальный ноль).


Пока всё логично и правильно? На первый взгляд, да. Но что это?! Что это за константа 0x127?!


Это вовсе не 127. Совсем не 127 :)


Константа задана в шестнадцатеричной системе. Если перевести в десятичную, то получается 295.


Итак, написанный код эквивалентен следующему:


int Assemble(char *str) {  char astr[128];  if ((!strlen(str)) || (strlen(str) > 295)) return 1;  strcpy(astr,str);  ....}

Как видите, проверка никак не защищает от переполнения буфера, и анализатор совершенно правильно предупреждает о проблеме.


Раньше анализатор не мог найти ошибку, будучи не в состоянии понять, что две функции strlen работают с одной строкой. И эта строка не меняется между двумя вызовами strlen. С точки зрения программиста, всё это очевидно, а вот анализатор нужно учить всё это понимать :).


Теперь PVS-Studio выводит из выражения, что длина строки str лежит в диапазоне [1..295], а значит, может возникнуть выход за границу массива, если попытаться его скопировать в буфер astr.


0824_DataFlow_And_Strlen_ru/image4.png


Новые вызовы


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


int Assemble(unsigned char *output, int addr, char *str) {  output[0] = output[1] = output[2] = 0;  char astr[128],ins[4];  int len = strlen(str);  if ((!len) || (len > 0x127)) return 1;  strcpy(astr,str);  ....}

Человеку такой код может показаться даже проще, но, с точки зрения статического анализа, он труден для отслеживания значений. Нужно учитывать, что значение переменной len является длиной строки str. Дополнительно требуется аккуратно отслеживать, когда разоврётся эта взаимосвязь при модификации содержимого строки или переменной len.


Пока это анализатор PVS-Studio делать не умеет. Зато видно, куда можно и нужно развиваться! Со временем научимся находить ошибку и в этом новом коде.


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


А чтобы тестировать анализатор на современном коде, написанном на C++14, C++17 и т.д., мы постепенно пополняем базу новыми проектами. Например, относительно недавно мы добавили коллекцию header-only C++ библиотек (awesome-hpp).


Заключение


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


  1. Анализатор кода не прав, да здравствует анализатор
  2. Ложные срабатывания в PVS-Studio: как глубока кроличья нора
  3. Технологии, используемые в анализаторе кода PVS-Studio для поиска ошибок и потенциальных уязвимостей
  4. Использование машинного обучения в статическом анализе исходного кода программ

Приглашаю скачать анализатор PVS-Studio и проверить свои проекты.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio Learns What strlen is All About.

Подробнее..

Статический анализ baseline файлы vs diff

25.06.2020 12:16:27 | Автор: admin

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


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



baseline или так называемый suppress profile


Этот метод имеет несколько названий: baseline файл в Psalm и Android Lint, suppress база (или профиль) в PVS-Studio, code smell baseline в detekt.


Данный файл генерируется линтером при запуске на проекте:


superlinter --create-baseline baseline.xml ./project

Внутри себя он содержит все предупреждения, которые были выданы на этапе создания.


При запуске статического анализатора с baseline.xml, все предупреждения, которые содержатся в этом файле, будут проигнорированы:


superlinter --baseline baseline.xml ./project

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


Обычно, мы хотим достичь следующего:


  • На новый код выдаются все предупреждения
  • На старый код предупреждения выдаются только если его редактировали
  • (опционально) Переносы файлы не должны выдавать предупреждения на весь файл

То, что мы можем конфигурировать в этом подходе это какие поля предупреждения формируют хеш (или "сигнатуру") срабатывания. Чтобы уйти от проблем смещения строк кода, лучше не добавлять номер строки в эти поля.


Вот примерный список того, что вообще может являться полем сигнатуры:


  • Название или код диагностики
  • Текст предупреждения
  • Название файла
  • Строка исходного кода, на которую сработало предупреждение

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


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


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


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


Хорошо подобранный набор признаков увеличивает эффективность baseline подхода.


Коллизии в методе baseline


Допустим, диагностика W104 находит вызовы die в коде.


В проверяемом проекте есть файл foo.php:


function legacy() {  die('test');}

Предположим, используются признаки {имя файла, код диагностики, строка исходного кода}.


Наш анализатор при создании baseline добавляет вызов die('test') в свою базу исключений:


{  "filename": "foo.php",  "diag": "W104",  "line": "die('test');"}

Теперь добавим немного нового кода:


+ function newfunc() {+   die('test');+ }  function legacy() {    die('test');  }

По всем используемым признакам новый вызов die('test') будет распознаваться как игнорируемый код. Совпадение сигнатур для потенциально разных кусков кода мы и называем коллизией.


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


Что делать, если вызов die('test') был добавлен в ту же функцию? Этот вызов может иметь одинаковые соседние строки в обоих случаях, поэтому добавление предыдущей и следующей строки в сигнатуре не поможет.


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


При этом мы теряем некоторую точность: нельзя определить, какая из строк новая, а какая старая. Мы будем ругаться на ту, что будет идти после проигнорированных.


Метод, основанный на diff возможностях VCS


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


Утилита revgrep принимает на stdin поток предупреждений, анализирует git diff и выдаёт на выход только те предупреждения, которые исходят от новых строк.


golangci-lint использует форк revgrep как библиотеку, так что в основе его вычисления diff'а лежат те же алгоритмы.

Если выбран этот путь, придётся искать ответ на следующие вопросы:


  • Как выбрать окно коммитов для вычисления diff?
  • Как будем обрабатывать коммиты, пришедшие из основной ветки (merge/rebase)?

Вдобавок нужно понимать, что иногда мы всё же хотим выдавать предупреждения, выходящие за рамки diff. Пример: вы удалили класс директора по мемам, MemeDirector. Если этот класс упоминался в каких-либо doc-комментариях, желательно, чтобы линтер сообщил об этом.


Нам нужно не только получить правильный набор затронутых строк, но и расширить его так, чтобы найти побочные эффекты изменений во всём проекте.


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


diff режим в NoVerify


NoVerify имеет два режима работы: diff и full diff.


Обычный diff может найти предупреждения по файлам, которые были затронуты изменениями в рамках рассматриваемого диапазона коммитов. Это работает быстро, но хорошего анализа зависимостей изменений не происходит, поэтому новые предупреждения из незатронутых файлов мы найти не можем.


Full diff запускает анализатор дважды: один раз на старом коде, затем на новом коде, а потом фильтрует результаты. Это можно сравнить с генерацией baseline файла на лету с помощью того, что мы можем получить предыдущую версию кода через git. Ожидаемо, этот режим увеличивает время выполнения почти вдвое.


Изначальная схема работы предполагалась такая: на pre-push хуках запускается более быстрый анализ, в режиме обычного diff'а, чтобы люди получали обратную связь как можно быстрее. На CI агентах полный diff. В результате время от времени люди спрашивают, почему на агентах проблемы были найдены, а локально всё чисто. Удобнее иметь одинаковые процессы проверки, чтобы при прохождении pre-push хука была гарантия прохождения на CI фазы линтера.


full diff за один проход


Мы можем делать приближенный к full diff аналог, который не требует двойного анализа кода.


Допустим, в diff попала такая строка:


- class Foo {

Если мы попробуем классифицировать эту строку, то определим её как "Foo class deletion".


Каждая диагностика, которая может как-то зависеть от наличия класса, должна выдавать предупреждение, если имеется факт удаления этого класса.


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


Переименования не требуют дополнительной обработки. Мы считаем, что символ со старым именем был удалён, а с новым добавлен:


- class MemeManager {+ class SeniorMemeManager {

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


Выводы


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


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


baseline diff
+ легко сделать эффективным + не требует хранить файлов
+ простота реализации и конфигурации + проще отличать новый код от старого
- нужно решать коллизии - сложно правильно приготовить

Могут существовать гибридные подходы: сначала берёшь baseline файл, а потом разрешаешь коллизии и вычисляешь смещения строк кода через git.


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

Подробнее..

Статический анализ от знакомства до интеграции

06.08.2020 18:12:42 | Автор: admin
Устав от нескончаемого code review или отладки, временами задумываешься, как бы упростить себе жизнь. И немного поискав, ну или случайно наткнувшись, можно увидеть волшебное словосочетание: "Статический анализ". Давайте посмотрим, что это такое и как он может взаимодействовать с вашим проектом.

Evolution

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

issues

В этом выводе мы видим, что переменная var так и не была использована нигде в функции. Так что на самом деле вы почти всегда пользовались простеньким статическим анализатором кода. Однако, в отличие от профессиональных анализаторов, таких как Coverity, Klocwork или PVS-Studio, предоставляемые компилятором предупреждения могут указывать только на небольшой спектр проблем.

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

Зачем нужен статический анализ?


В двух словах: ускорение и упрощение.

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

auto x = obj.x;auto y = obj.y;auto z = obj.z;

Вы написали следующий код:

auto x = obj.x;auto y = obj.y;auto z = obj.x;

Как видите, в последней строке появилась опечатка. Например, PVS-Studio выдаёт следующее предупреждение:

V537 Consider reviewing the correctness of 'y' item's usage.

Если хотите потыкать в эту ошибку руками, то попробуйте готовый пример на Compiler Explorer: *клик*.

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

Однако это явная ошибка. А если разработчик написал неоптимальный код из-за того, что позабыл какую-либо тонкость языка? Или же вовсе допустил в коде undefined behavior? К сожалению, подобные случаи совершенно обыденны и львиная часть времени тратится на то, чтобы отладить специфично работающий код, который содержит опечатки, типичные ошибки или undefined behavior.

Именно для этих ситуаций и появился статический анализ. Это помощник для разработчика, который укажет ему на различные проблемы в коде и объяснит в документации почему так писать не нужно, к чему это может привести и как это исправить. Вот пример как это может выглядеть: *клик*.

Больше интересных ошибок, которые может обнаружить анализатор, вы можете найти в статьях:


Теперь, прочитав этот материал и убедившись в пользе статического анализа, вы могли захотеть испытать его в деле. Но с чего начать? Как интегрировать новый инструмент в текущий проект? И как познакомить команду с ним? На эти вопросы вы найдёте ответы ниже.

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

0. Знакомство с инструментом


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

Что вы узнаете на этом этапе:

  • Какие есть способы взаимодействия с анализатором;
  • Совместим ли анализатор с вашей средой разработки;
  • Какие проблемы есть сейчас в ваших проектах.

После того, как вы установили себе всё необходимое, то первым делом стоит запустить анализ всего проекта (Windows, Linux, macOS). В случае с PVS-Studio в Visual Studio вы увидите подобную картину (кликабельно):

list

Дело в том, что обычно на проекты с большой кодовой базой статические анализаторы выдают огромное количество предупреждений. Нет необходимости исправлять их все, так как ваш проект уже работает, а значит эти проблемы не являются критичными. Однако вы можете посмотреть на самые интересные предупреждения и исправить их при необходимости. Для этого нужно отфильтровать вывод и оставить только наиболее достоверные сообщения. В плагине PVS-Studio для Visual Studio это делается фильтрацией по уровням и категориям ошибок. Для наиболее точного вывода оставьте включёнными только High и General (тоже кликабельно):

list

Действительно, 178 предупреждений просмотреть значительно проще, чем несколько тысяч

Во вкладках Medium и Low часто попадаются хорошие предупреждения, однако в эти категории занесены те диагностики, которые имеют меньшую точность (достоверность). Подробнее про уровни предупреждений и варианты работы под Windows можно посмотреть тут: *клик*.

Успешно просмотрев самые интересные ошибки (и успешно исправив их) стоит подавить оставшиеся предупреждения. Это нужно для того, чтобы новые предупреждения не терялись среди старых. К тому же статический анализатор это помощник для программиста, а не список для багов. :)

1. Автоматизация


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

Что вы узнаете на данном этапе:

  • Какие варианты автоматизации предоставляет инструмент;
  • Совместим ли анализатор с вашей сборочной системой.

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

А теперь приступим к сервисам непрерывной интеграции (CI). Любой анализатор можно внедрить в них без каких-либо серьезных проблем. Для этого нужно создать отдельный этап в pipeline, который обычно находится после сборки и юнит-тестов. Делается это при помощи различных консольных утилит. Например, PVS-Studio предоставляет следующие утилиты:


Для интеграции анализа в CI нужно сделать три вещи:

  • Установить анализатор;
  • Запустить анализ;
  • Доставить результаты.

Например, для установки PVS-Studio на Linux (Debian-base) нужно выполнить следующие команды:

wget -q -O - https://files.viva64.com/etc/pubkey.txt \    | sudo apt-key add -sudo wget -O /etc/apt/sources.list.d/viva64.list \  https://files.viva64.com/etc/viva64.list  sudo apt-get update -qqsudo apt-get install -qq pvs-studio

В системах под управлением Windows отсутствует возможность установить анализатор из пакетного менеджера, однако есть возможность развернуть анализатор из командной строки:

PVS-Studio_setup.exe /verysilent /suppressmsgboxes /norestart /nocloseapplications

Подробнее о развёртывании PVS-Studio в системах под управлением Windows можно почитать *тут*.

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

Так как способ запуска зависит от платформы и особенностей проекта, я покажу вариант для C++ (Linux) в качестве примера:

pvs-studio-analyzer analyze -j8 \                            -o PVS-Studio.logplog-converter -t errorfile PVS-Studio.log --cerr -w

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

Примечание. Текстовый формат это неудобно. Он приводится просто для примера. Обратите внимание на более интересный формат отчёта FullHtml. Он позволяет осуществлять навигацию по коду.

Подробнее про настройку анализа на CI можно прочитать в статье "PVS-Studio и Continuous Integration" (Windows) или "Как настроить PVS-Studio в Travis CI" (Linux).

Хорошо, вы настроили работу анализатора на сборочном сервере. Теперь, если кто-то залил непроверенный код, будет падать этап проверки, и вы сможете обнаружить проблему, однако это не совсем удобно, так как эффективнее проверять проект не после того, как произошло слияние веток, а до него, на этапе pull request'а.

В целом настройка анализа pull request'а не сильно отличается от обычного запуска анализа на CI. За исключением необходимости получить список изменённых файлов. Обычно их можно получить, запросив разницу между ветками при помощи git:

git diff --name-only HEAD origin/$MERGE_BASE > .pvs-pr.list

Теперь нужно передать анализатору на вход этот список файлов. Например, в PVS-Studio это реализовано при помощи флага -S:

pvs-studio-analyzer analyze -j8 \                            -o PVS-Studio.log \                            -S .pvs-pr.list

Подробнее про анализ pull request'ов можно узнать *тут*. Даже если вашего CI нет в списке указанных в статье сервисов, вам будет полезен общий раздел, посвященный теории этого типа анализа.

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

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

2. Интеграция на машины разработчиков


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

Как самый простой вариант разработчики сами могут установить необходимый анализатор. Однако это займёт много времени и отвлечёт их от разработки, поэтому вы можете автоматизировать этот процесс, используя установщик и нужные флаги. Для PVS-Studio есть различные флаги для автоматизированной установки. Впрочем, всегда есть пакетные менеджеры, например, Chocolatey (Windows), Homebrew (macOS) или десятки вариантов для Linux.

Затем нужно будет установить необходимые плагины, например, для Visual Studio, IDEA, Rider etc.

3. Ежедневное использование


На этом этапе пора сказать пару слов о способах ускорения работы анализатора при ежедневном использовании. Полный анализ всего проекта занимает очень много времени, однако часто ли мы меняем код разом во всём проекте? Едва ли существует настолько масштабный рефакторинг, что сразу затронет всю кодовую базу. Количество изменяемых файлов за раз редко превышает десяток, поэтому их и есть смысл анализировать. Для подобной ситуации существует режим инкрементального анализа. Только не пугайтесь, это не ещё один инструмент. Это специальный режим, который позволяет анализировать только изменённые файлы и их зависимости, причём это происходит автоматически после сборки, если вы работаете в IDE c установленным плагином.

В случае, если анализатор обнаружит в недавно измененном коде проблемы, то сообщит об этом самостоятельно. Например, PVS-Studio скажет вам об этом при помощи оповещения:

notification

Само собой недостаточно сказать разработчикам использовать инструмент. Нужно как-то им рассказать, что это вообще и как это есть. Вот, например, статьи про быстрый старт для PVS-Studio, однако подобные туториалы вы сможете найти для любого предпочитаемого вами инструмента:


Подобные статьи дают всю необходимую для повседневного использования информацию и не отнимают много времени. :)

Ещё на этапе знакомства с инструментом мы подавили очень много предупреждений во время одного из первых запусков. Увы, но статические анализаторы не идеальны, поэтому время от времени выдают ложные срабатывания. Подавить их обычно легко, например в плагине PVS-Studio для Visual Studio достаточно нажать на одну кнопку:

suppress

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

После интеграции


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

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

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



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Maxim Zvyagintsev. Static Analysis: From Getting Started to Integration.
Подробнее..

Анализатор кода не прав, да здравствует анализатор

01.12.2020 22:06:36 | Автор: admin
Foo(std::move(buffer), line_buffer - buffer.get());

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

Порядок вычисления аргументов


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

Краткая суть заключается в следующем. Порядок вычисления аргументов функции это неуточненное поведение. Стандарт не регламентирует, в каком именно порядке разработчики компиляторов обязаны произвести вычисление аргументов. Например, слева направо (Clang) или справа налево (GCC, MSVC). До стандарта C++17, когда при вычислении аргументов возникали побочные эффекты, это могло приводить к неопределённому поведению.

С появлением стандарта C++17 ситуация изменилась в лучшую сторону: теперь вычисление аргумента и его побочные эффекты начнут выполняться лишь с того момента, как будут выполнены все вычисления и побочные эффекты предыдущего аргумента. Однако, это не значит, что теперь нет места для ошибки.

Рассмотрим простую тестовую программу:

#include <cstdio>int main(){  int i = 1;  printf("%d, %d\n", i, i++);  return 0;}

Что распечатает этот код? Ответ, по-прежнему, зависит от компилятора, его версии и настроения. В зависимости от компилятора может быть распечатано как "1, 1", так и "2, 1". И действительно, воспользовавшись Compiler Explorer я получит следующие результаты:

  • программа, скомпилированная с помощью Clang 11.0.0, выдаёт "1, 1".
  • программа, скомпилированная с помощью GCC 10.2, выдаёт "2, 1".

В этой программе нет неопределённого поведения, но есть неуточнённое поведение (порядок вычисления аргументов).

Код из проекта CSV Parser


Вернёмся к фрагменту кода из проекта CSV Parser, о котором я упоминал в статье "Проверка коллекции header-only C++ библиотек (awesome-hpp)".

Мы с анализатором знаем о том, что аргументы могут вычисляться в разном порядке. Поэтому анализатор, а вслед за ним и я, посчитали этот код ошибочным:

std::unique_ptr<char[]> buffer(new char[BUFFER_UPPER_LIMIT]);....this->feed_state->feed_buffer.push_back(    std::make_pair<>(std::move(buffer), line_buffer - buffer.get()));

Предупреждение PVS-Studio: V769 The 'buffer.get()' pointer in the 'line_buffer buffer.get()' expression equals nullptr. The resulting value is senseless and it should not be used. csv.hpp 4957

На самом деле, мы оба неправы, и никакой ошибки нет. Про нюансы будет дальше, а пока начнём с простого.

Итак, давайте разберёмся, почему опасно писать код следующего вида:

Foo(std::move(buffer), line_buffer - buffer.get());

Я думаю, вы догадываетесь про ответ. Результат зависит от последовательности вычисления аргументов. Рассмотрим это на следующем синтетическом коде:

#include <iostream>#include <memory>   void Print(std::unique_ptr<char[]> p, ptrdiff_t diff){    std::cout << diff << std::endl;} void Print2(ptrdiff_t diff, std::unique_ptr<char[]> p){    std::cout << diff << std::endl;} int main(){    {        std::unique_ptr<char[]> buffer(new char[100]);        char *ptr = buffer.get() + 22;        Print(std::move(buffer), ptr - buffer.get());    }    {        std::unique_ptr<char[]> buffer(new char[100]);        char *ptr = buffer.get() + 22;        Print2(ptr - buffer.get(), std::move(buffer));    }    return 0;}

Вновь воспользуемся Compiler Explorer и посмотрим результат работы этой программы, собранной разными компиляторами.

Компилятор Clang 11.0.0. Результат:

2338784622

Компилятор GCC 10.2. Результат:

2226640070

Результат ожидаем, и писать так нельзя. О чём, собственно, и предупреждает анализатор PVS-Studio.

На этом бы хотелось поставить точку, но всё немного сложнее. Дело в том, что речь идёт о передаче аргументов по значению, а при инстанцировании шаблона функции std::make_pair всё будет иначе. Продолжим погружаться в нюансы и узнаем, почему PVS-Studio в данном случае неправ.

std::make_pair


Обратимся к сайту cppreference и посмотрим, как менялся шаблон функции std::make_pair.

Until C++11:
template< class T1, class T2 >
std::pair<T1,T2> make_pair( T1 t, T2 u );
Since C++11, until C++14:
template< class T1, class T2 >
std::pair<V1,V2> make_pair( T1&& t, T2&& u );
Since C++14:
template< class T1, class T2 >
constexpr std::pair<V1,V2> make_pair( T1&& t, T2&& u );
Как видите, когда-то давным-давно std::make_pair принимал аргументы по значению. Если бы в те времена существовал std::unique_ptr, то рассмотренный выше код действительно был некорректным. Работал бы ли этот код или нет, зависело от везения. На практике, конечно, такая ситуация бы никогда не возникла, так как std::unique_ptr появился в C++11 как замена std::auto_ptr.

Вернёмся в наше время. Начиная с версии стандарта C++11, конструктор начал использовать семантику перемещения.

Здесь есть тонкий момент в том, что std::move на самом деле ничего не перемещает, а всего-навсего производит преобразование объекта к rvalue-ссылке. Это позволит std::make_pair передать указатель новому std::unique_ptr, оставив nullptr в исходном умном указателе. Но эта передача указателя не произойдет, пока мы не попадём внутрь std::make_pair. К тому времени мы уже вычислим line_buffer buffer.get(), и всё будет хорошо. То есть, вызов функции buffer.get() не может вернуть nullptr в момент, когда он вычисляется, вне зависимости от того, когда именно это произойдёт.

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

Король умер, да здравствует король!


Мы разобрались, что срабатывание, описанное в статье, оказалось ложным. Спасибо одному нашему читателю, который обратил наше внимание на особенность реализации std::make_pair.

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

Здесь уместно вспомнить статью "False positives are our enemies, but may still be your friends". Публикация не наша, но мы с ней согласны.

Это, пожалуй, тот самый случай. Пусть предупреждение ложное, но оно указывает на место, где лучше провести рефакторинг. Достаточно написать что-то вроде этого:

auto delta = line_buffer - buffer.get();this->feed_state->feed_buffer.push_back(  std::make_pair(std::move(buffer), delta));

А можно в данной ситуации сделать код еще лучше, воспользовавшись методом emplace_back:

auto delta = line_buffer - buffer.get();this->feed_state->feed_buffer.emplace_back(std::move(buffer), delta);

Такой код создаст итоговый объект std::pair в контейнере "по месту", минуя создание временного объекта и его перемещение в контейнер. Кстати, анализатор PVS-Studio, предлагает сделать такую замену, выдавая предупреждение V823 из набора правил по микрооптимизациям кода.

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

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

Заключение


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

Код вида:

Foo(std::move(buffer), line_buffer - buffer.get());

легко сломать, изменяя что-то в другом месте программы. Такой код тяжело сопровождать. А ещё он неприятен тем, что может возникать ложное ощущение, что всё работает правильно. На самом же деле, это просто стечение обстоятельств, и всё может сломаться при смене компилятора или настроек оптимизации.

Пишите код проще!


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. The Code Analyzer is wrong. Long live the Analyzer!.
Подробнее..

PVS-Studio и Continuous Integration TeamCity. Анализ проекта Open RollerCoaster Tycoon 2

20.07.2020 18:22:00 | Автор: admin

Один из самых актуальных сценариев использования анализатора PVS-Studio его интеграция с CI системами. И хотя анализ проекта PVS-Studio практически из-под любой continuous integration системы можно встроить всего в несколько команд, мы продолжаем делать этот процесс ещё удобнее. В PVS-Studio появилась поддержка преобразования вывода анализатора в формат для TeamCity TeamCity Inspections Type. Давайте посмотрим, как это работает.

Информация об используемом ПО


PVS-Studio статический анализатор С, С++, C# и Java кода, предназначенный для облегчения задачи поиска и исправления различного рода ошибок. Анализатор можно использовать в Windows, Linux и macOS. В данной статье мы будем активно использовать не только сам анализатор, но и некоторые утилиты из его дистрибутива.

CLMonitor представляет собой сервер мониторинга, который осуществляет отслеживание запусков компиляторов. Его необходимо запустить непосредственно перед началом сборки вашего проекта. В режиме отслеживания сервер будет перехватывать запуски всех поддерживаемых компиляторов. Стоит отметить, что данную утилиту можно использовать только для анализа C/С++ проектов.

PlogConverter утилита для конвертации отчёта анализатора в разные форматы.

Информация об исследуемом проекте


Давайте попробуем данную функциональность на практическом примере проанализируем проект OpenRCT2.

OpenRCT2 открытая реализация игры RollerCoaster Tycoon 2 (RCT2), расширяющая её новыми функциями и исправляющая ошибки. Игровой процесс вращается вокруг строительства и содержания парка развлечений, в котором находятся аттракционы, магазины и объекты. Игрок должен постараться получить прибыль и поддерживать хорошую репутацию парка, сохраняя при этом гостей счастливыми. OpenRCT2 позволяет играть как в сценарии, так и в песочнице. Сценарии требуют, чтобы игрок выполнил определенную задачу в установленное время, в то время как песочница позволяет игроку построить более гибкий парк без каких-либо ограничений или финансов.

Настройка


В целях экономии времени я, пожалуй, опущу процесс установки и начну с того момента, когда у меня на компьютере запущен сервер TeamCity. Нам нужно перейти: localhost:{указанный в процессе установки порт}(в моём случае, localhost:9090) и ввести данные для авторизации. После входа нас встретит:

image3.png

Нажмём на кнопку Create Project. Далее выберем Manually, заполним поля.

image5.png

После нажатия на кнопку Create, нас встречает окно с настройками.

image7.png

Нажмём Create build configuration.

image9.png

Заполняем поля, нажимаем Create. Мы видим окно с предложением выбора системы контроля версий. Так как исходники уже лежат локально, жмём Skip.

image11.png

Наконец, мы переходим к настройкам проекта.

image13.png

Добавим шаги сборки, для этого жмём: Build steps -> Add build step.

image15.png

Тут выберем:

  • Runner type -> Command Line
  • Run -> Custom Script

Так как мы будем проводить анализ во время компиляции проекта, сборка и анализ должны быть одним шагом, поэтому заполним поле Custom Script:

image17.png

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

Последнее, что нам нужно сделать, установить переменные окружения, которыми я обозначил некоторые пути для улучшения их читабельности. Для этого перейдём: Parameters -> Add new parameter и добавим три переменные:

image19.png

Остаётся нажать на кнопку Run в правом верхнем углу. Пока идёт сборка и анализ проекта расскажу вам о скрипте.

Непосредственно скрипт


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

choco install pvs-studio -y

Далее запустим утилиту отслеживания сборки проекта CLMonitor.

%CLmon% monitor -attach

Потом произведём сборку проекта, в качестве переменной окружения MSB выступает путь к нужной мне для сборки версии MSBuild

%MSB% %ProjPath% /t:clean%MSB% %ProjPath% /t:rebuild /p:configuration=release%MSB% %ProjPath% /t:g2%MSB% %ProjPath% /t:PublishPortable

Введём логин и ключ лицензии PVS-Studio:

%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%

После завершения сборки ещё раз запустим CLMonitor для генерации препроцессированных файлов и статического анализа:

%CLmon% analyze -l "c:\ptest.plog"

После воспользуемся ещё одной утилитой из нашего дистрибутива. PlogConverter преобразует отчёт из стандартного в специфичный для TeamCity формат. Благодаря этому мы сможем посмотреть его прямо в окне сборки.

%PlogConverter% "c:\ptest.plog" --renderTypes=TeamCity -o "C:\temp"

Последним действием выведем форматированный отчёт в stdout, где его подхватит парсер TeamCity.

type "C:\temp\ptest.plog_TeamCity.txt"

Полный код скрипта:

choco install pvs-studio -y%CLmon% monitor --attachset platform=x64%MSB% %ProjPath% /t:clean%MSB% %ProjPath% /t:rebuild /p:configuration=release%MSB% %ProjPath% /t:g2%MSB% %ProjPath% /t:PublishPortable%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%%CLmon% analyze -l "c:\ptest.plog"%PlogConverter% "c:\ptest.plog" --renderTypes=TeamCity -o "C:\temp"type "C:\temp\ptest.plog_TeamCity.txt"

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

image21.png

Теперь кликнем на Inspections Total, чтоб перейти к просмотру отчёта анализатора:

image23.png

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

Просмотр результатов работы анализатора


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

Предупреждение N1

V773 [CWE-401] The exception was thrown without releasing the 'result' pointer. A memory leak is possible. libopenrct2 ObjectFactory.cpp 443

Object* CreateObjectFromJson(....){  Object* result = nullptr;  ....  result = CreateObject(entry);  ....  if (readContext.WasError())  {    throw std::runtime_error("Object has errors");  }  ....}Object* CreateObject(const rct_object_entry& entry){  Object* result;  switch (entry.GetType())  {    case OBJECT_TYPE_RIDE:      result = new RideObject(entry);      break;    case OBJECT_TYPE_SMALL_SCENERY:      result = new SmallSceneryObject(entry);      break;    case OBJECT_TYPE_LARGE_SCENERY:      result = new LargeSceneryObject(entry);      break;    ....    default:      throw std::runtime_error("Invalid object type");  }  return result;}

Анализатор заметил ошибку, заключающуюся в том, что после динамического выделения памяти в CreateObject, при возникновении исключения память не очищается, соответственно, возникает утечка памяти.

Предупреждение N2

V501 There are identical sub-expressions '(1ULL << WIDX_MONTH_BOX)' to the left and to the right of the '|' operator. libopenrct2ui Cheats.cpp 487

static uint64_t window_cheats_page_enabled_widgets[] = {  MAIN_CHEAT_ENABLED_WIDGETS |  (1ULL << WIDX_NO_MONEY) |  (1ULL << WIDX_ADD_SET_MONEY_GROUP) |  (1ULL << WIDX_MONEY_SPINNER) |  (1ULL << WIDX_MONEY_SPINNER_INCREMENT) |  (1ULL << WIDX_MONEY_SPINNER_DECREMENT) |  (1ULL << WIDX_ADD_MONEY) |  (1ULL << WIDX_SET_MONEY) |  (1ULL << WIDX_CLEAR_LOAN) |  (1ULL << WIDX_DATE_SET) |  (1ULL << WIDX_MONTH_BOX) |  // <=  (1ULL << WIDX_MONTH_UP) |  (1ULL << WIDX_MONTH_DOWN) |  (1ULL << WIDX_YEAR_BOX) |  (1ULL << WIDX_YEAR_UP) |  (1ULL << WIDX_YEAR_DOWN) |  (1ULL << WIDX_DAY_BOX) |  (1ULL << WIDX_DAY_UP) |  (1ULL << WIDX_DAY_DOWN) |  (1ULL << WIDX_MONTH_BOX) |  // <=  (1ULL << WIDX_DATE_GROUP) |  (1ULL << WIDX_DATE_RESET),  ....};

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

Предупреждения N3

V703 It is odd that the 'flags' field in derived class 'RCT12BannerElement' overwrites field in base class 'RCT12TileElementBase'. Check lines: RCT12.h:570, RCT12.h:259. libopenrct2 RCT12.h 570

struct RCT12SpriteBase{  ....  uint8_t flags;  ....};struct rct1_peep : RCT12SpriteBase{  ....  uint8_t flags;  ....};

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

Предупреждение N4

V793 It is odd that the result of the 'imageDirection / 8' statement is a part of the condition. Perhaps, this statement should have been compared with something else. libopenrct2 ObservationTower.cpp 38

void vehicle_visual_observation_tower(...., int32_t imageDirection, ....){  if ((imageDirection / 8) && (imageDirection / 8) != 3)  {    ....  }  ....}

Давайте разберёмся поподробнее. Выражение imageDirection / 8 будет false в том случае, если imageDirection находится в диапазоне от -7 до 7. Вторая часть: (imageDirection / 8) != 3 проверяет imageDirection на нахождение вне диапазона: от -31 до -24 и от 24 до 31 соответственно. Мне кажется довольно странным проверять числа на вхождение в определённый диапазон таким способом и, даже если в данном фрагменте кода нет ошибки, я бы рекомендовал переписать данные условия на более явные. Это существенно упростило бы жизнь людям, которые будут читать и поддерживать этот код.

Предупреждение N5

V587 An odd sequence of assignments of this kind: A = B; B = A;. Check lines: 1115, 1118. libopenrct2ui MouseInput.cpp 1118

void process_mouse_over(....){  ....  switch (window->widgets[widgetId].type)  {    case WWT_VIEWPORT:      ebx = 0;      edi = cursorId;                                 // <=      // Window event WE_UNKNOWN_0E was called here,      // but no windows actually implemented a handler and      // it's not known what it was for      cursorId = edi;                                 // <=      if ((ebx & 0xFF) != 0)      {        set_cursor(cursorId);        return;      }      break;      ....  }  ....}

Данный фрагмент кода, скорее всего, был получен путем декомпиляции. Затем, судя по оставленному комментарию, была удалена часть нерабочего кода. Однако осталась пара операций над cursorId, которые также не несут особого смысла.

Предупреждение N6

V1004 [CWE-476] The 'player' pointer was used unsafely after it was verified against nullptr. Check lines: 2085, 2094. libopenrct2 Network.cpp 2094

void Network::ProcessPlayerList(){  ....  auto* player = GetPlayerByID(pendingPlayer.Id);  if (player == nullptr)  {    // Add new player.    player = AddPlayer("", "");    if (player)                                          // <=    {      *player = pendingPlayer;       if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)       {         _serverConnection->Player = player;       }    }    newPlayers.push_back(player->Id);                    // <=  }  ....}

Данный код поправить довольно просто, нужно или третий раз проверять player на нулевой указатель, либо внести его в тело условного оператора. Я бы предложил второй вариант:

void Network::ProcessPlayerList(){  ....  auto* player = GetPlayerByID(pendingPlayer.Id);  if (player == nullptr)  {    // Add new player.    player = AddPlayer("", "");    if (player)    {      *player = pendingPlayer;      if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)      {        _serverConnection->Player = player;      }      newPlayers.push_back(player->Id);    }  }  ....}

Предупреждение N7

V547 [CWE-570] Expression 'name == nullptr' is always false. libopenrct2 ServerList.cpp 102

std::optional<ServerListEntry> ServerListEntry::FromJson(...){  auto name = json_object_get(server, "name");  .....  if (name == nullptr || version == nullptr)  {    ....  }  else  {    ....    entry.name = (name == nullptr ? "" : json_string_value(name));    ....  }  ....}

Можно одним махом избавиться от трудночитаемой строки кода и решить проблему с проверкой на nullptr. Предлагаю изменить код следующим образом:

std::optional<ServerListEntry> ServerListEntry::FromJson(...){  auto name = json_object_get(server, "name");  .....  if (name == nullptr || version == nullptr)  {    name = ""    ....  }  else  {    ....    entry.name = json_string_value(name);    ....  }  ....}

Предупреждение N8

V1048 [CWE-1164] The 'ColumnHeaderPressedCurrentState' variable was assigned the same value. libopenrct2ui CustomListView.cpp 510

void CustomListView::MouseUp(....){  ....  if (!ColumnHeaderPressedCurrentState)  {    ColumnHeaderPressed = std::nullopt;    ColumnHeaderPressedCurrentState = false;    Invalidate();  }}

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

Вывод


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


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Vladislav Stolyarov. PVS-Studio and Continuous Integration: TeamCity. Analysis of the Open RollerCoaster Tycoon 2 project.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru