Skip to content

Compile-time semantics of ub_checks intrinsic are unsound #129552

Closed

Description

The ub_checks intrinsic currently just evaluates to the value of tcx.sess.ub_checks() at compile-time. However, tcx.sess can differ between different crates in the same crate graph, which can lead to unsoundness:

Crate A:

pub const N: usize = if intrinsics::ub_checks() { 100 } else { 1 };

Crate B:

pub static ARRAY: [i32; A::N] = [0; A::N];

Crate C:

#[inline(never)]
pub fn get_elem(arr: &[i32; A::N]) -> i32 { arr[50] }

Crate D:

fn main() {
    C::get_elem(&B::ARRAY);
}

If we now build crate C with -Zub-checks but the rest not, then in get_elem the value of A::N is 100 so the access is in-bounds, but the actual size of B::ARRAY is just 1 since in crate B, A::N evaluates to 1. (Actually causing this to crash may need slightly more elaborate tricks to ensure we truly evaluate the right queries at the right times and use the result in the right way, but you get the gist.)

We can't have a safe intrinsic evaluate to different values in different crates, that's unsound. I can think of two options:

  1. We make the intrinsic always behave the same in all crates at compile-time (e.g. by using the fallback body, which is cfg!(ub_checks) and thus returns whether libcore was built with debug assertions).
  2. We make the intrinsic unsafe and make it a safety requirement that the caller may only use the return value to decide whether a constant successfully evaluates, but not the value it evaluates to.

I feel a bit uneasy about the second option since it turns what is usually fully ensured by the interpreter itself (even in the presence of unsafe code) into an invariant upheld by user code -- albeit only by code we control, assuming we will never stabilize this intrinsic.

@lcnr @BoxyUwU For the second option to be sound, it must be okay for the same constant to sometimes return a value and sometimes fail to evaluate. Currently I am fairly sure that is the case since a constant failing to evaluate will stop compilation. However, if the type system ever needs to "speculatively" evaluate a constant in a way such that const-eval failure does not stop compilation, then I think option 2 above would be unsound and we have to go with option 1. And I seem to recall from the last time we spoke that indeed the type system would like to have such a "speculative" form of constant evaluation?

Cc @rust-lang/wg-const-eval @saethlin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    A-const-evalArea: Constant evaluation (MIR interpretation)T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.WG-const-evalWorking group: Const evaluation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions