libstdc++ std::random 上未定义的行为(根据 clang -fsanitize=integer),由



我在 Ubuntu 20.04 LTS 上使用 clang++ 10,带有-fsanitize-undefined-trap-on-error -fsanitize=address,undefined,nullability,implicit-integer-truncation,implicit-integer-arithmetic-value-change,implicit-conversion,integer

我的代码正在生成随机字节

std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<uint8_t> dd(0, 255);
...
ch = uint8_t(dd(gen));

最后一行导致清理器报告未定义的行为,以 bits/random.tcc 为单位

template<...> void  mersenne_twister_engine<...>::
_M_gen_rand(void)   {
const _UIntType __upper_mask = (~_UIntType()) << __r;
const _UIntType __lower_mask = ~__upper_mask;
for (size_t __k = 0; __k < (__n - __m); ++__k)
{
_UIntType __y = ((_M_x[__k] & __upper_mask)
| (_M_x[__k + 1] & __lower_mask));
_M_x[__k] = (_M_x[__k + __m] ^ (__y >> 1)
^ ((__y & 0x01) ? __a : 0));
}
for (size_t __k = (__n - __m); __k < (__n - 1); ++__k)
{
_UIntType __y = ((_M_x[__k] & __upper_mask)
| (_M_x[__k + 1] & __lower_mask));
_M_x[__k] = (_M_x[__k + (__m - __n)] ^ (__y >> 1)  <<<<===== this line
^ ((__y & 0x01) ? __a : 0));
}
_UIntType __y = ((_M_x[__n - 1] & __upper_mask)
| (_M_x[0] & __lower_mask));
_M_x[__n - 1] = (_M_x[__m - 1] ^ (__y >> 1)
^ ((__y & 0x01) ? __a : 0));
_M_p = 0;
}

错误如下:

/usr/include/c++/10/bits/random.tcc:413:33: runtime error: unsigned integer overflow: 397 - 624 cannot be represented in type 'unsigned long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /usr/include/c++/10/bits/random.tcc:413:33 in 
/usr/include/c++/10/bits/random.tcc:413:26: runtime error: unsigned integer overflow: 227 + 18446744073709551389 cannot be represented in type 'unsigned long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /usr/include/c++/10/bits/random.tcc:413:26 in

似乎存在一个明显为负数的差__m-__n == 397 - 624,但操作数都是无符号的。

减去的变量是定义为size_t __n, size_t __m的模板参数,因此这不是随机边缘情况,而是正在实现的实际模板。

这是 STL 实现中的错误还是我的用法错误?

一个最小的可重现示例:https://godbolt.org/z/vvjWscPnj

>更新:提交给 GCC https://gcc.gnu.org/bugzilla/show_bug.cgi?id=106469 的问题(不是错误) - 作为"不会修复"关闭

GCC的团队称为clang的ubsan无符号整数溢出检查不良做法,因为该行为在ISO C++中是明确定义的(作为模包装)。尽管在PRNG中使用模数算法,但在这种特殊情况下并非如此。

然而,在大多数用户空间代码中,无符号溢出几乎总是要捕获的错误,而GCC STL上的这个非错误会阻止用户从这个有用的检查中受益。

尽管另一个答案表明使用模板参数实例化std::uniform_int_distribution是每个标准的未定义行为uint8_t但此处的 UBsan 警告与此无关。

UBSan正在标记Mersenne twister本身的实现,但该实现没有任何未定义的行为或错误。

如果你仔细观察,你会发现冒犯性的表达是

_M_x[__k + (__m - __n)]

其中__k是通过for循环从(__n - __m)(__n - 1)范围内的值。

这些操作中涉及的所有类型都是无符号std::size_t的。因此,这些运算都使用模算术,因此即使__m - __n是负数并且不能在无符号类型中表示,结果

__k + (__m - __n)

将位于0__m - 1之间,因此用它索引数组不是问题。不涉及未定义的行为、未指定的行为或实现定义的行为。

标记此行为的 UBSan 检查不会标记实际的未定义行为。如果有人意识到这一点,完全可以依赖像这样的无符号算术的环绕行为。未签名的溢出检查仅用于标记此类环绕的实例,而不是故意的。您不应该在可能依赖它的其他人的代码上使用它,或者如果您可能依赖它,则不应该在您自己的代码上使用它。

-fsanitize=address,undefined,nullability,implicit-integer-truncation,implicit-integer-arithmetic-value-change,implicit-conversion,integeraddressundefined之外的所有检查中,UBsan检查不是标记实际的未定义行为,而是在许多情况下可能是无意的条件。由于上述原因,默认的-fsanitize=undefined清理器标志默认不启用无符号整数溢出检查。有关详细信息,请参阅 https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html。

std::uniform_int_distribution中使用uint8_t的结果是未定义的,因此:

std::uniform_int_distribution<uint8_t> dd(0, 255); // Don't do this!

您可以使用shortintlonglong longunsigned shortunsigned intunsigned longunsigned long long中的任何一种。

引用自rand.req.gen/1.5

在整个子句 [rand] 中,实例化具有名为IntType的模板类型参数的模板:
的效果是未定义的,除非相应的模板参数是 cv 非限定的,并且是shortintlonglong longunsigned shortunsigned intunsigned longunsigned long long之一。

