Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ref parameter incorrectly decorated with `noalias` attribute #63787

Open
RalfJung opened this issue Aug 21, 2019 · 28 comments

Comments

@RalfJung
Copy link
Member

commented Aug 21, 2019

The following program:

use std::cell::*;

pub fn break_it(rc: &RefCell<i32>, r: Ref<'_, i32>) {
    drop(r);
    *rc.borrow_mut() = 2;
}

pub fn main() {
    let rc = RefCell::new(0);
    break_it(&rc, rc.borrow())
}

generates IR for break_it that starts like

; playground::break_it
; Function Attrs: nonlazybind uwtable
define internal void @_ZN10playground8break_it17h1f6183cbc4703855E({ i64, i32 }* align 8 dereferenceable(16), i32* noalias readonly align 4 dereferenceable(4), i64* align 8 dereferenceable(8)) unnamed_addr #0 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* @rust_eh_personality !dbg !1248 {

Note the noalias. That is incorrect, this function violates the noalias assumptions by mutating the data r points to through a pointer (rc) not derived from c.

RefMut has the same problem when -Zmutable-noalias is set.

Cc @rkruppe @comex

@gnzlbg

This comment has been minimized.

Copy link
Contributor

commented Aug 21, 2019

cc @eddyb

@bjorn3

This comment has been minimized.

Copy link
Contributor

commented Aug 21, 2019

Maybe pour an UnsafeCell into Ref instead of &T?

@RalfJung

This comment has been minimized.

Copy link
Member Author

commented Aug 21, 2019

It should probably use a raw pointer (something I have been arguing for for years now ;). For RefMut, UnsafeCell wouldn't even help.

@gnzlbg

This comment has been minimized.

Copy link
Contributor

commented Aug 21, 2019

Changing Ref value field to value: &'b std::cell::UnsafeCell<T> fixes this.

@RalfJung

This comment has been minimized.

Copy link
Member Author

commented Aug 21, 2019

Also see #60076: vec_deque::Drain has a similar problem.

Changing Ref value field to value: &'b std::cell::UnsafeCell fixes this.

It does, but I think a raw pointer is the better fix.

@gnzlbg

This comment has been minimized.

Copy link
Contributor

commented Aug 21, 2019

It does, but I think a raw pointer is the better fix.

Why?

The value field inside RefCell is of type UnsafeCell<T>, not T, so IMO Ref has a bug by using &T instead of &UnsafeCell<T>. Using a raw pointer (e.g. NonNull<T>) would mean that we lose the dereferenceable annotation.

@comex

This comment has been minimized.

Copy link
Contributor

commented Aug 21, 2019

Proof of concept miscompilation: playground link

Edit: To be fair, it works based on the “a == b implies the compiler can replace b with a” optimization, which can be unsound all by itself in some cases involving pointer arithmetic or allocation/deallocation... but eh, this code does neither.

@eddyb

This comment has been minimized.

Copy link
Member

commented Aug 22, 2019

In rust-lang/unsafe-code-guidelines#125 (comment) I asked:

Is Ref having noalias readonly on the data reference a problem because, unlike a regular reference, it can be invalidated by Drop?

Could we then rely on the presence of a Drop impl to disable noalias readonly, or should Ref itself use a raw pointer or &UnsafeCell?

But it seems like the preferred solution is to change Ref to not contain a reference.

For RefMut, UnsafeCell wouldn't even help.

@RalfJung Why not? It would have to be &UnsafeCell, not &mut UnsafeCell, but I expect it to work.

@RalfJung

This comment has been minimized.

Copy link
Member Author

commented Aug 22, 2019

It would have to be &UnsafeCell

Hm okay that might work. That seems real ugly though.

Why?

IMO raw pointers are more honest, as laid out years ago. The lifetime in that reference is a lie.

@RalfJung

This comment has been minimized.

Copy link
Member Author

commented Aug 22, 2019

Nominated for lang team as well.

@rust-lang/lang the interesting question from a lang team perspective here is to decide whether this is a library bug or a language bug. I.e., is the code wrong and the noalias right, or vice versa? Do we want non-repr(transparent)-structs containing references to be marked noalias?

