Help : incorrect results when run in release mode

First, and before all, I apologize for all the code that is present in this playground, but I didn't manage to produce a smaller example.
I am doing a personal project to try to learn a bit about optimization, and I was trying to implement a matrix dot vector product.

During my test, I encoutered a bug where the test was passing when runned in debug, but failed when run in release mode.
And I can't explain why there is a difference between debug and release mode in my code, except for the index checks, but that shouldn't produce a different output.

I know that my code is far from being good, especially the for loop I wrote without using Rust powerfull iterators, but I would really like to understand what is causing this behaviour.

To explain a bit more in detail what is happening, I have two writing for the product I want to compute : _matrix_dot_vector_s (exact and simple version) and _matrix_dot_vector_par2 (parallelized with rayon and using SIMD). In main(), I do the test to see if both outputs the same result, and I obtain differents results depending on the run mode. The only debug assertion are in read, write and read_many, but shouldn't cause an issue.

Anyways, thank you a lot in advance for helping me

This almost certainly means that your code contains some kind of Undefined Behavior. You should approach this problem not as “mysterious difference” but as “identify and fix the UB”.

It looks like your unsafe code is all about managing a multidimensional array (tensor). I would recommend that you try replacing it entirely with existing well-tested code such as ndarray, or just replace the memory management part with a Vec<T>.

If you want to keep the unsafe code and fix the UB-causing bug (unsoundness) then your first stop should be testing your code under Miri.

5 Likes

You debug guards are wrong. Replace idx > len with idx >= len.

offset + count >= self.shape.iter().product() will panic

1 Like

Yikes, that 's a lot of unsafe

2 Likes

There should be some sort of a maxim like "Every instance of the unsafe keyword in your code makes it more likely that the program's dev and release behaviors diverge"

2 Likes

I am stupid
Thx <3

I don’t know – I feel like “diverging dev and release behavior” is still very much the lucky case; there’s by no means any guarantee that UB is at all noticeable. So teaching it like you put it, in my view, would make it sound as if there’s a promise that UB would be so nice as to make itself known, which can be misleading (probably one of the more common misconceptions about UB).

Maybe “the maxim” then is better just “Every instance of the unsafe keyword makes UB more likely”? Although that’s not really correct either as a rule; it would suggest e.g. that unsound API are superior to sound, properly documented, and properly marked unsafe API, because with unsound API you spare the unsafe keywords when using them.

1 Like

Nevermind, it wasn't the fix. The second guard was actually correct (if you had a offset on the last element and had a count of 1 for example)

Thank you a lot, I actually managed to find the solution thanks to miri. I was casting an array to a *mut but I didn't declare this array as mut. I didn't know it was undefined behaviour. By any chances, do you know where I should look to understand better this undefined behaviour ?

It depends a bit on the context. In this case, I suppose you’ve found the place where you use vec_arr.as_ptr() and then mutate through that pointer? For that, there’s a lengthy note on the method’s documentation already:

pub const fn as_ptr(&self) -> *const T

Returns a raw pointer to the slice’s buffer.

The caller must ensure that the slice outlives the pointer this function returns, or else it will end up dangling.

The caller must also ensure that the memory the pointer (non-transitively) points to is never written to (except inside an UnsafeCell) using this pointer or any pointer derived from it. If you need to mutate the contents of the slice, use as_mut_ptr.

Modifying the container referenced by this slice may cause its buffer to be reallocated, which would also make any pointers to it invalid.


An additional datapoint can be information from this reference page which calls out more generally as UB:

  • Mutating immutable bytes. All bytes reachable through a const-promoted expression are immutable, as well as bytes reachable through borrows in static and const initializers that have been lifetime-extended to 'static. The bytes owned by an immutable binding or immutable static are immutable, unless those bytes are part of an UnsafeCell<U>.Moreover, the bytes pointed to by a shared reference, including transitively through other references (both shared and mutable) and Boxes, are immutable; transitivity includes those references stored in fields of compound types.A mutation is any write of more than 0 bytes which overlaps with any of the relevant bytes (even if that write does not change the memory contents).

And also these 2 sections in the pointer module documentation; this one:

