访问共享数据应遵循合理的同步机制
16.1 ID_dataRaces
如果一份数据同时被多个线程、进程或中断处理过程读写,会产生不确定的结果,这种情况称为“数据竞争(data race)”,会导致标准未定义的行为,应落实合理的同步机制来控制访问共享数据的先后顺序。
示例:
int foo() {
static int id = 0;
return id++; // Data races in multithreading
}
这个函数意在每次被调用都可以返回不同的整数,但如果多个线程同时执行 id++,会使读取、计算、写入等步骤交织在一起,得到错误的结果,这是一种典型的数据竞争。
应改为:
int foo() {
static atomic<int> id(0);
return id.fetch_add(1); // OK
}
其中 atomic 是 C++ 标准原子类,fetch_add 将对象持有的整数增 1 并返回之前的值,这个过程不会被多个线程同时执行,只能依次执行,从而保证了返回值的唯一性和正确性。
对共享数据访问次序的控制称为“同步(synchronization)”,可使用锁、条件变量、原子操作等方法实现对线程的同步。与共享数据相关,但未落实同步机制的函数不应在多线程环境中使用,如:
asctime // use asctime_r or asctime_s instead
ctime // use ctime_r or ctime_s instead
localtime // use localtime_r or localtime_s instead
gmtime // use gmtime_r or gmtime_s instead
strtok // use strtok_r or strtok_s instead
strerror // use strerror_r or strerror_s instead
tmpnam // use tmpnam_r or tmpnam_s instead
setlocale // use mutex to protect multithreaded access
rand, srand // use random, srandom or BCryptGenRandom instead
与线程同步不同,中断处理过程的同步较为特殊,可参见 ID_sig_dataRaces 的进一步讨论。
考虑比数据竞争更高层面的问题,如果程序的正确性依赖进线程处理数据的特定时序,一旦这种特定时序被打破便会产生错误或漏洞,攻击者可以抢在某关键过程前后通过修改共享数据达到攻击目的,这种情况称为“竞态条件(race condition)”,如:
int* p = get_shared(); // #0, ‘p’ points to shared data
if (*p == 0) { // #1, ‘*p’ is unreliable
....
}
else if (*p == 1) { // #2, ‘*p’ is unreliable
....
}
else { // #3
....
}
如果 p 指向共享数据,那么攻击者可以通过修改共享数据实现对程序流程的劫持,比如在 #0 处 *p 的值本为 0,攻击者在 #1 之前改变 *p 的值,迫使流程向 #2 或 #3 处跳转。
关于竞态条件的进一步讨论可参见 ID_TOCTOU、ID_forbidSignalFunction 等规则。
相关
依据
ISO/IEC 9899:2011 5.1.2.4(3)-undefined
ISO/IEC 9899:2011 5.1.2.4(20)-undefined
ISO/IEC 9899:2011 5.1.2.4(25)-undefined