Операционные системы – это тот вид софта, для которого качество исходного кода особенно критично. В этот раз под объективы анализатора PVS-Studio попала MuditaOS. Так давайте же посмотрим, что нам удалось найти в данной операционной системе с открытым исходным кодом. MuditaOS – это операционная система, которая основана на FreeRTOS (статья о проверке). Данный продукт работает на устройствах Mudita, среди которых есть телефон, будильники и часы. Исходный код программы написан на языках С и С++. Ну что, давайте попробуем проверить этот проект? Насколько будильники окажутся хороши? :) Сборка проекта происходила под Ubuntu 20.04 по инструкции из официального репозитория. Проверялась отладочная версия для будильника Mudita Bell. По информации на конец 2021 года, он стоит 60$ и выглядит так: Так как проект регулярно обновляется, для проверки я зафиксировал его версией 8cc1f77. Перед тем как перейти к ошибкам, я хочу рассказать один забавный случай. Недавно мне довелось выступать с лекцией в ТулГУ про неопределённое поведение. Там, в слайде о себе, я представился так: Тут нужно немного пояснить: в анализаторе PVS-Studio одно из промежуточных представлений кода – абстрактное синтаксическое дерево. В нём разные языковые конструкции реализованы в виде узлов и имеют иерархию наследования. Преобразования между узлами как раз происходят при помощи кастов. В начале работы в PVS-Studio я несколько раз валил анализатор (при пробных запусках), полагаясь на то, что я без сомнений знаю точный тип узла, к которому преобразую узел базового типа. Сейчас я докажу вам, что разработчики MuditaOS тоже не очень любят проверять результат операции по преобразованию типов. Давайте посмотрим предупреждения анализатора: V595 [CERT-EXP12-C] The 'result' pointer was utilized before it was verified against nullptr. Check lines: 81, 82. AudioModel.cpp 81 Взглянем на фрагмент кода. Тут происходит приведение типа при помощи dynamic_cast. Потом результат этой операции — потенциально нулевой указатель — разыменовывается, а уже позже проверяется на nullptr. Поправить такой код очень просто, нужно всего лишь сделать проверку указателя result перед его использованием. Также удалось обнаружить пару более интересных случаев: V757 [CERT-EXP12-C] It is possible that an incorrect variable is compared with nullptr after type conversion using 'dynamic_cast'. Check lines: 214, 214. CallLogDetailsWindow.cpp 214 Здесь мы видим ситуацию, когда указатель на базовый класс приводится к указателю на производный при помощи dynamic_cast. Затем приводящийся указатель проверяется на nullptr, хотя, скорее всего, предполагалось проверить на nullptr результат приведения. В случае подобной опечатки код можно поправить таким образом: Возможно, такая правка придётся не всем по душе, но мы считаем этот вариант лаконичным – инициализация указателя и его проверка происходят в одно действие — и поэтому используем его везде. Примечание. Это отличается от случая, когда в условии происходит присваивание уже существующей переменной. Вот такой код плох: Непонятно, хотели написать сравнение, но опечатались, или действительно хотели выполнить присваивание с одновременной проверкой. Большинство компиляторов и анализаторов ругаются на подобный код и совершенно правы. Код опасен и непонятен. Другое дело, когда создаётся новая переменная, как в рассмотренном примере. В нём явно хотели создать новую переменную и инициализировать её определённым значением. А написать == здесь не получится даже при желании. Вернёмся к коду проекта. Вот ещё один похожий случай: V757 [CERT-EXP12-C] It is possible that an incorrect variable is compared with nullptr after type conversion using 'dynamic_cast'. Check lines: 47, 47. PhoneNameWindow.cpp 47 Корректный вариант будет выглядеть так: Также хочется обратить внимание на то, что упрощение подобных проверок – один из наших советов по рефакторингу кода из этого видео. Если кто-то ещё не видел – советую посмотреть. Оно короткое и познавательное :) V522 [CERT-EXP34-C] Dereferencing of the null pointer 'document' might take place. TextBlockCursor.cpp 332 Этот фрагмент кода заслуживает отдельного фейспалма. Давайте разберёмся, что тут происходит: мы видим явную проверку указателя document на nullptr и его разыменование в обоих ветках тернарного оператора. Данный код корректен только в том случае, если целью программиста было уронить программу. V517 [CERT-MSC01-C] The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. Check lines: 1053, 1056. avdtp_util.c 1053 Здесь мы видим классический пример копипасты. Вариантов всего два: либо на месте второго ветвления должна быть другая проверка, либо вторая проверка просто избыточна и не нужна. Судя по тому, что в двух ветках содержится разная логика, то имеет место всё же первый вариант. В любом случае разработчикам MuditaOS стоит обратить внимание на этот фрагмент кода. V571 Recurring check. The 'if (activeInput)' condition was already verified in line 249. ServiceAudio.cpp 250 V547 Expression 'activeInput' is always true. ServiceAudio.cpp 250 Давайте проведём мини-расследование. Тип activeInput - std::optional от указателя на AudioMux::Input. Если посмотреть на код под вложенным if, то видно, что у него вызывается функция-член value. Она гарантированно вернёт нам указатель без броска исключения. После этого результат разыменовывается. Вот только* *функция может вернуть как валидный, так и нулевой указатель, который мы, наверное, и хотели проверить во вложенном if. Ммм, я тоже люблю оборачивать указатели и булевые значения в std::optional! И потом наступать на грабли :) Исправленный вариант кода: V668 [CERT-MEM52-CPP] There is no sense in testing the 'pcBuffer' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. syscalls_stdio.cpp 384 Здесь анализатор обнаружил ситуацию, когда значение указателя, возвращаемого оператором new (который, насколько я могу судить по коду проекта, не перегружен), сравнивается с NULL. Однако если оператор new не смог выделить память, то, согласно стандарту языка, сгенерируется исключение std::bad_alloc(). Таким образом, проверять указатель на равенство нулю не имеет смысла. Особенно странно писать такое в коде операционной системы, которая работает в режиме реального времени. Скорее всего, в ситуации, когда невозможно выделить память, будет происходить завершение работы программы, и дальнейший код будет просто недостижим. Проверка может иметь место при использовании nothrow перегрузки new: Анализатор обнаружил ещё несколько подобных случаев: V668 [CERT-MEM52-CPP] There is no sense in testing the 'fontData' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. FontManager.cpp 56 V668 [CERT-MEM52-CPP] There is no sense in testing the 'data' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. ImageManager.cpp 85 V668 [CERT-MEM52-CPP] There is no sense in testing the 'data' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. ImageManager.cpp 131 V509 [CERT-DCL57-CPP] The noexcept function '=' calls function 'setName' which can potentially throw an exception. Consider wrapping it in a try..catch block. Device.cpp 48 Здесь анализатор обнаружил ситуацию, когда из функции, помеченной как noexcept, вызывается функция, бросающая исключение. В случае покидания исключения из тела nothrow-функции вызовется std::terminate, и программа аварийно завершится. Возможно, стоит подумать о том, чтобы обернуть функцию setName в function-try блок и обработать там возникшую исключительную ситуацию или заменить генерацию исключения на что-то ещё. Анализатор обнаружил очень много фрагментов кода, в которых происходят бессмысленные проверки. Давайте разберём парочку из них, а остальные оставим на откуп разработчикам: V547 Expression 'snoozeCount == 0' is always true. NotificationProvider.cpp 117 Как видно по коду, переменная snoozeCount имеет беззнаковый тип, а значит, она не может быть меньше нуля. Соответственно, вторая проверка лишняя. Код станет более лаконичным, если поменять else if на безусловный else: Также анализатор выдал предупреждение на данный фрагмент кода: V547 Expression 'currentState == ButtonState::Off' is always true. ButtonOnOff.cpp 33 Данное срабатывание интересно тем, что в обычной ситуации разработчики его могли бы просто подавить. Давайте разберёмся, что тут происходит: у нас есть enum с нижележащим типом bool и двумя состояниями, которые мы проверяем. Все мы знаем, что перечисления имеют свойство расширяться новыми значениями. Возможно, в скором времени состояний могло бы стать больше двух, и тогда анализатор бы перестал ругаться на данный фрагмент кода. Однако хочу обратить внимание на то, что это состояния кнопки. Она может быть прожата или нет, и вряд ли в скором времени авторы сделают кнопку Шрёдингера, добавив третье состояние. Данный код можно поправить так же, поменяв else if на безусловный else. Ещё несколько V547, на которые стоит обратить внимание: V547 Expression 'status != 0x00' is always false. AVRCP.cpp 68 V547 Expression 'stream_endpoint->close_stream == 1' is always false. avdtp.c 1223 V547 Expression 'stream_endpoint->abort_stream == 1' is always false. avdtp.c 1256 V547 Expression 'what == info_type::start_sector' is always true. disk_manager.cpp 340 V609 [CERT-EXP37-C] Divide by zero. The 'qfilter_CalculateCoeffs' function processes value '0'. Inspect the third argument. Check lines: 'Equalizer.cpp:26', 'unittest_equalizer.cpp:91'. Equalizer.cpp 26 Да, это срабатывание на юнит-тест. Однако, как мне кажется, оно интересно и показательно. Это очень странная операция, выявленная нашим межмодульным анализом. Кстати, межмодульный анализ – большая новая фича PVS-Studio. Почитать про него подробнее можно в данной статье. Вернёмся к срабатыванию. Здесь программист, который писал тест, скорее всего, не смотрел внутрь функции qfilter_CalculateCoeffs. Ведь результатом деления на 0 будет: неопределённое поведение для целых чисел, после которого можно уже ничего не тестировать, так как может произойти что угодно; значение ±Inf для вещественных чисел, если тип поддерживает арифметику с плавающей запятой по стандарту IEC 559 / IEEE 754, в ином случае это также неопределённое поведение. У нас число с плавающей точкой, поэтому в результате деления на 0, скорее всего, получится бесконечность. Как мне кажется, автора кода не порадовал бы этот результат. Подробнее данную тему можно изучить тут. В итоге мы видим явно опасный код, который мешает корректно тестировать продукт. V617 Consider inspecting the condition. The 'purefs::fs::inotify_flags::close_write' argument of the '|' bitwise operation contains a non-zero value. InotifyHandler.cpp 76 V617 Consider inspecting the condition. The 'purefs::fs::inotify_flags::del' argument of the '|' bitwise operation contains a non-zero value. InotifyHandler.cpp 79 Ситуация похожа на классический паттерн, когда программист хочет проверить, что в inotify->flags выставлен один из флагов. В первом случае это close_write или move_dst, во втором — del или move_src соответственно. Давайте подумаем, как это можно сделать. Для этого сначала нужно объединить константы при помощи операции |, как и сделал разработчик, а потом проверить, что один из них выставлен в flags при помощи операции &. Данный фрагмент кода странный и вряд ли корректный. Ведь второй операнд у оператора && всегда истинен. Скорее всего, разработчик перепутал логическое && и побитовое &. Корректный вариант кода: В данной статье описана только часть GA предупреждений, найденных на этом проекте, на деле их больше. Также отмечу, что это не конец опуса по исследованию исходного кода данной операционной системы при помощи анализатора PVS-Studio. Будет как минимум ещё одна статья, в которой мы продолжим искать ответ на вопрос: "А зазвонит ли ваш будильник?". Также мы рекомендуем разработчикам MuditaOS самостоятельно запустить анализатор PVS-Studio на своём проекте и изучить проблемные места. Это бесплатно для проектов с открытым исходным кодом. Разработчик