@comex

This comment has been minimized.

Copy link
Contributor

commented Aug 22, 2019

Could we then rely on the presence of a Drop impl to disable noalias readonly, or should Ref itself use a raw pointer or &UnsafeCell?

Well, we have other types such as Box, Vec*, etc. that have Drop impls but still want noalias.

With such types, the pointer can be deallocated by Drop and later reused, but... for one thing, the deallocation function takes the pointer itself as an argument, which is treated as the pointer escaping. If the function makes a new allocation and get the same pointer value back, the new pointer can be said to be 'based on' the pointer it had deallocated – depending on the allocator implementation, it often actually is, and when it isn't, LLVM generally can't prove that.

More importantly, __rust_alloc has its return value declared noalias, so LLVM will always assume that newly allocated pointers don't alias other pointers, regardless of whether the other pointers are themselves noalias. This attribute can in fact lead to miscompilations: it's one of the issues that the 'twin allocation' paper tried to address. There might be a case for removing it. But as long as it's there, removing noalias from Box and co. wouldn't make a difference, regardless of the 'based on' question.

* Though Vec can't actually get noalias treatment, at least on x86, because the ABI passes it by a pointer. You know more about that than I do.

@rkruppe

This comment has been minimized.

Copy link
Member

commented Aug 22, 2019

With such types, the pointer can be deallocated by Drop and later reused, but... for one thing, the deallocation function takes the pointer itself as an argument, which is treated as the pointer escaping. If the function makes a new allocation and get the same pointer value back, the new pointer can be said to be 'based on' the pointer it had deallocated – depending on the allocator implementation, it often actually is, and when it isn't, LLVM generally can't prove that.

I would argue for the opposite: if an allocation is freed and the underlying memory is recycled to satisfy another allocation later on, the later allocation should result in a pointer of different provenance from the one that was freed, regardless of the physical address reuse. Each new allocation should be considered a fresh allocation for the purposes of the abstract machine (to justify the different provenance). This is somewhat realized today by allocation functions returning noalias pointers, as you noted yourself.

This should not lead to any issues because all uses of the "old" allocation must happen before it is freed, and all uses of the new allocation must happen after it was allocated. If these two time spans don't overlap, everything is fine. It's a bit weird that pointers to the same memory address but with different provenance are floating around next to each other, but this can happen even in safe code with e.g. int-to-pointer casts or wrapping_offset. And if these two time spans do overlap (i.e., the new allocation is used before the old one is freed), then the new allocation is made before the old one is freed, so it can't reuse the same address.

Note that all of this is completely independent of allocator internals and data structures, which are neither relevant nor usually visible to optimizers.

Freeing an allocation to which one has a safe pointer or reference is problematic for other reasons, namely dereferenceable (see e.g. https://internals.rust-lang.org/t/is-it-possible-to-be-memory-safe-with-deallocated-self/8457/12), which we add not only to references but also to Box and other owned pointers. We do need to solve this somehow, but I don't see a similar issue for noalias.

More importantly, __rust_alloc has its return value declared noalias, so LLVM will always assume that newly allocated pointers don't alias other pointers, regardless of whether the other pointers are themselves noalias. This attribute can in fact lead to miscompilations: it's one of the issues that the 'twin allocation' paper tried to address. There might be a case for removing it. But as long as it's there, removing noalias from Box and co. wouldn't make a difference, regardless of the 'based on' question.

I wrote above why I think noalias on those functions is fine. I also don't recall seeing (and couldn't find with a quick scan just now) any mention of such an issue in the twin allocation paper. Leaving aside that noalias literally doesn't occur at all (IIRC it's out of scope / left for future work), what is there goes in quite the opposite direction: if my reading of figure 5 is correct, malloc always returns a fresh, logical pointer!

  • Though Vec can't actually get noalias treatment, at least on x86, because the ABI passes it by a pointer. You know more about that than I do.

FWIW this is more a consequence of deliberate choices in the Rust ABI(s) to pass structs of two scalars as those scalars but not do the same for structs with more fields. It's neither platform-specific nor otherwise externally-imposed. Also, if we had usable "local noalias" (e.g., via metadata on loads) then this wouldn't matter anyway.

