Would be lovely if s/o could check this simple async executor for UB

⚓ Rust    📅 2025-08-01    👤 surdeus    👁️ 10      

surdeus

Warning

This post was published 124 days ago. The information described in this article may have changed.

Hi!

I just wrote an very simply async executor for an exclusively owned interrupt. As it requires me to synchronize a pinned task between the spawning thread and the interrupt handler (both on the same CPU of course), I have to juggle with atomics, fences and pointers.

While this approach “seems” to work on real hardware, I’m unsure whether it contains UB as I still have very little experience with unsafe code.

A quick review by s/o having more experience would be highly appreciated!

use core::{
    pin::{Pin, pin},
    ptr::{self, null_mut},
    sync::atomic::compiler_fence,
    task::{Context, RawWaker, RawWakerVTable, Waker},
};

use cortex_m::asm::wfe;
use nrf52840_pac::{NVIC, SWI0, interrupt};
use portable_atomic::{AtomicPtr, Ordering};
use static_cell::StaticCell;

static WAKER: Waker = unsafe { Waker::from_raw(raw_waker()) };
static TASK: AtomicPtr<Pin<&mut dyn Future<Output = ()>>> = AtomicPtr::new(null_mut());
static EXECUTOR: StaticCell<InterruptExecutor> = StaticCell::new();

/// Transferring ownership of the SWI peripheral proves that only a single
/// instance of the executor can be requested.
pub fn executor(_swi: SWI0) -> &'static mut InterruptExecutor {
    let executor = EXECUTOR.init(InterruptExecutor);
    executor.init()
}

pub struct InterruptExecutor;

impl InterruptExecutor {
    fn init(&'static mut self) -> &'static mut Self {
        NVIC::unpend(interrupt::SWI0_EGU0);
        unsafe { NVIC::unmask(interrupt::SWI0_EGU0) };
        self
    }

    /// Associates a task with the executor and drives it to completion.
    ///
    /// Requiring a mutable reference ensures that only a single task can be
    /// scheduled at any time.
    pub fn spawn_task<Task: Future<Output = ()>>(&mut self, task: Task) {
        debug_assert!(NVIC::is_enabled(interrupt::SWI0_EGU0), "not initialized");

        let mut pinned_task: Pin<&mut dyn Future<Output = ()>> = pin!(task);
        compiler_fence(Ordering::Release);
        TASK.store(ptr::from_mut(&mut pinned_task).cast(), Ordering::Relaxed);

        // Initially poll once.
        NVIC::pend(interrupt::SWI0_EGU0);

        loop {
            rtos_trace::trace::system_idle();
            wfe();
            if TASK.load(Ordering::Relaxed).is_null() {
                compiler_fence(Ordering::Acquire);

                // We need to extend lifetime until we're sure the task is no
                // longer being accessed.
                #[allow(clippy::drop_non_drop)]
                drop(pinned_task);
                break;
            }
        }
    }
}

#[interrupt]
fn SWI0_EGU0() {
    let task = TASK.load(Ordering::Relaxed);
    compiler_fence(Ordering::Acquire);

    // Safety: We're converting from a pointer that has been generated verbatim
    //         from a valid &mut reference. Therefore the pointer will be
    //         properly aligned, dereferenceable and point to a valid pinned
    //         future. Pinning and synchronizing via the pointer ensures that
    //         pointer cannot dangle.
    if let Some(task_ref) = unsafe { task.as_mut() } {
        let mut cx = Context::from_waker(&WAKER);
        match task_ref.as_mut().poll(&mut cx) {
            core::task::Poll::Ready(_) => {
                compiler_fence(Ordering::Release);
                TASK.store(null_mut(), Ordering::Relaxed);
            }
            core::task::Poll::Pending => {}
        }
    }
}

static VTABLE: RawWakerVTable = RawWakerVTable::new(clone_waker, wake, wake_by_ref, drop_waker);

const fn raw_waker() -> RawWaker {
    RawWaker::new(ptr::null(), &VTABLE)
}

unsafe fn clone_waker(data: *const ()) -> RawWaker {
    // Safety: We always return the same (static) vtable reference to ensure
    //         that `Waker::will_wake()` recognizes the clone.
    RawWaker::new(data, &VTABLE)
}

unsafe fn wake(_: *const ()) {
    // Safety: Pending an interrupt is atomic and idempotent.
    NVIC::pend(interrupt::SWI0_EGU0);
}

unsafe fn wake_by_ref(_: *const ()) {
    // Safety: Pending an interrupt is atomic and idempotent.
    NVIC::pend(interrupt::SWI0_EGU0);
}

unsafe fn drop_waker(_: *const ()) {
    // no-op
}

2 posts - 2 participants

Read full topic

🏷️ Rust_feed