原文链接:

CON52-CPP. Prevent data races when accessing bit-fields from multiple threads


当访问位域时,线程可能会无意中访问相邻内存中的另一个位域。这是因为编译器在合适的情况下会将多个相邻位域存储于一个存储单元中。因此,数据竞争可能不仅存在于被多个线程访问的位域中,也可能存在于共享同一个字节或字的其他位域中。此问题难以诊断,因为多个线程同时修改同一内存区域可能不易被察觉。

在并发编程中,一种防止数据竞争的方法是使用互斥量。当所有线程都遵循互斥量规则,互斥量可以提供对共享对象安全可靠的访问。然而,当互斥量不受访问线程控制时,互斥量对其他可能被访问的对象不提供保证。不幸的是,还没有可移植的方法可以区分哪些相邻位域可能会和期望位域存储在一起。

另一种方法是在任意两个位域之间插入一个非位域成员,以确保每个位域都是其存储单元中唯一被访问的。这种技术有效地保证了没有两个位域会同时被访问。

不合规代码示例(位域)

相邻的位域可能存储在一个内存位置中。因此,在不同线程中修改相邻位域是未定义行为,如此非符合代码示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MultiThreadedFlags {
unsigned int flag1 : 2;
unsigned int flag2 : 2;
};

MultiThreadedFlags flags;

void thread1() {
flags.flag1 = 1;
}

void thread2() {
flags.flag2 = 2;
}

例如,可能存在以下指令序列:

1
2
3
4
5
6
7
8
Thread 1: register 0 = flags
Thread 1: register 0 &= ~mask(flag1)
Thread 2: register 0 = flags
Thread 2: register 0 &= ~mask(flag2)
Thread 1: register 0 |= 1 << shift(flag1)
Thread 1: flags = register 0
Thread 2: register 0 |= 2 << shift(flag2)
Thread 2: flags = register 0

合规解决方案(位域,C++11及以上版本,互斥量)

这个合规解决方案使用互斥量来保护所有对flags的访问,从而避免任何数据竞争。

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

struct MultiThreadedFlags {
unsigned int flag1 : 2;
unsigned int flag2 : 2;
};

struct MtfMutex {
MultiThreadedFlags s;
std::mutex mutex;
};

MtfMutex flags;

void thread1() {
std::lock_guard<std::mutex> lk(flags.mutex);
flags.s.flag1 = 1;
}

void thread2() {
std::lock_guard<std::mutex> lk(flags.mutex);
flags.s.flag2 = 2;
}

合规解决方案(C++11)

在这个符合解决方案中,两个线程同时修改一个结构体的两个不同的非位域成员。由于这些成员在内存中占据不同的字节,因此不需要并发保护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MultiThreadedFlags {
unsigned char flag1;
unsigned char flag2;
};

MultiThreadedFlags flags;

void thread1() {
flags.flag1 = 1;
}

void thread2() {
flags.flag2 = 2;
}

与标准的早期版本不同,C++11及其后续版本明确定义了一个内存位置,并在[intro.memory]段落4 [ISO/IEC 14882-2014]中提供以下说明:

[注释:因此,位域和相邻的非位域位于不同的内存位置,因此可以通过两个执行线程并发更新而不会产生干扰。如果一个位域声明在嵌套的结构体申明中而另一个位域不是,或者两个位域被一个零长度位域声明或非位域声明分隔开,同样适用。如果同一个结构体中的两个位域之间的所有字段都是非零宽度的位域,那么并发地更新它们是不安全的。 - 结束注释]

Risk Assessment

Although the race window is narrow, an assignment or an expression can evaluate improperly because of misinterpreted data resulting in a corrupted running state or unintended information disclosure.

Rule Severity Likelihood Remediation Cost Priority Level
CON52-CPP Medium Probable Medium P8 L2

Automated Detection

Tool Version Checker Description
Astrée 22.10 **read_write_data_race write_write_data_race ** Supported
Axivion Bauhaus Suite 7.2.0 CertC++-CON52
CodeSonar 7.3p0 CONCURRENCY.DATARACE Data Race
Coverity 6.5 RACE_CONDITION Fully implemented
Helix QAC 2023.1 C++1774, C++1775
Parasoft C/C++test 2022.2 CERT_CPP-CON52-a Use locks to prevent race conditions when modifying bit fields
Polyspace Bug Finder R2023a CERT C++: CON52-CPP Checks for data races (rule partially covered)
PRQA QA-C++ 4.4 1774, 1775 Enforced by MTA

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

SEI CERT C Coding Standard CON32-C. Prevent data races when accessing bit-fields from multiple threads

Bibliography

[ISO/IEC 14882-2014] Subclause 1.7, “The C++ memory model”

img img img