Введение
C(ontinued)MaNGOS является активно развивающимся ответвлением старого проекта MaNGOS (Massive Network Game Object Server), призванного создать свободный альтернативный сервер для игры World of Warcraft. Большая часть разработчиков MaNGOS продолжает работу в проекте CMaNGOS.
Как пишут сами разработчики, их цель – создать открытый «well written server in C++» для одной из лучших MMORPG. Постараюсь немного помочь им в этом, и проверю CMaNGOS при помощи статического анализатора PVS-Studio.
Примечание: Для проверки использовался сервер CMaNGOS-Classic, доступный в репозитории проекта на github.
Результаты проверки
Ошибка в приоритете операций
Предупреждение PVS-Studio: V593 Consider reviewing the expression of the 'A = B < C' kind. The expression is calculated as following: '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. Однако, приоритет операции сравнения выше, чем приоритет операции присваивания (см. таблицу приоритетов операций), поэтому в действительности сначала случайное число будет сравниваться с 3, а затем результат этого сравнения (0 или 1) будет записан в переменную roll.
Данную ошибку можно исправить таким образом:
uint32 roll = urand(0, 99);
if (roll < 3)
{
//....
}
Одинаковые действия в блоках if и else
Предупреждение PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. 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 Undefined behavior. The 'm_uiMovePoint' variable is modified while being used twice between sequence points. 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 Undefined behavior. The 'm_uiCrystalPosition' variable is modified while being used twice between sequence points. boss_ossirian.cpp 150
Ошибка в условии
Предупреждение PVS-Studio: V547 Expression is always false. Probably the '||' operator should be used here. 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 двум разным значениям одновременно. Результат такой проверки, естественно, всегда false. Скорее всего, автор ошибся и вместо оператора '||' использовал оператор '&&'.
Примечательно, что в комментариях отмечено странное поведение данного блока кода и, возможно, что оно как раз вызвано этой ошибкой.
В проекте нашлось еще несколько подобных ошибок, вот их полный список:
- V547 Expression is always false. Probably the '||' operator should be used here. SpellEffects.cpp 2872
- V547 Expression is always true. Probably the '&&' operator should be used here. genrevision.cpp 261
- V547 Expression is always true. Probably the '&&' operator should be used here. vmapexport.cpp 361
- V547 Expression is always true. Probably the '&&' operator should be used here. MapTree.cpp 125
- V547 Expression is always true. Probably the '&&' operator should be used here. MapTree.cpp 234
Подозрительное форматирование
Предупреждение PVS-Studio: V640 The code's operational logic does not correspond with its formatting. The statement is indented to the right, but it is always executed. It is possible that curly brackets are missing. 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 The '?:' operator, regardless of its conditional expression, always returns one and the same value: 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 The '0.1f' literal of the 'float' type is compared to a value of the 'unsigned int' type. 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 An unconditional 'break' within a loop. 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 An unconditional 'break' within a loop. Pet.cpp 895
Избыточное условие
Предупреждение PVS-Studio: V728 An excessive check can be simplified. The '||' operator is surrounded by opposite expressions '!realtimeonly' and '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);
....
}
Проверка this на null
Предупреждение PVS-Studio: V704 '!this ||!pVictim' expression should be avoided: 'this' pointer can never be NULL on newer compilers. Unit.cpp 1417
void Unit::CalculateSpellDamage(....)
{
....
if (!this || !pVictim) // <=
return;
....
}
Согласно современному стандарту С++, указатель this никогда не может быть нулевым. Зачастую использование сравнения this с нулем может приводить к неожиданным ошибкам. Подробнее прочитать об этом можно в описании диагностики V704.
Аналогичные проверки:
- V704 '!this ||!pVictim' expression should be avoided: 'this' pointer can never be NULL on newer compilers. Unit.cpp 1476
- V704 '!this ||!pVictim' expression should be avoided: 'this' pointer can never be NULL on newer compilers. Unit.cpp 1511
- V704 '!this ||!pVictim' expression should be avoided: 'this' pointer can never be NULL on newer compilers. Unit.cpp 1797
Неоправданная передача по ссылке
Предупреждение PVS-Studio: V669 The 'uiHealedAmount' argument is a non-constant reference. The analyzer is unable to determine the position at which this argument is being modified. It is possible that the function contains an error. 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 The 'stat' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 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 два раза подряд присваиваются разные значения. Определенно стоит проверить этот участок кода на предмет ошибок.
Проверка указателя на null после new
Предупреждение PVS-Studio: V668 There is no sense in testing the 'pmmerge' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. 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 бессмысленна. При невозможности выделить память, оператор new генерирует исключение std::bad_alloc(), а не возвращает nullptr. Соответственно, программа никогда не зайдет в блок после условия.
Чтобы исправить эту ошибку можно сделать выделение памяти в блоке try {....} catch(const std::bad_alloc &) {....}, или использовать для выделения памяти конструкцию new(std::nothrow), которая не будет генерировать исключение в случае неудачи.
Аналогичные проверки указателей:
- V668 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. loadlib.cpp 36
- V668 There is no sense in testing the 'dmmerge' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. MapBuilder.cpp 560
- V668 There is no sense in testing the 'm_session' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. WorldSocket.cpp 426
Неправильный порядок аргументов
Предупреждение PVS-Studio: V764 Possible incorrect order of arguments passed to 'loadVMap' function: 'tileY' and '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 Two identical blocks of text were found. The second block begins from line 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 Recurring check. The 'isDirectory' condition was already verified in line 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 The 'SPELL_DAMAGE_CLASS_NONE' named constant with the value of 0 is used in the bitwise operation. 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 имеет нулевое значение, а побитовое И любого числа и нуля, является нулем, следовательно условие будет всегда иметь значение false, а блок, следующий за ним никогда не выполнится.
Аналогичная ошибка:
- V616 The 'SPELL_DAMAGE_CLASS_NONE' named constant with the value of 0 is used in the bitwise operation. Spell.cpp 692
Потенциальное разыменование нулевого указателя
Предупреждение PVS-Studio: V595 The 'model' pointer was utilized before it was verified against nullptr. Check lines: 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 проверяется на null, т.е. допускается его равенство нулю, однако, выше указатель уже используется и без всяких проверок. Налицо потенциальное разыменовывание нулевого указателя.
Чтобы исправить эту ошибку, следует проверить значение указателя model до того, как вызывать метод model->setModelFlags(spawn.flags).
Аналогичные предупреждения:
- V595 The 'model' pointer was utilized before it was verified against nullptr. Check lines: 374, 375. MapTree.cpp 374
- V595 The 'unit' pointer was utilized before it was verified against nullptr. Check lines: 272, 290. Object.cpp 272
- V595 The 'updateMask' pointer was utilized before it was verified against nullptr. Check lines: 351, 355. Object.cpp 351
- V595 The 'dbcEntry1' pointer was utilized before it was verified against nullptr. Check lines: 7123, 7128. ObjectMgr.cpp 7123
Заключение
PVS-Studio как всегда нашел много подозрительных мест и ошибок в коде. Надеюсь, разработчики CMaNGOS поправят все недочеты, а так же начнут постоянно использовать статический анализ в своем проекте, поскольку разовая проверка не так уж эффективна.
Также напомню, что теперь любой желающий может воспользоваться возможностью бесплатного использования PVS-Studio при соблюдении описанных по ссылке условий.
P.S. Вы можете предложить для проверки нашим анализатором любой интересный для вас проект через форму обратной связи или с помощью GitHub. Подробности по ссылке.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Bredikhin Egor. Checking the World of Warcraft CMaNGOS open source server.
Комментарии (24)
eao197
14.02.2017 16:22+4А вы уверены, что вот эта ваша рекомендация:
if ((uint32 roll = urand(0, 99)) < 3)
Вообще скомпилируется?EgorBredikhin
14.02.2017 16:58+5Спасибо, поправил. Кстати, в C++17 можно написать так:
if (uint32 roll = urand(0, 99); roll < 3)
Deosis
15.02.2017 07:41+1Зачем вообще так писать? Какие преимущества у этого кода по сравнению с присваиванием в отдельной строке?
(кроме «Так тоже можно»?)eao197
15.02.2017 08:37+1Мне вы зачем этот вопрос задаете? Такой вариант первоначально был приведен автором статьи как рецепт для «улучшения кода». Только этот рецепт оказался недействительным, т.к. он не компилируется.
Если же вас интересует, почему в C++ вообще можно объявлять локальные переменные внутри условия if-а, то это сделано для удобства. Объявленная в if-е переменная будет видна только в ветках then и else. Т.е. для вот такого кода:
int a = 0; if(int b=(a+1)) { ... } else { ... }
полным аналогом будет вот такой код:
int a = 0; { int b = a+1; if(b) { ... } else { ... } }
про эти лишние фигурные скобочки забывают, невольно продляя время жизни и область видимости для b. Что может быть чревато, если у b тип не такой простой, как int, а с нетривиальным деструктором.
Antervis
15.02.2017 08:40область видимости переменной будет ограничена условным блоком.
Deosis
15.02.2017 12:06Довольно слабое преимущество по сравнению с возможностью ошибиться с приоритетами.
Antervis
15.02.2017 12:58+1с одной стороны, таким трюком в отсутствие с++17 можно очень изящно пользоваться если функции возвращают коды ошибок:
например, такenum Result { Success = 0, Error1, Error2 //... }; Result job() { if (Result res = task1()) return res; if (Result res = task2()) return res; //... return Success; }
T-362
14.02.2017 17:24+3if (Player::TeamForRace(target->getRace()) == ALLIANCE) modelid = 2428; // <= else modelid = 2428; // <=
В контексте не совсем и ошибка, водная форма друида в старых версиях одинаковая для орды и альянса, так что если и чинить то удалением проверки на «сторону».
trux
14.02.2017 17:25В разделе «Избыточное условие» всё, конечно, зависит от того, есть ли побочные эффекты у кода, который скрывается под многоточием.
EgorBredikhin
14.02.2017 17:30+3Под многоточием скрывается вот это:
if ((realtimeonly && (item->GetProto()->ExtraFlags & ITEM_EXTRA_REAL_TIME_DURATION)) || !realtimeonly)
Прототип функции GetProto():
ItemPrototype const* Item::GetProto() const;
Анализатор не выдал бы предупреждение в случае, когда код обозначенный многоточием влиял бы на переменную realtimeonly.
BeLove
14.02.2017 18:37Ох, cmangos жив :) Странно, что выбор пал на него.
Я уж думал только TrinityCore активно дальше развивается (и надо бы перепроверить проблемные куски выше в TC).
BratSinot
15.02.2017 00:54А проверка этого кода легальная? :)
LoadRunner
15.02.2017 12:29А что нелегального в проверке любого кода любыми методами, выложенного в открытый доступ? Есть ли вообще open source лицензии, которые запрещают чтение и анализ кода?
BratSinot
15.02.2017 12:31Так это же не официальный сервер WoW, или я ошибаюсь?
LoadRunner
15.02.2017 12:38А разница? Проект open source, выложен на github. Речь же не о запуске своего шарда, а об анализе кода. Лицензия, под которой код опубликован, не запрещает этого.
edge790
15.02.2017 17:09Не неофициальный сервер WoW, а «любительская реализация серверной части одной из лучших MMORPG».
Нелегально запускать свой публичный сервер.
А этот код написан фанатами и распространяется по GNU GPL (т.е. автор передает творение в общественную собственность).
Причём на сайте указано что поднятие сервера незаконно и комманда mangos не оказывает никакой поддержки в этом.
igordata
16.02.2017 09:17+8Стараюсь не читать ваши статьи, но не потому, что плохие! Нет, напротив — потому что программа у вас умная, и находит такое, что волосы шевелятся. Порой прочитаю статью, а потом думаю по пути на работу: «Как оно вообще может работать? Как оно работает-то вообще с такой ошибкой?»
Ваша программа на меня оказывает экзистенциальное давление. Вот живёшь себе в мире, в котором программы пишут профессионалы, работают они так, как задумано, и всё хорошо. Потом бах! Что это? Как оно вообще может месяцами работать и не падать? Как оно вообще запускается? Это что получается, я тоже такое дерьмо пишу?
Вот с такими мыслями и еду на работу — писать код.
Спасибо большое! :D
mrguardian
18.02.2017 01:29Спасибо, очень интересная статья! Желаю вам финансовых успехов! Небольшой вопрос не по теме: а подписку на тулзу не продаёте? Ведь сами же говорили, что проверять проект надо регулярно.
Andrey2008
18.02.2017 11:21+3Так можно сказать мы и продаем подписку. Просто минимальная единица времени — 1 год.
Tujh
Обработается корректно?
Andrey2008
Анализатор знает и учитывает это. Цитата из документации V668:
Примечание N1. Анализатор не будет выдавать предупреждение, если используется placement new или «new (std::nothrow) T». Пример такого кода:
Tujh
Прикольно, спасибо!