rust-lang / rust-lang/rust

Miscompilation from interaction between mir-opt, Codegen and LLVM on merged statics

Open
#161,973 37 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-codegen A-const-eval A-linkers A-LLVM A-mir-opt C-bug I-miscompile I-unsound P-medium T-compiler T-opsem
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

View all comments

Found in this zulip thread, based on @RalfJung's idea.

The comments in the code describe what I think is going on based on the way I constructed this code. Please take them with a grain of salt ^^

NOTE: The version below abuses that MIR-opt propagates the uninitialized memory from a global, which is then passed on to LLVM as zeroed out. See this comment for a version that instead uses LLVM propagating undef before passing it to the linker as zeroed out. It just happens to be that rustc and LLVM both have this "propagate uninit + pass on zero to the next stage" logic, which is only correct if the memory model allows different allocations with different contents to have the same address. See Ralf's comment right below this.

godbolt

use std::mem::MaybeUninit;

#[inline(always)]
fn check<'a>(a: &[u8], b: &'a [MaybeUninit<u8>]) -> &'a [u8] {
    if a.as_ptr().addr() != b.as_ptr().addr() || a.len() != b.len() {
        std::process::abort()
    }
    // SAFETY: these point to the same memory, and that memory must be
    // initialized and immutable based on the safety invariant of `a`.
    unsafe { std::mem::transmute(b) }
}

static INIT: [u8; 1024] = [0; _];

unsafe extern "C" {
    fn observe_addr(p: *const (), q: *const ());
}

#[unsafe(no_mangle)]
pub fn weird() {
    let fixed_uninit: &'static [MaybeUninit<u8>; INIT.len()] = const {
        let mut buf = [MaybeUninit::uninit(); _];
        // interleave init and uninit memory to encourage merging with `INIT`
        let mut i = 0;
        while i < buf.len() {
            buf[i].write(0);
            i += 2;
        }
        &{ buf }
    };
    if fixed_uninit.as_ptr().addr() != INIT.as_ptr().addr() {
        // This code runs unconditionally because the code below has UB
        // This is bad if `observe_addr` requires that the args have different addresses!
        loop { unsafe { observe_addr(INIT.as_ptr().cast(), fixed_uninit.as_ptr().cast()) } }
    }
    
    // There's a poison here. The match turns it into UB
    match check(&INIT, fixed_uninit)[1] {
        0 => {},
        1.. => {},
    }
}

@rustbot label T-opsem A-LLVM I-unsound I-miscompile A-mir-opt

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the reduced Rust example in the issue and reproduce it using the linked Godbolt example. Trace how MIR-opt, Codegen, and LLVM handle the merged statics and the differing initialization states. Done means the responsible compiler interaction and the expected memory-model behavior are established well enough to guide a fix.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.