-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
incorrect UB when when a !
place is constructed (but not loaded!)
#117288
Comments
!
place is constructed (but not loaded!)!
place is constructed (but not loaded!)
Labeling as "unsound" since under the rules that @rust-lang/opsem decided for |
Note that this triggers for |
This is already wrong in the initially generated MIR: _6 = &_2;
_5 = &raw const (*_6);
_4 = move _5 as *const ! (PtrToPtr);
AscribeUserType(_4, o, UserTypeProjection { base: UserType(1), projs: [] });
_3 = _4;
StorageDead(_5);
FakeRead(ForLet(None), _3);
AscribeUserType(_3, o, UserTypeProjection { base: UserType(2), projs: [] });
StorageDead(_6);
StorageDead(_4);
StorageLive(_7);
StorageLive(_8);
_8 = (*_3); // an actual load from the place! If we use _5 = &_1;
_4 = &raw const (*_5);
_3 = move _4 as *const bool (PtrToPtr);
AscribeUserType(_3, o, UserTypeProjection { base: UserType(1), projs: [] });
_2 = _3;
StorageDead(_4);
FakeRead(ForLet(None), _2);
AscribeUserType(_2, o, UserTypeProjection { base: UserType(2), projs: [] });
StorageDead(_5);
StorageDead(_3);
PlaceMention((*_2)); // no load @cjgillot any idea why we don't just get a |
|
Oh, then this looks like fallback behavior: if I annotate |
Yeah that's also wrong, but different.^^ MIR for that case:
So we get a |
Yeah that's probably the right trail. MIR building might not even be at fault, the THIR is already wrong since it thinks there's a never value being constructed here. These THIR nodes seem to be constructed from Who are the right people to ping for THIR issues...? |
!
place is constructed (but not loaded!)!
place is constructed (but not loaded!)
@rustbot label F-never_type |
Note that stable workarounds to access the never type, or cases where the type might be inferred still produce identical behavior, so this does not require the feature. trait Extractor {
type Output;
}
impl<T> Extractor for fn() -> T {
type Output = T;
}
type RealNever = <fn() -> ! as Extractor>::Output;
fn main() {
unsafe {
let x = 3u8;
let x: *const RealNever = &x as *const u8 as *const _;
let _ = *x;
}
} |
#79735 mentions some similar cases that |
WG-prioritization assigning priority (Zulip discussion). @rustbot label -I-prioritize +P-high |
This compiles, so this would also need some changes to type checking rules. #![feature(never_type)]
fn make_string() -> String {
unsafe {
let x = 3u8;
let x: *const ! = &x as *const u8 as *const _;
let _ = *x; // Makes the unsafe block diverge, same as writing return String::new(); or let _ = panic!(); would
}
} |
Uff, yeah that should never have compiled... |
The reason this compiles is probably rust/compiler/rustc_hir_typeck/src/expr.rs Lines 251 to 254 in 5777f2c
HIR typeck thinks that an expression of type ! cannot possibly not diverge.. which is a reasonable assumption tbh. So I think we need to somehow special case that in HIR typeck... the let _ = *x semantics make sense at the MIR level but are pretty inconsistent for HIR
|
Can HIR tell apart places and values? A value expression of type The comment is accurate, "Any expression that produces a value of type |
@rustbot labels -I-lang-nominated We had an extensive discussion of this in the 2023-12-06 T-lang planning meeting last week. The consensus1 was general agreement with @RalfJung:
Specifically, we felt that it was 1) OK to have a place of Anywhere the compiler can statically determine that insta-UB is happening it would be desirable (but not required) to reject the code, preferably with a deny-by-default lint, but if it's more straightforward to emit a hard error, that's OK too. Since we're agreeing to not do never-to-any coercions on places, anywhere that such a coercion would be required would now be a hard error due to the resulting type mismatch. To the degree that changes to the behavior here break uses of the "never type hack", that's probably OK. In the meeting, we settled on the general principles above, but did not go through every example. Following the logic of what we agreed, here's how this author would annotate the examples: #![feature(never_type)]
use core::ptr::addr_of;
#[cfg(False)]
fn deref_never<T>() {
unsafe {
let x = 3u8;
let x: *const ! = &x as *const u8 as *const _;
//
// In all cases, we do not enforce validity invariants on
// places, and we do not perform never-to-any coercion on
// places.
//
// `addr_of!` does not covert a place to a value.
let _val = addr_of!(*x); //~ OK.
//
// The `_` binding does not convert a place to a value.
let _ = *x; //~ OK.
let _: ! = *x; //~ OK.
//
// Parentheses do not convert a place to a value.
let _ = (*x); //~ OK.
//
// Since these do not convert a place to a value and
// never-to-any coercion is not done on places, these do not
// type check and are hard errors.
let _: () = *x; //~ ERROR type mismatch
let _: String = *x; //~ ERROR type mismatch
let _: T = *x; //~ ERROR type mismatch
//
// In all cases, the compiler is free, on a best-effort basis,
// to reject code that it can prove is insta-UB. Preferably
// it would do this as a deny-by-default lint, but it's also
// OK to emit a hard error if that is much more
// straightforward to implement.
//
// If a place of `!` type is converted to a value, that is
// always insta-UB.
//
// The block expression converts a place to a value.
let _ = { *x }; //~ Insta-UB.
//
// The tuple constructor converts a place to a value.
let _ = (*x,); //~ Insta-UB.
let (_,) = (*x,); //~ Insta-UB.
//
// Binding to a variable converts a place to a value. Because
// of never-to-any coercion on values these type check.
let _val = *x; //~ Insta-UB.
let _val: () = *x; //~ Insta-UB.
let _val: String = *x; //~ Insta-UB.
let _val: T = *x; //~ Insta-UB.
//
// Even if the resulting type of the value is explicitly `!`,
// it's still insta-UB to convert a place of `!` type to a
// value, since a value of type `!` is always insta-UB in
// Rust.
let _val: ! = *x; //~ Insta-UB.
let _: ! = { *x }; //~ Insta-UB.
let (_,): (!,) = (*x,); //~ Insta-UB.
}
} (Thanks to @WaffleLapkin for joining the meeting and for helpful discussions on this topic and to @scottmcm for staying on to the bitter end of the call to hammer out the details.) Footnotes
|
@traviscross thanks for the detailed summary! So... looks like the next step would be to actually implement these semantics. Since this occurs on the HIR layer I'm somewhat out of my depth here. Who are the HIR experts that we can ping to ask for help here? :) |
Why did this get marked "requires-nightly"? It reproduces on 1.79.0. Also, I found a particularly funny version where codegen places the https://godbolt.org/z/fhdfK6jT1 trait Type { type T; }
impl<T> Type for fn() -> T { type T = T; }
// Make `!` nameable on stable
type Never = <fn() -> ! as Type>::T;
pub fn main() {
let x = &();
let y = (x as *const ()).cast::<Never>();
// Should be sound (doesn't produce a value of `!`, only a place, so does not diverge)
let z = unsafe { std::ptr::addr_of!(*y) };
println!("huh: {z:p}");
} If you change rust-lang/unsafe-code-guidelines#261 (comment) Seems to suggest that |
I'd call (in the same way as using |
isn't an empty enum equivalent to |
No this bug is specific to |
This is now blocking #129248: making |
Well, I can put up a hack to unblock that specifically. As @Nadrieril pointed out, this is actually two different "bugs" in HIR typeck:
In the above code:
Only (2.) actually matters for #129248, and if we were to land that PR right now, it would be unsound: #![feature(never_type)]
fn make_up_a_value<T>() -> T {
let x: *const ! = 0 as _;
&raw const *x;
// Since `*x` is `!`, HIR typeck thinks it diverges and allows the block to coerce to any value.
} I will probably just put up a hack for (2.), since it's basically the only expression1 where this matters. This doesn't fix the Footnotes
|
I would say it is unsound in the sense that the reference doesn't say that this is UB. Admittedly the reference isn't clear on the entire point about But fixing one of two bugs is still progress, so that'd still be a great first step. :) |
…verge-but-more, r=<try> [EXPERIMENT] Do not consider match/let/raw-ref of deref that evalautes to ! to diverge This is the more involved version of rust-lang#129371. It doesn't fully fix rust-lang#117288, since we still need to fix the fact that never type fallback can cause an unintended load via the `NeverToAny` coercion. But I did want to probe crater to see if anyone relies on this behavior currently, since that's almost certainly UB. r? `@ghost`
…verge-but-more, r=<try> Do not consider match/let/ref of place that evaluates to `!` to diverge, disallow coercions from them too Fixes rust-lang#117288. This PR does two things: ### Don't consider `match`/`let`/`ref`/assignment-LHS of a place expression that evaluates to `!` to diverge. Which fixes this unsoundness: ``` fn make_up_a_value<T>() -> T { unsafe { let x: *const ! = &0 as *const u8 as *const !; let _ = *x; } } ``` Before this PR, it was UB since we consider the `unsafe` block to diverge which means the outer block evalutes to `!`, even though we've never actually *read* a value of type `!`. ### Do not perform coercions of those same place expressions. Which fixes this inadvertent, sneaky unsoundness: ``` unsafe { let x: *const ! = &0 as *const u8 as *const !; let _: () = *x; } ``` which is UB because currently rust emits an *implicit* NeverToAny coercion even though we really shouldn't be, since there's no read of the value pointed by `x`. --- Detecting both of these situations is implemented in a heuristic function called `expr_consitutes_read`. It is tasked with detecting the situations where we have a place expression being passed to some parent expression that would not constitute a read necessarily, like a `let _ = *never_ptr` where `never_ptr: *const !`. --- Specifically, for `let` and `match`, we don't consider it to be a read unless any of the subpatterns (i.e. the LHS of the `let` or the arms of the match) constitute a read. Almost all patterns constitute a read except for `_`, an `|` pattern, or the currently experimental `!` pattern. --- I'm not totally certain that this deserves an FCP, since it's really a bugfix for UB. If it does, I'd be comfortable with it being a T-types FCP since we should be responsible with determining which coercions in the type system are sound (similar to how we adjusted subtyping behavior in rust-lang#118247 to be more sound).
…verge-but-more, r=<try> Do not consider match/let/ref of place that evaluates to `!` to diverge, disallow coercions from them too Fixes rust-lang#117288. This PR does two things: ### Don't consider `match`/`let`/`ref`/assignment-LHS of a place expression that evaluates to `!` to diverge. Which fixes this unsoundness: ``` fn make_up_a_value<T>() -> T { unsafe { let x: *const ! = &0 as *const u8 as *const !; let _ = *x; } } ``` Before this PR, it was UB since we consider the `unsafe` block to diverge which means the outer block evalutes to `!`, even though we've never actually *read* a value of type `!`. ### Do not perform coercions of those same place expressions. Which fixes this inadvertent, sneaky unsoundness: ``` unsafe { let x: *const ! = &0 as *const u8 as *const !; let _: () = *x; } ``` which is UB because currently rust emits an *implicit* NeverToAny coercion even though we really shouldn't be, since there's no read of the value pointed by `x`. --- Detecting both of these situations is implemented in a heuristic function called `expr_consitutes_read`. It is tasked with detecting the situations where we have a place expression being passed to some parent expression that would not constitute a read necessarily, like a `let _ = *never_ptr` where `never_ptr: *const !`. --- Specifically, for `let` and `match`, we don't consider it to be a read unless any of the subpatterns (i.e. the LHS of the `let` or the arms of the match) constitute a read. Almost all patterns constitute a read except for `_`, an `|` pattern, or the currently experimental `!` pattern. --- I'm not totally certain that this deserves an FCP, since it's really a bugfix for UB. If it does, I'd be comfortable with it being a T-types FCP since we should be responsible with determining which coercions in the type system are sound (similar to how we adjusted subtyping behavior in rust-lang#118247 to be more sound).
…verge-but-more, r=<try> Do not consider match/let/ref of place that evaluates to `!` to diverge, disallow coercions from them too Fixes rust-lang#117288. This PR does two things: ### Don't consider `match`/`let`/`ref`/assignment-LHS of a place expression that evaluates to `!` to diverge. Which fixes this unsoundness: ``` fn make_up_a_value<T>() -> T { unsafe { let x: *const ! = &0 as *const u8 as *const !; let _ = *x; } } ``` Before this PR, it was UB since we consider the `unsafe` block to diverge which means the outer block evalutes to `!`, even though we've never actually *read* a value of type `!`. ### Do not perform coercions of those same place expressions. Which fixes this inadvertent, sneaky unsoundness: ``` unsafe { let x: *const ! = &0 as *const u8 as *const !; let _: () = *x; } ``` which is UB because currently rust emits an *implicit* NeverToAny coercion even though we really shouldn't be, since there's no read of the value pointed by `x`. --- Detecting both of these situations is implemented in a heuristic function called `expr_consitutes_read`. It is tasked with detecting the situations where we have a place expression being passed to some parent expression that would not constitute a read necessarily, like a `let _ = *never_ptr` where `never_ptr: *const !`. --- Specifically, for `let` and `match`, we don't consider it to be a read unless any of the subpatterns (i.e. the LHS of the `let` or the arms of the match) constitute a read. Almost all patterns constitute a read except for `_`, an `|` pattern, or the currently experimental `!` pattern. --- I'm not totally certain that this deserves an FCP, since it's really a bugfix for UB. If it does, I'd be comfortable with it being a T-types FCP since we should be responsible with determining which coercions in the type system are sound (similar to how we adjusted subtyping behavior in rust-lang#118247 to be more sound).
…verge-but-more, r=lcnr Do not consider match/let/ref of place that evaluates to `!` to diverge, disallow coercions from them too Fixes rust-lang#117288. This PR implements a heuristic which disables two things that are currently being performed on the HIR when we have **expressions that involve place-like expressions that point to `!`**. Specifically, it will (in certain cases explained below): ### (1.) Disable the `NeverToAny` coercion we implicitly insert for `!`. Which fixes this inadvertent, sneaky unsoundness: ``` unsafe { let x: *const ! = &0 as *const u8 as *const !; let _: () = *x; } ``` which is UB because currently rust emits an *implicit* NeverToAny coercion even though we really shouldn't be, since there's no read of the value pointed by `x`. ### (2.) Disable the logic which considers expression which evaluate to `!` to diverge, which affects the type returned by the containing block. Which fixes this unsoundness: ``` fn make_up_a_value<T>() -> T { unsafe { let x: *const ! = &0 as *const u8 as *const !; let _ = *x; } } ``` We disable these two operations **if** the expression is a place-like expression (locals, statics, field projections, index operations, and deref operations), and if the parent expression is either: (1.) the LHS of an assignment (2.) AddrOf (3.) A match or let **unless** all of the *patterns consitute a read*, which is explained below: And finally, a pattern currently is considered to constitute a read **unless** it is a wildcard, or an OR pattern. An OR pattern is considered to constitute a read if all of its subpatterns constitute a read, to remain as conservative as possible in cases like `_ | subpat` or `subpat | _`. All other patterns are considered currently to constitute a read. Specifically, because `NeverToAny` is a coercion performed on a *value* and not a *place*, `Struct { .. }` on a `!` type must be a coercion currently, and we currently rely on this behavior to allow us to perform coercions like `let _: i32 = x;` where `x: !`. This is already considered UB by [miri](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=daf3a2246433fe43fdc07d1389c276c9), but also means it does not affect the preexisting UB in this case: ``` let Struct { .. } = *never_ptr; ``` Even though it's likely up for debate since we're not actually reading any data out of the struct, it almost certainly causes inference changes which I do *NOT* want to fix in this PR.
…verge-but-more, r=lcnr Do not consider match/let/ref of place that evaluates to `!` to diverge, disallow coercions from them too Fixes rust-lang#117288. This PR implements a heuristic which disables two things that are currently being performed on the HIR when we have **expressions that involve place-like expressions that point to `!`**. Specifically, it will (in certain cases explained below): ### (1.) Disable the `NeverToAny` coercion we implicitly insert for `!`. Which fixes this inadvertent, sneaky unsoundness: ``` unsafe { let x: *const ! = &0 as *const u8 as *const !; let _: () = *x; } ``` which is UB because currently rust emits an *implicit* NeverToAny coercion even though we really shouldn't be, since there's no read of the value pointed by `x`. ### (2.) Disable the logic which considers expression which evaluate to `!` to diverge, which affects the type returned by the containing block. Which fixes this unsoundness: ``` fn make_up_a_value<T>() -> T { unsafe { let x: *const ! = &0 as *const u8 as *const !; let _ = *x; } } ``` We disable these two operations **if** the expression is a place-like expression (locals, statics, field projections, index operations, and deref operations), and if the parent expression is either: (1.) the LHS of an assignment (2.) AddrOf (3.) A match or let **unless** all of the *patterns consitute a read*, which is explained below: And finally, a pattern currently is considered to constitute a read **unless** it is a wildcard, or an OR pattern. An OR pattern is considered to constitute a read if all of its subpatterns constitute a read, to remain as conservative as possible in cases like `_ | subpat` or `subpat | _`. All other patterns are considered currently to constitute a read. Specifically, because `NeverToAny` is a coercion performed on a *value* and not a *place*, `Struct { .. }` on a `!` type must be a coercion currently, and we currently rely on this behavior to allow us to perform coercions like `let _: i32 = x;` where `x: !`. This is already considered UB by [miri](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=daf3a2246433fe43fdc07d1389c276c9), but also means it does not affect the preexisting UB in this case: ``` let Struct { .. } = *never_ptr; ``` Even though it's likely up for debate since we're not actually reading any data out of the struct, it almost certainly causes inference changes which I do *NOT* want to fix in this PR.
Rollup merge of rust-lang#129392 - compiler-errors:raw-ref-op-doesnt-diverge-but-more, r=lcnr Do not consider match/let/ref of place that evaluates to `!` to diverge, disallow coercions from them too Fixes rust-lang#117288. This PR implements a heuristic which disables two things that are currently being performed on the HIR when we have **expressions that involve place-like expressions that point to `!`**. Specifically, it will (in certain cases explained below): ### (1.) Disable the `NeverToAny` coercion we implicitly insert for `!`. Which fixes this inadvertent, sneaky unsoundness: ``` unsafe { let x: *const ! = &0 as *const u8 as *const !; let _: () = *x; } ``` which is UB because currently rust emits an *implicit* NeverToAny coercion even though we really shouldn't be, since there's no read of the value pointed by `x`. ### (2.) Disable the logic which considers expression which evaluate to `!` to diverge, which affects the type returned by the containing block. Which fixes this unsoundness: ``` fn make_up_a_value<T>() -> T { unsafe { let x: *const ! = &0 as *const u8 as *const !; let _ = *x; } } ``` We disable these two operations **if** the expression is a place-like expression (locals, statics, field projections, index operations, and deref operations), and if the parent expression is either: (1.) the LHS of an assignment (2.) AddrOf (3.) A match or let **unless** all of the *patterns consitute a read*, which is explained below: And finally, a pattern currently is considered to constitute a read **unless** it is a wildcard, or an OR pattern. An OR pattern is considered to constitute a read if all of its subpatterns constitute a read, to remain as conservative as possible in cases like `_ | subpat` or `subpat | _`. All other patterns are considered currently to constitute a read. Specifically, because `NeverToAny` is a coercion performed on a *value* and not a *place*, `Struct { .. }` on a `!` type must be a coercion currently, and we currently rely on this behavior to allow us to perform coercions like `let _: i32 = x;` where `x: !`. This is already considered UB by [miri](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=daf3a2246433fe43fdc07d1389c276c9), but also means it does not affect the preexisting UB in this case: ``` let Struct { .. } = *never_ptr; ``` Even though it's likely up for debate since we're not actually reading any data out of the struct, it almost certainly causes inference changes which I do *NOT* want to fix in this PR.
This code shouldn't have UB but Miri reports UB and rustc generates a SIGILL:
Thanks to @Nadrieril for finding this.
Zulip discussion
The text was updated successfully, but these errors were encountered: