【问题标题】:Pointer addition and integer overflow with Clang 5.0 and UBsan?Clang 5.0 和 UBsan 的指针加法和整数溢出?
【发布时间】:2018-05-31 08:00:00
【问题描述】:

我试图理解我们最近在使用 Clang 5.0 和未定义行为清理器 (UBsan) 时解决的问题。我们有代码可以向前或向后处理缓冲区。简化后的大小写为similar to the code shown below

0-len 可能看起来有点不寻常,但早期的 Microsoft .Net 编译器需要它。 Clang 5.0 和 UBsan produced integer overflow findings:

adv-simd.h:1138:26: runtime error: addition of unsigned offset to 0x000003f78cf0 overflowed to 0x000003f78ce0
adv-simd.h:1140:26: runtime error: addition of unsigned offset to 0x000003f78ce0 overflowed to 0x000003f78cd0
adv-simd.h:1142:26: runtime error: addition of unsigned offset to 0x000003f78cd0 overflowed to 0x000003f78cc0
...

第 1138、1140、1142 行(和朋友)是增量,可能 由于0-len而后退。

ptr += inc;

根据Pointer comparisons in C. Are they signed or unsigned?(也讨论了C++),指针既不是有符号的也不是无符号的。我们的偏移量是无符号的,我们依靠无符号整数换行来实现反向跨步。

代码在 GCC UBsan 和 Clang 4 以及更早的 UBsan 下运行良好。我们最终使用help with the LLVM devs 为 Clang 5.0 清除了它。我们需要使用ptrdiff_t,而不是size_t

我的问题是,构造中的整数溢出/未定义行为在哪里? ptr + <unsigned> 是如何导致有符号整数溢出并导致未定义行为的?


这是一个镜像真实代码的 MSVC。

#include <cstddef>
#include <cstdint>
using namespace std;

uint8_t buffer[64];

int main(int argc, char* argv[])
{
    uint8_t * ptr = buffer;
    size_t len = sizeof(buffer);
    size_t inc = 16;

    // This sets up processing the buffer in reverse.
    //   A flag controls it in the real code.
    if (argc%2 == 1)
    {
        ptr += len - inc;
        inc = 0-inc;
    }

    while (len > 16)
    {
        // process blocks
        ptr += inc;
        len -= 16;
    }

    return 0;
}

【问题讨论】:

  • 指针本身既没有签名也没有签名。但是指针加法是有符号的,因为当指针指向数组内的某个位置时,指针可以递增或递减。根据此问题中的信息无法进一步说明,因为它不足以确定是否存在未定义的行为,除非提供 minimal reproducible example
  • 如果你有一个指向数组第三个元素的指针,你可以给它加上-1,最后得到一个指向数组第二个元素的指针。指针添加已签名。
  • 我记得几个月前(或者可能是去年)在与此 MCVE 中相同的问题中存在一个重要的实时错误

标签: c++ pointers clang undefined-behavior ubsan


【解决方案1】:

给指针加整数的定义是(N4659 expr.add/4):

我在这里使用了一张图片来保留格式(这将在下面讨论)。

请注意,这是一个新的措辞,它取代了以前标准中不太清楚的描述。

在您的代码中(当argc 为奇数时)我们最终得到的代码相当于:

uint8_t buffer[64];
uint8_t *ptr = buffer + 48;
ptr = ptr + (SIZE_MAX - 15);

对于应用于代码的标准引用中的变量,i48j(SIZE_MAX - 15)n64

现在的问题是 0 ≤ i + j ≤ n 是否为真。如果我们将“i + j”解释为 表达式 i + j 的结果,那么它等于 32,它小于 n。但如果它是指数学结果,那么它比n要大得多。

该标准在此处使用数学方程式的字体,而不使用源代码的字体。 也不是有效的运算符。所以我认为他们打算用这个方程来描述数学值,即这是未定义的行为。

【讨论】:

  • 理由:假设您在一个具有 16 位 size_t 但指针为 32 位的系统上。编译器不可能用ptr -= 16 替换ptr += 65540,因为第一个可能是最大大小对象中的有效添加
  • size_tptrdiff_t 具有不同大小的系统上,这是正确的。分段模式 8086 很有趣,因为对象可能超过 32K,但指针减法通常会产生 16 位有符号值。如果两个指针标识了一个对象中相隔 60000 字节的部分,则差值为 -5536,但将 -5536 添加到一个指针将产生另一个。
  • @M.M. - 抱歉挖了一个老问题。我不确定这是否等同于原始程序:ptr = ptr + (SIZE_MAX - 15);inc = 0-inc; 是它自己的语句并使用整数换行。 Wrap 定义明确,表达式在ptr = ptr + ... 中使用之前完成。 ptr + &lt;wrapped expression&gt; 仍在原始数组中。
【解决方案2】:

C 标准将类型 ptrdiff_t 定义为由指针差异运算符产生的类型。系统可能有 32 位 size_t 和 64 位 ptrdiff_t;这样的定义自然适合使用 64 位线性或准线性指针但确实要求单个对象每个小于 4GiB 的系统。

如果已知对象每个小于 2GiB,则存储类型为 ptrdiff_t 而不是 size_t 的值可能会使程序不必要地低效。然而,在这种情况下,代码不应使用size_t 来保存可能为负的指针差异,而应使用int32_t [如果每个对象小于 2GiB,这将足够大]。即使 ptrdiff_t 是 64 位,int32_t 类型的值在被添加或从任何指针中减去之前都会被正确地进行符号扩展。

【讨论】:

    猜你喜欢
    • 2015-03-14
    • 2018-02-02
    • 1970-01-01
    • 2020-06-12
    • 1970-01-01
    • 2022-01-07
    • 1970-01-01
    • 2016-08-05
    • 1970-01-01
    相关资源
    最近更新 更多