Проверка операционной системы Haiku (семейство BeOS) c помощью PVS-Studio. Часть 1



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

    Haiku — свободная операционная система для персональных компьютеров, которая нацелена на двоичную совместимость с операционной системой BeOS. Haiku воплощает в себе основные идеи BeOS. Это модульная система, архитектурно решённая как гибридное ядро: микроядерная архитектура, способная динамически подгружать необходимые модули.

    Проект для проверки был предложен пользователем, знакомым с продуктом PVS-Studio и нашей работе по проверке open-source проектов. После сравнительно недавней проверки Linux Kernel, я догадывался, с какими проблемами мне придётся столкнуться и описал их в ответном письме. Неожиданно мне предложили содействие в сборке операционной системы и интеграции анализатора. Дополнительно на официальном сайте была доступна очень обширная документация и я решил попробовать.

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

    Результаты проверки


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

    Предупреждения #1, #2:


    V501 There are identical sub-expressions to the left and to the right of the '<' operator: lJack->m_jackType < lJack->m_jackType MediaJack.cpp 783
    int __CORTEX_NAMESPACE__ compareTypeAndID(....)
    {
      int retValue = 0;
      ....
      if (lJack && rJack)
      {
        if (lJack->m_jackType < lJack->m_jackType)           //<==
        {
          return -1;
        }
        if (lJack->m_jackType == lJack->m_jackType)          //<==
        {
          if (lJack->m_index < rJack->m_index)
          {
            return -1;
          }
          else
          {
            return 1;
          }
        }
        else if (lJack->m_jackType > rJack->m_jackType)
        {
          retValue = 1;
        }
      }
      return retValue;
    }

    На эту функцию анализатор выдал целых два предупреждения. В обоих случаях хорошо заметна опечатка (когда уже анализатор «тыкнул пальцем», конечно) в именах lJack и rjack.

    V575 The 'strchr' function processes value '2112800'. Inspect the second argument. CommandActuators.cpp 1517
    extern char    *strchr(const char *string, int character);
    
    SendMessageCommandActuator::
    SendMessageCommandActuator(int32 argc, char** argv)
      :
      CommandActuator(argc, argv),
      fSignature((argc > 1) ? argv[1] : "")
    {
      ....
      const char* arg = argv[i];
      BString argString(arg);
      const char* equals = strchr(arg, ' = ');  //<==
      ....
    }

    Функция strchr() возвращает указатель на первое вхождение указанного символа в указанной строке. Символ преобразуется в int, в данном случае ' = ' будет представлен как число 2112800. Скорее всего хотели искать одиночный символ '=', а его код будет 61.

    Если хотели найти подстроку " = ", то явно используется не та функция и её следует заменить на вызов strstr().

    Предупреждения #3, #4:


    V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '-' operator. AbstractLayout.cpp 244
    bool IsVisible(bool ancestorsVisible) const
    {
      int16 showLevel = BView::Private(view).ShowLevel();
      return (showLevel - (ancestorsVisible) ? 0 : 1) <= 0;
    }

    К сожалению, в данном случае взятие в скобочки переменной 'ancestorsVisible' никак не влияет на порядок вычислений в этом выражении. Поэтому первой по приоритету будет выполняться операция вычитания (из int16 вычитается bool), только потом выполняется тернарный оператор '?:'.

    Правильный код:
    bool IsVisible(bool ancestorsVisible) const
    {
      int16 showLevel = BView::Private(view).ShowLevel();
      return (showLevel - (ancestorsVisible ? 0 : 1) ) <= 0;
    }

    V502 Perhaps the '?:' operator works in a different way than it was expected. The '?:' operator has a lower priority than the '&&' operator. fnmatch.c 58
    #define FOLD(c) \
      ((flags & FNM_CASEFOLD) && ISUPPER ((unsigned char) (c)) \
      ? tolower ((unsigned char) (c)) \
      : (c))

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

    Предупреждения #5, #6


    V562 It's odd to compare 0 or 1 with a value of 0. cmp.c 300
    #ifndef same_file
    # define same_file(s, t) \
        ((((s)->st_ino == (t)->st_ino) \
         && ((s)->st_dev == (t)->st_dev)) \
         || same_special_file (s, t))
    #endif
    
    int
    main (int argc, char **argv)
    {
      ....
      if (0 < same_file (&stat_buf[0], &stat_buf[1])           //<==
          && same_file_attributes (&stat_buf[0], &stat_buf[1])
          && file_position (0) == file_position (1))
        return EXIT_SUCCESS;
      ....
    }

    На первый взгляд обычное условие, но «same_file» является макросом, преобразующимся в логическое выражение, как и 'same_file_attributes', в итоге получили странное сравнение «0 < value_of_boolean_type». В языке Си нет типа 'bool'. Выражение справа будет иметь тип 'int', но по своей сути она всегда «истина» или «ложь», поэтому мы и назвали его 'boolean'. И сравнение «0 < boolean_expr» весьма подозрительно.

    Аналогичное использование макроса:
    • V562 It's odd to compare 0 or 1 with a value of 0. cmp.c 313

    V562 It's odd to compare a bool type value with a value of 18: 0x12 == IsProfessionalSpdif(). CEchoGals_mixer.cpp 533
    #define ECHOSTATUS_DSP_DEAD 0x12                          //<==
    
    virtual BOOL IsProfessionalSpdif()                        //<==
    { 
      ....
      return( (....) ? TRUE : FALSE ); 
    }
    
    ECHOSTATUS CEchoGals::ProcessMixerFunction
    (
      PMIXER_FUNCTION  pMixerFunction,
      INT32 &          iRtnDataSz
    )
    {
      ....
      case MXF_GET_PROF_SPDIF :
          if ( ECHOSTATUS_DSP_DEAD == IsProfessionalSpdif() ) //<==
          {
            Status = ECHOSTATUS_DSP_DEAD;        
          }
          else
          {
            pMixerFunction->Data.bProfSpdif = IsProfessionalSpdif();
          }
      ....
    }

    Ещё одно некорректное сравнение с использованием макросов. Функция IsProfessionalSpdif() возвращает TRUE или FALSE, а мы сравниваем возвращаемый результат с числом 0x12.

    Предупреждения #7, #8


    V583 The '?:' operator, regardless of its conditional expression, always returns one and the same value. impactv.c 520
    void Radeon_CalcImpacTVRegisters(....)
    {
      ....
      values->tv_hstart =
        internal_encoder ? 
        values->tv_hdisp + 1 - params->mode888 + 12 :
        values->tv_hdisp + 1 - params->mode888 + 12;
      ....
    }

    Независимо от значения переменной 'internal_encoder', тернарный оператор возвращает одинаковые значения. Необходимо проверить это место на наличие опечаток.

    V523 The 'then' statement is equivalent to the 'else' statement. mkntfs.c 1132
    static int insert_positioned_attr_in_mft_record(....)
    {
      ....
      if (flags & ATTR_COMPRESSION_MASK) {
        hdr_size = 72;
        /* FIXME: This compression stuff is all wrong. .... */
        /* now. (AIA) */
        if (val_len)
          mpa_size = 0; /* get_size_for_compressed_....; */
        else
          mpa_size = 0;
      } else {
      ....
      }
      ....
    }

    Анализатор напоминает, что необходимо «пофиксить» отложенные места.

    Ещё такое место:
    • V523 The 'then' statement is equivalent to the 'else' statement. mkntfs.c 1334

    Предупреждения #9, #10


    V503 This is a nonsensical comparison: pointer <= 0. Header.cpp 900
    extern
    char *strstr(const char *string, const char *searchString);
    
    void
    TTextControl::MessageReceived(BMessage *msg)
    {
      ....
      while (node.GetNextAttrName(buffer) == B_OK) {
        if (strstr(buffer, "email") <= 0)
          continue;
      ....
    }

    Функция strstr() возвращает указатель на первое вхождение строки «email» в строке 'buffer', если такого соответствия не найдено, то возвращается NULL. Следовательно, в данном случае необходимо с NULL и сравнивать.

    Возможное решение:
    void
    TTextControl::MessageReceived(BMessage *msg)
    {
      ....
      while (node.GetNextAttrName(buffer) == B_OK) {
        if (strstr(buffer, "email") == NULL)
          continue;
      ....
    }

    V512 A call of the 'memcmp' function will lead to underflow of the buffer '«Private-key-format: v»'. dst_api.c 858
    dst_s_read_private_key_file(....)
    {
      ....
      if (memcmp(in_buff, "Private-key-format: v", 20) != 0)
        goto fail;
      ....
    }

    Длина сравниваемой строки не совпадает с указанным количеством сравниваемых символов. В строке «Private-key-format: v» 21 символ.

    Предупреждения #11, #12


    V547 Expression '* r && * r == ' ' && * r == '\t'' is always false. Probably the '||' operator should be used here. selection.c 546
    static int
    selection_rel(....)
    {
      char *r, *rname;
      ....
      while (*r && *r == ' ' && *r == '\t')
        r++;
      ....
    }

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

    Возможный корректный вариант:
    static int
    selection_rel(....)
    {
      char *r, *rname;
      ....
      while (*r == ' ' || *r == '\t')
        r++;
      ....
    }

    V590 Consider inspecting the 'path[i] == '/' && path[i] != '\0'' expression. The expression is excessive or contains a misprint. storage_support.cpp 309
    status_t
    parse_first_path_component(const char *path, int32& length,
                   int32& nextComponent)
    {
      ....
      for (; path[i] == '/' && path[i] != '\0'; i++);  //<==
      if (path[i] == '\0')  // this covers "" as well
        nextComponent = 0;
      else
        nextComponent = i;
      ....
    }

    Здесь всё хорошо, но одна проверка является лишней и её стоит удалить. Для сохранения логики работы, достаточно оставить: «for (; path[i] == '/'; i++);».

    Похожие места:
    • V590 Consider inspecting this expression. The expression is excessive or contains a misprint. PoseView.cpp 5773
    • V590 Consider inspecting this expression. The expression is excessive or contains a misprint. Tracker.cpp 1728
    • V590 Consider inspecting the '* ptr == ';' && * ptr != '\0'' expression. The expression is excessive or contains a misprint. pc.c 316

    Предупреждение #13, #14


    V547 Expression is always true. Probably the '&&' operator should be used here. StatusView.cpp 1397
    void
    TDragRegion::Draw(BRect)
    {
      ....
      if (fDragLocation != kDontDrawDragRegion ||
          fDragLocation != kNoDragRegion)
        DrawDragRegion();
    }

    В этой функции что-то всегда отрисовывается. Если построить таблицу истинности логического выражения в условии, можно убедиться, что оно всегда истинно. Возможно, тут должен быть оператор '&&'.

    V547 Expression 'reservedBase < 0' is always false. Unsigned type value is never < 0. agp_gart.cpp 1172
    /* address types */
    typedef  unsigned long int  __haiku_addr_t;   //<==
    typedef __haiku_addr_t    addr_t;             //<==
    
    static status_t
    bind_aperture(...., addr_t reservedBase, ....)
    {
      ....
      if (status < B_OK) {
        if (reservedBase < 0)                     //<==
          aperture->DeleteMemory(memory);
    
        return status;
      }
      ....
    }

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

    Предупреждения #15, #16


    V713 The pointer lp was utilized in the logical expression before it was verified against nullptr in the same logical expression. util.c 311
    char *
    bittok2str(register const struct tok *lp, ....)
    {
      ....
      while (lp->s != NULL && lp != NULL) {
        ....
      }
      ....
    }

    В условии цикла перепутана последовательность проверки указателя. В начале он разыменовывается, а уже потом проверяется на равенство нулю. Правильный вариант:
    while (lp != NULL && lp->s != NULL) {

    V593 Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. VideoProducer.cpp 766
    int32
    VideoProducer::_FrameGeneratorThread()
    {
      ....
      err = B_OK;
      // Send the buffer on down to the consumer
      if (wasCached || (err = SendBuffer(buffer, fOutput.source,
          fOutput.destination) != B_OK)) {
            ....
          }
      ....
    }

    Анализатор обнаружил потенциальную ошибку в выражении, которое, скорее всего, работает не так, как задумывал программист. Планировалось выполнить присваивание «err = SendBuffer()», а результат сравнить с константой 'B_OK', но приоритет оператора '!=' выше, чем у '=', поэтому в переменную 'err' записывается результат логической операции.

    Похожие места:
    • V593 Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. if_age.c 590
    • V593 Consider reviewing the expression of the 'A = B != C' kind. The expression is calculated as following: 'A = (B != C)'. if_alc.c 954
    • V593 Consider reviewing the expression of the 'A = B >= C' kind. The expression is calculated as following: 'A = (B >= C)'. RAW.cpp 2601

    Предупреждения #17, #18


    V547 Expression 'nogscale >= 0' is always true. Unsigned type value is always >= 0. tvp3026.c 212
    status_t mil2_dac_init (void)
    {
      uint32   rfhcnt, nogscale, memconfig;
      ....
      for (nogscale = 1; nogscale >= 0; nogscale--) {           //<==
        int max = -1 + 33.2 * mclk / (nogscale? 1: 4);
        for (rfhcnt = 15; rfhcnt > 0; rfhcnt--) {
          int value = (rfhcnt & 0x0e) * 256 + (rfhcnt & 0x01) * 64;
          LOG(2,("mil2_dac_init factor %d, rfhcnt %2d: %d ?<= %d\n",
            nogscale, rfhcnt, value, max));
          if (value <= max) goto rfhcnt_found;
        }
      }
      ....
    }

    Скорее всего из-за оператора перехода 'goto' никогда не замечали, что один из циклов «вечный», т.к. при проверке «nogscale >= 0» декремент беззнаковой переменной можно делать бесконечно.

    V621 Consider inspecting the 'for' operator. It's possible that the loop will be executed incorrectly or won't be executed at all. if_ae.c 1670
    #define  AE_IDLE_TIMEOUT 100
    
    static void
    ae_stop_rxmac(ae_softc_t *sc)
    {
      ....
      /*
       * Wait for IDLE state.
       */
      for (i = 0; i < AE_IDLE_TIMEOUT; i--) {
        val = AE_READ_4(sc, AE_IDLE_REG);
        if ((val & (AE_IDLE_RXMAC | AE_IDLE_DMAWRITE)) == 0)
          break;
        DELAY(100);
      }
      ....
    }

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

    Похожее место:
    • V621 Consider inspecting the 'for' operator. It's possible that the loop will be executed incorrectly or won't be executed at all. if_ae.c 1706

    Предупреждение #19, #20


    V646 Consider inspecting the application's logic. It's possible that 'else' keyword is missing. Filter.cpp 760
    uchar
    Scaler::Limit(intType value)
    {
      if (value < 0) {
        value = 0;
      } if (value > 255) {
        value = 255;
      }
      return value;
    }

    В этой функции нет серьёзной ошибки, но код плохо оформлен. Необходимо добавить ключевое слово 'else', либо разместить условия на одном уровне.

    V640 The code's operational logic does not correspond with its formatting. The second statement will always be executed. It is possible that curly brackets are missing. strftime.c 1263
    #define DO_NUMBER(d, v) \
        digits = width == -1 ? d : width; \
        number_value = v; goto do_number
    
    size_t
    my_strftime (s, maxsize, format, tp extra_args)
    {
      ....
      if (modifier == L_('O'))
        goto bad_format;
      else
        DO_NUMBER (1, tp->tm_year + TM_YEAR_BASE);
      ....
    }

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

    Аналогично неправильно макрос используется здесь:
    • V640 The code's operational logic does not correspond with its formatting. The second statement will always be executed. It is possible that curly brackets are missing. strftime.c 1267

    Заключение


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

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

    Эта статья на английском


    Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Svyatoslav Razmyslov. Analysis of Haiku Operating System (BeOS Family) by PVS-Studio. Part 1.

    Прочитали статью и есть вопрос?
    Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio и CppCat, версия 2015. Пожалуйста, ознакомьтесь со списком.
    PVS-Studio
    Static Code Analysis for C, C++, C# and Java

    Комментарии 10

      +1
      Хайку еще жива?
        0
        С солидным опозданием, но бета должна выйти весной-летом. Ритм у них упал, разработчики по контракту поразбегались, но надежда есть.
          0
          Обратный отсчет до вероятной бэты можно наблюдать здесь dev.haiku-os.org/milestone/R1/beta1 (44 открытых тикета осталось, если новые не добавят). А пока лучше ставить последние nightly-сборки, в которых есть все актуальные плюшки вроде пакетного менеджера (и баги, вестимо :))
        0
        Макросы и так доставляют неудобства при отладке, так ещё и являются источником вот таких ошибок: макрос 'DO_NUMBER' раскрывается в несколько строк, но только первая их них будет частью условного оператора, последующие же операторы будут выполняться независимо от условия.

        Конкретно в этом коде else вообще не нужен, потому что перед этим был переход на обработку исключительной ситуации. Так что по факту код работает, как задумывалось :-)
          +2
          Так-то да, но это скорее исключение/везение. Любой новый разработчик, который присоединится к проекту, может по-разному воспользоваться этим макросом, считая, что он задан по правилам: один макрос — один блок кода.
            +2
            Это мне напомнило адский код в Eclipse SDK — MarkerSet#shareStrings (Java):

            protected IMarkerSetElement[] elements;
            
            public void shareStrings(StringPool set) {
                //copy elements for thread safety
                Object[] array = elements; // Тип массива заменили на более общий, но виртуальную машину не обманешь
                if (array == null)
                    return;
                for (int i = 0; i < array.length; i++) {
                    Object o = array[i];
                    if (o instanceof String) // Это сравнение заведомо ложно: в исходном массиве строк быть не могло
                        array[i] = set.add((String) o); // add возвращает String — пытаемся записать строку 
                                                        // в массив IMarkerSetElement[], гарантированный ArrayStoreException в рантайме
                    if (o instanceof IStringPoolParticipant)
                        ((IStringPoolParticipant) o).shareStrings(set);
                }
            }

            По факту код работает стабильно: строчка, которая падает с исключением при выполнении, никогда не выполняется из-за заведомо невыполнимого условия перед этим. Когда я вижу такое, начинаю крепко задумываться: писать багрепорт или не беспокоить людей по пустякам :-)
              +2
              Пишите. Все такие пустяки скапливаются и накладываются друг на друга.
              0
              Странно, что код не обёрнут в do { } while(0), как обычно делают для многострочных макросов. Как раз от таких ошибок спасает.

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

                Самое читаемое