Merge #1177
1177: STD driver needs a reentrant mutex; logic fixed to be reentrancy-safe r=Dirbaio a=ivmarkov
...or to summarize it in another way, the code in the alarm thread loop is written as if - when calling the user-supplied callback - the callback will *never, ever* call `alarm.set_alarm()`.
But this happens of course - at least with the generic timer queue implementation. Not sure if that would happen with `embassy-executor`'s own queue, but probably yes?
The end result on Linux is that the code deadlocks because when calling the user-supplied callback, the mutex of the alarms is locked, yet - the code in `set_alarm` tries to take the lock again leading to UB. (I suspect on Windows this will crash rather than deadlock but that's a bit irrelevant.)
(Note also that calling the user-supplied callback *outside* of the alarms' lock is also NOK, because at that time, the callback and/or context itself might be invalid as well, as the user might had changed it with a new one by calling `set_callback`. Right?)
I also had to fix the logic that computed the next timestamp when the alarm should fire; it was running a simple `for {}` loop, not anticipating that the just-traversed alarm might get a new timestamp.
The new code is slightly less efficient, in that on each `loop {}` iteration it always starts traversing the alarms from the beginning, whereas in reality only the timestamp of the alarm that just-fired could've changed, but given the complexities introduced by `RefCell`, I don't think we should bother with these micro-optimizations, for just 4 alarms in total.
Co-authored-by: ivmarkov <ivan.markov@gmail.com>
			
			
This commit is contained in:
		
						commit
						ba18656e94
					
				| @ -1,10 +1,12 @@ | ||||
| use std::cell::UnsafeCell; | ||||
| use std::cell::{RefCell, UnsafeCell}; | ||||
| use std::mem::MaybeUninit; | ||||
| use std::sync::{Condvar, Mutex, Once}; | ||||
| use std::time::{Duration as StdDuration, Instant as StdInstant}; | ||||
| use std::{mem, ptr, thread}; | ||||
| 
 | ||||
| use atomic_polyfill::{AtomicU8, Ordering}; | ||||
| use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; | ||||
| use embassy_sync::blocking_mutex::Mutex as EmbassyMutex; | ||||
| 
 | ||||
| use crate::driver::{AlarmHandle, Driver}; | ||||
| 
 | ||||
| @ -35,7 +37,10 @@ struct TimeDriver { | ||||
|     alarm_count: AtomicU8, | ||||
| 
 | ||||
|     once: Once, | ||||
|     alarms: UninitCell<Mutex<[AlarmState; ALARM_COUNT]>>, | ||||
|     // The STD Driver implementation requires the alarms' mutex to be reentrant, which the STD Mutex isn't
 | ||||
|     // Fortunately, mutexes based on the `critical-section` crate are reentrant, because the critical sections
 | ||||
|     // themselves are reentrant
 | ||||
|     alarms: UninitCell<EmbassyMutex<CriticalSectionRawMutex, RefCell<[AlarmState; ALARM_COUNT]>>>, | ||||
|     zero_instant: UninitCell<StdInstant>, | ||||
|     signaler: UninitCell<Signaler>, | ||||
| } | ||||
| @ -53,7 +58,8 @@ crate::time_driver_impl!(static DRIVER: TimeDriver = TimeDriver { | ||||
| impl TimeDriver { | ||||
|     fn init(&self) { | ||||
|         self.once.call_once(|| unsafe { | ||||
|             self.alarms.write(Mutex::new([ALARM_NEW; ALARM_COUNT])); | ||||
|             self.alarms | ||||
|                 .write(EmbassyMutex::new(RefCell::new([ALARM_NEW; ALARM_COUNT]))); | ||||
|             self.zero_instant.write(StdInstant::now()); | ||||
|             self.signaler.write(Signaler::new()); | ||||
| 
 | ||||
| @ -66,25 +72,37 @@ impl TimeDriver { | ||||
|         loop { | ||||
|             let now = DRIVER.now(); | ||||
| 
 | ||||
|             let mut next_alarm = u64::MAX; | ||||
|             { | ||||
|                 let alarms = &mut *unsafe { DRIVER.alarms.as_ref() }.lock().unwrap(); | ||||
|                 for alarm in alarms { | ||||
|                     if alarm.timestamp <= now { | ||||
|                         alarm.timestamp = u64::MAX; | ||||
|             let next_alarm = unsafe { DRIVER.alarms.as_ref() }.lock(|alarms| { | ||||
|                 loop { | ||||
|                     let pending = alarms | ||||
|                         .borrow_mut() | ||||
|                         .iter_mut() | ||||
|                         .find(|alarm| alarm.timestamp <= now) | ||||
|                         .map(|alarm| { | ||||
|                             alarm.timestamp = u64::MAX; | ||||
| 
 | ||||
|                         // Call after clearing alarm, so the callback can set another alarm.
 | ||||
|                             (alarm.callback, alarm.ctx) | ||||
|                         }); | ||||
| 
 | ||||
|                     if let Some((callback, ctx)) = pending { | ||||
|                         // safety:
 | ||||
|                         // - we can ignore the possiblity of `f` being unset (null) because of the safety contract of `allocate_alarm`.
 | ||||
|                         // - other than that we only store valid function pointers into alarm.callback
 | ||||
|                         let f: fn(*mut ()) = unsafe { mem::transmute(alarm.callback) }; | ||||
|                         f(alarm.ctx); | ||||
|                         let f: fn(*mut ()) = unsafe { mem::transmute(callback) }; | ||||
|                         f(ctx); | ||||
|                     } else { | ||||
|                         next_alarm = next_alarm.min(alarm.timestamp); | ||||
|                         // No alarm due
 | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|                 alarms | ||||
|                     .borrow() | ||||
|                     .iter() | ||||
|                     .map(|alarm| alarm.timestamp) | ||||
|                     .min() | ||||
|                     .unwrap_or(u64::MAX) | ||||
|             }); | ||||
| 
 | ||||
|             // Ensure we don't overflow
 | ||||
|             let until = zero | ||||
| @ -121,18 +139,23 @@ impl Driver for TimeDriver { | ||||
| 
 | ||||
|     fn set_alarm_callback(&self, alarm: AlarmHandle, callback: fn(*mut ()), ctx: *mut ()) { | ||||
|         self.init(); | ||||
|         let mut alarms = unsafe { self.alarms.as_ref() }.lock().unwrap(); | ||||
|         let alarm = &mut alarms[alarm.id() as usize]; | ||||
|         alarm.callback = callback as *const (); | ||||
|         alarm.ctx = ctx; | ||||
|         unsafe { self.alarms.as_ref() }.lock(|alarms| { | ||||
|             let mut alarms = alarms.borrow_mut(); | ||||
|             let alarm = &mut alarms[alarm.id() as usize]; | ||||
|             alarm.callback = callback as *const (); | ||||
|             alarm.ctx = ctx; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     fn set_alarm(&self, alarm: AlarmHandle, timestamp: u64) -> bool { | ||||
|         self.init(); | ||||
|         let mut alarms = unsafe { self.alarms.as_ref() }.lock().unwrap(); | ||||
|         let alarm = &mut alarms[alarm.id() as usize]; | ||||
|         alarm.timestamp = timestamp; | ||||
|         unsafe { self.signaler.as_ref() }.signal(); | ||||
|         unsafe { self.alarms.as_ref() }.lock(|alarms| { | ||||
|             let mut alarms = alarms.borrow_mut(); | ||||
| 
 | ||||
|             let alarm = &mut alarms[alarm.id() as usize]; | ||||
|             alarm.timestamp = timestamp; | ||||
|             unsafe { self.signaler.as_ref() }.signal(); | ||||
|         }); | ||||
| 
 | ||||
|         true | ||||
|     } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user