0 AD is a three-dimensional game in the genre of historical real-time strategy, developed by a community of volunteers. The size of the code base is small and I decided to check out the game as a holiday from large projects such as Android and XNU Kernel. So, we have a project that contains 165,000 lines of C ++ code. Let's see what's interesting in it can be found with the help of the PVS-Studio static analyzer.
0 AD
0 AD (0 AD) is a free three-dimensional game in the genre of historical real-time strategy strategy, developed by a community of volunteers (the main developers are united in the Wildfire Games team). The game allows you to control civilizations that existed in the period of 500 years BC. e. —1 year BC. e. As of the summer of 2018, the project is in alpha version. [
Description taken from Wikipedia].
Why exactly 0 AD?
I asked my colleague
Yegor Bredikhin (
EgorBredikhin ) to select and check for me some small open project with which I could work in breaks between other tasks. He sent me a log for the 0 AD project. To the question: “Why this particular project?” Was the answer: “Yes, I just played this game, a good real-time strategy.” Ok, then it will be 0 AD :).
')
Error density
Just want to praise the authors of 0 AD for good quality C ++ code. Well done, rarely able to meet such a low density of errors. Of course, not all errors are meant, but those that can be detected with PVS-Studio. As I have already said, although PVS-Studio does not find all the errors, nevertheless, we can safely talk about the relationship between the density of errors and the quality of the code as a whole.
Few numbers. The total number of non-empty lines of code is 231270. Of these, 28.7% are comments. Total, 165,000 lines of pure C ++ code.
The number of messages issued by the analyzer was small, and after reviewing them all, I wrote out 19 errors. All these errors I will discuss later in the article. Perhaps I missed something, considering the error innocuous, inaccurate code. However, in general, this picture does not change.
So, I found 19 errors on 165,000 lines of code. We calculate the density of errors: 19 * 1000/165000 = 0.115.
For simplicity, we round and assume that the PVS-Studio analyzer detects 0.1 errors per 1000 lines of code in the game code.
Excellent result! For comparison, in my recent
article on Android, I thought that I detected at least 0.25 errors per 1000 lines of code. In fact, the density of errors there is even more, I just did not find the strength to carefully analyze the entire report.
Or take for example the EFL Core Libraries library, which I carefully
studied and counted the number of defects. In it, PVS-Studio detects 0.71 errors per 1000 lines of code.
So, the authors of AD 0 are great, but for the sake of fairness, it should be noted that a small amount of code written in C ++ helps them. Unfortunately, the larger the project, the faster its complexity increases, and the error density increases nonlinearly (
more ).
Errors
Let's now look at the 19 errors I found in the game. To analyze the project, I used
PVS-Studio version 6.24. I suggest trying to download the demo version and check the projects you are working on.
Note. We are positioning PVS-Studio as a B2B solution. For small projects and individual developers, we have the free license option: "
How to use PVS-Studio for free ."
Error n1Let's start with a complex error. In fact, it is not complicated, but you have to get acquainted with a fairly large code fragment.
void WaterManager::CreateWaveMeshes() { .... int nbNeighb = 0; .... bool found = false; nbNeighb = 0; for (int p = 0; p < 8; ++p) { if (CoastalPointsSet.count(xx+around[p][0] + (yy + around[p][1])*SideSize)) { if (nbNeighb >= 2) { CoastalPointsSet.erase(xx + yy*SideSize); continue; } ++nbNeighb;
PVS-Studio
warning :
V547 CWE-570 Expression 'nbNeighb> = 2' is always false. WaterManager.cpp 581
At first glance, the message analyzer seems strange. Why is the condition
nbNeighb> = 2 always false? Indeed, in the body of the loop there is an increment of the variable
nbNeighb !
Take a look below and you will see a
break statement that interrupts the loop. Therefore, if the variable
nbNeighb is increased, the cycle will be stopped. Thus, the value of the variable
nbNeighb will never reach a value greater than 1.
The code clearly contains some kind of logical error.
N2 error void CmpRallyPointRenderer::MergeVisibilitySegments( std::deque<SVisibilitySegment>& segments) { .... segments.erase(segments.end()); .... }
PVS-Studio
warning :
V783 CWE-119 Dereferencing of the invalid iterator 'segments.end ()' might take place. CCmpRallyPointRenderer.cpp 1290
Very, very strange code. Perhaps the programmer wanted to remove the last element from the container. In this case, the code should be like this:
segments.erase(segments.end() - 1);
Although, then it would be easier to write:
segments.pop_back();
To be honest, I do not quite understand what exactly was supposed to be written here.
Error N3, N4I decided to consider two errors together, as they are related to the leakage of resources and require you first to show what the
WARN_RETURN macro
is .
#define WARN_RETURN(status)\ do\ {\ DEBUG_WARN_ERR(status);\ return status;\ }\ while(0)
So, as you can see, the
WARN_RETURN macro causes the function to exit the body. Now look at the sloppy ways to use this macro.
The first fragment.
Status sys_generate_random_bytes(u8* buf, size_t count) { FILE* f = fopen("/dev/urandom", "rb"); if (!f) WARN_RETURN(ERR::FAIL); while (count) { size_t numread = fread(buf, 1, count, f); if (numread == 0) WARN_RETURN(ERR::FAIL); buf += numread; count -= numread; } fclose(f); return INFO::OK; }
PVS-Studio
warning :
V773 CWE-401 The function was exited without releasing the 'f' handle. A resource leak is possible. unix.cpp 332
If the
fread function cannot read the data, the
sys_generate_random_bytes function
will terminate without releasing the file descriptor. In practice, this is hardly possible. It is doubtful that data from "/ dev / urandom" cannot be read. However, the code is inaccurate.
The second fragment.
Status sys_cursor_create(....) { .... sys_cursor_impl* impl = new sys_cursor_impl; impl->image = image; impl->cursor = XcursorImageLoadCursor(wminfo.info.x11.display, image); if(impl->cursor == None) WARN_RETURN(ERR::FAIL); *cursor = static_cast<sys_cursor>(impl); return INFO::OK; }
PVS-Studio warning: V773 CWE-401 The function was exited. A memory leak is possible. x.cpp 421
If the cursor cannot load, then a memory leak occurs.
Error n5 Status LoadHeightmapImageOs(....) { .... shared_ptr<u8> fileData = shared_ptr<u8>(new u8[fileSize]); .... }
PVS-Studio
warning :
V554 CWE-762 Incorrect use of shared_ptr. The memory is allocated with 'new []' will be cleaned using 'delete'. MapIO.cpp 54
The correct option is:
shared_ptr<u8[]> fileData = shared_ptr<u8>(new u8[fileSize]);
Error N6 FUTrackedPtr(ObjectClass* _ptr = NULL) : ptr(_ptr) { if (ptr != NULL) FUTracker::TrackObject((FUTrackable*) ptr); ptr = ptr; }
PVS-Studio
warning :
V570 The 'ptr' variable is assigned to itself. FUTracker.h 122
Error N7, N8 std::wstring TraceEntry::EncodeAsText() const { const wchar_t action = (wchar_t)m_action; wchar_t buf[1000]; swprintf_s(buf, ARRAY_SIZE(buf), L"%#010f: %c \"%ls\" %lu\n", m_timestamp, action, m_pathname.string().c_str(), (unsigned long)m_size); return buf; }
PVS-Studio
warning :
V576 CWE-628 Incorrect format. Consider checking the fifth argument of the swprintf_s function. The char type argument is expected. trace.cpp 93
Here we are confronted with a confusing and unintelligible history of the alternative implementation of the
swprintf function in Visual C ++. I will not
retell it, but refer to the
V576 diagnostic
documentation (see the “Wide lines” section).
In this case, most likely, this code will work correctly when compiling in Visual C ++ for Windows and incorrectly when building for Linux or macOS.
Similar error: V576 CWE-628 Incorrect format. Consider checking the fourth argument of the swprintf_s function. The char type argument is expected. vfs_tree.cpp 211
Error N9, N10, N11Classic At the beginning, the pointer is used, and only then it is checked.
static void TEST_CAT2(char* dst, ....) { strcpy(dst, dst_val);
PVS-Studio
warning :
V595 CWE-476: The 'dst' pointer was used against nullptr. Check lines: 140, 143. test_secure_crt.h 140
I think the error does not require clarification. Similar warnings:
- V595 CWE-476 The 'dst' pointer was used before it was verified against nullptr. Check lines: 150, 153. test_secure_crt.h 150
- V595 CWE-476 The 'dst' pointer was used before it was verified against nullptr. Check lines: 314, 317. test_secure_crt.h 314
Error n12 typedef int tbool; void MikkTSpace::setTSpace(...., const tbool bIsOrientationPreserving, ....) { .... m_NewVertices.push_back(bIsOrientationPreserving > 0.5 ? 1.0f : (-1.0f)); .... }
V674 CWE-682 The '0.5' literal of the 'double' type is compared to the value of the 'int' type. Consider inspecting the 'bIsOrientationPreserving> 0.5' expression. MikktspaceWrap.cpp 137
It makes no sense to compare a variable of type
int with a constant of 0.5. Moreover, by its very meaning, this is generally a boolean variable, which means comparing it with 0.5 looks quite strange. I suppose that instead of
bIsOrientationPreserving , another variable should be used here.
Error N13 virtual Status ReplaceFile(const VfsPath& pathname, const shared_ptr<u8>& fileContents, size_t size) { ScopedLock s; VfsDirectory* directory; VfsFile* file; Status st; st = vfs_Lookup(pathname, &m_rootDirectory, directory, &file, VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE);
PVS-Studio Warning: CWE-675 Destructor's
V749 Destructor will be
taken away after leaving the object's scope. vfs.cpp 165
Before you create a file, you need an object of type
ScopedLock to unlock an object. To do this, explicitly call the destructor. The trouble is that the destructor for the object
s will be automatically called again when exiting the function. Those. The destructor will be called twice. I have not studied the device class
ScopedLock , but in any case, this is not worth it. Often, such a double call to the destructor leads to undefined behavior or other unpleasant errors. Even if the code now works fine, it is very easy to break everything by changing the implementation of the
ScopedLock class.
Error N14, N15, N16, N17 CFsmEvent* CFsm::AddEvent( unsigned int eventType ) { .... pEvent = new CFsmEvent( eventType ); if ( !pEvent ) return NULL; .... }
PVS-Studio
warning :
V668 CWE-570, it’s not a problem. The exception will be generated in the case of memory allocation error. fsm.cpp 259
Checking the pointer does not make sense, since in case of a memory allocation error, the exception
std :: bad_alloc will be generated.
So, the check is superfluous, but the error is not serious. However, everything is much worse when some logic is executed in the body of the
if statement . Consider the following case:
CFsmTransition* CFsm::AddTransition(....) { .... CFsmEvent* pEvent = AddEvent( eventType ); if ( !pEvent ) return NULL;
Analyzer Warning: VW C8E-570 V668 The exception will be generated in the case of memory allocation error. fsm.cpp 289
An attempt is made to free the memory whose address is stored in the
pEvent pointer. Naturally, this will not happen and a memory leak will occur.
In fact, when I started to deal with this code, it turned out that everything is more complicated and perhaps there is not one mistake, but two. Now I will explain what is wrong with this code. To do this, we need to familiarize ourselves with the device function
AddEvent .
CFsmEvent* CFsm::AddEvent( unsigned int eventType ) { CFsmEvent* pEvent = NULL;
Note that the function does not always return a pointer to a new object created with the
new operator. Sometimes it takes an existing object from the
m_Events container. And the pointer to the newly created object will also be placed in
m_Events , by the way.
And then the question arises: who owns and must destroy the objects, the pointers to which are stored in the
m_Events container? I am not familiar with the project, but, most likely, somewhere there is code that destroys all these objects. Then deleting an object inside the
CFsm :: AddTransition function is generally redundant.
I got the impression that you can simply delete the following code fragment:
if ( !pNewTransition ) { delete pEvent; return NULL; }
Other errors:
- V668 CWE-571 has been defined as the “new” operator. The exception will be generated in the case of memory allocation error. TerrainTextureEntry.cpp 120
- The V668 CWE-571 has been defined as the “new” operator. The exception will be generated in the case of memory allocation error. SoundManager.cpp 542
Error N18, N19 static void dir_scan_callback(struct de *de, void *data) { struct dir_scan_data *dsd = (struct dir_scan_data *) data; if (dsd->entries == NULL || dsd->num_entries >= dsd->arr_size) { dsd->arr_size *= 2; dsd->entries = (struct de *) realloc(dsd->entries, dsd->arr_size * sizeof(dsd->entries[0])); } if (dsd->entries == NULL) {
PVS-Studio
warning :
V701 CWE-401 realloc () possible leak: when realloc () fails in allocating memory, original pointer 'dsd-> entries' is lost. Consider assigning realloc () to a temporary pointer. mongoose.cpp 2462
If the size of the array becomes insufficient, memory allocation is performed using the
realloc function. The error is that the value of the pointer to the original memory block is immediately overwritten by the new value returned by the
realloc function.
If the memory cannot be allocated, the
realloc function will return NULL, and this NULL will be written to the variable
dsd-> entries . After that, it will be impossible to free a block of memory whose address was previously stored in
dsd-> entries . There is a memory leak.
Another error: V701 CWE-401 realloc () possible leak: when realloc () fails in allocating memory, the original pointer 'Buffer' is lost. Consider assigning realloc () to a temporary pointer. Preprocessor.cpp 84
Conclusion
I can not say that this time the article turned out to be fascinating, or that I managed to show a lot of terrible mistakes. What to do, just once is not necessary.
I see what I write .
Thanks for attention. For a change, I’ll finish the article with an invitation to follow me on Instagram
@pvs_studio_unicorn and on Twitter
@Code_Analysis .

If you want to share this article with an English-speaking audience, then please use the link to the translation: Andrey Karpov.
Good job, authors of the game 0 AD!