Skip to content

Function pointer address compares are misleading #2882

Closed
@MasterAwesome

Description

@MasterAwesome

Consider the case where I want to take any type T and convert it into some Opaque struct as so, comparing just the function pointers would give me a meaningful way to determine that this Opaque was created by the opaquify function. To ensure that generics that alias because of similar implementations (as shown below) don't cause undefined behavior the type_name field is used.

use std::ffi::c_void;

#[derive(Debug)]
#[repr(C)]
struct Opaque {
    fptr: extern "C" fn(),
    type_name: &'static str,
    inner: *mut c_void,
}

fn opaquify<T>(val: T) -> Opaque {
    dbg!(std::any::type_name::<T>());
    dbg!(ffi::<T> as *const c_void);
    Opaque {
        fptr: ffi::<T>,
        type_name: std::any::type_name::<T>(),
        inner: Box::into_raw(Box::new(val)).cast(),
    }
}

fn unopaquify<T>(val: &Opaque) -> Option<&T> {
    dbg!(val);
    if val.fptr == ffi::<T> && val.type_name == std::any::type_name::<T>() {
        unsafe { Some(val.inner.cast::<T>().as_ref().unwrap_unchecked()) }
    } else {
        None
    }
}

extern "C" fn ffi<T>() {
    /* ... */
}

pub fn main() {
    let t1 = opaquify(1_u32);
    let t2 = opaquify(1_usize);

    assert!(unopaquify::<usize>(&t1).is_none());
    assert!(unopaquify::<u32>(&t2).is_none());


    assert_eq!(unopaquify::<u32>(&t1), Some(&1));
    assert_eq!(unopaquify::<usize>(&t2), Some(&1));
}

This above program executes as expected on debug and release however something weird happens when I introduce miri into the mix. Below is the comparisons between outputs on different modes.

Debug builds

[src/main.rs:12] std::any::type_name::<T>() = "u32"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000055be50049d20
[src/main.rs:12] std::any::type_name::<T>() = "usize"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000055be50049d10
[src/main.rs:22] val = Opaque {
    fptr: 0x000055be50049d20,
    type_name: "u32",
    inner: 0x000055be50421ba0,
}
[src/main.rs:22] val = Opaque {
    fptr: 0x000055be50049d10,
    type_name: "usize",
    inner: 0x000055be50421bc0,
}
[src/main.rs:22] val = Opaque {
    fptr: 0x000055be50049d20,
    type_name: "u32",
    inner: 0x000055be50421ba0,
}
[src/main.rs:22] val = Opaque {
    fptr: 0x000055be50049d10,
    type_name: "usize",
    inner: 0x000055be50421bc0,
}

Here we can see that generic fptrs don't alias and behaves as expected.

Release mode

[src/main.rs:12] std::any::type_name::<T>() = "u32"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000055613651b6c0
[src/main.rs:12] std::any::type_name::<T>() = "usize"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000055613651b6c0
[src/main.rs:22] val = Opaque {
    fptr: 0x000055613651b6c0,
    type_name: "u32",
    inner: 0x0000556137583ba0,
}
[src/main.rs:22] val = Opaque {
    fptr: 0x000055613651b6c0,
    type_name: "usize",
    inner: 0x0000556137583bc0,
}
[src/main.rs:22] val = Opaque {
    fptr: 0x000055613651b6c0,
    type_name: "u32",
    inner: 0x0000556137583ba0,
}
[src/main.rs:22] val = Opaque {
    fptr: 0x000055613651b6c0,
    type_name: "usize",
    inner: 0x0000556137583bc0,
}

Here functions do alias because of presumably an LLVM pass that merges functions but because of the type check unopaquify knows when it's safe to convert.

Miri

Run with: MIRIFLAGS="-Zmiri-ignore-leaks" cargo miri run

[src/main.rs:12] std::any::type_name::<T>() = "u32"
[src/main.rs:13] ffi::<T> as *const c_void = 0x000000000002f170
[src/main.rs:12] std::any::type_name::<T>() = "usize"
[src/main.rs:13] ffi::<T> as *const c_void = 0x0000000000046572
[src/main.rs:22] val = Opaque {
    fptr: 0x000000000003d3a3,
    type_name: "u32",
    inner: 0x000000000003d508,
}
[src/main.rs:22] val = Opaque {
    fptr: 0x00000000000548f9,
    type_name: "usize",
    inner: 0x0000000000054a90,
}
[src/main.rs:22] val = Opaque {
    fptr: 0x000000000003d3a3,
    type_name: "u32",
    inner: 0x000000000003d508,
}
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `None`,
 right: `Some(1)`', src/main.rs:41:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The dbg logs from opaquify suggests that fptr do NOT alias and should follow the same branches as debug build. However, the first unopaquify call has an fptr which has the value completely different than any of the ones returned. I'm not sure what 0x000000000003d3a3 or 0x00000000000548f9 point to but based on the initial logs it's neither ffi::<usize> nor ffi::<u32>. And hence the tests fail.

Is this an expected false positive from miri and can be safely ignored, or is this actual UB that I'm failing to understand?

https://play.rust-lang.org/?version=nightly&mode=release&edition=2021&gist=c7009ed9436d42f9d78d59d8ef95abd5

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions