Kimundi / owning-ref-rs

A library for creating references that carry their owner with them.
MIT License
359 stars 50 forks source link

OwningRef is in conflict with `noalias` #49

Open RalfJung opened 5 years ago

RalfJung commented 5 years ago

See the discussion starting here: It is possible to trigger UB in safe code using owning-ref.

Specifically, the assertion in the following snippet by @comex fails, even though it should succeed:

extern crate owning_ref; // 0.4.0
use owning_ref::OwningRef;
use std::cell::Cell;

fn helper(owning_ref: OwningRef<Box<Cell<u8>>, Cell<u8>>) -> u8 {
    owning_ref.as_owner().set(10);
    owning_ref.set(20);
    owning_ref.as_owner().get() // should return 20
}

fn main() {
    let val: Box<Cell<u8>> = Box::new(Cell::new(25));
    let owning_ref = OwningRef::new(val);
    let res = helper(owning_ref);
    assert_eq!(res, 20); // assertion fails!
}
est31 commented 5 years ago

@RalfJung is rental affected, too?

RalfJung commented 5 years ago

I don't know, haven't looked at that yet.

RalfJung commented 5 years ago

But given it mentions "self-referential structs", I heavily expect it to be affected, just like self-referential generators. (For the latter we have UB but no "miscompilation" due to that, yet.)

danielhenrymantilla commented 5 years ago

Here is a playground link showing how to fix OwningRef so that the code of the OP is sound. I hope it can be generalized to solving all the non-aliasing issues, while letting the current API of the crate remain unchanged.

The main lines of the solution are:

It passes MIRI and generates the following LLVM IR for helper:

; Function Attrs: nounwind nonlazybind uwtable
define i8 @helper(i8* nonnull, i8*) unnamed_addr #2 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* @rust_eh_personality {
bb10:
  store i8 10, i8* %0, align 1
  store i8 20, i8* %1, align 1
  %.val5 = load i8, i8* %0, align 1
  tail call void @__rust_dealloc(i8* nonnull %0, i64 1, i64 1) #5
  ret i8 %.val5
}
RalfJung commented 5 years ago

Cool! That sounds like it brings the right ingredients.

It passes MIRI

Notice that Miri is currently very permissive for raw pointers. It allows more than LLVM noalias does. Fixing that is blocked on https://github.com/rust-lang/rfcs/pull/2582.

bluss commented 5 years ago

Sorry to ask here, but would the same problem exist if the Box was replaced with a Vec (of one element)? Or does the Vec count as an aliasable owner?

comex commented 5 years ago

AFAIK, it should but currently doesn't. In theory it should be possible to apply noalias to any Unique<T>, which both Box and Vec use for their pointers, but instead the compiler special-cases Box itself. @RalfJung may have more information about that.

HeroicKatora commented 5 years ago

If it is not possible to store and pass a meaningful pointer next to a 'noalias' tagged original pointer, maybe a way to handle this would be to utilize the 'stability' of the dereference operation and store a typed offset from the dereferenced value instead. Then the pointer stored right now would instead be restored by dereferencing the original pointer and then offsetting it with the stored offset. owning-ref can not assert inbounds on the pointer operation (but maybe the std Generators could, for references which it knows to point into the locals). However, llvm is still able to make full use of the noalias on the owned Box in contrast to the other proposed solution where we lose that attribute for all other purposes as well.

Troubles: The offset is not a pure offset for T: ?Sized. Here is a sketch for T: Sized. The llvm IR for helper:

; Function Attrs: nounwind nonlazybind uwtable
define i8 @helper(i8* noalias align 1 dereferenceable(1), i64) unnamed_addr #2 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* @rust_eh_personality {
bb8:
  store i8 10, i8* %0, align 1
  %2 = getelementptr i8, i8* %0, i64 %1
  store i8 20, i8* %2, align 1
  %.val = load i8, i8* %0, align 1
  tail call void @__rust_dealloc(i8* nonnull %0, i64 1, i64 1) #5
  ret i8 %.val
}
danielhenrymantilla commented 5 years ago

@HeroicKatora that's, by the way, what ::rel-ptr does.

RalfJung commented 5 years ago

AFAIK, it should but currently doesn't. In theory it should be possible to apply noalias to any Unique, which both Box and Vec use for their pointers, but instead the compiler special-cases Box itself.

Agreed.

avitex commented 4 years ago

Hello folks. Kimundi has very kindly given me control over his repository. I'll be having a look at this in coming weeks, but I would like to make sure I understand the changes to fix this fully.

msizanoen1 commented 4 years ago

Interestingly, this code works as intended:

use owning_ref::OwningRef;
use std::cell::Cell;

fn helper(r: &mut OwningRef<Box<Cell<i32>>, Cell<i32>>) -> i32 {
    r.as_owner().set(20);
    r.set(10);
    r.as_owner().get()
}

fn main() {
    assert_eq!(helper(&mut OwningRef::new(Box::new(Cell::new(0)))), 10);
}

Also this:

use owning_ref::OwningRef;
use std::cell::Cell;

fn helper(r: Box<OwningRef<Box<Cell<i32>>, Cell<i32>>>) -> i32 {
    r.as_owner().set(20);
    r.set(10);
    r.as_owner().get()
}

fn main() {
    assert_eq!(helper(Box::new(OwningRef::new(Box::new(Cell::new(0))))), 10);
}

So it looks like the UB is gone when passing by pointer. cc @RalfJung This probably explains why generator does not miscompile.

RalfJung commented 4 years ago

So it looks like the UB is gone when passing by pointer.

All I can see is that the compiler doesn't exploit the UB any more. How do you conclude that there is no UB?

bluss commented 4 years ago

<this comment was on the wrong issue.. moving it. I'm sorry!>