Would be lovely if s/o could check this simple async executor for UB
⚓ Rust 📅 2025-08-01 👤 surdeus 👁️ 10Hi!
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
🏷️ Rust_feed