CertC++-CON56¶
Do not speculatively lock a non-recursive mutex that is already owned by the calling thread
Required inputs: IR
The C++ Standard Library supplies both recursive and non-recursive mutex
classes used to protect
critical
sections. The recursive mutex classes (
std::recursive_mutex and
std::recursive_timed_mutex) differ from the non-recursive mutex
classes (
std::mutex,
std::timed_mutex, and
std::shared_timed_mutex) in that a recursive mutex may be locked
recursively by the thread that currently owns the mutex. All mutex classes
support the ability to speculatively lock the mutex through functions such
as
try_lock(),
try_lock_for(),
try_lock_until(),
try_lock_shared_for(), and
try_lock_shared_until(). These speculative locking functions
attempt to obtain ownership of the mutex for the calling thread, but will not
block in the event the ownership cannot be obtained. Instead, they return a
Boolean value specifying whether the ownership of the mutex was obtained or
not.
The C++ Standard, [thread.mutex.requirements.mutex], paragraphs 14 and 15 [ ISO/IEC 14882-2014], state the following:
The expression
m.try_lock()shall be well-formed and have the following semantics:
Requires: Ifmis of typestd::mutex,std::timed_mutex, orstd::shared_timed_mutex, the calling thread does not own the mutex.
Further, [thread.timedmutex.class], paragraph 3, in part, states the following:
The behavior of a program is undefined if:
- a thread that owns atimed_mutexobject callslock(),try_lock(),try_lock_for(), ortry_lock_until()on that object
Finally, [thread.sharedtimedmutex.class], paragraph 3, in part, states the following:
The behavior of a program is undefined if:
- a thread attempts to recursively gain any ownership of ashared_timed_mutex.
Thus, attempting to speculatively lock a non-recursive mutex object that is
already owned by the calling thread is
undefined
behavior. Do not call
try_lock(),
try_lock_for(),
try_lock_until(),
try_lock_shared_for(), or
try_lock_shared_until() on a non-recursive mutex object from a
thread that already owns that mutex object.
Noncompliant Code Example
In this noncompliant code example, the mutex
m is locked by the thread's initial entry point and is
speculatively locked in the
do_work() function from the same thread, resulting in undefined
behavior because it is not a recursive mutex. With common implementations, this
may result in
deadlock.
#include <mutex>
#include <thread>
std::mutex m;
void do_thread_safe_work();
void do_work() {
while (!m.try_lock()) {
// The lock is not owned yet, do other work while waiting.
do_thread_safe_work();
}
try {
// The mutex is now locked; perform work on shared resources.
// ...
// Release the mutex.
catch (...) {
m.unlock();
throw;
}
m.unlock();
}
void start_func() {
std::lock_guard<std::mutex> lock(m);
do_work();
}
int main() {
std::thread t(start_func);
do_work();
t.join();
}
Compliant Solution
This compliant solution removes the lock from the thread's initial entry point, allowing the mutex to be speculatively locked, but not recursively.
#include <mutex>
#include <thread>
std::mutex m;
void do_thread_safe_work();
void do_work() {
while (!m.try_lock()) {
// The lock is not owned yet, do other work while waiting.
do_thread_safe_work();
}
try {
// The mutex is now locked; perform work on shared resources.
// ...
// Release the mutex.
catch (...) {
m.unlock();
throw;
}
m.unlock();
}
void start_func() {
do_work();
}
int main() {
std::thread t(start_func);
do_work();
t.join();
}
Risk Assessment
Speculatively locking a non-recursive mutex in a recursive manner is undefined behavior that can lead to deadlock.
| Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
|---|---|---|---|---|---|
| CON56-CPP | Low | Unlikely | High | P1 | L3 |
Related Guidelines
| MITRE CWE | CWE-667, Improper Locking |
Bibliography
| [ ISO/IEC 14882-2014] | Subclause 30.4.1, "Mutex Requirements" |
Possible Messages
Key |
Text |
Severity |
Disabled |
|---|---|---|---|
non_recursive_lock_add |
Non-recursive lock is acquired while it is already locked. |
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
enter_critical_functions¶
enter_critical_functions
Set of function names to enter a critical region.Type: set[bauhaus.analysis.config.QualifiedName]
Default:
{'mtx_lock', 'std::_Mutex_base::lock', 'std::_Mutex_base::try_lock', 'std::lock_guard::lock_guard', 'std::mutex::lock', 'std::mutex::try_lock'}
enter_critical_macros¶
enter_critical_macros : set[bauhaus.analysis.config.MacroName] = set()
exit_critical_functions¶
exit_critical_functions
Set of function names to exit a critical region.Type: set[bauhaus.analysis.config.QualifiedName]
Default:
{'mtx_unlock', 'std::_Mutex_base::unlock', 'std::lock_guard::~lock_guard', 'std::mutex::unlock'}
exit_critical_macros¶
exit_critical_macros : set[bauhaus.analysis.config.MacroName] = set()
nested_critical_regions¶
nested_critical_regions : bool = True