rp-rs / rp-hal

A Rust Embedded-HAL for the rp series microcontrollers
https://crates.io/crates/rp2040-hal
Apache License 2.0
1.46k stars 237 forks source link

Replace `static mut` for CORE1_STACK #864

Open jannic opened 1 month ago

jannic commented 1 month ago

Both in our docs and in some examples, we suggest using a static mut for allocating memory for core1's stack:

  static mut CORE1_STACK: Stack<SIZE: 4096> = Stack::new();
[...]
    core1.spawn(unsafe { &mut CORE1_STACK.mem }, [...] )

With rust 1.83, this will cause a warning (at least it does with current beta), and with edition 2024 the lint will become deny-by-default. (https://doc.rust-lang.org/nightly/edition-guide/rust-2024/static-mut-references.html)

Therefore, we should suggest something else. Maybe we should also change the signature of multicore::Core::spawn to take a pointer instead of a &'static mut for the stack?

thejpster commented 1 month ago

Yeah we shouldn't use a reference here - a pointer is fine. But not a *const u8 because it might not be sufficiently aligned.

For the global a UnsafeCell with a MaybeUninit makes sense because we don't need to init the stack to anything.

9names commented 2 weeks ago

Yeah we shouldn't use a reference here - a pointer is fine. But not a *const u8 because it might not be sufficiently aligned.

The Stack struct itself ensures the address is sufficiently aligned, is that not enough?

thejpster commented 2 weeks ago

If the argument is a pointer to a u8, what forces them to use the Stack struct?

jannic commented 2 weeks ago

If the argument is a pointer to a u8, what forces them to use the Stack struct?

Well if the spawn function takes a pointer it doesn't really matter if it's a *mut u8 or a *mut Stack<SIZE>: Casting pointers is safe, so the function signature doesn't guarantee that the pointer points to anything sensible.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=74374413e46fd1078eb93c58c381e2ae

So that would be an argument to find a different solution, where spawn gets passed some reference or struct that actually guarantees alignment. It's not like the current function signature is wrong. It's just that the most obvious way to get a reference with a static lifetime, using a static mut, is dangerous and deprecated.

9names commented 2 weeks ago

We could make multicore::spawn() fallible (and return Err if not aligned), or keep it infallible and panic instead?

jannic commented 2 weeks ago

As I understand it, as soon as the function takes a pointer, it must be unsafe, as a pointer could be anything, and there's no feasible runtime check to ensure it's valid. (Alignment is not the only issue.)

And once we make it unsafe, we are technically free to just assume it's properly aligned, given we document the function accordingly. Sure, panicking if we can detect an invalid pointer would be a nice safeguard.

I would prefer a safe API where the type system guarantees a valid argument, like we currently have.

thejpster commented 2 weeks ago

So, something like:

use std::sync::atomic::Ordering;
use std::sync::atomic::AtomicBool;
use std::mem::MaybeUninit;
use std::cell::UnsafeCell;

#[repr(C, align(65536))]
struct Stack<const N: usize> {
   inner: UnsafeCell<MaybeUninit<[u8; N]>>,
   taken: AtomicBool
}

unsafe impl<const N: usize> Sync for Stack<N> {}

impl<const N: usize> Stack<N> {
    const fn new() -> Stack<N> {
        Stack {
            inner: UnsafeCell::new(unsafe { MaybeUninit::zeroed().assume_init() }),
            taken: AtomicBool::new(false),
        }
    }

    fn get(&self) -> *const u8 {
        // I guess if you have CAS, you could do that
        if self.taken.load(Ordering::Relaxed) {
            panic!("Stack taken");
        }
        self.taken.store(true, Ordering::Relaxed);
        (&raw const self.inner).cast()
    }
}

fn spawn<const N: usize>(stack: &Stack<N>) {
    println!("Buffer starts at {:p}", stack.get())
}

static STACK: Stack<8192> = Stack::new();

fn main() {
    spawn(&STACK);
}
jannic commented 2 weeks ago

Yes, for example. I implemented it very similar in a proof-of-concept I wrote:

use std::cell::Cell;
use std::cell::UnsafeCell;

// Mock implementation so this snippet is executable
mod critical_section {
    pub fn with<T>(f: impl FnOnce(()) -> T) ->T {
        f(())
    }
}

#[repr(C, align(32))]
pub struct Stack<const SIZE: usize> {
    /// Memory to be used for the stack
    mem: UnsafeCell<[usize; SIZE]>,
    taken: Cell<bool>,
}

unsafe impl<const SIZE: usize> Sync for Stack<SIZE> {}

impl<const SIZE: usize> Stack<SIZE> {
    /// Construct a stack of length SIZE, initialized to 0
    pub const fn new() -> Stack<SIZE> {
        Stack {
            mem: UnsafeCell::new([0; SIZE]),
            taken: Cell::new(false),
        }
    }

    /// Get a mutable reference to the contained memory.
    ///
    /// Only succeeds once. Subsequent calls will return an empty option.
    pub fn take(&self) -> Option<&mut [usize; SIZE]> {
        let taken = critical_section::with(|_| self.taken.replace(true));
        if taken {
            None
        } else {
            // Safety: Above code ensures that this can only be executed once.
            Some(unsafe { &mut *self.mem.get() })
        }
    }
}

static CORE1_STACK: Stack<4096> = Stack::new();

fn spawn(stack: &'static mut [usize]) {
    println!("Buffer starts at {:p}", stack)
}

fn main() {
    spawn(CORE1_STACK.take().unwrap());
}

Advantage:

Disadvantage:

BTW, passing a &mut into spawn and then writing into that memory using pointer writes is probably UB. This is not a regression but an existing bug we should care about anyway.

jannic commented 2 weeks ago

BTW, passing a &mut into spawn and then writing into that memory using pointer writes is probably UB. This is not a regression but an existing bug we should care about anyway.

Also note that if this assessment is correct, we need to change the signature of spawn anyway to make it sound.

thejpster commented 2 weeks ago

I'm not sure I'm comfortable passing around an &mut [u8], given the second core will be writing to the same memory in the background. I think it's better to pass around a &Stack and get the pointer out of it right when you hand the pointer to Core 1 to use as the stack pointer.

jannic commented 2 weeks ago

Yes, that's exactly what I meant with "Also note that if this assessment is correct, we need to change the signature of spawn anyway to make it sound."

While it would be nice to not break the current API, I don't think this can't be sound. So we have to change it.

Still thinking about the best alternative. But yes, it probably needs to be some struct that's passed by shared reference.

jannic commented 2 weeks ago

How about such an interface?

pub struct Stack<const SIZE: usize> {
    [private fields]
}

unsafe impl<const SIZE: usize> Sync for Stack<SIZE> {}

impl<const SIZE: usize> Stack<SIZE> {
    /// Construct a stack of length SIZE, initialized to 0
    pub const fn new() -> Stack<SIZE> {
        [...]
    }

    /// Take the StackAllocation out of this Stack.
    ///
    /// This returns None if the stack is already taken.
    pub fn take(&self) -> Option<StackAllocation> {
        [...]
    }
}

/// This object represents a memory area which can be used for a stack.
///
/// It is essentially a range of pointers with these additional guarantees:
/// The begin / end pointers must define a stack
/// with proper alignment (at least 8 bytes, preferably 32 bytes)
/// and sufficient size (TBD bytes). The underlying memory must
/// have a static lifetime. No references to that memory must exist.
/// Therefore, a function that gets passed such an object is free to write
/// to arbitrary memory locations in the range, and may continue to do
/// so after dropping this object.
pub struct StackAllocation {
    mem: Range<*mut usize>,  // private field, so this object can't be created directly
}

impl StackAllocation {
    fn get(&self) -> Range<*mut usize> {
        self.mem.clone()
    }

    /// Unsafely construct a stack allocation
    ///
    /// This is mainly useful to construct a stack allocation in some memory region
    /// defined in a linker script, for example to place the stack in the SRAM4/5 regions.
    ///
    /// # Safety
    ///
    /// The caller must ensure that the guarantees that a StackAllocation provides
    /// are upheld.
    pub unsafe fn from_raw_parts(start: *mut usize, end: *mut usize) -> Self {
        StackAllocation { mem: start..end }
    }
}

[...]

    pub fn spawn<F>(&mut self, stack: StackAllocation, entry: F) -> Result<(), Error>
    where
        F: FnOnce() + Send + 'static,

So spawn gets an owned StackAllocation, guaranteeing exclusive access to the memory range. Such an allocation can be easily created without unsafe user code:

static CORE1_STACK: Stack<4096> = Stack::new();
[...]
    core1.spawn(CORE1_STACK.take().unwrap(), move || { [...] })

With this approach, it's also possible to place the stack in RAM4 by defining appropriate linker symbols and calling unsafe { StackAllocation::from_raw_parts(sram4_start, sram4_end) }. This obviously requires some unsafe code, but it provides flexibility if one wants to start with memory not managed by rust for some reason.

Design considerations:

thejpster commented 1 week ago

I love it.