Many functions in this module take raw pointers as arguments and read from or write to them. For this to be safe, these pointers must be valid for the given access. Whether a pointer is valid depends on the operation it is used for (read or write), and the extent of the memory that is accessed (i.e., how many bytes are read/written) – it makes no sense to ask “is this pointer valid”; one has to ask “is this pointer valid for a given access”. Most functions use *mut T and *const T to access only a single value, in which case the documentation omits the size and implicitly assumes it to be size_of::<T>() bytes.

The precise rules for validity are not determined yet. The guarantees that are provided at this point are very minimal:
……

and this one:

Pointers are not simply an “integer” or “address”. For instance, it’s uncontroversial to say that a Use After Free is clearly Undefined Behavior, even if you “get lucky” and the freed memory gets reallocated before your read/write (in fact this is the worst-case scenario, UAFs would be much less concerning if this didn’t happen!). As another example, consider that wrapping_offset is documented to “remember” the allocated object that the original pointer points to, even if it is offset far outside the memory range occupied by that allocated object. To rationalize claims like this, pointers need to somehow be more than just their addresses: they must have provenance.

A pointer value in Rust semantically contains the following information:

  • The address it points to, which can be represented by a usize.
  • The provenance it has, defining the memory it has permission to access. Provenance can be absent, in which case the pointer does not have permission to access any memory.

The exact structure of provenance is not yet specified, but the permission defined by a pointer’s provenance have a spatial component, a temporal component, and a mutability component:

  • Spatial: The set of memory addresses that the pointer is allowed to access.
  • Temporal: The timespan during which the pointer is allowed to access those memory addresses.
  • Mutability: Whether the pointer may only access the memory for reads, or also access it for writes. Note that this can interact with the other components, e.g. a pointer might permit mutation only for a subset of addresses, or only for a subset of its maximal timespan.

When an allocated object is created, it has a unique Original Pointer. For alloc APIs this is literally the pointer the call returns, and for local variables and statics, this is the name of the variable/static. (This is mildly overloading the term “pointer” for the sake of brevity/exposition.)

The Original Pointer for an allocated object has provenance that constrains the spatial permissions of this pointer to the memory range of the allocation, and the temporal permissions to the lifetime of the allocation. Provenance is implicitly inherited by all pointers transitively derived from the Original Pointer through operations like offset, borrowing, and pointer casts. Some operations may shrink the permissions of the derived provenance, limiting how much memory it can access or how long it’s valid for (i.e. borrowing a subfield and subslicing can shrink the spatial component of provenance, and all borrowing can shrink the temporal component of provenance). However, no operation can ever grow the permissions of the derived provenance: even if you “know” there is a larger allocation, you can’t derive a pointer with a larger provenance. Similarly, you cannot “recombine” two contiguous provenances back into one (i.e. with a fn merge(&[T], &[T]) -> &[T]).
……

are relevant in establishing that the question of “is this particular pointer valid for writes” [1] is a relevant question to consider in the first place :wink:


  1. even when the pointer’s address does point to memory that ‘should be mutable on the real hardware ↩︎

3 Likes

Yes, I phrased it intentionally slightly facetiously. UB is of course more devious than that, I just meant that this particular case is a really common way to mess around and find out about UB, so to say.

Interistingly, MIRI does not complain with this minimal example. Is it because it's library UB and not language UB?

fn main() {
    let x = vec![0.0];
    unsafe {write_ptr(x.as_ptr() as *mut f64);}
    dbg!(x);
}

unsafe fn write_ptr(p: *mut f64) {
    unsafe {*p = 42.42};
}

Yes, that’s library UB. Also note that it looks like Vec has its own as_ptr method which is taking precedence over the slice one here. (Though this one is also documented to forbid mutable access.) It’s only library-UB here, because the current implementation just produces a copy of the raw pointer contained within the Vec struct itself.[1]


  1. So it doesn’t matter that it’s a &self method, because its similar to how you can go from & *mut T to *mut T (by copying the pointer from behind the reference) and then mutate T through it, even though originally, the outer level of indirection was through immutable reference. ↩︎

"every instance of unsafe is somewhere the code cooties can get in"?

Nobody wants code cooties.

2 Likes