speice.io/_posts/2019-02-14-insane-allocators.md

2.7 KiB

layout title description category tags
post Insane Allocators: SEGFAULTs in safe Rust "Trusting trust" with allocators. rust, memory

Having recently spent a lot of time studying the weird ways that Rust uses memory, I like to think I finally understand the rules well enough to break them. Specifically - what are the assumptions that underpin Rust's memory model? It wasn't a question particularly relevant to understanding how Rust allocates memory, but is certainly worth discussing as an addendum. Let's finish off this series on Rust and memory by breaking the most important rules Rust has!

Rust's whole shtick is that it's "memory safe." In practice, this (should) mean that there's no undefined behavior in safe Rust, because the compiler/borrow checker makes sure you can't get yourself into a situation where you misuse or corrupt memory. But is it possible for Rust programs, written without using unsafe, to encounter a segfault?

Of course it is! Using an unmodified compiler, I can build a simple "Hello, world!" application that dies due to memory corruption:

Wait, wat?

There's obviously something nefarious going on. I mean, why would anyone use sudo to run the rustc compiler?

And for that matter, why does Rust 1.31.0 behave differently from Rust 1.32.0?

To pull off this chicanery, I'm making use of a special environment variable in Linux called LD_PRELOAD. I won't go into detail the way Matt Godbolt does, but the important bit is this: I can insert my own code in place of functions typically implemented by the C standard library.

Second, there's a very special implementation of malloc that is being picked up by LD_PRELOAD:

use std::ffi::c_void;
use std::ptr::null_mut;

// Start off with an empty pointer
static mut ALLOC: *mut c_void = null_mut();

#[no_mangle]
pub extern "C" fn malloc(size: usize) -> *mut c_void {
    unsafe { 
        // If we've never allocated anything, ask the operating system
        // for some memory...
        if ALLOC == null_mut() {
            ALLOC = libc::malloc(size)
        }
        // ...and then give that same section of memory to everyone,
        // corrupting the location.
        return ALLOC;
    }
}

Now, there are two questions yet to answer:

  1. Why was sudo used to compile?
  2. Why did Rust 1.31 work when 1.32 didn't?