flak rss random

what if the poison were rust?

The OpenBSD kernel has a set of functions to help detect memory corruption, the poison subroutines. The memory management code uses these functions, but they themselves have a very simple interface, no complicated types or data structures, meaning they’re easy to replace. What if we rewrite the memory corruption detection functions in rust so it’s impossible for them to cause memory corruption?

poison

There’s three functions needed. One to overwrite memory, one to check it, and one helper to pick the poison value.

void	poison_mem(void *, size_t);
int	poison_check(void *, size_t, size_t *, uint32_t *);
uint32_t poison_value(void *);

Conveniently, we don’t need to include or translate the prototypes from the C header. Just need to make the ABI close enough, and let the CPU do the casting for us. More or less transliterated from the C implementation, we get this.

const POISON0: u32 = 0xdeadbeef;
const POISON1: u32 = 0xdeafbead;

#[no_mangle]
pub extern "C" fn
poison_value(v: usize) -> u32 {
    let l = v >> 12;
    match l & 3 {
        0 => return POISON0,
        1 => return POISON1,
        2 => return POISON0,
        3 => return POISON1,
        4_usize.. => todo!(), // are you kidding me???
    }
}

#[no_mangle]
pub extern "C" fn
poison_mem(m: *mut u32, l: usize) {
    let poison = poison_value(m as usize);
    unsafe {
        for i in 0..l/4 {
            *m.add(i) = poison;
        }
    }
}

#[no_mangle]
pub extern "C" fn
poison_check(m: *mut u32, l: usize, pidx: *mut usize, pval: *mut u32) -> i32 {
    let poison = poison_value(m as usize);
    unsafe {
        for i in 0..l/4 {
            if *m.add(i) != poison {
                *pidx = i;
                *pval = *m.add(i);
                return 1;
            }
        }
    }
    return 0;
}

Stick that in kern/subr_poison.rs.

compile

First order of business is to build the rest of the kernel; cd down into the compile directory; make obj; make config; make. Let that cook over night and in the morning our radeon bootloader will be ready for surgery.

The quick and dirty way to get a manually built object file into the kernel is to remove it; rm subr_poison.o; and then compile a replacement.

The rust compiler is very smart and knows that people only want apps these days, but we’re stuck in the past, so we need to wrestle it into giving us object files.

rustc --emit obj --crate-type lib -C panic=abort -O $SYS/kern/subr_poison.rs

We want an object file, so --emit obj.

We don’t have a main function, not in this file anyway, so --crate-type lib.

The compiler wants to bless us with all sorts of exception handlers, but yolo, so -C panic=abort.

The compiler also wants to jam in a bunch of overflow debugging hooks unless we turn on optimizations, so -O.

Now that we have a new object file, running make again will relink the kernel, rust poison and all. Done!

makefile

Ideally, the makefile should be doing some of the boring work for us. Mostly just needs a few edits to replace the name of the source file, and a new suffix rule.

.SUFFIXES: .s .S .c .o .rs .o

.rs.o:
    rustc --emit obj --crate-type lib -C panic=abort -O $<

The suffix rules are actually encoded into config, so we’d have to edit another program to keep them around long term. After that, and with some light edits to the files list, and we should be good.

Posted 09 Apr 2025 04:48 by tedu Updated: 09 Apr 2025 04:48
Tagged: openbsd rust