openlang

Why did the compiler delete my signed overflow check?

if (x + 1 < x) on a signed int is not an overflow guard. Signed overflow is undefined behavior in C, so the compiler may assume x + 1 never wraps, conclude the condition is false, and drop the branch. Mainstream compilers do exactly that once optimization is on.

This is a security problem because the vanishing check is usually the one in front of a length passed to malloc or memcpy. The source reads safe in review. The binary has no check in it.

Check before the operation. if (x > INT_MAX - 1) for the simple case. For general addition, __builtin_add_overflow in GCC and Clang, or ckd_add from <stdckdint.h> if your toolchain has C23. -fwrapv makes signed overflow wrap on compilers that support it, which is a fine mitigation and not portable C. And unsigned arithmetic such as size_t wraps by definition, which only means you get a well-defined wrong length, so compare against the limit explicitly there too.

The rule is one sentence in section 6.5 of the standard. It explains a lot of bug reports that blame the optimizer.

后续讨论 4

顺着彼此的依据读下去
  1. 固定链接

    Swift went the other way here. I like it. + on Int traps on overflow, so the bad length crashes instead of sailing through. If you want wrapping you have to say so with &+, and addingReportingOverflow hands back the result plus a flag when you'd rather check than crash. A crash isn't free either. It's still much easier to find than a check that silently vanished. (Unless someone builds with -Ounchecked, which removes those checks.)

  2. 固定链接

    The listing makes it plain. clang -O2, x86-64 Linux target, Intel syntax.

    int next_len(int x) {
        if (x + 1 < x)
            return -1;
        return x + 1;
    }
    next_len:
    	lea	eax, [rdi + 1]
    	ret

    That is the whole function. lea computes x + 1 and returns it. No cmp, no path that can produce -1. At -O0 the same source still has add eax, 1, then cmp eax, dword ptr [rbp - 8] and a jge around the store of -1. The add wraps in the register, the compare sees INT_MIN, and the guard fires. So a debug build passes an overflow test and the optimized build has nothing to test.

    With -fwrapv the check returns in another form, cmp edi, 2147483647 and a cmovne that keeps -1 only when x is INT_MAX.

  3. 固定链接

    This shows up in DPI-C reference models. SystemVerilog int arithmetic is two's complement and wraps, so a testbench check written as x + 1 < x behaves the way it reads. Move the same line into the C model behind an import "DPI-C" function and it falls under the C rule, and the optimizer may remove it.

    Two common habits make it worse. Reference models get compiled with -O2 to keep simulation time down, and the SystemVerilog copy of the check keeps passing, so nobody looks at the C copy. Write the C side with __builtin_add_overflow, return the overflow flag across the DPI call as a bit, and put the assertion on the SystemVerilog side where the check stays visible in the testbench.

同一个问题的其他角度

  1. Solidity reverts on overflow, Rust release builds wrap

    Since Solidity 0.8, arithmetic reverts on overflow and underflow. Turning that off takes an explicit unchecked { ... } block.…

    回复 4最近活动 solidityrust
  2. Testing a Go channel handoff without sleep

    Most handoff tests I run into end with a sleep and a hopeful assertion. This one can do without. A request carries an identif…

    回复 23最近活动 go
  3. partial def is the honest answer for Collatz

    Lean has to know a recursive def terminates before it accepts it as a definition. Structural recursion is found automatically…

    回复 10最近活动 lean-4