Can a stack allocated Rust buffer be accessed through C++?

In order to avoid head allocations, and since I know the maximum MTU of an ethernet packet, I created a small buffer: [u8, MAX_BYTES_TRANSPORT] in Rust, that C++ should fill for me:

pub fn receive(&mut self, f: &dyn Fn(&[u8])) -> std::result::Result<(), &str> {
    let mut buffer: [u8; MAX_BYTES_TRANSPORT] = [0; MAX_BYTES_TRANSPORT];
    //number of bytes written in buffer in the C++ side
    let written_size: *mut size_t = std::ptr::null_mut::<size_t>();
    let r = unsafe{openvpn_client_receive_just(
        buffer.as_mut_ptr(), buffer.len(), written_size, self.openvpn_client)};

So, the function openvpn_client_receive_just, which is a C++ function with C interface, should write to this buffer. Is this safe? I couldn’t find information about a stack allocated Rust buffer being used in C++

This is the function:

uint8_t openvpn_client_receive_just(
    uint8_t *buffer, size_t buffer_size, size_t *written_size, OpenVPNSocket *client)

Answer

Can a stack allocated buffer be accessed through C++?

Yes.

From the type-system perspective there is no difference between statically allocated, stack allocated, or heap allocated: the C signature only takes pointer and size, and cares little where that pointer points to.

Is this safe?

Most likely.

As long as the C function is correctly written and respects the bounds of the buffer, this will be safe. If it doesn’t, well, that’s a bug.

One could argue that it’s better to have a heap-allocated buffer, but honestly once one starts writing out of bounds, overwriting arbitrary stack bytes or overwriting arbitrary heap bytes are both bad, and have undefined behavior.

For extra security, you could use a heap allocated nested between 2 guard pages. Using OS specific facilities, you could allocate 3 contiguous OS pages (typically 4KB each on x86), then mark the first and last as read-only and put your buffer in the middle one. Then any (close) write before or after the buffer would be caught by the OS. Larger jumps, though, wouldn’t… so it’s a lot of effort for a mitigation.

Is your code safe?

You most likely need to know how many bytes were written, so using a null pointer is strange.

I’d expect to see:

let mut written: size_t = 0;
let written_size = &mut written as *mut _;

And yes, that’s once again a pointer to a stack variable, just like you would in C.


A note on style. Your Rust code is unusual in that you use fully typed variables and full paths, a more idiomatic style would be:

// Result is implicitly in scope.

pub fn receive(&mut self, f: &dyn Fn(&[u8])) -> Result<(), &str) {
    let mut buffer = [0u8; MAX_BYTES_TRANSPORT];

    let mut written: size_t = 0;
    let written_size = &mut written as *mut _;

    //  Safety:
    //  <enumerate preconditions to safely call the function here, and why they are met>
    let result = unsafe {
        openvpn_client_receive_just(
            buffer.as_mut_ptr(), buffer.len(), written_size, self.openvpn_client)
    };

    translate_openvpn_error(result)?;

    let buffer = &buffer[0..written];
    f(buffer);

    Ok(())
}

I did annotate the type for written, to help along inference, but strictly speaking it should not be necessary.

Also, I like to preface every unsafe call I make with the list of pre-conditions that make it safe, and for each why they are met. It helps me audit my unsafe code later on.

Leave a Reply

Your email address will not be published. Required fields are marked *