В этой статье я хотел бы поделиться результатами нашего анализа открытой реализации сервера World of Warcraft CMaNGOS, выполненного статическим анализатором PVS-Studio.
Введение
C (продолжение) MaNGOS - это активно развивающееся ответвление старого проекта MaNGOS (Massive Network Game Object Server), который был создан для создания альтернативного сервера для игры World of Warcraft. Большинство разработчиков MaNGOS продолжают работать в CMaNGOS.
По словам самих разработчиков, их цель - создать «хорошо написанный сервер на C ++» для одной из лучших MMORPG. Я постараюсь им в этом помочь, проверив CMaNGOS с помощью статического анализатора PVS-Studio.
Примечание: для анализа мы использовали сервер CMaNGOS-Classic, доступный в репозитории проекта на GitHub.
Результаты анализа
Ошибка в приоритете операции
Предупреждение PVS-Studio: V593 Рассмотрите возможность просмотра выражения типа A = B‹ C . Выражение рассчитывается следующим образом: A = (B‹ C) . SpellEffects.cpp 473
void Spell::EffectDummy(SpellEffectIndex eff_idx)
{
....
if (uint32 roll = urand(0, 99) < 3) // <=
....
else if (roll < 6)
....
else if (roll < 9)
....
....
}
Автор предположил, что переменной roll будет присвоено случайное значение, а затем это значение будет сравниваться с 3. Однако приоритет операции сравнения выше, чем у операции присваивания (см. таблица Приоритеты операций в C / C ++, поэтому случайное число сначала будет сравниваться с 3, а затем результат сравнения (0 или 1) будет записан в переменную roll.
Эту ошибку можно исправить, разместив круглые скобки следующим образом:
if ((uint32 roll = urand(0, 99)) < 3)
Подобные действия в блоках if и else
Предупреждение PVS-Studio: V523 Оператор then эквивалентен оператору else. SpellAuras.cpp 1537
void Aura::HandleAuraModShapeshift(bool apply, bool Real)
{
switch (form)
{
case FORM_CAT:
....
case FORM_TRAVEL:
....
case FORM_AQUA:
if (Player::TeamForRace(target->getRace()) == ALLIANCE)
modelid = 2428; // <=
else
modelid = 2428; // <=
....
}
....
}
В обоих блоках переменной modelid присваивается одно и то же значение; скорее всего, это ошибка и следует заменить константу в одном из блоков на другую.
Неопределенное поведение
Предупреждение PVS-Studio: V567 Неопределенное поведение. Переменная m_uiMovePoint модифицируется, когда используется дважды между точками последовательности. boss_onyxia.cpp 405
void UpdateAI(const uint32 uiDiff) override
{
....
switch (urand(0, 2))
{
case 0:
....
case 1:
{
// C++ is stupid, so add -1 with +7
m_uiMovePoint += NUM_MOVE_POINT - 1;
m_uiMovePoint %= NUM_MOVE_POINT;
break;
}
case 2:
++m_uiMovePoint %= NUM_MOVE_POINT; // <=
break;
}
....
}
В указанной строке переменная m_uiMovePoint изменяется дважды в пределах одной точки следования, что приводит к неопределенному поведению программы. Более подробную информацию вы можете найти в описании диагностики V567.
Похожая ошибка:
- V567 Неопределенное поведение. Переменная m_uiCrystalPosition изменяется, когда используется дважды между точками последовательности. boss_ossirian.cpp 150
Ошибка в условии
Предупреждение PVS-Studio: Выражение V547 всегда ложно. Вероятно, здесь следует использовать оператор ||. SpellEffects.cpp 2872
void Spell::EffectEnchantItemTmp(SpellEffectIndex eff_idx)
{
....
// TODO: Strange stuff in following code
// shaman family enchantments
if (....)
duration = 300;
else if (m_spellInfo->SpellIconID == 241 &&
m_spellInfo->Id != 7434)
duration = 3600;
else if (m_spellInfo->Id == 28891 &&
m_spellInfo->Id == 28898) // <=
duration = 3600;
....
}
В указанном условии переменная m_spellInfo- ›Id проверяется на соответствие двум различным значениям одновременно. Конечно, результат этой проверки всегда ложный. Скорее всего, автор ошибся и вместо оператора «||» использовал «&&».
Следует отметить, что программа прокомментировала странное поведение кода, и, возможно, оно было вызвано именно этой ошибкой.
Таких ошибок было несколько, вот полный список:
- V547 Выражение всегда ложно. Вероятно, здесь следует использовать оператор «||». SpellEffects.cpp 2872
- V547 Выражение всегда верно. Вероятно, здесь следует использовать оператор «&&». genrevision.cpp 261
- V547 Выражение всегда верно. Вероятно, здесь следует использовать оператор «&&». vmapexport.cpp 361
- V547 Выражение всегда верно. Вероятно, здесь следует использовать оператор «&&». MapTree.cpp 125
- V547 Выражение всегда верно. Вероятно, здесь следует использовать оператор «&&». MapTree.cpp 234
Подозрительное форматирование
Предупреждение PVS-Studio: V640. Логика работы кода не соответствует его форматированию. Оператор смещен вправо, но выполняется всегда. Возможно, что фигурные скобки отсутствуют. instance_blackrock_depths.cpp 111
void instance_blackrock_depths::OnCreatureCreate(Creature* pCreature)
{
switch (pCreature->GetEntry())
{
....
case NPC_HAMMERED_PATRON:
....
if (m_auiEncounter[11] == DONE)
pCreature->SetFactionTemporary(....);
pCreature->SetStandState(UNIT_STAND_STATE_STAND); // <=
break;
case NPC_PRIVATE_ROCKNOT:
case NPC_MISTRESS_NAGMARA:
....
}
}
Возможно, автор забыл поставить фигурные скобки после оператора if, что привело к выполнению вызова pCreature- ›SetStandState (UNIT_STAND_STATE_STAND) независимо от if условие.
Если такое поведение было задумано намеренно, следует исправить форматирование:
if (m_auiEncounter[11] == DONE)
pCreature->SetFactionTemporary(....);
pCreature->SetStandState(UNIT_STAND_STATE_STAND);
Подобные операнды в тернарном операторе
Предупреждение PVS-Studio: V583 Оператор ?:, независимо от его условного выражения, всегда возвращает одно и то же значение: SAY_BELNISTRASZ_AGGRO_1. razorfen_downs.cpp 104
void AttackedBy(Unit* pAttacker) override
{
....
if (!m_bAggro)
{
DoScriptText(urand(0, 1) ?
SAY_BELNISTRASZ_AGGRO_1 : // <=
SAY_BELNISTRASZ_AGGRO_1, // <=
m_creature, pAttacker);
m_bAggro = true;
}
....
}
Второй и третий операнды тернарного оператора идентичны; это скорее всего ошибка. Судя по коду проекта, можно предположить, что один из операндов должен иметь значение SAY_BELNISTRASZ_AGGRO_2.
Целочисленное деление
Предупреждение PVS-Studio: V674 Литерал 0.1f типа float сравнивается со значением типа unsigned int. item_scripts.cpp 44
bool ItemUse_item_orb_of_draconic_energy(....)
{
....
// If Emberstrife is already mind controled or above 10% HP:
// force spell cast failure
if (pEmberstrife && pEmberstrife->HasAura(SPELL_DOMINION_SOUL)
|| pEmberstrife->GetHealth() /
pEmberstrife->GetMaxHealth() > 0.1f) // <=
{
....
return true;
}
return false;
}
Метод Unit :: GetHealth () возвращает значение типа uint32_t, а метод Unit :: GetMaxHealth () также возвращает значение типа uint32_t, , поэтому результат деления является целым числом, и сравнивать его с 0.1f бессмысленно.
Чтобы правильно определить 10% здоровья, этот код можно переписать так:
// If Emberstrife is already mind controled or above 10% HP:
// force spell cast failure
if (pEmberstrife && pEmberstrife->HasAura(SPELL_DOMINION_SOUL)
|| ((float)pEmberstrife->GetHealth()) /
((float)pEmberstrife->GetMaxHealth()) > 0.1f)
{
....
return true;
}
Безусловный выход из цикла for
Предупреждение PVS-Studio: V612 Безусловный разрыв внутри цикла. Pet.cpp 1956 г.
void Pet::InitPetCreateSpells()
{
....
for (SkillLineAbilityMap::const_iterator
_spell_idx = bounds.first; _spell_idx != bounds.second;
++_spell_idx)
{
usedtrainpoints += _spell_idx->second->reqtrainpoints;
break; // <=
}
....
}
Было неясно, что здесь имелось в виду, но безусловный оператор break в теле цикла for выглядит очень подозрительно. Даже если здесь нет ошибки, лучше провести рефакторинг кода и избавиться от ненужного цикла, потому что итератор _spell_idx принимает одно значение.
То же предупреждение:
- V612 Безусловный «разрыв» внутри цикла. Pet.cpp 895
Резервное состояние
Предупреждение PVS-Studio: V728 Излишнюю проверку можно упростить. Оператор || окружен противоположными выражениями ! Realtimeonly и realtimeonly. Player.cpp 10536
void Player::UpdateItemDuration(uint32 time, bool realtimeonly)
{
....
if ((realtimeonly && (....)) || !realtimeonly) // <=
item->UpdateDuration(this, time);
....
}
Чек (a && b) || ! a можно упростить до! a || b, который можно увидеть в таблице истинности:
Таким образом, исходное выражение можно упростить до:
void Player::UpdateItemDuration(uint32 time, bool realtimeonly)
{
....
if (!(realtimeonly) || (....))
item->UpdateDuration(this, time);
....
}
Тестирование на нуль
Предупреждение PVS-Studio: V704 ‘! this ||! pVictim’ следует избегать: указатель ‘this’ никогда не может иметь значение NULL на новых компиляторах. Unit.cpp 1417
void Unit::CalculateSpellDamage(....)
{
....
if (!this || !pVictim) // <=
return;
....
}
Согласно современным стандартам C ++, указатель this никогда не может быть нулевым. Часто сравнение this с нулем может вызвать непредвиденные ошибки. Подробнее об этом читайте в описании диагностики V704.
Подобные проверки:
- V704 следует избегать выражения «! This ||! PVictim»: указатель «this» никогда не может иметь значение NULL в новых компиляторах. Unit.cpp 1476
- V704 следует избегать выражения «! This ||! PVictim»: указатель «this» никогда не может иметь значение NULL в новых компиляторах. Unit.cpp 1511
- V704 следует избегать выражения «! This ||! PVictim»: указатель «this» никогда не может иметь значение NULL в новых компиляторах. Unit.cpp 1797
Неоправданная передача по ссылке
Предупреждение PVS-Studio: V669 Аргумент uiHealedAmount - это непостоянная ссылка. Анализатор не может определить позицию, в которой этот аргумент изменяется. Возможно, функция содержит ошибку. boss_twinemperors.cpp 109
void HealedBy(Unit* pHealer, uint32& uiHealedAmount) override // <= { if (!m_pInstance) return;
if (Creature* pTwin = m_pInstance->GetSingleCreatureFromStorage( m_creature->GetEntry() == NPC_VEKLOR ? NPC_VEKNILASH : NPC_VEKLOR)) { float fHealPercent = ((float)uiHealedAmount) / ((float)m_creature->GetMaxHealth()); uint32 uiTwinHeal = (uint32)(fHealPercent * ((float)pTwin->GetMaxHealth())); uint32 uiTwinHealth = pTwin->GetHealth() + uiTwinHeal; pTwin->SetHealth(uiTwinHealth < pTwin->GetMaxHealth() ? uiTwinHealth : pTwin->GetMaxHealth()); } }
Переменная uiHealedAmount передается по ссылке, но не изменяется в теле функции. Это может ввести в заблуждение, поскольку создается впечатление, что функция HealedBy () что-то записывает в uiHealedAmount. Лучше передать переменную с помощью постоянной ссылки или ценность.
Повторное присвоение
Предупреждение PVS-Studio: V519 переменной stat дважды подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 1776, 1781. DetourNavMeshQuery.cpp 1781
dtStatus dtNavMeshQuery::findStraightPath(....) const { .... if (....) { stat = appendPortals(apexIndex, i, closestEndPos, // <= path, straightPath, straightPathFlags, straightPathRefs, straightPathCount, maxStraightPath, options); }
stat = appendVertex(closestEndPos, 0, path[i], // <= straightPath, straightPathFlags, straightPathRefs, straightPathCount, maxStraightPath); .... }
Анализатор обнаружил подозрительный фрагмент, в котором переменной stat дважды присвоены разные значения. Этот код определенно заслуживает внимания.
Проверка указателя на нуль после нового
Предупреждение PVS-Studio: V668 Нет смысла проверять указатель pmmerge на нуль, так как память была выделена с помощью оператора new. Исключение будет сгенерировано в случае ошибки выделения памяти. MapBuilder.cpp 553
void MapBuilder::buildMoveMapTile(....)
{
....
rcPolyMesh** pmmerge =
new rcPolyMesh*[TILES_PER_MAP * TILES_PER_MAP];
if (!pmmerge) // <=
{
printf("%s alloc pmmerge FIALED! \r", tileString);
return;
}
....
}
После нового оператора проверка указателя на нуль бессмысленна. Если выделить память невозможно, оператор new генерирует исключение std :: bad_alloc (), он не возвращает nullptr. Это означает, что программа никогда не войдет в кадр после условия.
Чтобы исправить эту ошибку, мы могли бы выделить память в блоке try {….} Catch (const std :: bad_alloc &) {….} или использовать new (std :: nothrow ) конструкция для выделения памяти, которая не будет генерировать исключения в случае сбоя.
Аналогичные проверки указателей:
- V668 Нет смысла проверять указатель «данные» на нуль, так как память была выделена с помощью оператора «новый». Исключение будет сгенерировано в случае ошибки выделения памяти. loadlib.cpp 36
- V668 Нет смысла проверять указатель «dmmerge» на нуль, так как память была выделена с помощью оператора «new». Исключение будет сгенерировано в случае ошибки выделения памяти. MapBuilder.cpp 560
- V668 Нет смысла проверять указатель «m_session» на нуль, так как память была выделена с помощью оператора «new». Исключение будет сгенерировано в случае ошибки выделения памяти. WorldSocket.cpp 426
Неправильный порядок аргументов
Предупреждение PVS-Studio: V764 Возможный неправильный порядок аргументов, переданных в функцию loadVMap: tileY и tileX. MapBuilder.cpp 279
void MapBuilder::buildTile(uint32 mapID, uint32 tileX, uint32 tileY, dtNavMesh* navMesh, uint32 curTile, uint32 tileCount) { .... // get heightmap data m_terrainBuilder->loadMap(mapID, tileX, tileY, meshData);
// get model data m_terrainBuilder->loadVMap(mapID, tileY, tileX, // <= meshData); .... }
Анализатор обнаружил подозрительную передачу аргументов функции - аргументы tileX и tileY поменялись местами.
Если мы посмотрим на прототип функции loadVMap (),, то ясно увидим, что это ошибка.
bool loadVMap(uint32 mapID,
uint32 tileX, uint32 tileY,
MeshData& meshData);
Два идентичных блока кода
Предупреждение PVS-Studio: V760 Обнаружены два идентичных блока текста. Второй блок начинается со строки 213. BattleGround.cpp 210
BattleGround::BattleGround() : m_BuffChange(false), m_StartDelayTime(0), m_startMaxDist(0) { .... m_TeamStartLocO[TEAM_INDEX_ALLIANCE] = 0; m_TeamStartLocO[TEAM_INDEX_HORDE] = 0;
m_BgRaids[TEAM_INDEX_ALLIANCE] = nullptr; m_BgRaids[TEAM_INDEX_HORDE] = nullptr;
m_PlayersCount[TEAM_INDEX_ALLIANCE] = 0; // <= m_PlayersCount[TEAM_INDEX_HORDE] = 0; // <=
m_PlayersCount[TEAM_INDEX_ALLIANCE] = 0; // <= m_PlayersCount[TEAM_INDEX_HORDE] = 0; // <=
m_TeamScores[TEAM_INDEX_ALLIANCE] = 0; m_TeamScores[TEAM_INDEX_HORDE] = 0; .... }
В этом фрагменте дважды выполняются одни и те же действия. Этот код, скорее всего, возник в результате использования Copy-Paste.
Повторяющееся условие
Предупреждение PVS-Studio: V571 Периодическая проверка. Условие isDirectory уже было проверено в строке 166. FileSystem.cpp 169
FileSystem::Dir&
FileSystem::getContents(const std::string& path,
bool forceUpdate)
{
// Does this path exist on the real filesystem?
if (exists && isDirectory) // <=
{
// Is this path actually a directory?
if (isDirectory) // <=
{
....
}
....
}
....
}
Условие IsDirectory проверяется дважды. Мы можем удалить дубликат чека.
Побитовое И с нулевой константой
Предупреждение PVS-Studio: V616 Именованная константа SPELL_DAMAGE_CLASS_NONE со значением 0 используется в побитовой операции. Spell.cpp 674
void Spell::prepareDataForTriggerSystem()
{
....
if (IsPositiveSpell(m_spellInfo->Id))
{
if (m_spellInfo->DmgClass & SPELL_DAMAGE_CLASS_NONE) // <=
{
m_procAttacker = PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_POS;
m_procVictim = PROC_FLAG_TAKEN_SPELL_NONE_DMG_CLASS_POS;
}
}
....
}
Константа SPELL_DAMAGE_CLASS_NONE имеет нулевое значение, а побитовое И любого числа и null является нулевым, поэтому условие всегда будет ложным, и следующий блок никогда не будет выполнен.
Похожая ошибка:
- V616 Именованная константа SPELL_DAMAGE_CLASS_NONE со значением 0 используется в побитовой операции. Spell.cpp 692
Возможное разыменование нулевого указателя
Предупреждение PVS-Studio: V595 Указатель модель использовался до того, как он был проверен на соответствие nullptr. Проверьте строки: 303, 305. MapTree.cpp 303
bool StaticMapTree::InitMap(const std::string& fname,
VMapManager2* vm)
{
....
WorldModel* model =
vm->acquireModelInstance(iBasePath, spawn.name);
model->setModelFlags(spawn.flags); // <=
....
if (model) // <=
{
....
}
....
}
Указатель model проверяется на нулевое значение; т.е. может быть равен нулю, однако указатель используется ранее без каких-либо проверок. Понятно, что это потенциальное разыменование нулевого указателя.
Чтобы исправить эту ошибку, вы должны проверить значение указателя model перед вызовом метода model- ›setModelFlags (spawn.flags).
Подобные предупреждения:
- V595 Указатель «модель» использовался до того, как он был проверен на соответствие nullptr. Проверить строки: 374, 375. MapTree.cpp 374
- V595 Указатель «единицы» использовался до того, как он был проверен на соответствие nullptr. Проверить строки: 272, 290. Object.cpp 272
- V595 Указатель «updateMask» использовался до проверки на nullptr. Проверить строки: 351, 355. Object.cpp 351
- V595 Указатель dbcEntry1 использовался до того, как он был проверен на соответствие nullptr. Проверьте строки: 7123, 7128. ObjectMgr.cpp 7123
Вывод
Как всегда, PVS-Studio обнаружил в коде большое количество подозрительных мест и ошибок. Я надеюсь, что разработчики CMaNGOS исправят все эти недостатки, а также начнут регулярно использовать статический анализ, потому что разовая проверка не так эффективна.
Также напоминаю, что любой желающий может использовать статический анализатор PVS-Studio бесплатно при определенных условиях, описанных на сайте.
P.S. Вы можете предложить проверить любой интересный проект с помощью нашего анализатора, используя форму обратной связи или GitHub. Все подробности вы можете найти здесь.