A race condition occurs when two or more threads can access shared data and they try to change it at the same time. Because the thread scheduling algorithm can swap between threads at any time, you don't know the order in which the threads will attempt to access the shared data. Therefore, the result of the change in data is dependent on the thread scheduling algorithm, i.e. both threads are "racing" to access/change the data.
Blow are some examples of race condition:
Deadlocks
Shared Resource
Temp Files in Shell Scripts
Time of Check to Time of Use
Unsafe Functions in Signal Handlers
Deadlock in a multi-threaded program
Definitions: |
|||||||||
Mutex - A mechanism supplied by threading libraries that can be used to make a block of code be executed atomically.
Deadlock - Locking resources such that they will never become unlocked, causing multiple threads of execution to stop.
|
|||||||||
What You Should Not Do |
|||||||||
Causing a deadlock to occur. They can be incredibly difficult to detect in situations where many mutexes/semaphors are used. mutex A; mutex B; void thread1() { do something... lock(A); lock(B); do something... unlock(B); unlock(A) } void thread2() { do something... lock(B); lock(A); do something... unlock(A); unlock(B); } The above example is a trivialization of what could go wrong. If the following sequence of events were to happen, the above threads would go into a deadlock: thread1: lock(A); thread2: lock(B); // no deadlock yet thread2: lock(A); // thread2 is blocked until A is unlocked thread1: lock(B); // thread1 is blocked until B is unlocked, but thread2 // won't unlock B until A is unlocked, because thread1 // locked A. Thus, neither thread of execution can proceed |
|||||||||
What You Should Do |
|||||||||
Deadlocks in more complicated cases can be difficult to detect. About the only way to prevent them is to design the code to never let it happen. The above example could be fixed by replacing both mutexes with a single mutex, assuming there isn't an important reason for having the two mutexes. Design strategies usually recommend minimizing the amount of code that needs the mutexes' protection, and isolating it. This is the basic idea behind monitors.
|
Accessing shared resources (unlocked) in the multi-threaded program
What You Should Not Do |
|||||
You need to access a shared variable in a multithreaded program, so you access the value directly, without any sort of locking. For example: transferAmount = GetTransferAmount(); balance = GetBalanceFromDatabase(); if (transferAmount < 0) { printf("Bad Transfer Amount\n"); exit(0); } newBalance = balance - transferAmount; if ((balance - transferAmount) < 0) { printf("Insufficient Funds\n"); newBalance = balance; } SendNewBalanceToDatabase(newBalance); printf("Transfer of %f succeeded.\n",transferAmount); NotifyUser("New balance: %f\n", newBalance); In this example, we have no assurance that one instance of this user will get the correct transferAmount in either of the if-statements. Since, these statements control the flow of our program (and determine how much money is passed around) we could get situations where one instance transfers all of the money while another instance still has a full balance. |
|||||
What You Should Do |
|||||
If a resource is shared between threads and each thread has a function that may change the resource state, you need a way to ensure that that resource will remain in the state expected by each state. This can be achieved by using synchronization primitives when available (such as 'synchronized' in Java), mutexes and atomic operations, or simply avoiding sharing resources across threads. Taking our example from above: transferAmount = GetTransferAmount(); if( accountBusy ) wait accountFree else { signal accountBusy balance = GetBalanceFromDatabase(); if (transferAmount < 0) { printf("Bad Transfer Amount\n"); exit(0); } newBalance = balance - transferAmount; if ((balance - transferAmount) < 0) { printf("Insufficient Funds\n"); newBalance = balance; } SendNewBalanceToDatabase(newBalance); signal accountFree } printf("Transfer of %f succeeded.\n",transferAmount); NotifyUser("New balance: %f\n", newBalance); Since we ensure that the shared resources can only be accessed by one thread at a time, we ensure that the state remains consistent between threads.
|
Avoid race condition while writing to a file using shell script
What You Should Not Do |
|||||
NOTE! THIS FILE NEEDS TO BE REDONE!
#!/bin/sh echo "This is a test" > /tmp/test$$ # Someone can manipulate or alter the file between these calls cat < /tmp/test$$ |
|||||
What You Should Do |
|||||
Use pipelines or your home directory. Ordinary shells usually don't support file descriptors.
#!/bin/sh echo "This is a test" | cat
|
Use the file descriptor to avoid race conditions
Definitions: |
|||||
Time of Check to Time of Use - (TOCTTOU − pronounced "TOCK too") A race condition where a problem/attack occurs between when a resource is checked and when it is actually used.
|
|||||
What You Should Not Do |
|||||
You check a file to see if it meets some criteria before you open and use it. Meanwhile, the file is replaced with another file, so the check is no longer valid.
struct stat mystat; int myfd; if(stat("filename",&mystat) != 0) // get the file's info to check error stat()'ing the file!
do checks on the file's properties // the file could be swapped out/ // modified in here if(the checks are ok) { if((myfd=open("filename") != 0) // open the file (use it) error getting fd to file!
do something with the file // use the file } |
|||||
What You Should Do |
|||||
Some systems allow you to lock the file on the operating system level. Many systems allow you to lock a file, but the lock is only honored by programs that respect the lock. This includes UNIX and the Mac OS. Depending on the application, it could be solved by opening a pointer to the file and doing all checks from that instead of the file name. That way, if the file gets swapped out for another file, the file pointer/descriptor still refers to the same object. (which the file name does not necessarily point to)
struct stat mystat; int myfd; if((myfd=open("filename", "r")) != 0) //open the file to get a file descriptor error getting fd to file! if(fstat(myfd,&mystat) != 0) // fstat(): get the file's info // using the fd error stat'ing the file!
do checks on the file's properties // if someone swaps in their own // file, it won't sneak in on the // program anymore if(the checks are ok) { do something with the file // use the file } This change will ensure that the file is the same file between the check and use. (It prevents someone from swapping in another file) It will not prevent someone changing the file's permissions to allow someone else to read/write the file between the check and use.
|
Unsafe functions cause race conditions
Definitions: |
|
Unsafe function - A function that cannot be interrupted during use, and then called again; in other words, it is non-reentrant. This can be due to use of global variables, or due to calls to
malloc() and free() . |
|
What You Should Not Do |
|
Using an unsafe function non-atomically in a program that has signal handlers. For example, calling free() on certain global variables, as well as calling unsafe functions that utilize malloc() /free() , such as syslog() from a signal handler. If these calls get interrupted and called again, it is possible for them to execute arbitrary code. |
|
What You Should Do |
|
Not using unsafe function calls at all in signal handlers is safest. Executing every use of a particular unsafe function atomically by blocking signals that call handlers that could cause them to be reentered may also be safe. |