CertC-CON40¶
Do not refer to an atomic variable twice in an expression
Required inputs: IR
A consistent locking policy guarantees that multiple threads cannot simultaneously access or modify shared data. Atomic variables eliminate the need for locks by guaranteeing thread safety when certain operations are performed on them. The thread-safe operations on atomic variables are specified in the C Standard, subclauses 7.17.7 and 7.17.8 [ ISO/IEC 9899:2011]. While atomic operations can be combined, combined operations do not provide the thread safety provided by individual atomic operations.
Every time an atomic variable appears on the left side of an assignment
operator, including a compound assignment operator such as
*=, an atomic write is performed on the variable. The use of the
increment (++
) or decrement
(--) operators on an atomic variable constitutes an atomic
read-and-write operation and is consequently thread-safe. Any reference of an
atomic variable anywhere else in an expression indicates a distinct atomic read
on the variable.
If the same atomic variable appears twice in an expression, then two atomic reads, or an atomic read and an atomic write, are required. Such a pair of atomic operations is not thread-safe, as another thread can modify the atomic variable between the two operations. Consequently, an atomic variable must not be referenced twice in the same expression.
Noncompliant Code Example (
atomic_bool)
This noncompliant code example declares a
shared
atomic_bool
flag variable and provides a
toggle_flag() method that negates the current value of
flag:
#include <stdatomic.h>
#include <stdbool.h>
static atomic_bool flag = ATOMIC_VAR_INIT(false);
void init_flag(void) {
atomic_init(&flag, false);
}
void toggle_flag(void) {
bool temp_flag = atomic_load(&flag);
temp_flag = !temp_flag;
atomic_store(&flag, temp_flag);
}
bool get_flag(void) {
return atomic_load(&flag);
}
Execution of this code may result in unexpected behavior because the value
of
flag is read, negated, and written back. This occurs even
though the read and write are both atomic.
Consider, for example, two threads that call
toggle_flag(). The expected effect of toggling
flag twice is that it is restored to its original value.
However, the scenario in the following table leaves
flag in the incorrect state.
toggle_flag() without Compare-and-Exchange
| Time | flag |
Thread | Action |
|---|---|---|---|
| 1 | true |
t1 | Reads the current value of
flag, which is
true, into a cache
|
| 2 | true |
t2 | Reads the current value of
flag, which is still
true, into a different cache
|
| 3 | true |
t1 | Toggles the temporary variable in the cache to
false
|
| 4 | true |
t2 | Toggles the temporary variable in the different cache to
false
|
| 5 | false |
t1 | Writes the cache variable's value to
flag
|
| 6 | false |
t2 | Writes the different cache variable's value to
flag
|
As a result, the effect of the call by t2 is not
reflected in
flag; the program behaves as if
toggle_flag() was called only once, not twice.
Compliant Solution (
atomic_compare_exchange_weak())
This compliant solution uses a compare-and-exchange to guarantee that the
correct value is stored in
flag. All updates are visible to other threads. The call
to
atomic_compare_exchange_weak() is in a loop in
conformance with .
#include <stdatomic.h>
#include <stdbool.h>
static atomic_bool flag = ATOMIC_VAR_INIT(false);
void init_flag(void) {
atomic_init(&flag, false);
}
void toggle_flag(void) {
bool old_flag = atomic_load(&flag);
bool new_flag;
do {
new_flag = !old_flag;
} while (!atomic_compare_exchange_weak(&flag, &old_flag, new_flag));
}
bool get_flag(void) {
return atomic_load(&flag);
}
An alternative solution is to use the
atomic_flag data type for managing Boolean values
atomically. However,
atomic_flag does not support a toggle operation.
Compliant Solution (Compound Assignment)
This compliant solution uses the
^= assignment operation to toggle
flag. This operation is guaranteed to be atomic, according to the
C Standard, 6.5.16.2, paragraph 3. This operation performs a
bitwise-exclusive-or between its arguments, but for Boolean arguments, this is
equivalent to negation.
#include <stdatomic.h>
#include <stdbool.h>
static atomic_bool flag = ATOMIC_VAR_INIT(false);
void toggle_flag(void) {
flag ^= 1;
}
bool get_flag(void) {
return flag;
}
An alternative solution is to use a mutex to protect the atomic operation, but this solution loses the performance benefits of atomic variables.
Noncompliant Code Example
This noncompliant code example takes an atomic global variable
n and computes
n + (n - 1) + (n - 2) + ... + 1, using the formula
n * (n + 1) / 2:
#include <stdatomic.h>
atomic_int n = ATOMIC_VAR_INIT(0);
int compute_sum(void) {
return n * (n + 1) / 2;
}
The value of
n may change between the two atomic reads of
n in the expression, yielding an incorrect result.
Compliant Solution
This compliant solution passes the atomic variable as a function argument, forcing the variable to be copied and guaranteeing a correct result. Note that the function's formal parameter need not be atomic, and the atomic variable can still be passed as an actual argument.
#include <stdatomic.h>
int compute_sum(int n) {
return n * (n + 1) / 2;
}
Risk Assessment
When operations on atomic variables are assumed to be atomic, but are not atomic, surprising data races can occur, leading to corrupted data and invalid control flow.
| Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
|---|---|---|---|---|---|
| CON40-C | Medium | Probable | Medium | P8 | L2 |
Related Guidelines
| Taxonomy | Taxonomy item | Relationship |
|---|---|---|
| CWE 2.11 | CWE-366, Race Condition within a Thread | 2017-07-07: CERT: Rule subset of CWE |
Bibliography
| [ ISO/IEC 9899:2011] | 6.5.16.2, "Compound Assignment" 7.17, "Atomics" |
Possible Messages
Key |
Text |
Severity |
Disabled |
|---|---|---|---|
atomics_used_twice |
Do not refer to an atomic variable twice in an expression. |
None |
False |
unsafe_atomic_load_store |
‘atomic_store’ after an ‘atomic_load’ is unsafe in multi-threaded environments. |
None |
False |
Options¶
This rule shares the following common options: exclude_in_macros, exclude_messages_in_system_headers, excludes, extend_exclude_to_macro_invocations, includes, justification_checker, languages, post_processing, provider, report_at, severity
The following places define options that affect this rule: Stylechecks, Analysis-GlobalOptions
statements_to_look_ahead¶
statements_to_look_ahead : int = 3
atomic_load that should be
checked. Default is 3.