What drop guarantees does NLL provide for authors of unsafe code?

An issue was created regarding my recent WASM as a Platform for Abstraction blog post pointing out potential undefined behaviour.

I've created an example of what we're doing on the playground. It could be minimised a bit further, but this topic is subtle so I don't want to accidentally miss something.

The scenario is like this:

  • There's some Ctx object which has a void * property for attaching arbitrary data to the context
  • We create a State object as a local variable and make context.data point to it
  • To prevent accidental aliased pointers, shadow the state variable (let state = ())
  • Call a function with the Ctx that casts context.data to State and does something non-trivial with it
  • Catch any panics which the function may have triggered
  • Set set context.data = NULL so we can't accidentally have any dangling pointers when this function returns (context is a long-lived object)
  • Continue unwinding or return the function's result
Example
use std::os::raw::c_void;
use std::panic::{self, AssertUnwindSafe};
use std::ptr;

struct Ctx {
    data: *mut c_void,
}

struct State {
    something_important: u32,
}

/// # Safety
///
/// This function relies on `ctx.data` pointing to a valid `State`.
unsafe fn execute_function_with_ctx(ctx: &mut Ctx) {
    if ctx.data.is_null() {
        return;
    }

    let state = &mut *(ctx.data as *mut State);

    // do something important using `state`
    state.something_important += 1;
}

fn main() {
    // pretend this was created elsewhere and passed into the function
    let mut ctx = Ctx {
        data: ptr::null_mut(),
    };

    // set up the state
    let mut state = State {
        something_important: 42,
    };
    ctx.data = &mut state as *mut State as *mut c_void;
    // we can't use the old state variable any more (we'd have aliased
    // pointers) so deliberately shadow it
    #[allow(unused_variables)]
    let state = ();

    let outcome =
        unsafe { panic::catch_unwind(AssertUnwindSafe(|| execute_function_with_ctx(&mut ctx))) };

    // make sure the data pointer is always cleared so we don't have a dangling pointer
    ctx.data = ptr::null_mut();

    // continue unwinding or return the result
    match outcome {
        Ok(value) => println!("Returned {:?}", value),
        Err(panic_object) => panic::resume_unwind(panic_object),
    }
}

I also ran the playground example through clippy and miri and it doesn't see anything unsound.

1 Like

The person who filed the issue is incorrect. NLL is only a change in compile-time borrow checking. It did not change the run-time behavior of any programs, and in particular it did not change when drop gets called. A value is still dropped deterministically, when its owner goes out of scope or is reassigned.

13 Likes

More generally, it would make no sense for a static analysis tool, such as borrowck, to change the behavior / semantics of a program.

3 Likes