Creating a Stack for Low Privilege App

I need to create a separate stack within my program to run code as user-level privilege in regards to security (RISC-V Architecture). This is my first step. An example program in C does so with this line
uint8_t my_stack[768] __attribute__((aligned(16)))
It seems it is creating an array with 768 elements of type usigned char. The aligned 16 is making sure it aligns in memory as 16 bytes wide? How do I recreate this in Rust?

Since its #no_std, maybe the heapless crate?

Well you could wrap the array into a struct requiring proper alignment like so:

#[repr(C, align(16))]
struct MyStack {
    stack: [u8; 768]
}

fn main() {
    let mut my_stack = MyStack { stack: [0; 768] };
}

the instance my_stack is properly aligned to 16Byte and thus the array as well.

1 Like

A stack pointer value is created from this aligned array structure by taking the memory address of the array and adding the size of the array to this address. How is this done in Rust?

This could be done in Rust like so:

fn main() {
    let mut my_stack = MyStack { stack: [0; 768] };
    let raw_ptr = &my_stack as * const MyStack;
    let end_ptr = unsafe { raw_ptr.offset(1) };
    println!("{}", end_ptr as usize - raw_ptr as usize);
}

As raw pointer operations are unsafe we need the unsafe keyword to get the offset address from the strat pointer. We can use an offset of 1 as the we used *const MyStruct as raw pointer type which means each element in the raw pointer points to is of MyStructsize.

The question is what your final goal will be as the use of raw pointers is unsafe and typically unsound.

Appreciate the reply. Im working with a RISC-V microcontroller that has pmp or physical memory protection, with machine and user level privileges. On power, machine mode sets up a memory stack that can be sandboxed by the pmp. Then i will enter the created stack with sp address in user mode for security. The example in C I am looking at is here https://github.com/sifive/example-user-mode/blob/master/example-user-mode.c

Also, I was messing around in the playground with the memory alignment

The align(16) with a struct works. That said, for things like this, modifying the linker script might give more flexibility and certainty where things end up in memory.

That is a little beyond my skill level at the current moment. However, I will definitely investigate your suggestion.

The SiFive guys are telling me that 8 byte alignment should be sufficient for my needs. From using the Rust playground it seems I can simply create a mutable array of type u64, which is 8-byte aligned by default. Does that seem correct?

Yes. core::mem::align_of::<u64>() == 8 on RISC-V (both 32-bit and 64-bit) so that is guaranteed.
Note that, although the hardware doesn't require it, the RISC-V calling convention specifies a 16-byte alignment for the stack pointer. I don't know if Rust makes any assumptions based on this but deviating from it might possibly result in undefined behavior.

2 Likes

Great link, I didn't think of it that way. I'm going to post that on the SiFive forum for more detail on that.

Presumably that's to provide forward-compatible support for RV128. SiFive does not offer RV128, so they didn't bother to support it compatibly.

Here was the best response from SiFive

1 Like

So if you don't intend your code to be usable on RV128 you can go with 64-bit alignment, and if you don't need it to be usable on RV64 you can go with 32-bit alignment. Makes sense, provided that the code contains a big, obvious warning about it's lack of compatibility on larger-word-width architectures.

2 Likes

Yes, it's probably with respect to RV128 and extensions such as the V (Vector) extension. I also vaguely remember discussion that there is some D floating point instruction that relies on 16-byte alignment. But yea neither of these are relevent to SiFive E310.

I was thinking though that if you want to lock down the user stack area using the PMP there may be other alignment constraints for the memory area.

Edit: apparently not, the FE310 manual claims a minimum granularity of 4 bytes:

The E31 PMP unit has 8 regions and a minimum granularity of 4 bytes. Overlapping regions are
permitted. The E31 PMP unit implements the architecturally defined pmpcfgX CSRs pmpcfg0
and pmpcfg1 supporting 8 regions. pmpcfg2 and pmpcfg3 are implemented but hardwired to
zero

The PMP should be fairly simple, as I can program these registers with the riscv crate. This crate provides the means to read/write the CSRs.

For my project, I need to set MEPC with the address of my user level function. Then I set sp to the top of my user level stack, set ra to zero, set the MPP field in MSTATUS to user mode (00), then enter user mode with the MRET instruction. The riscv crate supports all of this except setting the stack pointer, return address, and the mret instruction. I will probably have to write my own inline assembly for that, which I've done it in C, but not in Rust (plus it is unstable, must use nightly?)

I will throw the PMP and MTVEC (machine trap vector) in there after the user mode entry is working.

Is this the best method for getting my new stack pointer? I guess there is no way around unsafe code for this

fn main() {
    let mut my_stack = MyStack { stack: [0; 768] };
    let raw_ptr = &my_stack as * const MyStack;
    let sp = unsafe { raw_ptr.offset(1) };
    println!("{}", sp as usize - raw_ptr as usize);
}

Yes, it has to be unsafe because there is no way that the compiler can analyze all the potential consequences of that assignment and prove that it's safe.

On the stack alignment issue, unless you're totally out of space—in which case you're already screwed—you should use 16-byte alignment for stack frames. Using reduced alignment before measurement proves it necessary smells like a case of premature optimization.

16-byte alignment it is, which is what is used in the C version of the SiFive example (also for the same platform I am using). I was just wondering on how to implement the 16 byte alignment in Rust, and it turned into a pretty complex discussion :sweat_smile:. Next up, use inline assembly and nightly, or precompile the assembly so stable can still be used. (and how to do it)

I think so, reading and writing the CSRs is straightforward through the riscv crate.

The thing that made me think about PMP alignment is the NAPOT "Natural Aligned Power-Of-Two" (as used in example-pmp too). From what I understand this essentially means "AND-mask part of the address". So the region you want to protect would need to be aligned to the size of that region. This simplifies the hardware, but is a much stronger requirement than any for the stack so I hope you can avoid it.

I think so. If you do low-level pointer arithmethic there's no way around unsafe code. These kind of operations are inherently unsafe!

As of right now, I'm planning on using TOR (Top of Range) which eats up an extra PMPcfg entry, but I think that will help with a possible alignment issue and make things a little simpler.