如果这没有帮助,请跳过-fsanitize=integer选项,因为

-fsanitize=integer:检查未定义或可疑的整数行为(例如无符号整数溢出)。启用signed-integer-overflow

。无符号整数溢出没有未定义的行为。使用-fsanitize=undefined将自动启用有符号整数溢出检查,因此您不必单独启用它。

如果这仍然没有帮助,则可能是clang++使用的 gcc 库实现中的错误导致了这种情况。您可以尝试使用clang++的库实现,看看是否有帮助:

clang++ -stdlib=libc++ ...

unsigned类型在C++中具有明确定义的包装行为。这就是为什么它们被用于PRNG和其他位操作用例的原因之一,这些用例是需要和期望的(并且是算法所必需的),而不是错误。

GCC 开发人员是对的:将所有未签名的包装视为问题是不合理的。打印出它是"未定义的行为"而不是可能的问题更不合理。如果 clang 的 ubsan 一开始就告诉你它在 C++ 中定义得很好,也许是有意的,你就不必用对他们没有用的错误报告来打扰 GCC 开发人员。 或者,您可以在了解问题后将其表述为功能请求。

但你也是对的:在标头中的库函数中,它们成为你自己代码的一部分,这使得当库代码(例如这个 PRNG)与你自己的代码分离时,当它内联到同一个编译单元中时,很难将库代码(例如这个 PRNG)与你自己的代码分开。 ubsan选项是按文件进行的。


libc++ 的 mt19937 实现在必要时禁用 ubsan 检查。 它是作为LLVM的一部分开发的C++标准库的最新实现,主要与clang一起使用。 如果任何标头库要迎合这个将一些有效的C++操作标记为问题的清理器,那就是libc++。 https://godbolt.org/z/aeY5Yn9c6 表明,将-stdlib=libc++添加到 Godbolt 上的编译选项中可以让您的测试用例干净地运行。 您必须在本地安装它才能实际使用它。

libc++ 将预处理器宏_LIBCPP_DISABLE_UBSAN_UNSIGNED_INTEGER_CHECK定义为__attribute__((__no_sanitize__("unsigned-integer-overflow")))(如果支持),因此它可以基于每个函数禁用它。 例如,请参阅 libcxx 的<utility>标头,其中各种函数使用该标签,并在<random>mersenne_twister_engine<...>::seed()。 但有趣的是,它并没有在任何地方使用它,因此您仍然可以获得溢出检查的好处。

或者,您可以围绕随机数生成编写一个包装器函数,并将其放在单独的.cpp中,而无需sanitize=integer在带有-flto的发布版本中,它仍然可以内联。 或者,如果您不需要高质量的随机性,请使用 libcrandom(3);它是单独编译的,而不是内联标头。 Linux的random()并不可怕,尽管也不是很好。 其他 PRNG 如 xorshift/xoroshiro 既快速又好,但也将使用unsigned类型并依靠它们的包装进行乘法和/或加/子,除非它们只像 LFSR 那样使用移位和异或。


在 ISO C++ 中,无法仅将某些未签名的操作标记为预期的包装。

至少有一种语言Rust确实解决了这个问题:对于任何整数类型(包括有符号和无符号)的纯+-*/等,值范围的溢出总是一个错误。 您可以使用 x.wrapping_sub(y) 进行有符号或无符号减法,并具有明确定义的环绕。 同样适用于 add/mul/div/rem/shift/pow。 还有saturating_sub/添加/等,还有overflowing_...返回包装后的结果和一个布尔值,或返回可以是 None 而不是保存整数的类型的 checked_add/sub/etc。 因此,如果您想对整数溢出大惊小怪,Rust 可能是适合您的语言。

(如果LLVM的后端检查未签名溢出部分是由Rust驱动的,并且有人认为有时公开它以供C++使用可能会很有用,我不会感到惊讶。 但是,在编写代码时,预计会出现误报,而不是考虑到该检查器。


GNU C 整数包装溢出扩展

GCC/Clang 和其他理解 C 和 C++ 的 GNU 方言的编译器内置了整数溢出。 这包括signedunsigned包装添加/子/mul。 但仅适用于(未签名)int/long/long long;你必须弄清楚在libstdc++中用于size_t哪一个。 (例如,在Windows x64上,size_t必须long long,但在x86-64系统V上它是long)

unsigned long wrapping_sub(unsigned long x, unsigned long y)
{
// return x - y;       // ISO C++ without working around sanitize=integer
unsigned long res;
bool borrow = __builtin_usubl_overflow(x, y, &res);
return res;
}

Godbolt 上的测试用例表明,__builtin_usubl_overflow确实安全地执行了1UL, 2UL的包装减法。 (制作甚至不尝试检测包装的 asm,因为我们已经告诉编译器这一次操作不是错误。 取消注释return x-y;确实会捕获溢出。

在标准库代码中,将它用于每个未签名的操作会非常麻烦,其中包装不是错误,这就是为什么 libc++ 在每个函数的基础上禁用未签名包装清理器的原因。


由于无符号数学被明确定义为包装,使用这些 GNU C 内置的无符号版本的正常原因是捕获进位/借用输出,以便您知道它们是否包装。 与其使用 clang 的sanitize=integer,不如在自己的unsigned操作中使用这些函数,并assert()布尔结果为 false(无包装溢出)。

最新更新