Dangerous assert (click bait :)

Example from:

pub fn insert(&mut self, index: usize, elem: T) {
    // Note: `<=` because it's valid to insert after everything
    // which would be equivalent to push.
    assert!(index <= self.len, "index out of bounds");//THIS ASSERT
    if self.cap == self.len { self.grow(); }

    unsafe {
        // ptr::copy(src, dest, len): "copy from src to dest len elems"
        ptr::copy(self.ptr.as_ptr().add(index),
                  self.ptr.as_ptr().add(index + 1),
                  self.len - index);
        ptr::write(self.ptr.as_ptr().add(index), elem);
        self.len += 1;
    }
}

If I understand correctly, in "non-debug" mode we have UB (if the index is > self.len). The assert is elided from that build. Am I correct?

Standard asserts are in the release build.

You're thinking of debug_assert ¶ Uses .

1 Like

Thanks, that comes as a surprise to me (that assert is present in non-debug). We learn something new everyday - those one who are lucky that is!
Best regards!

1 Like

You can't trigger UB in safe Rust even by mistake or serious bugs on your code. With bugs the program may not work as expected, but at least it won't trigger UB and work as written. With UB the program may not work as written.

3 Likes

The code uses unsafe. But thanks for the reminder of one of the guarantees of safe rust.

The code uses unsafe but it's a safe function so it can be called in safe Rust without unsafe keyword on your code. Bug in unsafe code may trigger UB so you need to write it with much more care, add extensive tests and make it checked by enough eyeballs. It is generally believed that the stdlib meet these criteria and reliable(but it still has some bugs like every other nontrivial softwares and we regularly fix it).

Safe functions using unsafe code MUST NEVER triger UB in every imaginable possible cases. If someone managed to trigger UB by writing purely safe Rust, it's due to the bug from unsafe code and should be fixed there.

2 Likes

TBH, I really am not sure what point you are making...

