原文链接:

CON54-CPP. Wrap functions that can spuriously wake up in a loop


std::condition_variable 类的 wait()wait_for()wait_until() 成员函数暂时放弃互斥量的所有权,以便其它可能正在请求互斥量的线程可以继续执行。这些函数必须始终从由被互斥量锁定保护的代码中调用。等待线程只有在被通知后才会恢复执行,通常是由另一个线程调用成员函数 notify_one()notify_all() 触发。

wait() 函数必须从检查条件谓词的循环中调用。条件谓词是由函数变量构建的表达式,必须为真, 线程才被允许继续执行。线程通过 wait()wait_for()wait_until() 或者其它机制暂停,稍后可能在条件谓词为真且线程被通知时恢复执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <condition_variable>
#include <mutex>

extern bool until_finish(void);
extern std::mutex m;
extern std::condition_variable condition;

void func(void) {
std::unique_lock<std::mutex> lk(m);

while (until_finish()) { // 谓词不成立。
condition.wait(lk);
}

// 等待条件为真时恢复。
}

通知机制通知等待线程并允许它检测它的条件谓词。在另一个线程中调用 notify_all() 不能准确地判定哪个等待线程将被恢复。条件谓词语句允许被通知的线程在接收到通知后决定是否应该恢复。

不合规代码示例

这个不合规代码示例监视一个链表,并分配一个线程在链表非空时消费其中的元素。

这个线程使用 wait() 暂停执行,并在被通知时恢复,若列表有可消费的元素。即使链表仍然为空,线程也有可能被通知,可能是因为通知线程使用了 notify_all(),它会通知所有线程。使用 notify_all() 的通知通常比使用 notify_one() 更受欢迎(有关更多信息请参见 CON55-CPP. Preserve thread safety and liveness when using condition variables )。

条件谓词通常是循环中否定的条件表达式。在这个不合规代码示例中,从链表中删除元素的条件谓词是 (list->next != nullptr),而循环条件中的条件表达式是 (list->next == nullptr)

这个不合规代码示例在 if 代码块内嵌套了 wait() 的调用,因此在接收到通知后条件谓词不为真。如果通知是虚假或恶意的,线程将会过早地唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <condition_variable>
#include <mutex>

struct Node {
void *node;
struct Node *next;
};

static Node list;
static std::mutex m;
static std::condition_variable condition;

void consume_list_element(std::condition_variable &condition) {
std::unique_lock<std::mutex> lk(m);

if (list.next == nullptr) {
condition.wait(lk);
}

// 等待条件为真时恢复。
}

合规解决方案(带谓词的显式循环)

这个合规解决方案在 while 循环中调用成员函数 wait() ,在调用 wait() 前后都进行条件判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <condition_variable>
#include <mutex>

struct Node {
void *node;
struct Node *next;
};

static Node list;
static std::mutex m;
static std::condition_variable condition;

void consume_list_element() {
std::unique_lock<std::mutex> lk(m);

while (list.next == nullptr) {
condition.wait(lk);
}

// Proceed when condition holds.
}

合规解决方案(带 lambda 谓词的隐式循环)

std::condition_variable::wait() 函数有一种重载形式,它接受一个谓词形式的函数对象。这种 wait() 的形式表现得像是通过 while(!pred()) wait(lock) 实现的。这个合规的解决方案使用一个 lambda 作为谓词并将它传递给 wait() 函数。谓词为真, 程序继续执行,这和使用显式循环谓词的合规方案中的谓词逻辑刚好相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

#include <condition_variable>
#include <mutex>

struct Node {
void *node;
struct Node *next;
};

static Node list;
static std::mutex m;
static std::condition_variable condition;

void consume_list_element() {
std::unique_lock<std::mutex> lk(m);

condition.wait(lk, []{ return list.next; });
// Proceed when condition holds.
}

Risk Assessment

Failure to enclose calls to the wait(), wait_for(), or wait_until() member functions inside a while loop can lead to indefinite blocking and denial of service (DoS).

Rule Severity Likelihood Remediation Cost Priority Level
CON54-CPP Low Unlikely Medium P2 L3

Automated Detection

Tool Version Checker Description
CodeSonar 7.3p0 LANG.STRUCT.ICOL CONCURRENCY.BADFUNC.CNDWAIT Inappropriate Call Outside Loop Use of Condition Variable Wait
Helix QAC 2023.1 C++5019
Klocwork 2023.1 CERT.CONC.WAKE_IN_LOOP
Parasoft C/C++test 2022.2 CERT_CPP-CON54-a Wrap functions that can spuriously wake up in a loop
Polyspace Bug Finder R2023a CERT C++: CON54-CPP Checks for situations where functions that can spuriously wake up are not wrapped in loop
PRQA QA-C++ 4.4 5019

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

CERT Oracle Secure Coding Standard for Java THI03-J. Always invoke wait() and await() methods inside a loop
SEI CERT C Coding Standard CON36-C. Wrap functions that can spuriously wake up in a loop
SEI CERT C++ Coding Standard CON55-CPP. Preserve thread safety and liveness when using condition variables

Bibliography

[ISO/IEC 9899:2011] 7.17.7.4, “The atomic_compare_exchange Generic Functions”
[Lea 2000] 1.3.2, “Liveness” 3.2.2, “Monitor Mechanics”

img img img