Skip to content

Soundness issue with msg_send! #112

Open
@madsmtm

Description

@madsmtm

So, during my work on objc2 I decided to test some things in Miri, to try to get a feel for how sound everything is, and found this issue (which is currently present in both crates).

Below is a piece of code that tries to mutate an ivar on an object, resembling fairly closely something that is quite commonly done in winit. Try placing it under objc/examples/msg_send_unsound.rs, and running cargo miri run --example msg_send_unsound.

#[macro_use]
extern crate objc; // v0.2.7

use objc::runtime::{Object, Sel};

// Assume this is registered to the object with `ClassDecl::add_method`
extern "C" fn my_selector(obj: *mut Object, _sel: Sel) {
    let obj = unsafe { &mut *obj };
    let a = unsafe { obj.get_mut_ivar::<i32>("a") };
    *a += 1;
}

fn main() {
    let ptr: *mut Object = new_object(42); // ivar a = 42
    let obj: &mut Object = unsafe { &mut *ptr };

    // Get an immutable reference to the instance variable
    let a = unsafe { obj.get_ivar::<i32>("a") };

    unsafe {
        // Uses `obj` mutably, but the signature says it's used immutably
        let _: () = msg_send![obj, my_selector];
    }

    // So the compiler can't catch that we're not allowed to access `a` here!
    assert_eq!(*a, 43);

    free_object(ptr);
}

// ------------------------------------
//
// HACKY STUBS BELOW TO MAKE MIRI WORK!
//
// ------------------------------------

use std::ffi::CStr;
use std::os::raw::c_char;
use std::ptr;

use objc::runtime::{Class, Ivar};

#[repr(C)]
struct MyObject {
    isa: *const Class,
    a: i32,
}

fn new_object(a: i32) -> *mut Object {
    let obj = Box::new(MyObject {
        isa: ptr::null(),
        a,
    });
    Box::into_raw(obj) as *mut Object
}

fn free_object(obj: *mut Object) {
    unsafe { Box::from_raw(obj as *mut MyObject) };
}

#[no_mangle]
extern "C" fn sel_registerName(name: *const c_char) -> Sel {
    unsafe { Sel::from_ptr(name.cast()) }
}

#[no_mangle]
extern "C" fn objc_msgSend(obj: *mut Object, sel: Sel) {
    my_selector(obj, sel)
}

#[no_mangle]
extern "C" fn object_getClass(obj: *const Object) -> *const Class {
    // Must be a valid pointer, so don't return isa
    obj.cast()
}

#[no_mangle]
extern "C" fn class_getInstanceVariable(cls: *const Class, _name: *const c_char) -> *const Ivar {
    cls.cast()
}

#[no_mangle]
extern "C" fn ivar_getTypeEncoding(_ivar: *const Ivar) -> *const c_char {
    CStr::from_bytes_with_nul(b"i\0").unwrap().as_ptr()
}

#[no_mangle]
extern "C" fn ivar_getOffset(_ivar: *const Ivar) -> isize {
    // isa is 64 bits
    8
}

You should see the following miri error, which makes sense as explained in the code comments:

error: Undefined Behavior: trying to reborrow <6300> for SharedReadOnly permission at alloc1621[0x8], but that tag does not exist in the borrow stack for this location
   --> examples/msg_send_unsound.rs:26:5
    |
26  |     assert_eq!(*a, 43);
    |     ^^^^^^^^^^^^^^^^^^
    |     |
    |     trying to reborrow <6300> for SharedReadOnly permission at alloc1621[0x8], but that tag does not exist in the borrow stack for this location
    |     this error occurs as part of a reborrow at alloc1621[0x8..0xc]
    |
    = help: this indicates a potential bug in the program: it performed an invalid operation, but the rules it violated are still experimental
    = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <6300> was created by a retag at offsets [0x8..0xc]
   --> examples/msg_send_unsound.rs:18:22
    |
18  |     let a = unsafe { obj.get_ivar::<i32>("a") };
    |                      ^^^^^^^^^^^^^^^^^^^^^^^^
help: <6300> was later invalidated at offsets [0x8..0xc]
   --> src/runtime.rs:510:9
    |
510 |         &mut *ptr
    |         ^^^^^^^^^
    = note: inside `main` at $TOOLCHAIN/lib/rustlib/src/rust/library/core/src/macros/mod.rs:38:16
    = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
A bit more info

The problem is that you have to somehow specify that the object is mutated by the message send, which you can't really do properly.

Just to illustrate that this is indeed an issue with how msg_send! works, try applying the following quick patch which changes msg_send! to always require mutable objects (obviously not a real fix):

diff --git a/src/macros.rs b/src/macros.rs
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -130,7 +130,7 @@ macro_rules! msg_send {
     ($obj:expr, $name:ident) => ({
         let sel = sel!($name);
         let result;
-        match $crate::__send_message(&*$obj, sel, ()) {
+        match $crate::__send_message(&mut *$obj, sel, ()) {
             Err(s) => panic!("{}", s),
             Ok(r) => result = r,
         }
@@ -139,7 +139,7 @@ macro_rules! msg_send {
     ($obj:expr, $($name:ident : $arg:expr)+) => ({
         let sel = sel!($($name:)+);
         let result;
-        match $crate::__send_message(&*$obj, sel, ($($arg,)*)) {
+        match $crate::__send_message(&mut *$obj, sel, ($($arg,)*)) {
             Err(s) => panic!("{}", s),
             Ok(r) => result = r,
         }

Then you'll get the expected rustc error:

error[E0502]: cannot borrow `*obj` as mutable because it is also borrowed as immutable
  --> examples/msg_send_unsound.rs:22:21
   |
18 |     let a = unsafe { obj.get_ivar::<i32>("a") };
   |                      ------------------------ immutable borrow occurs here
...
22 |         let _: () = msg_send![obj, my_selector];
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
...
26 |     assert_eq!(*a, 43);
   |     ------------------ immutable borrow later used here
   |
   = note: this error originates in the macro `msg_send` (in Nightly builds, run with -Z macro-backtrace for more info)

This is similar to the problem with nulls I noted in #102, and probably requires some kind of change in msg_send!, though I don't actually know how to do this yet (suggestions appreciated).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions