In May 2016, the German company Crytek decided to publish the source code for the CryEngine V game engine on GitHub. The project is under active development, which entails the appearance of many errors in the code. We have already checked the project with PVS-Studio for Windows, and now we were able to test the project with PVS-Studio for Linux. The material again scored on a large article describing only very serious errors.
Introduction
CryEngine is a game engine created by the German company
Crytek in 2002, and originally used in the first-person shooter Far Cry. On CryEngine of different versions, many excellent games were made from several game studios that licensed the engine: Far Cry, Crysis, Entropia Universe, Blue Mars, Warface, Homefront: The Revolution, Sniper: Ghost Warrior, Armored Warfare, Evolve and
many others . In March 2016, Crytek announced the release of its new engine, the CryEngine V, and soon published the source code on
GitHub .
To check the open source code, we used the
PVS-Studio static analyzer version 6.14 for Linux. Now, developers of cross-platform projects have become even more convenient to monitor the quality of the code with a single code analysis tool. Download version for Linux can be in the form of an archive or package for the package manager. For most distributions, you can customize the
installation and upgrade using our repository.
The article includes general-purpose warnings and only the “High” confidence level (there is also Medium and Low). Honestly, I did not master to watch carefully all the “High” warnings, because almost immediately picked up errors for the article with a quick review. I took up the project several times and for a long time could not find the time to write a review article, so I can say about the given bugs that they have been living in the code for more than one month. Also, some errors were not corrected from the
previous article on the verification of the project.
')
Downloading and testing source code in Linux was very easy. Here is a list of all the necessary commands:
mkdir ~/projects && cd ~/projects git clone https:
The
cryengine_ga.tasks report
file can be opened and viewed in QtCreator. What did you find in the source code of CryEngine V?
Unhappy Active () Function
V501 operator: bActive == bActive LightEntity.h 124
void SetActive(bool bActive) { if (bActive == bActive) return; m_bActive = bActive; OnResetState(); }
Because of the typo, the function does nothing. It seems to me that if there was a Miss Misprint contest, then this piece of code would definitely take first place. I think this error has every chance of getting into the "
C / C ++ bugs of the month " section.
But this is not all, here is a function from another class:
V501 There are identical sub-expressions 'm_staticObjects' operator. FeatureCollision.h 66
class CFeatureCollision : public CParticleFeature { public: CRY_PFX2_DECLARE_FEATURE public: CFeatureCollision(); .... bool IsActive() const { return m_terrain || m_staticObjects || m_staticObjects; } .... bool m_terrain; bool m_staticObjects; bool m_dynamicObjects; };
Here, in the
IsActive () function, the
m_staticObjects variable is used
twice , although there is an unused variable
m_dynamicObjects next to it . Perhaps in the code they wanted to use it.
Code above has no bugs
V547 Expression 'outArrIndices [i] <0' is always false. Unsigned type value is never <0. CGFLoader.cpp 881
static bool CompactBoneVertices(...., DynArray<uint16>& outArrIndices, ....)
This error is worth a separate section. In general, in the CryEngine code, there are a lot of places where unsigned variables are meaninglessly compared to zero. There are
hundreds of such places, but I would like to pay special attention to this fragment, since The code was written intentionally.
So, there is an array of unsigned numbers -
outArrIndices . Next, the array is filled by some algorithm. After that, a brilliant check of each element of the array is performed, so that among them there are no negative numbers. The array elements are of type
uint16 .
Errors when working with memory
V512 A call of the memcpy function will lead to hashableData. GeomCacheRenderNode.cpp 285
void CGeomCacheRenderNode::Render(....) { .... CREGeomCache* pCREGeomCache = iter->second.m_pRenderElement; .... uint8 hashableData[] = { 0, 0, 0, 0, 0, 0, 0, 0, (uint8)std::distance(pCREGeomCache->....->begin(), &meshData), (uint8)std::distance(meshData....->....begin(), &chunk), (uint8)std::distance(meshData.m_instances.begin(), &instance) }; memcpy(hashableData, pCREGeomCache, sizeof(pCREGeomCache)); .... }
Pay attention to the
memcpy () function arguments. The
pCREGeomCache object
is planned to be copied into the
hashableData array, but by chance the size of the pointer was not calculated using the
sizeof operator, but the size of the object. Due to an error, the object is not completely copied, but only 4 or 8 bytes.
If you’re a sizeof () ’ ClipVolumeManager.cpp 145
void CClipVolumeManager::GetMemoryUsage(class ICrySizer* pSizer) const { pSizer->AddObject(this, sizeof(this)); for (size_t i = 0; i < m_ClipVolumes.size(); ++i) pSizer->AddObject(m_ClipVolumes[i].m_pVolume); }
A similar error was made by calculating instead of the class size, the size of the pointer
this . The correct option is
sizeof (* this) .
V530 ClipVolumes.cpp 492
vector<unique_ptr<CFullscreenPass>> m_jitteredDepthPassArray; void CClipVolumesStage::PrepareVolumetricFog() { .... for (int32 i = 0; i < m_jitteredDepthPassArray.size(); ++i) { m_jitteredDepthPassArray[i].release(); } m_jitteredDepthPassArray.resize(depth); for (int32 i = 0; i < depth; ++i) { m_jitteredDepthPassArray[i] = CryMakeUnique<....>(); m_jitteredDepthPassArray[i]->SetViewport(viewport); m_jitteredDepthPassArray[i]->SetFlags(....); } .... }
If you refer to the documentation for the class
std :: unique_ptr , then the
release () function should be used approximately as follows:
std::unique_ptr<Foo> up(new Foo()); Foo* fp = up.release(); delete fp;
Most likely, in this code snippet, we wanted to use the
reset () function instead of
release () .
V549 memcpy function is equal to the second argument. ObjectsTree_Serialize.cpp 1135
void COctreeNode::LoadSingleObject(....) { .... float* pAuxDataDst = pObj->GetAuxSerializationDataPtr(....); const float* pAuxDataSrc = StepData<float>(....); memcpy(pAuxDataDst, pAuxDataDst, min(....) * sizeof(float)); .... }
Forgot to pass
pAuxDataSrc to the
memcpy () function. Instead, the same
pAuxDataDst variable is used as the source and
destination . No one is immune from typos. By the way, those who wish can check their attentiveness by passing our test to identify such errors:
q.viva64.com .
Strange code
V501 There are identical to the left and the right of the | || operator: val == 0 || val == - 0 XMLCPB_AttrWriter.cpp 363
void CAttrWriter::PackFloatInSemiConstType(float val, ....) { uint32 type = PFSC_VAL; if (val == 0 || val == -0)
The developers planned to compare the real variable
val with zero and negative zero (signed zero / negative zero), but they did it wrong. By declaring integer constants, the values ​​of the zeros are the same.
Most likely, the code should be corrected as follows, declaring constants of real type:
if (val == 0.0f || val == -0.0f) type = PFSC_0;
On the other hand, the conditional expression is redundant, since it is enough to compare a variable with a normal zero. For this reason, the original code is executed as the programmer expects.
But if it is necessary to identify exactly negative zero, then correctly do it with the help of the function
std :: signbit .
V501 There are identical sub-expressions' m_joints [i] .limits [1] [j] articulatedentity.cpp 1326
int CArticulatedEntity::Step(float time_interval) { .... for (j=0;j<3;j++) if (!(m_joints[i].flags & angle0_locked<<j)&& isneg(m_joints[i].limits[0][j]-m_joints[i].qext[j]) + isneg(m_joints[i].qext[j]-m_joints[i].limits[1][j]) + isneg(m_joints[i].limits[1][j]-m_joints[i].limits[1][j]) < 2) { .... }
The last part of the conditional expression contains subtraction from the
m_joints [i] .limits [1] [j] variable. The code looks suspicious. There are many indexes in the expression, perhaps one of them has an error.
Another such place:
- V501 There are identical sub-expressions 'm_joints [op [1]]. Limits [1] [i]' to the left and to the right of the '-' operator. articulatedentity.cpp 513
V590 Consider inspecting this expression. The expression is misprint. GoalOp_Crycd2.cpp 3779
void COPCrysis2FlightFireWeapons::ParseParam(....) { .... bool paused; value.GetValue(paused); if (paused && (m_State != eFP_PAUSED) && (m_State != eFP_PAUSED_OVERRIDE)) { m_NextState = m_State; m_State = eFP_PAUSED; m_PausedTime = 0.0f; m_PauseOverrideTime = 0.0f; } else if (!paused && (m_State == eFP_PAUSED) &&
The conditional expression is written in such a way that the result does not depend on the
m_State subexpression
! = EFP_PAUSED_OVERRIDE . Although I’m telling someone, the developers haven’t fixed this code snippet after the first article.
If anyone is interested, then I described this kind of errors earlier in a separate article "
Logical expressions in C / C ++. How professionals are mistaken ."
V529 Odd semicolon ';' after 'for' operator. boolean3d.cpp 1077
int CTriMesh::Slice(...) { .... pmd->pMesh[0]=pmd->pMesh[1] = this; AddRef();AddRef(); for(pmd0=m_pMeshUpdate; pmd0->next; pmd0=pmd0->next);
Another piece of code, uncorrected since the previous check. The code is still not clear: is there an erroneous formatting or a logical error?
About pointers
V522 Dereferencing of the null pointer 'pCEntity' might take place. BreakableManager.cpp 2396
int CBreakableManager::HandlePhysics_UpdateMeshEvent(....) { CEntity* pCEntity = 0; .... if (pmu && pSrcStatObj && GetSurfaceType(pSrcStatObj)) { .... if (pEffect) { .... if (normal.len2() > 0) pEffect->Spawn(true, pCEntity->GetSlotWorldTM(...);
The analyzer detected null pointer dereferencing. The function code is written or refactored in such a way that a code branch is formed in which the
pCEntity pointer initialized by zero will be used.
Now consider a potential null pointer dereference.
V595 The 'pTrack' pointer was used before it was counted against nullptr. Check lines: 60, 61. AudioNode.cpp 60
void CAudioNode::Animate(SAnimContext& animContext) { .... const bool bMuted = gEnv->IsEditor() && (pTrack->GetFlags() & IAnimTrack::eAnimTrackFlags_Muted); if (!pTrack || pTrack->GetNumKeys() == 0 || pTrack->GetFlags() & IAnimTrack::eAnimTrackFlags_Disabled) { continue; } .... }
The author of this code fragment first used the
pTrack pointer, and on the very next line of the code checks its validity before dereferencing. Most likely, this is a potential place for incorrect operation of the program.
There are a lot of warnings for the
V595 , and they won't fit in the article. Very often such code is a real mistake, but thanks to luck, it works correctly.
V571 Recurring check. The 'if (rLightInfo.m_pDynTexture)' condition was already verified in line 69. ObjMan.cpp 70
There is no serious error in this code snippet, but you should not write an extra code if the corresponding checks are already included in the special macro.
Another place with extra code:
- V571 Recurring check. The 'if (m_pSectorGroups)' condition was already verified in line 48. PartitionGrid.cpp 50
V575 The memcpy function doesn't copy the whole string. Use 'strcpy / strcpy_s' function to preserve terminal null. SystemInit.cpp 4045
class CLvlRes_finalstep : public CLvlRes_base { .... for (;; ) { if (*p == '/' || *p == '\\' || *p == 0) { char cOldChar = *p; *p = 0;
This code may have an error. When copying a string, the last
terminal zero is lost. In this case, you must copy
strlen () + 1 character, or use special functions to copy strings:
strcpy or
strcpy_s .
Comma problems
V521 Such expressions using the ',' operator are dangerous. Make sure the expression! SWords [iWord] .empty (), iWord ++ 'is correct. TacticalPointSystem.cpp 3243
bool CTacticalPointSystem::Parse(....) const { string sInput(sSpec); const int MAXWORDS = 8; string sWords[MAXWORDS]; int iC = 0, iWord = 0; for (; iWord < MAXWORDS; !sWords[iWord].empty(), iWord++)
Note the section of the
for loop with counters. What does a logical expression do there? Most likely, it should be moved to another place and write like this:
for (; iWord < MAXWORDS && !sWords[iWord].empty(); iWord++) {...}
V521 Such expressions using the ',' operator are dangerous. Make sure the expression is correct. HommingSwarmProjectile.cpp 187
void CHommingSwarmProjectile::HandleEvent(....) { .... explodeDesc.normal = -pCollision->n,pCollision->vloc[0]; .... }
Another strange code snippet with the operator ','.
Suspicious conditions
V571 Recurring check. The 'if (pos == npos)' condition was already verified in line 1530. CryString.h 1539
The analyzer detected a repeated check of the
pos variable. Because of this error, part of the code is never executed. There is also duplicate code in the function, so the function should be rewritten better.
This code was successfully duplicated in another place:
- V571 Recurring check. The 'if (pos == npos)' condition was already verified in line 1262. CryFixedString.h 1271
V523 The 'then' statement is equivalent to the 'else' statement. ScriptTable.cpp 789
bool CScriptTable::AddFunction(const SUserFunctionDesc& fd) { .... char sFuncSignature[256]; if (fd.sGlobalName[0] != 0) cry_sprintf(sFuncSignature, "%s.%s(%s)", fd.sGlobalName, fd.sFunctionName, fd.sFunctionParams); else cry_sprintf(sFuncSignature, "%s.%s(%s)", fd.sGlobalName, fd.sFunctionName, fd.sFunctionParams); .... }
Regardless of the contents of the line, it is still trying to print the same way. This code is also very much in the project, here are some warnings:
- V523 The 'then' statement is equivalent to the 'else' statement. BudgetingSystem.cpp 718
- V523 The 'then' statement is equivalent to the 'else' statement. D3DShadows.cpp 627
- V523 The 'then' statement is equivalent to the 'else' statement. livingentity.cpp 967
Undefined behavior
V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. physicalplaceholder.h 25
class CPhysicalEntity; const int NO_GRID_REG = -1<<14; const int GRID_REG_PENDING = NO_GRID_REG+1; const int GRID_REG_LAST = NO_GRID_REG+2;
The analyzer is able to find several types of errors leading to undefined behavior. According to the modern standard of language, a shift of a negative number to the left leads to undefined behavior.
Here are some more questionable places:
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '~ (TFragSeqStorage (0))' is negative. UDPDatagramSocket.cpp 757
- V610 Undefined behavior. Check the shift operator '<<'. The right operand ('cpu' = [0..1023]) is the operand. CryThreadUtil_posix.h 115
- V610 Undefined behavior. Check the shift operator '>>'. The right operand is negative ('comp' = [-1..3]). ShaderComponents.cpp 399
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. trimesh.cpp 4126
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. trimesh.cpp 4559
- V610 Unspecified behavior. Check the shift operator '>>'. The left operand '-NRAYS' is negative. trimesh.cpp 4618
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. tetrlattice.cpp 324
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. tetrlattice.cpp 350
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. tetrlattice.cpp 617
- V610 Undefined behavior. Check the shift operator '<<'. The left operand '-1' is negative. tetrlattice.cpp 622
Another type of indefinite behavior is associated with repeated changes in the variable between two points of repetition:
V567 Undefined behavior. The variable "m_current" variable OperatorQueue.cpp 101
boolCOperatorQueue::Prepare(....) { ++m_current &= 1; m_ops[m_current].clear(); return true; }
Unfortunately, this place is not the only one in the code:
- V567 Undefined behavior. The 'm_commandBufferIndex' variable is modified while being used between sequence points. XConsole.cpp 180
- V567 Undefined behavior. The variable is variable. trimesh.cpp 3119
- V567 Undefined behavior. The variable is variable. trimesh.cpp 3126
- V567 Undefined behavior. I'tvtx variable boolean3d.cpp 957
- V567 Undefined behavior. I'tvtx variable boolean3d.cpp 965
- V567 Undefined behavior. I'tvtx variable boolean3d.cpp 983
- V567 Undefined behavior. The m_iNextAnimIndex variable is modified. HitDeathReactionsDefs.cpp 192
Questions to the developers
In the code of CryEngine V, I found an interesting way for developers to communicate through comments in the code.
Here is the funniest comment I found with the warning:
V763 Parameter 'enable' is always rewritten in function body before being used.
void CNetContext::EnableBackgroundPassthrough(bool enable) { SCOPED_GLOBAL_LOCK;
Then I decided to search for similar texts with the help of searching through the contents of files and wrote out several places:
....
Well and the most important question to developers: why they do not use specialized tools to improve the quality of the code? Of course, I mean PVS-Studio. :)
Once again I want to note that the article contains only a fraction of the errors. I have not watched the end of even High level warnings. So the project is still waiting for those who carefully check it. I, unfortunately, can not find so much time, dozens of other projects
are waiting for me.
Conclusion
For many years of developing a static code analyzer, I came to the conclusion that in development teams with a certain number of people it is already impossible to write code without errors. No, I am not against Code Review, but calculate for yourself how long it will take a manager to review the code of 10 people? And the next day? And more people? In such circumstances, Code Review is required when only key product components are changed. In large volumes, this approach is extremely inefficient. Automated code checking with static analyzers will help to significantly improve the situation for the better. This is not a replacement for existing tests, but a completely different approach to code quality control (by the way, the tests also contain errors using static code analysis). Correction of errors at the time of writing the code is almost worthless, in contrast to those that are at the stage of the testers, and errors in the released product already incur enormous costs.
Download and try PVS-Studio at
this link.
Discuss licensing options, prices and options for discounts you can write to us in
support .

Do not grieve the unicorn with your code ...
If you want to share this article with an English-speaking audience, then please use the link to the translation: Svyatoslav Razmyslov.
Critical errors in CryEngine V code