原文链接:

CON55-CPP. Preserve thread safety and liveness when using condition variables

C++标准库提供递归和非递归互斥量类型用于保护临界区。递归互斥体类(std::recursive_mutex 和 std::recursive_timed_mutex)与非递归互斥量(std::mutexstd::timed_mutexstd::shared_timed_mutex)的不同之处在于,递归互斥量可以被当前拥有互斥量线程递归地锁定。所有互斥量类型都具有对互斥量进行尝试锁定的能力,通过诸如 try_lock()try_lock_for()try_lock_until()try_lock_shared_for()try_lock_shared_until() 等函数实现。这些尝试锁定函数试图为当前线程获取互斥量的所有权,但在无法获得所有权的情况下不会阻塞。相反,它们会返回一个布尔值,表明是否已经获得了互斥量的所有权。

C++标准中的[thead.mutex.requirements.mutex] 第14和15段(ISO/IEC 14882-2014)规定如下:

表达式 m.try_lock() 应该是形式完备的,并具有以下语义:
要求:如果 m 是类型 std::mutexstd::timed_mutexstd::shared_timed_mutex,则调用线程不拥有互斥体。

此外,[thread.timedmutex.class]第3段部分规定如下:

如果一个拥有timed_mutex对象的线程调用该对象的lock()try_lock()try_lock_for()try_lock_until(),程序的行为未定义。

最后,[thread.sharedtimedmutex.class]第3段部分规定如下:

如果一个线程试图递归地获取shared_timed_mutex的任何所有权,则程序的行为未定义。

因此,试图对已经被当前线程拥有所有权的非递归互斥量对象进行尝试锁定是未定义行为。不要对那些已经被线程拥有所有权的非递归互斥量对象上调用 try_lock()try_lock_for()try_lock_until()try_lock_shared_for()try_lock_shared_until()

不合规代码示例

在下面这个不合规代码示例代码中,互斥量 m 被线程的初始入口点锁定,然后在同一线程的 do_work() 函数中再尝试尝试锁定,导致未定义行为,因为它不是一个递归互斥量 。这通常可能导致死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#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();
}

合规解决方案

这个合规方案从线程的初始入口点中删除了锁定,允许对互斥体进行尝试锁定,但不允许递归。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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

Automated Detection

Tool Version Checker Description
CodeSonar 7.3p0 CONCURRENCY.TL Try-lock that will never succeed
Helix QAC 2023.1 C++4986, C++4987
Parasoft C/C++test 2022.2 CERT_CPP-CON56-a Avoid double locking

Search for vulnerabilities resulting from the violation of this rule on the CERT website.

MITRE CWE CWE-667, Improper Locking

Bibliography

[ISO/IEC 14882-2014] Subclause 30.4.1, “Mutex Requirements”

img img img