The pointer with address 0 does not equal 0 when converted to usize

代码在这里
https://play.rust-lang.org/?version=stable&mode=release&edition=2024&gist=26fc276ea72cdc7088085e225bca48ea

addr=0那个,必定报错,用其他值都没问题

thread 'main' panicked at src/main.rs:32:9:
assertion left == right failed
left: 0
right: 0

Null references are UB:

You can run miri on the playground, it will throw an error like this:

error: Undefined Behavior: constructing invalid value: encountered a null reference
  --> src/main.rs:16:26
   |
16 |         let f = unsafe { &mut *(addr as *mut Foo) };
   |                          ^^^^^^^^^^^^^^^^^^^^^^^^ constructing invalid value: encountered a null reference
   |
   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
   = note: BACKTRACE:
   = note: inside `main` at src/main.rs:16:26: 16:50
15 Likes

References aren't just pointers. References must always be initialized and non-NULL.

The compiler takes advantage of references never being 0. If your unsafe code will try to turn NULL into a reference, it will cause bugs, and you'll get buggy results. This is Undefined Behavior, and UB doesn't give any guarantees or bounds or reasons why it does what it does. You're just not allowed to cause it.

5 Likes

Specifically in this case the optimizer saw a value converted from a reference compared to a null, and just replaced it with false, because that can never be true.

Heres' the best I can do to show what the optimizer did, seems it's tricky to do cleanly (playground):

struct Foo;

#[unsafe(no_mangle)]
fn test() -> bool {
    let addr = 0;
    let f = unsafe { &mut *(addr as *mut Foo) };
    eq_addr(f, addr)
}

fn eq_addr(f: &mut Foo, addr: usize) -> bool {
    std::hint::black_box(&f);
    f as *mut Foo as usize == addr
}

LLVM IR:

; Function Attrs: nonlazybind uwtable
define noundef zeroext i1 @test() unnamed_addr #0 {
start:
  %0 = alloca [8 x i8], align 8
  %f.i = alloca [8 x i8], align 8
  call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %f.i)
  store ptr null, ptr %f.i, align 8
  call void @llvm.lifetime.start.p0(i64 8, ptr nonnull %0)
  store ptr %f.i, ptr %0, align 8
  call void asm sideeffect "", "r,~{memory}"(ptr nonnull %0) #2, !srcloc !3
  call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %0)
  call void @llvm.lifetime.end.p0(i64 8, ptr nonnull %f.i)
  ret i1 false
}

Here you can see it's just unconditionally returning false after the cruft put in by black_box.

It's weirdly tricky to get it to reproduce this result with a minimized output: it seems you need to hide just enough from the optimizer that it can't see that f / self came from addr without hiding that it's a ref in general.

The latter is much cleaner to see as just:

struct Foo;

#[unsafe(no_mangle)]
fn is_null(f: &mut Foo) -> bool {
    f as *mut Foo as usize == 0
}

LLVM IR:

; Function Attrs: mustprogress nofree norecurse nosync nounwind nonlazybind willreturn memory(none) uwtable
define noundef zeroext i1 @is_null(ptr noalias nocapture noundef nonnull readnone align 1 %f) unnamed_addr #0 {
start:
  ret i1 false
}
9 Likes