Will rustc emit memory barriers for calling into an FFI function

Hi,

I am trying to achieve something in rust that:

  1. setup some memory
  2. then call an FFI function which uses the memory

The code looks like:

extern "C" {
  fn f();
}

fn task(buf: &mut [u8]) {
  buf[0] = 42;
  unsafe { f(); }
}

Since f() as a foreign function uses the memory pointed by buf (but this information is hard for rustc to infer because f takes no argument) and assumes the memory is correctly initialized there, it would be really bad if rustc can reorder buf[0] = 42 after the function call. Is there any guarantee that memory reordering won't happen around an FFI call?

Not sure how it's possible for f to use the memory on foreign stack, to be honest. Anyway, I've made a little test (indexing replaced with get_mut, so that there's no panic in the assembly):

extern "C" {
  fn f();
}

fn task(buf: &mut [u8]) {
  match buf.get_mut(0) {
      Some(v) => *v = 42,
      None => unsafe { std::hint::unreachable_unchecked(); }
  }
  unsafe { f(); }
}

pub fn run_task() -> u8 {
    let mut buf = [0; 4];
    task(&mut buf);
    buf[1]
}

Playground
If you choose "Show assembly", you'll see that the memory initialization in this case was elided at all.

Because buf is an exclusive reference, it is UB for any code (including foreign code) to access the buffer in any way except through the buf local variable, or something derived from it.


If you use &[Cell<u8>] instead, and pass the buffer over the FFI barrier somewhere, the compiler appears to do the right thing:

use std::cell::Cell;

extern "C" {
  fn f();
  fn g(_:*const Cell<u8>);
}

fn task(buf: &[Cell<u8>]) {
  match buf.get(0) {
      Some(v) => v.set(42),
      None => unsafe { std::hint::unreachable_unchecked(); }
  }
  unsafe { f(); }
}

pub fn run_task() -> u8 {
    let mut buf = [0; 4];
    let cells = Cell::as_slice_of_cells(Cell::from_mut(&mut buf));
    unsafe { g(cells.as_ptr()) }
    task(cells);
    buf[1]
}

( Playground )

playground::run_task:
	pushq	%rax
	movl	$0, 4(%rsp)
	leaq	4(%rsp), %rdi
	callq	*g@GOTPCREL(%rip)
	movb	$42, 4(%rsp)
	callq	*f@GOTPCREL(%rip)
	movb	5(%rsp), %al
	popq	%rcx
	retq
4 Likes

The compiler will infer from your code that f will not modify the values in buf because mutable references are guaranteed to have exclusive access.

4 Likes

Thanks for the reply! I wasn't clear that buf is some memory mapped region and the following code is more similar to the original intent:

extern "C" {
  fn f();
}

fn task(buf: &mut [u8]) {
  match buf.get_mut(0) {
      Some(v) => *v = 42,
      None => unsafe { std::hint::unreachable_unchecked(); }
  }
  unsafe { f(); }
}

pub fn run_task(addr: usize, len: usize) {
    let buf = unsafe {
       std::slice::from_raw_parts_mut(addr as *mut u8, len)
    }
    task(buf)
}

The assembly then looks reasonable to me, but I don't know what in this code tells rustc not to do the elision. I know ptr::write_volatile is a thing but I am just wondering if it is necessary for the case.

When it comes to safety of something like this, looking at the assembly is not particularly illuminating. Since if your code has undefined behavior, the compiler is allowed to compile your code into literally anything, which includes it doing what you wanted (but changing its mind in a week!).

You need to make sure that the kind of reference you are using permits this kind of access. A &[Cell<u8>] is one such reference type. Raw pointers is another. Check out the table here for a list of the guarantees in play.

6 Likes

Hi thanks for the suggestion! Seems like giving f an argument is enough to get the memory store back:

extern "C" {
  fn f(_: *mut u8);
}

fn task(buf: &mut [u8]) {
  match buf.get_mut(0) {
      Some(v) => *v = 42,
      None => unsafe { std::hint::unreachable_unchecked(); }
  }
  unsafe { f(buf.as_mut_ptr()); }
}


pub fn run_task() -> u8 {
    let mut buf = [0; 4];
    task(&mut buf);
    buf[1]
}

playground

1 Like

That link is useful! it's not clear to me that it is UB if the foreign function takes a pointer argument, I think this is something allowed by Stacked Borrows?

It's fine if you pass f a raw pointer to buf.

1 Like