@comex

This comment has been minimized.

Copy link
Contributor

commented Aug 22, 2019

Note that all of this is completely independent of allocator internals and data structures, which are neither relevant nor usually visible to optimizers.

They're visible if LTO is turned on, but unless the allocator is trivial, the optimizer is nowhere near smart enough to notice that newly allocated pointers might not be 'based on' the previously freed versions. However, assuming that the optimizer will never be smart enough to notice something is a risky strategy in the long term. :)

I wrote above why I think noalias on those functions is fine. I also don't recall seeing (and couldn't find with a quick scan just now) any mention of such an issue in the twin allocation paper. Leaving aside that noalias literally doesn't occur at all (IIRC it's out of scope / left for future work), what is there goes in quite the opposite direction: if my reading of figure 5 is correct, malloc always returns a fresh, logical pointer!

Well, it's a combination of two issues mentioned in the paper.

In 4.6 it discusses the problems with propagating pointer equalities in GVN – that is, in a code path where a == b must have previously evaluated to true, replacing a with b. This is the same optimization I mentioned in my proof-of-concept for the Ref issue earlier in this thread.

In 4.8, it discusses how newly allocated pointers can be equal to previously freed ones, albeit mostly in the context of wanting to optimize the comparison to false. However, one of the approaches it describes would be an alternative way to avoid this issue:

Another solution is to define comparisons with freed blocks as UB (as C and C++ standards do)

To be more concrete, here is an example 'miscompilation' (but not really, because it cheats) based on propagating pointer equalities across a deallocation and reallocation. Under the model proposed by the twin allocation paper, the miscompilation is the optimizer's fault rather than Rust's; I believe the authors' proof-of-concept LLVM modification would have prevented it from occurring, by disabling GVN for pointers outside of limited scenarios where it's known to be safe.
Playground link

The steps:

  • Start with a Box<i32> parameter, x. Store 2 to it.
  • Save x's pointer value as a raw pointer variable.
  • Deallocate x.
  • Obtain a new Box<i32>, y, from another function; it has value 4.
  • Assert that y's pointer value equals the previously saved one from x.
  • Load from y.

Since LLVM knows the new and old boxes' pointers are equal at the point of the load, it treats it as a load from the old box... and since the old box is noalias, it assumes that nobody else could have written to it after it stored a 2, so it optimizes the load to always return 2.

As I said, the example cheats. This optimization can only happen if the pointer doesn't escape from the function, and calling __rust_dealloc on it counts as it escaping. So instead of having the function free the Box directly, it uses a callback which reconstitutes the Box from a previously saved pointer and frees it – which is UB.

However, there are other circumstances where this could theoretically happen without UB. Suppose that instead of a Box, the function takes a reference-counted pointer to immutable data, and that pointer is hypothetically noalias. Rc won't work for this purpose: the pointer it contains cannot be noalias because it's used to access the reference count, so it's not fully immutable. But you could imagine a type where the reference count is stored separately from the data itself:

struct DetachedRc<T> {
    refcount: *mut i32,
    ptr: *const T,
}

In this case, ptr could hypothetically be legally marked as noalias (with some suitable wrapper type that doesn't currently exist), assuming that T does not contain UnsafeCell.

But, assuming DetachedRc's drop routine is inlined into the caller, it does not necessarily cause ptr to escape. As long as there's an additional reference, dropping it will only mutate the refcount, and then some other code can drop the last reference and actually free it. You could use this to construct an example similar to my cheating one but without the UB, yet still vulnerable to the unsound optimization.

(Some practical issues: (1) you would have to make LLVM somehow know statically that the victim function cannot drop the last reference, perhaps by asserting on the refcount, and (2) you'd be working with immutable data, whereas my PoC involves mutation; assert!(*x == 2); ought to work just as well as *x = 2; to let LLVM know that future dereferences should return 2, but it didn't seem to work when I tested it.)

@RalfJung

This comment has been minimized.

Copy link
Member Author

commented Aug 23, 2019

Another argument for using raw pointers: not only noalias but also derefencable is wrong. Consider this example:

use std::cell::*;

pub fn break_it(rc: &RefCell<Option<Box<i32>>>, r: Ref<'_, i32>) {
    drop(r);
    *rc.borrow_mut() = None;
}

pub fn main() {
    let rc = RefCell::new(Some(Box::new(0)));
    break_it(&rc, Ref::map(rc.borrow(), |x| &**x.as_ref().unwrap()))
}

The memory r points to is deallocated while break_it still runs.

Of course, this could also be viewed as more evidence that UnsafeCell should inhibit dereferencable as well.


Could we then rely on the presence of a Drop impl to disable noalias readonly, or should Ref itself use a raw pointer or &UnsafeCell?

I don't think that would be sufficient in general. For example, consider a silly version of Ref that does not implement drop, but provides a manual "destroy" method. That would still be a sound data structure, but adding noalias / dereferencable would be just as wrong as with the real Ref.

One could say this is silly, but I am not sure interpreting "fuzzy" signals like "is there a Drop impl" for properties like this is a good idea. It also doesn't solve the dereferencable problem.

Btw, readonly is fine I think. That just means that no writes happen through this particular pointer, right?

@comex

More importantly, __rust_alloc has its return value declared noalias, so LLVM will always assume that newly allocated pointers don't alias other pointers, regardless of whether the other pointers are themselves noalias.

That's a very different noalias though -- its behavior in return position is not really the same as its behavior in argument position. Notably, the return position one has "infinite scope", whereas the argument position one only has an effect during the one function call.

Custom allocators are a very underexplored topic, and the LLVM "twin allocation" paper did not explore them either, that one assumed a built-in allocator. The paper does not even mention noalias. It is all about modelling allocators. LLVM uses noalias to indicate that a function behaves "allocator-like", but that doesn't mean that the "twin allocation" model is the right one for custom allocators, it might well only apply to the built-in one.

I don't think that pointer helps us determine how to model LLVM argument-position noalias, other than generally paving the way for a formal treatment of pointer provenance.

the deallocation function takes the pointer itself as an argument, which is treated as the pointer escaping

"escaping" is not an Abstract Machine operation, it is an approximation used by compiler. So it can never be used to argue for correctness.

@rkruppe

FWIW this is more a consequence of deliberate choices in the Rust ABI(s) to pass structs of two scalars as those scalars but not do the same for structs with more fields.

Does this mean what I wrote at #60076 (comment) is wrong? I saw a noalias for a vec_deque::Drain and concluded that it is also affected by this bug.


I have no idea why you are talking about custom allocators and return-position noalias, how is that relevant for the discussion?

@rkruppe

This comment has been minimized.

Copy link
Member

commented Aug 23, 2019

Another argument for using raw pointers: not only noalias but also derefencable is wrong. Consider this example:

👍

Does this mean what I wrote at #60076 (comment) is wrong? I saw a noalias for a vec_deque::Drain and concluded that it is also affected by this bug.

Oh, yeah, the noalias that shows up in the IR when passing vec_deque::Drain by value is on a pointer to the Drain (which points to a stack slot allocated for passing the parameter by value, which indeed isn't aliased), not on an actual data pointer contained in the iterator.

I have no idea why you are talking about custom allocators and return-position noalias, how is that relevant for the discussion?

I don't know why @comex brought it up here, I don't think it's related (nor am I convinced there is any issue to begin with). In any case I've opened rust-lang/unsafe-code-guidelines#196 to discuss it separately.

@gnzlbg

This comment has been minimized.

Copy link
Contributor

commented Aug 23, 2019

@rust-lang/lang the interesting question from a lang team perspective here is to decide whether this is a library bug or a language bug. I.e., is the code wrong and the noalias right, or vice versa?

Can we answer this question without an aliasing model ?

Independently of who is at fault, fixing this at the "library level", by using raw pointers, would be a small and localized change, that we could justify by trying to avoid relying on unspecified details of the aliasing model.

Fixing this at the language level, by removing noalias for all infringing types, would require changing the ABI of non-repr-transparent Adts containing fields of reference type. I can imagine that we could always pass these by memory, or that we could remove noalias annotations of reference fields when passing these by value. But these changes would make struct Foo<'a, T>(&'a mut T); subtly different from &'a mut T Adts containing references subtly different from their "destructured" representation, and could result in users destructuring Adts manually before function calls to regain noalias. Example:

struct Foo<'a, T>(&'a mut T, &'a mut T);
let foo = Foo { ... };

fn before<'a, T>(x: Foo<'a, T>) { ...x.0/x.1 aren't noalias... }
before(foo);

fn after<'a, T>(x0: &'a mut T, x1: &'a mut T) { ...x0,x1 are noalias... }
let Foo(x0, x1) = foo;
after(x0, x1);
@RalfJung

This comment has been minimized.

Copy link
Member Author

commented Aug 23, 2019

Can we answer this question without an aliasing model ?

This would be putting a constraint on the aliasing model. We already have many those constraints (e.g. from code where everyone agrees noalias is correct). This would be adding one more.

would require changing the ABI of non-repr-transparent Adts containing fields of reference type.

Not really. At least, I don't view whether noalias is added as part of the ABI. This is a callee choice. There is no representation change.

E.g., we say that Option<&mut T> in Rust and *T in C have the same ABI -- but one gets noalias and the other does not.

@gnzlbg

This comment has been minimized.

Copy link
Contributor

commented Aug 23, 2019

E.g., we say that Option<&mut T> in Rust and *T in C have the same ABI -- but one gets noalias and the other does not.

They have the same calling convention (the values are passed in the same way from caller to callee and vice-versa), but they are different types, just like u32 and NonZeroU32, and this means that one can't use them interchangeably. The C type that one can probably use interchangeably with Option<&mut T> is T* restrict, and this matters because T* and T* restrict and not compatible C types. See this libc soundness bug, and the tracking ctest issue.

This is a callee choice.

This choice must be made depending on Layout/ABI/PointerKind/etc.

@comex

This comment has been minimized.

Copy link
Contributor

commented Aug 23, 2019

I don't know why @comex brought it up here, I don't think it's related (nor am I convinced there is any issue to begin with). In any case I've opened rust-lang/unsafe-code-guidelines#196 to discuss it separately.

Sorry, I got pretty far into the weeds. My point is:

  • noalias is wrong for Ref arguments, because you can drop a Ref, then get a unique reference to the same object from another source.
  • Is noalias also wrong for Box arguments, because you can drop a Box, then get a unique reference to a new allocation which happens to be at the same address?
  • Answer: No, because LLVM would assume old allocations don’t alias new even if Box arguments weren’t noalias, thanks to the allocator’s return value being noalias. Therefore we may as well keep Box arguments noalias.
  • That is, unless we decide to reconsider the allocator return value being noalias, which I guess we can discuss in the other thread.
@comex

This comment has been minimized.

Copy link
Contributor

commented Aug 23, 2019

[Note: before this reply, I posted and then deleted a reply that was meant for another thread.]

They have the same calling convention (the values are passed in the same way from caller to callee and vice-versa), but they are different types, just like u32 and NonZeroU32, and this means that one can't use them interchangeably. The C type that one can probably use interchangeably with Option<&mut T> is T* restrict, and this matters because T* and T* restrict and not compatible C types. See this libc soundness bug, and the tracking ctest issue.

Is there any ABI that actually represents T * and T *restrict differently? I suppose it could come up that ABIs that check the type signature out of band in some way, like asm.js using a different array for indirect calls depending on the function type. But neither GCC nor Clang complains if you convert a function pointer of type void (*a)(void *restrict) to one of type void (*a)(void *), even with -Wall -Wextra -pedantic, and they're represented the same in LLVM IR (so it doesn't affect asm.js as compiled by LLVM).

@gnzlbg

This comment has been minimized.

Copy link
Contributor

commented Aug 24, 2019

But neither GCC nor Clang complains if you convert a function pointer of type void (*a)(void *restrict) to one of type void (*a)(void *), even with -Wall -Wextra -pedantic, and they're represented the same in LLVM IR (so it doesn't affect asm.js as compiled by LLVM).

I'm not sure how good their checking is, but they (GCC and Clang) do complain here, and in both directions: https://godbolt.org/z/ierWF8 AFAICT the C program is illegal because the types are not compatible, and the warning should be an error.

@RalfJung

This comment has been minimized.

Copy link
Member Author

commented Aug 24, 2019

@comex

Is noalias also wrong for Box arguments, because you can drop a Box, then get a unique reference to a new allocation which happens to be at the same address?

Oh I see. Yes that is a good, if only remotely related, question. Answering it in detail is non-trivial without a proper spec for noalias in argument position. But noalias does not imply dereferencable, and it does also not mean that the pointer doesn't alias -- it means that accesses done through the pointer don't alias (conflict) with accesses done through other pointers, so they can be reordered.

The new allocation, even if it is at the same address, really has different pointers pointing to it (recorded through their provenance), so those will not be confused.

@gnzlbg
I think we wall agree that it is legal to have C FFI with type Option<&mut T> where C uses a pointer, right? That means C pointers and Option<&mut T> are ABI-compatible as far as their value and memory ABI is concerned. This is even true with &mut T and C pointers. So surely the same is true on the Rust side between &mut T and a newtype around &mut T that drops noalias.

Whatever C specifies for restrict is not very relevant for us, unless LLVM explicitly does the same for noalias.

@gnzlbg

This comment has been minimized.

Copy link
Contributor

commented Aug 25, 2019

Consider this soundness issue #46188 applied to Rust (godbolt):

pub mod bad {
    extern "Rust" { fn foo(x: Option<&mut i32>); }
}

extern "Rust" { fn foo(y: *mut i32); }
pub unsafe fn baz(x: *mut i32) { foo(x) }

LLVM will "optimize" the declaration of crate::foo making its y: *mut i32 noalias..., because there is a different declaration in bad::foo to the same item that has all those attributes due to its type being Option<&mut i32>.

If the type of crate::foo were fn(Option<&mut T>), this wouldn't be an issue (both declarations match).

Is this optimization sound ? I don't know. If we translate this example to C, this program has UB.

I think we wall agree that it is legal to have C FFI with type Option<&mut T> where C uses a pointer, right?

Now imagine what happens when you move the declaration of fn crate::foo to a C translation unit that you link with the bad module and LTO kicks in. The only thing that matters there is whether this optimization is sound in LLVM-IR, and today, because we have used Option<&mut T> in Rust to interface with C code expecting T*, the declaration in C suddenly gets turned into an "Option<&mut T>" as well.


So is it legal to have C FFI with type Option<&mut T> where C uses a pointer?

I don't know. I wish it was so that we can use Rust types to "enrich" C function declarations with information that C APIs express in comments.

We can definitely say that Rust's &mut T, &T, Option<&T>, *mut T, ... (for T: Sized) have all the same size, align, fields, and call ABI, as C's T*, T* const volatile restrict, ... They do not have the same niches (e.g. nonnull) and other attributes like dereferenceable, noalias, etc. and this results in miscompilations today.

When some document currently says that Option<&mut T> is ABI compatible with C pointers, that isn't necessarily wrong. It depends on what that document means by "ABI": are niches/nonnull part of the ABI? what about dereferenceable, noalias, etc.?

What's wrong is to say that one can substitute one for the other. Without LTO, that might be true, since there is no way for LLVM to tell. With LTO, if C code calls such a declaration with aliasing pointers, then the code might have been miscompiled. But if the C code does not do that, and e.g., has a comment saying that the pointer has the same semantics as Option<&mut T>, then linking a Rust TU with such a declaration might enable further optimizations.

@RalfJung

This comment has been minimized.

Copy link
Member Author

commented Aug 25, 2019

Now imagine what happens when you move the declaration of fn crate::foo to a C translation unit that you link with the bad module and LTO kicks in. The only thing that matters there is whether this optimization is sound in LLVM-IR, and today, because we have used Option<&mut T> in Rust to interface with C code expecting T*, the declaration in C suddenly gets turned into an "Option<&mut T>" as well.

