Боремся с хедкрабами в исходниках Source SDK
GameDev тернист и неисповедим. Как и любой проект, он проходит испытания кровью и потом, сражаясь с тварями, тьмой порождёнными: барнаклы, пиявки, муравьиные львы. И это ещё не сказочка, это только смазочка. Больше всего стоит остерегаться жуков хедкрабов. Да, это те самые баги в коде. Если их вовремя не убить монтировкой, то ваша участь — стать отвратительным кадавром. Предлагаем читателю побывать в роли Гордона Фримена, погрузиться в недры Source SDK и побороться с хедкрабами. В качестве монтировки же выступит PVS-Studio.
О проекте
Перед тем как пересечь межпространственные врата, наш дорогой Гордон, изучим небольшую справку о мире, который предстоит посетить.
Source SDK — это необходимые зависимости для модов, позволяющие создавать собственный контент для игр на движке Source Engine, разработанный корпорацией Valve. При помощи этого инструментария можно создавать кастомные карты, моды, изменять выражение лица NPC или моделей персонажей.
Мы поистине рады тому, что разработчики исправили ошибки с нашей прошлой проверки. Если интересно, с ней можно ознакомится в этой статье. Но проект Source SDK не стоит на месте, растёт и развивается, как и PVS-Studio, с помощью которого была проведена проверка исходного кода.
Думаю, важно сказать, что повторная проверка приурочена к обновлению движка, в котором было добавлено немало изменений: прирост производительности игры благодаря возможности использования более чем 4 Гб оперативной памяти, корректное масштабирование элементов VGUI на более высоких разрешениях, таких как 4K. Но с последним крупным обновлением в Source SDK был добавлен код Team Fortress 2. Это позволит моддерам изменять TF2 под свои нужды, начиная от небольших предметов и заканчивая созданием новых игр на основе TF2.
Проект собирался из исходного кода по следующей инструкции. Хочется отметить, что разработчики постарались сделать сборку максимально простой и понятной. Коммит, на котором был собран проект для проверки — 68c8b82.
В статью вошли предупреждения только уровня High (есть ещё Medium и Low).
Результаты проверки
Ошибки при работе с массивами
Выход за границу массива
Предупреждение PVS-Studio: V557 Array overrun is possible. The '3' index is pointing beyond array bound. dt_recv.cpp 369, 373
Поле `m_Value` типа `DVariant` содержит внутри себя массив `m_Vector` с фиксированным размером из 3 элементов. В тот момент, когда в функции `RecvProxy_QuaternionToQuaternion` пытаются получить данные у 4-го элемента массива, поведение будет не определено.
Проблема найдена, можем расходиться. Однако это ещё не всё. На самом деле декларация поля `m_Vector` выглядит несколько хитрее:
Судя из комментария, когда-то размер вектора хотели увеличить до 4, при этом изменив весь сопровождающий код. Спустя какое-то время разработчики осознали, что это сломает обратную совместимость с модами, и откатили правки. Но, как оказалось, не во всех местах.
Неверное освобождение памяти массива
Предупреждение PVS-Studio: V611 The memory was allocated using the 'operator new[]' but was released using the 'operator delete'. The 'delete[] m_pointsInNormalizedBox;' statement should be used instead. Check lines: 164, 561. camomaterialproxy.cpp 164
В конструкторе класса `CCamoMaterialProxy` динамическая память выделяется при помощи выражения `new[]` и затем удаляется в деструкторе при помощи выражения `delete`. По стандарту поведение последней операции будет не определено. Для более подробной информации о том, почему массивы необходимо удалять через `delete[]`, лучше ознакомиться с нашей статьёй.
Исправленный код:
Использование неинициализированного массива
Анализатор обнаружил использование неинициализированной переменной `source`, что может привести к непредсказуемым результатам. Пройдёмся по коду и посмотрим, что происходит. Объявляется массив `source` типа `char[1024]` без инициализации, из-за этого в ячейках массива будут хранится произвольные данные. Далее `source` передаётся в функцию `LoadCmdLineFromFile`, а та в свою очередь передаёт его в функцию `FindKey`. И уже здесь неинициализированный буфер будет прочтён, и поведение такой операции не определено.
Невероятно, но факт
Same, same but different
Предупреждение анализатора PVS-Studio: V762 It is possible a virtual function was overridden incorrectly. See qualifiers of function 'CanHolster' in derived class 'C_WeaponAR2' and base class 'C_BaseCombatWeapon'. weapon_ar2.h 48
Из текста предупреждения заметно, что проблема в ошибочном переопределении виртуальной функции. Но что могло пойти не так?
Прежде чем разобраться в проблеме, немного вникнем в код. Есть базовый класс `CBaseCombatWeapon`, у которого есть виртуальная функция `CanHolster`. Также у нас есть дочерний класс `CWeaponAR2` с функцией-членом `CanHolster`, который по задумке должен был переопределить виртуальную функцию из базового класса.
Как видим, эти две функции с именем `CanHolster` имеют почти что одинаковые сигнатуры. Единственное различие — квалификатор `const` после имени функции.
Вспомним, что при переопределении виртуальной функции сигнатура функции производного класса должна в точности соответствовать сигнатуре функции базового класса, а именно:
- имена функций в базовом и производном классах должны быть одинаковы;
- типы параметров функций должны быть одинаковы;
- квалификаторы нестатических функций в базовом и производном классах должны совпадать;
- возвращаемые типы и спецификации исключений функций в базовом и производном классах должны быть коварианты;
- [дополнительно] ссылочные квалификаторы функций должны быть идентичными.
Так как одно из условий не выполняется, то в производный класс будет добавлена новая функция, которая перекроет имя из базового класса. Чтобы исправить возникшую ситуацию, необходимо добавить квалификатор `const` после имени функции-члена из дочернего класса. И не забудем добавить спецификатор `override`, проверяющий, что есть функция из базового, которую переопределяем:
Неподалёку разместились ещё два класса, в которых содержится та же ошибка:
- V762 It is possible a virtual function was overridden incorrectly. See qualifiers of function 'CanHolster' in derived class 'C_WeaponPhysCannon' and base class 'C_BaseCombatWeapon'. weapon_physcannon.h 289
Проверка на nullptr производится после использования указателя
Предупреждение PVS-Studio: V595 The 'pRenderable' pointer was utilized before it was verified against nullptr. Check lines: 3535, 3553. clientshadowmgr.cpp 3535
В начале функции `RemoveAllShadowsFromReceiver` разыменовывается указатель `pRenderable`. Но далее в кейсе `SHADOW_RECEIVER_STUDIO_MODEL` конструкции `switch` есть условие, которое проверяет этот указатель на валидность. Разработчик предполагает, что он может быть нулевым. Исходя из того, что этот указатель — параметр функции, логичным кажется всё же проверить его и сделать ранний возврат из функции:
Если же эта функция имеет неявный контракт "первый параметр всегда должен быть ненулевым", то проверку в `switch` стоит убрать.
Таких предупреждений довольно много, и на них следует обратить внимание:
V595 The 'pPlayer' pointer was utilized before it was verified against nullptr. Check lines: 426, 442. weapon_frag.cpp 426
V595 The 'pPlayer' pointer was utilized before it was verified against nullptr. Check lines: 146, 152. weapon_hl2mpbasebasebludgeon.cpp 146
V595 The 'pOwner' pointer was utilized before it was verified against nullptr. Check lines: 1364, 1380. weapon_physcannon.cpp 1364
V595 The 'node' pointer was utilized before it was verified against nullptr. Check lines: 2338, 2341. nav_generate.cpp 2338
V595 The 'adjNode' pointer was utilized before it was verified against nullptr. Check lines: 2339, 2341. nav_generate.cpp 2339
V595 The 'm_goal' pointer was utilized before it was verified against nullptr. Check lines: 730, 744. NextBotPathFollow.cpp 730
V595 The 'body' pointer was utilized before it was verified against nullptr. Check lines: 1748, 1762. NextBotPathFollow.cpp 1748
V595 The 'm_pszString' pointer was utilized before it was verified against nullptr. Check lines: 819, 827. convar.cpp 819
V595 The 'pPortal->nodes[nOtherSideIndex]' pointer was utilized before it was verified against nullptr. Check lines: 712, 715. vbsp.cpp 712
V595 The 'front' pointer was utilized before it was verified against nullptr. Check lines: 397, 475, 477. polylib.cpp 475
V595 The 'm_pCommand' pointer was utilized before it was verified against nullptr. Check lines: 251, 252. consoledialog.cpp 251
V595 The '_title' pointer was utilized before it was verified against nullptr. Check lines: 1594, 1603. Frame.cpp 1594
V595 The 'pActionSignalTarget' pointer was utilized before it was verified against nullptr. Check lines: 636, 642. perforcefilelistframe.cpp 636
V595 The '_activeTab' pointer was utilized before it was verified against nullptr. Check lines: 1063, 1087. PropertySheet.cpp 1063
V595 The 'm_pTree' pointer was utilized before it was verified against nullptr. Check lines: 132, 144. TreeViewListControl.cpp 132
V595 The 'pShaderShadow' pointer was utilized before it was verified against nullptr. Check lines: 103, 334, 335. example_model_dx9_helper.cpp 334
Подозрительное преобразование
Предупреждение PVS-Studio: V674 The literal '0.5f' of 'float' type is being implicitly cast to 'unsigned char' type while calling the 'SetRenderColor' function. Inspect the second argument. grenade_bugbait.cpp 168
Функция `SetRenderColor` устанавливает значение цвета по RGBA, где каждый параметр имеет тип `unsigned char` с возможным диапазоном значений `[0 .. 255]`. При попытке передать аргументы с типом `float` дробная часть будет отброшена. Это приведёт к тому, что параметры функции `r`, `g`, `b`, `a` будут иметь значения, равные `0`.
К сожалению, в репозитории отсутствует информация по blame, поэтому у меня есть два предположения, как ошибка появилась в кодовой базе:
1. Когда-то функция обрабатывала цвета в вещественном представлении. Затем обработку сменили на целые числа, но забыли поправить все точки вызова.
2. Разработчик ошибочно посчитал, что функция `SetRenderColor` обрабатывает вещественные числа, и задал их таким способом.
Дополнительно несколько аналогичных предупреждений:
V674 The literal '0.5f' of 'float' type is being implicitly cast to 'unsigned char' type while calling the 'SetRenderColor' function. Inspect the second argument. weapon_bugbait.cpp 171
V674 The literal '25.6' of 'double' type is being implicitly cast to 'int' type while calling the 'SetScrollRate' function. Inspect the first argument. grenade_tripmine.cpp 179
Некорректное сравнение
Предупреждение PVS-Studio: V698 Expression 'stricmp(....) == -1' is incorrect. This function can return not only the value '-1', but any negative value. Consider using 'stricmp(....) < 0' instead. utlintrusivelist.h 509
Функция `stricmp` может вернуть любое отрицательное значение, если `node->m_Name` лексикографически меньше, чем `head->m_Name` (без учёта регистра). Поэтому сравнивать результат функции `stricmp` со значением `-1` некорректно.
Исправленный код:
Аномалии
Истинная и ложная ветки оператора 'if' полностью совпадают
Аналогичные срабатывания.
V523 The 'then' statement is equivalent to the 'else' statement. c_basehlplayer.cpp 634
V523 The 'then' statement is equivalent to the 'else' statement. hl2_triggers.cpp 752
V523 The 'then' statement is equivalent to the 'else' statement. npc_combine.cpp 2528
V523 The 'then' statement is equivalent to the 'else' statement. npc_ichthyosaur.cpp 724
V523 The 'then' statement is equivalent to the 'else' statement. Menu.cpp 1185
Независимо от условия, будет выполнено одно и то же действие
Предупреждение PVS-Studio: V583 The '?:' operator, regardless of its conditional expression, always returns one and the same value: 0.0. in_main.cpp 605
Здесь, вероятнее всего, опечатка, так как возврат одного значения в условии выглядит подозрительно.
Присваивания разных значений одной переменной
Предупреждение анализатора PVS-Studio: V519 The 'val[3]' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 124, 125. BaseVSShader.cpp 125
Анализатор обнаружил, что в переменную `val[3]` несколько раз подряд присваиваются разные значения. Причём между этими присваиваниями сама переменная не используется. Возможно, это опечатка, и планировалось написать что-то вроде такого выражения: `val[3] += fWValue`.
Переменная присваивается сама себе
Предупреждение анализатора PVS-Studio: V570 The same value is assigned twice to the 'm_flFieldOfView' variable. npc_monk.cpp 274
Полю `m_flFieldOfView` дважды присваивается одно и то же значение `-0.707` (косинус угла обзора в 135 градусов). Либо это опечатка, и должно использоваться также другое поле, либо одно присваивание лишнее, и его можно убрать.
Параметр стал ненужным
Предупреждение анализатора PVS-Studio: V763 Parameter 'iElement' is always rewritten in function body before being used. dt_utlvector_send.cpp 49
Параметр `iElement`, который функция принимает по копии, всегда перезаписывается и лишь затем используется. Это странное поведение. Возможно, надобность в параметре пропала, но на рефакторинг кода не было времени. Тогда логично сделать параметр неименованным, и у других разработчиков будет меньше вопросов, почему он всегда перезаписывается:
Предупреждения.
- V763 Parameter 'flDist' is always rewritten in function body before being used. npc_cranedriver.cpp 180
- V763 Parameter 'flDist' is always rewritten in function body before being used. npc_strider.cpp 2561
Заключение
Хочется поблагодарить вас, читателей, что отложили свои дела и провели это время со старейшим ронином Source SDK, сражаясь с чужеродными формами жизни. Надеюсь, статья была интересной и познавательной.
Если вы захотите попробовать анализатор PVS-Studio, для open source проектов у нас предоставляется бесплатная лицензия.
Что ж, нам пора прощаться. Всех благ вам, и до новых встреч!
Подожди, слышите звуки? Кажется, мы нашли логово хедкрабов. Предлагаю, подлатать свои раны и перезарядить обойму, чтобы окончательно победить этих тварей. А пока сделаем привал перед нашим следующим путешествием.
Продолжение следует.