The point is, when some unsafe code uses assert!() to enforce a soundness invariant, that is sound, because assert!() is never removed. It would not be sound (and thus the standard library wouldn't do this) if assert!() were removed in release builds, as this would allow callers of safe functions to bypass assertions (and thus cause UB), which is the very definition of unsoundness.

1 Like

Asserting a function's preconditions, like here, is incredibly common and important in Rust. It allows the rest of the function to know that something is true, allowing both unsafe code and the optimizer to do better based on that information.

My favourite examples is that the assert makes the following code way better:

fn sum_first_three(x: &[i32]) -> i32 {
    assert!(x.len() >= 3);
    x[0] + x[1] + x[2]
}

Not only does it mean that the caller gets a more helpful message if they got it wrong, but having the assert there means that LLVM removes the bounds checks from each of the three array accesses. The code with the assert is both faster and smaller!

7 Likes

Thanks, that is really, really interesting and helpful. I was not aware of the fact that llvm can deduce that purely basing it on that assert. If that is correct what you're saying then it is really great news!

Generally, LLVM is very smart, and you can trust that it will notice trivial things such as "if this array has length 3 or more, I don't need to check that indices 0, 1, and 2 are valid separately". A much more interesting question to ask is what it can't do, because then you'll potentially need to write clever (or at least different) code to make it optimize well.

Modern compilers rely a whole lot on optimizations to remove cruft introduced by collapsing abstractions. Consequently, your default assumption should be that code is optimized aggressively, and trivial inefficiencies like the one above are almost always completely removed.

See this for more information (the talk is "about C++" in theory, but not much other than surface syntax is specific to C++. LLVM optimizes IR generated from Rust the same way it optimizes IR generated from C++.)

1 Like

Maybe the more complete explanation would be:

You can't trigger UB in safe Rust even by mistake as long as all involved functions using unsafe code are sound.

The following function foo triggers UB because it uses an (unsound) buggy function:

pub mod unsound {
    pub fn buggy() {
        unsafe {
            println!("{}", *std::ptr::null::<i32>());
        }
    }
}

pub mod safe {
    pub fn foo() {
        super::unsound::buggy();
    }
}

fn main() {
    safe::foo();
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
warning: dereferencing a null pointer
 --> src/main.rs:4:28
  |
4 |             println!("{}", *std::ptr::null::<i32>());
  |                            ^^^^^^^^^^^^^^^^^^^^^^^^ this code causes undefined behavior when executed
  |
  = note: `#[warn(deref_nullptr)]` on by default

warning: `playground` (bin "playground") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 4.05s
     Running `target/debug/playground`
timeout: the monitored command dumped core
/playground/tools/entrypoint.sh: line 11:     8 Segmentation fault      timeout --signal=KILL ${timeout} "$@"

Of course you might say the UB is caused by buggy and not by foo. But still, foo kinda "triggers" the UB (which might have not been happening otherwise). In a way, all functions in the call stack could be understood as "triggering" the UB (even though buggy is the only function to "blame" because it is unsound).

Rust's safety guarantees only hold if every function that uses unsafe code is sound.

I'm saying this because when you have a huge dependency tree, it might be possible that one of your dependencies has unsound code and may let you trigger UB even with safe Rust.

Unfortunately godbolt doesn't confirm that. Assembly generated with and assert is larger than without it and that would suggest that the optimizations because of that assert were not performed.
Link:

You need to enable optimizations. (and forbid inlining and actually do something so that main won't be just a ret, or make the function pub without a main)

3 Likes

Thanks! Appreciate it!

You can see it with this godbolt link. With the assert uncommented, as in the link, you have the assertion check, followed by the obvious chain of instructions to do the additions (with carry for the high half).

Commenting the assert results in much more code - it starts with three bounds checks (for 0, 1 and 2), then does the obvious chain of instructions for the additions.

Godbolt demo: https://rust.godbolt.org/z/TjMx4Ea6M

Look for the ; <-- annotations in the below.

With the assert:

example::sum_first_three:
        push    rax
        cmp     rsi, 2    ; <-- one check for the assert
        jbe     .LBB0_1
        mov     eax, dword ptr [rdi + 4]
        add     eax, dword ptr [rdi]
        add     eax, dword ptr [rdi + 8]
        pop     rcx
        ret
.LBB0_1:    ; <-- code to panic if the assert fails
        lea     rdi, [rip + .L__unnamed_1]
        lea     rdx, [rip + .L__unnamed_2]
        mov     esi, 30
        call    qword ptr [rip + core::panicking::panic@GOTPCREL]
        ud2

Without the assert:

example::sum_first_three_noassert:
        push    rax
        test    rsi, rsi    ; <-- bounds check
        je      .LBB1_4
        cmp     rsi, 1    ; <-- bounds check
        je      .LBB1_5
        cmp     rsi, 2    ; <-- bounds check
        jbe     .LBB1_3
        mov     eax, dword ptr [rdi + 4]
        add     eax, dword ptr [rdi]
        add     eax, dword ptr [rdi + 8]
        pop     rcx
        ret
.LBB1_4:
        lea     rdx, [rip + .L__unnamed_3]
        xor     edi, edi    ; <-- code to panic mentioning index 0
        xor     esi, esi
        call    qword ptr [rip + core::panicking::panic_bounds_check@GOTPCREL]
        ud2
.LBB1_5:
        lea     rdx, [rip + .L__unnamed_4]
        mov     edi, 1    ; <-- code to panic mentioning index 1
        mov     esi, 1
        call    qword ptr [rip + core::panicking::panic_bounds_check@GOTPCREL]
        ud2
.LBB1_3:
        lea     rdx, [rip + .L__unnamed_5]
        mov     edi, 2    ; <-- code to panic mentioning index 2
        mov     esi, 2
        call    qword ptr [rip + core::panicking::panic_bounds_check@GOTPCREL]
        ud2
1 Like