Race Condition in OS
Overview
A Race condition is a scenario that occurs in a multithreaded environment due to multiple threads sharing the same resource or executing the same piece of code. If not handled properly, this can lead to an undesirable situation, where the output state is dependent on the order of execution of the threads.
What is the Race Condition in OS?
Let us take an example of a logic gate to understand race conditions better.
Consider the logic gates A && A' as shown in the diagram. Essentially A ended with its complement should always give false.
However, let us assume that A changes its state from false to true. If there is a propagation delay to the not gate, its output will still be true (since A was initially false). This will result in a true output which is undesirable.
Race conditions result in non-deterministic output and reduce confidence in the durability of our application.
In Operating Systems, we use threads to achieve concurrency. Concurrency is the ability to perform multiple operations at the same time. We use threads to achieve concurrency in OS.
If multiple threads access shared data and are not synchronized with each other, we can face situations wherein the threads processing the shared data generate different results every time.
Examples of Race Condition in OS
Let us assume there is a producer and a consumer thread that operate on a shared queue. As soon as the producer produces one message, the consumer consumes it. This happens message by message. Consider the following 2 scenarios:
- The producer threads are faster than the consumer.
- In this case, extra messages will be produced that the consumer will not be able to consume in time.
- The consumer thread is faster than the producer.
- In this, the consumer might get the same message multiple times
Thus, the absence of synchronization between the threads operating on a common resource results in unpredictable behavior in the system.
Types of Race Conditions in OS
In programming, two main types of race conditions occur in a critical section of code, which is a section of code executed by multiple threads. When multiple threads try to read a variable and then each acts on it, one of the following situations can occur:
- Read-modify-write. This kind of race condition happens when two processes read a value in a program and write back a new value.
It often causes a software bug. Like the example above, the expectation is that the two processes will happen sequentially -- the first process produces its value and then the second process reads that value and returns a different one.
For example, if checks against a checking account are processed sequentially, the system will make sure there are enough funds in the account to process check A first and then look again to see if there are enough funds to process check B after processing check A. However, if the two checks are processed at the same time, the system may read the same account balance value for both processes and give an incorrect account balance value, causing the account to be overdrawn.
- Check-then-act. This race condition happens when two processes check a value on which they will take each take external action. The processes both check the value, but only one process can take the value with it. The later-occurring process will read the value as null. This results in a potentially out-of-date or unavailable observation being used to determine what the program will do next. For example, if a map application runs two processes simultaneously that require the same location data, one will take the value first so the other can't use it. The later process reads the data as null.
What Security Vulnerabilities Do Race Condition in OS Cause?
Race conditions cause the system to be vulnerable to attacks. Attackers could take advantage of multiple threads accessing shared data to tamper with the data.
For example, when we execute an update query in a database, the database creates a temporary file that has a snapshot of the updates.
With a well-timed attack, when this snapshot is created, an attacker can cause a race condition that manipulates the data in the snapshot file. If the snapshot holds sensitive data like user credentials, it can easily tamper with such information in the database.
How to Identify Race Conditions in OS?
Before we understand how to identify possible race scenarios, let us understand what a critical section of a code is:
Critical Section
A critical section of a code is that part that is executed by multiple threads. Different orders of execution across threads that run concurrently can lead to different outputs, which makes the critical section susceptible to a race condition.
Consider the operation of adding money to your bank account. Let us say you are doing so using two different apps simultaneously. The following steps take place while doing so:
- The app reads your current balance
- The additional amount is added to the current balance
- The bank balance is finally updated
Assume your current balance is ₹1000, and you add ₹200 from app A, and ₹500 from app B
The following race condition occurs:
- App A reads the current balance, which is ₹1000
- App A adds ₹200 to ₹1000 and gets ₹1200 as the final balance
- Meanwhile, app B fetches the current balance, which is still ₹1000, as app A has not executed step 3
- App B adds ₹500 to ₹1000 and gets ₹1500 as the final balance
- App B updates the account balance to ₹1500
- App A updates the account balance to ₹1200
Thus the final balance is ₹1200 instead of ₹1700
The above 3 steps in the app's flow belong to its critical section, as the flow allows multiple threads to access the same resource. In the above case, the bank balance variable is a shared resource
This is an example of a read-write-modify race condition.
Consider another example where we delete a key from a map. If the map is empty, then the delete operation should throw an exception.
Assume two threads, T1 and T2 are operating on the same map, which has a single element remaining, and are trying to delete elements from the map.
- T1 invokes the isEmpty() function which returns false
- T2 invokes the isEmpty() function which returns false
- T1 ends up removing the key
- T2 also tries to remove the key, and we get an exception
In a way, T1 and T2 were in a race that T2 lost.
In the snippet, the labeled section in the code snippet represents the critical section where multiple threads can access the shared resource, which is the map.
Such kinds of race conditions are called check-then-act race conditions.
How Do You Prevent Race Condition in OS?
The ideal way to prevent a race-around condition is by controlling access to the critical section of your code.
In Java, we can make use of the following constructs:
-
synchronized keyword: It is is a valuable tool in multithreaded programming, used to prevent race conditions by enforcing mutual exclusion. When a method or code block is marked as synchronized, it ensures that only one thread can access it at any given time.
This safeguard is essential for protecting shared resources and critical sections of code from concurrent access, maintaining data integrity and thread safety. However, it's crucial to use synchronization judiciously to avoid potential issues like deadlock and performance overhead, following best practices for acquiring locks in a consistent order.
-
volatile keyword: Multiple threads can change a variable's value by using the volatile keyword. Making classes thread safe is another application for it. It indicates that using a method or an instance of a class by several threads is not problematic. Both primitive types and objects are compatible with the volatile keyword.
The volatile keyword always reads the variable's value from the main memory rather than caching it. Classes and methods cannot be combined with the volatile keyword. It is utilized with variables, though. It also ensures order and visibility. It stops code from being rearranged by the compiler.
Conclusion
- Race condition in OS is an undesirable condition that happens due to interleaved processing across threads
- It usually happens due to multiple threads accessing a shared resource or executing a common code block
- Race conditions can leave the system vulnerable to security attacks where attackers can tamper with the shared data
- A critical section is a piece of code that can be accessed by multiple threads concurrently. Identifying critical sections helps in tracking down possible race scenarios
- Race conditions can be avoided by proper synchronization between threads. In Java, the usage of synchronized and volatile keywords helps us achieve the same