mrustc icon indicating copy to clipboard operation
mrustc copied to clipboard

Signed integer overflow causes undefined behaviour

Open Serentty opened this issue 6 years ago • 9 comments

Signed integer overflow in Rust is supposed to wrap, except in debug builds where it panics to warn developers of possibly unintentional overflow. However, when the following code is compiled into C, the overflow is left as-is, leaving it at the mercy of the C compiler to be optimized in ways that aren't allowed in Rust.

fn main() {
    let mut i = 0i32;
    while i > -1 {
        i += 1;
    }
    println!("Done");
}

When compiled with the reference implementation, this prints “Done”, in accordance with the defined behaviour for signed wrapping in Rust. However, when compiled into C and then compiled with GCC, it optimizes the entire main function away, and leaves just an infinite loop.

Serentty avatar Jun 08 '19 23:06 Serentty

I have some good news. I was able to trick the compiler into treating signed overflow as wrapping using the following C code, without requiring -fwrapv.

typedef union {
    signed s;
    unsigned u;
} sint;

int count_up()
{
    sint i;
    i.s = 0;
    while(i.s > -1) {
        i.u += 1;
    }
    return i.s;
}

int count_up_ub()
{
    int i;
    i = 0;
    while(i > -1) {
        i += 1;
    }
    return i;
}

I wonder if this technique could be used by the C code generator.

Serentty avatar Jun 08 '19 23:06 Serentty

Signed integer overflow in Rust is supposed to wrap

Not exactly. Integer overflow (both signed and unsigned) in Rust is invalid. That is, if your code causes an integer overflow, then your code is defective. Whether or not the language catches you doing this is up to the compiler options. By default, in debug builds, overflow reliably causes a panic. In release builds, the behavior is that overflow occurs, and you get whatever you get. This just happens to be wrapping arithmetic, but this is not a guarantee of the language or compiler. The behavior, even in release builds, can be changed by enabling overflow checks.

If your code overflows, it is broken. You should never rely on overflow wrapping. If you want wrapping behavior, then you should use the wrapping operators:

i = i.overflowing_add(1);

See:

https://github.com/rust-lang/rfcs/issues/359 http://huonw.github.io/blog/2016/04/myths-and-legends-about-integer-overflow-in-rust/

On Sat, Jun 8, 2019 at 4:20 PM Serentty [email protected] wrote:

I have some good news. I was able to trick the compiler into treating signed overflow as wrapping using the following C code, without requiring -fwrapv.

typedef union { signed s; unsigned u; } sint; int count_up() { sint i; i.s = 0; while(i.s > -1) { i.u += 1; } return i.s; } int count_up_ub() { int i; i = 0; while(i > -1) { i += 1; } return i; }

I wonder if this technique could be used by the C code generator.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/thepowersgang/mrustc/issues/117?email_source=notifications&email_token=ADLILBDBAIJVGO4EVZRZW3LPZQ5CXA5CNFSM4HWIJRC2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXIAN5Q#issuecomment-500172534, or mute the thread https://github.com/notifications/unsubscribe-auth/ADLILBCDB2HQSLWFJN7EHELPZQ5CXANCNFSM4HWIJRCQ .

arlied-google avatar Jun 10 '19 18:06 arlied-google

This just happens to be wrapping arithmetic, but this is not a guarantee of the language or compiler.

I believe it is guaranteed to either panic or wrap around on overflow.

Quoting the reference:

When the programmer has enabled debug_assert! assertions (for example, by enabling a non-optimized build), implementations must insert dynamic checks that panic on overflow. Other kinds of builds may result in panics or silently wrapped values on overflow, at the implementation's discretion.

In the case of implicitly-wrapped overflow, implementations must provide well-defined (even if still considered erroneous) results by using two's complement overflow conventions.

bjorn3 avatar Jun 10 '19 18:06 bjorn3

"It may be X or it may be Y" is not something you can rely on, though. If you're writing a library and publishing it as a crate, you don't have control over the compiler flags that are used. So you shouldn't rely on one behavior or the other -- you should express the semantics you want, by using the overflowing_xxx() operations. Yes, they're more verbose, but they are precise.

On Mon, Jun 10, 2019 at 11:50 AM bjorn3 [email protected] wrote:

This just happens to be wrapping arithmetic, but this is not a guarantee of the language or compiler.

I believe it is guaranteed to either panic or wrap around on overflow.

Quoting the reference https://doc.rust-lang.org/reference/behavior-not-considered-unsafe.html:

When the programmer has enabled debug_assert! assertions (for example, by enabling a non-optimized build), implementations must insert dynamic checks that panic on overflow. Other kinds of builds may result in panics or silently wrapped values on overflow, at the implementation's discretion.

In the case of implicitly-wrapped overflow, implementations must provide well-defined (even if still considered erroneous) results by using two's complement overflow conventions.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/thepowersgang/mrustc/issues/117?email_source=notifications&email_token=ADLILBHMYGPWWDJ5FAQJL2TPZ2O5TA5CNFSM4HWIJRC2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXK3HAA#issuecomment-500544384, or mute the thread https://github.com/notifications/unsubscribe-auth/ADLILBCEN2C4V6LCJNT4PVDPZ2O5TANCNFSM4HWIJRCQ .

arlied-google avatar Jun 10 '19 19:06 arlied-google

"It may be X or it may be Y" is not something you can rely on, though.

You can rely on the fact that it isn't UB.

bjorn3 avatar Jun 10 '19 19:06 bjorn3

As far as I understand, overflow in Rust is defined to either panic or wrap. Which it does is not defined, but it is defined that it does one of those two, and which actually happens depends on compiler flags. What is not allowed by the language standard is for the compiler to assume that overflow will not occur, the way that the C language standard allows. This means that optimizations based on the assumption that overflow will not occur are not valid.

Serentty avatar Jun 12 '19 04:06 Serentty

We can solve this in two different ways:

  • For GCC and Clang, we can use -fwrapv in release builds.
  • On other platforms, we can add runtime checks that unconditionally panic.

DemiMarie avatar Apr 05 '21 13:04 DemiMarie

While adding -fwrapv to the default gcc arguments would be the easiest approach, there doesn't seem to be an equivalent option for MSVC (well, not an officially supported one).

thepowersgang avatar Feb 14 '22 11:02 thepowersgang