In other words, when declaring an FFI function with references, you have to make sure that the body of that function actually respects the noalias guarantees expressed by those references. That seems like a fair requirement to make -- but this is a very subtle interaction indeed. :(

if C code calls such a declaration with aliasing pointers, then the code might have been miscompiled.

Nit: noalias is not about whether the pointers alias but about whether the function uses the pointers in an aliasing way.

Anyway, all of that seems far off-topic for the topic here. If you think these issues are important, I suggest opening a new issue somewhere (not sure where... UCG or reference or nomicon, I guess).

In Rust, modulo issues like #28179, every function only has one declaration, so I don't think we have to worry about these multi-declaration issues. Transmutes back and forth and even casting function pointers should still be fine -- or do function pointers also have attributes like noalias?

@gnzlbg

This comment has been minimized.

Copy link
Contributor

commented Aug 26, 2019

In Rust, modulo issues like #28179, every function only has one declaration

Yeah, without #[no_mangle], if you were linking two Rust libraries and wanted to use a function from one in the other by declaring it, you would need to extern "Rust" { fn $mangled_name(...) -> ... ; } . That could be made to work correctly and reliably, e.g., by generating $mangled_name using a build.rs (IIRC our mangling scheme was RFC'ed? not sure how stable this is), but feels like a long shot.

or do function pointers also have attributes like noalias?

Not in LLVM-IR so AFAICT this cannot happen either (when calling a function pointer, we use the Rust types to manually annotate the call, but that's it).

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

commented Sep 12, 2019

We discussed this issue in today's @rust-lang/lang meeting. We didn't reach a firm conclusion, but we did enumerate a few interesting observations.

First off, to summarize, there are a few questions happening at once here:

  • How to resolve the "safe code UB" that one can get via RefCell and Ref?
  • Should people be able to implement Ref on their own?

The second question seems like a longer term one -- it is basically a part of the aliasing rules discussion that has been ongoing. We should definitely keep pressing on this -- and perhaps press harder -- but regardless it's a longer term conversation, and one that I would want to have with @RalfJung in the room.

The first question though is narrower and has somewhat more urgency, though it wasn't clear to us if this UB has been "weaponized" (i.e., do we have some realistic-ish looking code that misbehaves?).

In any case, that first problem could be resolved by convering Ref to use *const T, and that is perhaps a prudent step in the short term.

However, the other option for resolving would be to change our ABI and add noalias only for &T that is passed as a direct argument (or perhaps some suitable extension thereof, e.g. including transparent types and tuples or something).

I'm not really clear on which of these options I prefer -- the former is more targeted, but it leaves LLVM-UB "in practice" in the wild for people building their own types, even if we might eventually decide those types to be legal.

On the other hand, the latter is broader and avoids LLVM-UB, even in cases where we might ultimately want UB. It will mean less optimization (though it's not clear how much less).

Interestingly, this same set of questions seems to arise around the unwinding policy (e.g., #63909), with a very similar set of tradeoffs. One can argue that eliminating LLVM-UB might either reduce the urgency of finding a full fix and/or weaken our ability to define rules in the future by implicitly "blessing" patterns we never meant to bless. One can also argue that eliminating more LLVM-UB heightens the urgency since it prohibits us from doing optimizations, and similarly it means that we can better illustrate the benefits of the freshly minted rules.

We didn't have enough quorum to really decide between these two points of view, and it may also vary case by case, but it was interesting to make the analogy and suggested that we may want to develop a "guideline" for this in the future.

@joshtriplett

This comment has been minimized.

Copy link
Member

commented Sep 19, 2019

We discussed that further in today's lang team meeting.

We don't see any obvious downsides to using raw pointers for Ref.

@withoutboats made the point that we may want to use NonNull, and may wish to comment on that further.

That aside, we do think that changing Ref seems like the best option here.

@Centril

This comment has been minimized.

Copy link
Member

commented Sep 19, 2019

(@RalfJung may want to clone the current implementation as a Miri test to use as a canary or something.)

@Centril Centril removed the I-nominated label Sep 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
10 participants
You can’t perform that action at this time.