25 августа 2021 года ядро ​​Linux отметило свое 30-летие. С тех пор он сильно изменился. Мы тоже изменились. В настоящее время ядро ​​Linux — это огромный проект, которым пользуются миллионы. Мы проверяли ядро ​​5 лет назад. Итак, мы не можем пропустить это событие и хотим еще раз взглянуть на код этого эпического проекта.

Введение

В прошлый раз мы нашли 7 своеобразных ошибок. Примечательно, что на этот раз мы нашли меньше ошибок!

Это кажется странным. Размер ядра увеличился. В анализаторе PVS-Studio появились десятки новых диагностических правил. Мы улучшили внутренние механизмы и анализ потока данных. Кроме того, мы ввели межмодульный анализ и многое другое. Почему PVS-Studio обнаружил меньше интересных ошибок?

Ответ прост. Качество проекта улучшилось! Вот почему мы так рады поздравить Linux с 30-летием.

Инфраструктура проекта была значительно улучшена. Теперь вы можете собрать ядро ​​с помощью GCC и Clang — дополнительные патчи не требуются. Разработчики совершенствуют автоматизированные системы проверки кода (kbuild test robot) и другие инструменты статического анализа (внедрен GCC-fanalyzer, доработан анализатор Coccinelle, проверка проекта осуществляется через Clang Static Analyzer).

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

Во-первых, давайте обсудим, как запустить анализатор.

Запуск анализатора

Поскольку теперь для компиляции ядра можно использовать компилятор Clang, в проекте реализована специальная инфраструктура. Он включает генератор compile_commands.json, который создает файл базы данных компиляции JSON из файлов .cmd, созданных во время сборки. Итак, вам нужно скомпилировать ядро, чтобы создать файл. Вам не обязательно использовать компилятор Clang, но лучше скомпилировать ядро ​​с помощью Clang, потому что GCC может иметь несовместимые флаги.

Вот как вы можете сгенерировать полный файл compile_commands.json для проверки проекта:

make -j$(nproc) allmodconfig # full config
make -j$(nproc)              # compile
./scripts/clang-tools/gen_compile_commands.py

Затем вы можете запустить анализатор:

pvs-studio-analyzer analyze -f compile_commands.json -a 'GA;OP' -j$(nproc) \
                            -e drivers/gpu/drm/nouveau/nouveau_bo5039.c \
                            -e drivers/gpu/drm/nouveau/nv04_fbcon.c

Зачем исключать эти 2 файла из анализа? Они содержат большое количество макросов, которые разрастаются в огромные строки кода (до 50 тысяч символов в строке). Анализатор долго их обрабатывает, и анализ может дать сбой.

В недавнем релизе PVS-Studio 7.14 реализован межмодульный анализ для C/C++ проектов. Мы не могли упустить возможность попробовать его. Тем более, на такой огромной кодовой базе:

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

Для проверки проекта в межмодульном режиме необходимо добавить один флаг в команду pvs-studio-analyzer:

pvs-studio-analyzer analyze -f compile_commands.json -a 'GA;OP' -j$(nproc) \
                            --intermodular \
                            -e drivers/gpu/drm/nouveau/nouveau_bo5039.c \
                            -e drivers/gpu/drm/nouveau/nv04_fbcon.c

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

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

Разыменование указателя перед проверкой

V595 Указатель speakup_console[vc-›vc_num] использовался до того, как он был проверен на соответствие nullptr. Контрольные строки: 1804, 1822. main.c 1804

static void do_handle_spec(struct vc_data *vc, u_char value, char up_flag)
{
  unsigned long flags;
  int on_off = 2;
  char *label;
  if (!synth || up_flag || spk_killed) 
    return;
  ....
  switch (value) {
  ....
  case KVAL(K_HOLD):
    label = spk_msg_get(MSG_KEYNAME_SCROLLLOCK);
    on_off = vt_get_leds(fg_console, VC_SCROLLOCK);
    if (speakup_console[vc->vc_num])                     // <=
      speakup_console[vc->vc_num]->tty_stopped = on_off;
    break;
  ....
  }
  ....
}

Анализатор выдает предупреждение, так как указатель speakup_console[vc-›vc_num] был разыменован перед проверкой. Глядя на код, вы можете подумать, что это ложное срабатывание. На самом деле у нас есть разыменование здесь.

Угадай где? :) Разыменование происходит в макросе spk_killed. Да уж, переменная здесь ни при чем, как может показаться на первый взгляд:

#define spk_killed (speakup_console[vc->vc_num]->shut_up & 0x40)

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

Опечатка в маске

V519 Переменной данные дважды подряд присваиваются значения. Возможно, это ошибка. Контрольные строки: 6208, 6209. cik.c 6209

