Malicious Race Conditions and Data Races

Contents[Show]

This post is about malicious race conditions and data races. Malicious race conditions are race conditions that cause the breaking of invariants, blocking issues of threads, or lifetime issues of variables.

At first, let me remind you, what a race condition is.

• Race condition: A race condition is a situation, in which the result of an operation depends on the interleaving of certain individual operations.

That's fine as starting point. A race condition can break the invariant of a program.

Breaking of invariants

In the last post Race Conditions and Data Races, I use the transfer of money between two accounts to show a data race. There was a benign race condition involved. To be honest, there was also a malicious race condition.

The malicious race condition breaks an invariant of the program. The invariant is, that the sum of all balances should always have the same amount. Which is in our case is 200 because each account starts with 100 (1). For simplicity reason, the unit should be euro. Neither I want to create money by transferring it nor I want to destroy it.

```// breakingInvariant.cpp

#include <atomic>
#include <functional>
#include <iostream>

struct Account{
std::atomic<int> balance{100};                               // 1
};

void transferMoney(int amount, Account& from, Account& to){
using namespace std::chrono_literals;
if (from.balance >= amount){
from.balance -= amount;
to.balance += amount;
}
}

void printSum(Account& a1, Account& a2){
std::cout << (a1.balance + a2.balance) << std::endl;         // 3
}

int main(){

std::cout << std::endl;

Account acc1;
Account acc2;

std::cout << "Initial sum: ";
printSum(acc1, acc2);                                        // 4

std::cout << "Intermediate sum: ";
std::thread thr3(printSum, std::ref(acc1), std::ref(acc2));  // 5

thr1.join();
thr2.join();
thr3.join();
// 6
std::cout << "     acc1.balance: " << acc1.balance << std::endl;
std::cout << "     acc2.balance: " << acc2.balance << std::endl;

std::cout << "Final sum: ";
printSum(acc1, acc2);                                        // 8

std::cout << std::endl;

}
```

At the beginning, the sum of the accounts is 200 euro. (4) display the sum by using the function printSum (3). Line (5) makes the invariant visible. Because there is a short sleep of 1ns in line (2), the intermediate sum is 182 euro. In the end, all is fine. Each account has the right balance (6) and the sum is 200 euro (8).

Here is the output of the program.

The malicious story goes on. Let's create a deadlock by using conditions variables without a predicate.

Modernes C++ Mentoring

Stay informed about my mentoring programs.

Subscribe via E-Mail.

Blocking issues with race conditions

Only to make my point clear. You have to use a condition variable in combination with a predicate. For the details read my post Condition Variables. If not, your program may become the victim of a spurious wakeup or lost wakeup.

If you use a condition variable without a predicate, it may happen that the notifying thread sends it notification before the waiting thread is in the waiting state. Therefore, the waiting thread waits forever. That phenomenon is called a lost wake-up.

Here is the program.

```// conditionVariableBlock.cpp

#include <iostream>
#include <condition_variable>
#include <mutex>

std::mutex mutex_;
std::condition_variable condVar;

void waitingForWork(){

std::cout << "Worker: Waiting for work." << std::endl;

std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck);                           // 3
// do the work
std::cout << "Work done." << std::endl;

}

std::cout << "Sender: Data is ready."  << std::endl;
condVar.notify_one();                        // 1

}

int main(){

std::cout << std::endl;

t1.join();
t2.join();

std::cout << std::endl;

}
```

The first invocations of the program work fine. The second invocation locks because the notify call (1) happens before the thread t2 (2) is in the waiting state (3).

Of course, deadlocks and livelocks are other effects of race conditions. A deadlock depends in general on the interleaving of the threads and my sometimes happen or not. A livelock is similar to a deadlock. While a deadlock blocks, I livelock seems to make progress. The emphasis lies on seems. Think about a transaction in a transactional memory use case. Each time the transaction should be committed, a conflict happens. Therefore a rollback takes place. Here is my post about Transactional Memory.

Showing lifetime issues of variables is not so challenging.

The recipe of a lifetime issue is quite simple. Let the created thread run in the background and you are half done. That means the creator thread will not wait until its child is done. In this case, you have to be extremely careful that the child is not using something belonging to the creator.

```// lifetimeIssues.cpp

#include <iostream>
#include <string>

int main(){

std::cout << "Begin:" << std::endl;            // 2

std::thread t([&mess]{ std::cout << mess << std::endl;});
t.detach();                                    // 1

std::cout << "End:" << std::endl;              // 3

}
```

This is too simple. The thread t is using std::cout and the variable mess. Both belong to the main thread. The effect is that we don't see the output of the child thread in the second run. Only "Begin:" (2) and "End:" (3) are displayed.

I want to emphasise it very explicitly. All the programs in this post are up to this point without a data race. You know it was my idea to write about race conditions and data races. They are a related, but different concept.

I can even create a data race without a race condition.

A data race without a race condition

But first, let me remind you, what a data race is.

• Data race: A data race is a situation, in which at least two threads access a shared variable at the same time. At least one thread tries to modify the variable.

```// addMoney.cpp

#include <functional>
#include <iostream>
#include <vector>

struct Account{
int balance{100};                              // 1
};

to.balance += amount;                          // 2
}

int main(){

std::cout << std::endl;

Account account;

// 3

// 4
std::cout << "account.balance: " << account.balance << std::endl;

std::cout << std::endl;

}
```

100 threads are adding 50 euro (3) to the same account (1). They use the function addMoney. The key observation is, that the writing to the account is done without synchronisation. Therefore we have a data race and no valid result. That is undefined behaviour and the final balance (4) differs between 5000 and 5100 euro.

What's next?

I often hear at concurrency conference discussions about the terms non-blocking, lock-free, and wait-free. So let me write about these terms in my next post.

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, and Dominik Vošček.

Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.

My special thanks to PVS-Studio

My special thanks to Tipi.build

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

• C++ - The Core Language
• C++ - The Standard Library
• C++ - Compact
• C++11 and C++14
• Concurrency with Modern C++
• Design Pattern and Architectural Pattern with C++
• Embedded Programming with Modern C++
• Generic Programming (Templates) with C++

New

• Clean Code with Modern C++
• C++20

Subscribe to the newsletter (+ pdf bundle)

 Email Please enable the javascript to submit this form

Visitors

Today 4902

Yesterday 7929

Week 20719

Month 164890

All 11646044

Currently are 322 guests and no members online

• How can you recognise a good software architecture?

Modern code: "Naming is hard", so bad names are used. Comments forget to comment so are removed.

• How can you recognise a good software architecture?

Code should be as expressive as possible and ideally should be readable without any comments. As ...

• How can you recognise a good software architecture?

I began developing software in the late 1960's and have been at it ever since. The one thing missing ...

• The C++ Standard Library: The Fourth Edition includes C++23

Hi, In India from where do I buy hard copy of The C++ Standard Library: The Fourth Edition includes C++23 ...

• The Iterator Protocol

I think an std::forward_iterator has to return a reference from it's operator* (). This would mean a ...