static void cik_enable_uvd_mgcg(struct radeon_device *rdev,
        bool enable)
{
  u32 orig, data;
  if (enable && (rdev->cg_flags & RADEON_CG_SUPPORT_UVD_MGCG)) {
    data = RREG32_UVD_CTX(UVD_CGC_MEM_CTRL);
    data = 0xfff;                              // <=
    WREG32_UVD_CTX(UVD_CGC_MEM_CTRL, data);
    orig = data = RREG32(UVD_CGC_CTRL);
    data |= DCM;
    if (orig != data)
      WREG32(UVD_CGC_CTRL, data);
  } else {
    data = RREG32_UVD_CTX(UVD_CGC_MEM_CTRL);
    data &= ~0xfff;                            // <=
    WREG32_UVD_CTX(UVD_CGC_MEM_CTRL, data);
    orig = data = RREG32(UVD_CGC_CTRL);
    data &= ~DCM;
    if (orig != data)
      WREG32(UVD_CGC_CTRL, data);
  }
}

Пример взят из кода драйвера для видеокарт Radeon. Поскольку значение 0xfff используется в ветке else, можно предположить, что это битовая маска. При этом в ветке then значение, полученное в строке выше, перезаписывается без применения маски. Правильный код, вероятно, будет следующим:

data = RREG32_UVD_CTX(UVD_CGC_MEM_CTRL);
data &= 0xfff; 
WREG32_UVD_CTX(UVD_CGC_MEM_CTRL, data);

Ошибка в выборе типов

V610 Неопределенное поведение. Проверьте оператор сдвига ‘››=’. Правый операнд ("bitpos % 64" = [0..63]) больше или равен длине в битах расширенного левого операнда. мастер.с 354

// bitsperlong.h
#ifdef CONFIG_64BIT
#define BITS_PER_LONG 64
#else
#define BITS_PER_LONG 32
#endif /* CONFIG_64BIT */
// bits.h
/*
 * Create a contiguous bitmask starting at bit position @l and ending at
 * position @h. For example
 * GENMASK_ULL(39, 21) gives us the 64bit vector 0x000000ffffe00000.
 */
#define __GENMASK(h, l) ....
// master.h
#define I2C_MAX_ADDR      GENMASK(6, 0)
// master.c
static enum i3c_addr_slot_status
i3c_bus_get_addr_slot_status(struct i3c_bus *bus, u16 addr)
{
  int status, bitpos = addr * 2;                   // <=
  if (addr > I2C_MAX_ADDR)
    return I3C_ADDR_SLOT_RSVD;
  status = bus->addrslots[bitpos / BITS_PER_LONG];
  status >>= bitpos % BITS_PER_LONG;               // <=
  return status & I3C_ADDR_SLOT_STATUS_MASK;
}

Обратите внимание, что макрос BITS_PER_LONG может быть 64-разрядным.

Код содержит неопределенное поведение:

  • после выполнения проверки переменная addr может находиться в диапазоне [0..127]
  • если формальный параметр addr ›= 16, то переменная status сдвигается вправо на количество битов больше, чем содержит тип int (32 бита).

Возможно, автор хотел уменьшить количество строк и объявил переменную bitpos рядом с переменной status. Однако программист не учел, что int имеет 32-битный размер на 64-битных платформах, в отличие от типа long.

Чтобы исправить это, объявите переменную status с типом long.

Разыменование нулевого указателя после проверки

V522 Может иметь место разыменование нулевого указателя элемент. mlxreg-hotplug.c 294

static void
mlxreg_hotplug_work_helper(struct mlxreg_hotplug_priv_data *priv,
         struct mlxreg_core_item *item)
{
  struct mlxreg_core_data *data;
  unsigned long asserted;
  u32 regval, bit;
  int ret;
  /*
   * Validate if item related to received signal type is valid.
   * It should never happen, excepted the situation when some
   * piece of hardware is broken. In such situation just produce
   * error message and return. Caller must continue to handle the
   * signals from other devices if any.
   */
  if (unlikely(!item)) {
    dev_err(priv->dev, "False signal: at offset:mask 0x%02x:0x%02x.\n",
      item->reg, item->mask);
    return;
  }
  // ....
}

Здесь у нас классическая ошибка: если указатель равен нулю, мы получаем сообщение об ошибке. Однако этот же указатель используется при формировании сообщения. Конечно, такого рода ошибки легко обнаружить на этапе тестирования. Но этот случай немного другой — судя по комментарию, разыменование может произойти, если «железка сломалась». В любом случае это плохой код, и его нужно исправлять.

Вывод

Проверка проекта Linux была для нас захватывающим испытанием. Нам удалось опробовать новую функцию PVS-Studio — межмодульный анализ. Ядро Linux — это великий всемирно известный проект. Многие люди и организации борются за его качество. Мы рады видеть, что разработчики продолжают улучшать качество ядра. И мы тоже развиваем наш анализатор! Недавно мы открыли папку с изображениями. Он показал, как началась дружба нашего анализатора с Таксом. Вы только взгляните на эти фотографии!

Единорог N81:

Единорог N57:

Альтернативный единорог с пингвином N1:

Спасибо за ваше время! Попробуйте проверить свой проект с помощью PVS-Studio. Так как ядру Linux исполняется 30 лет, вот промокод на месяц: #linux30.