Skip to content

Commit 7aaa0aa

Browse files
cbifflerusty1968
authored andcommitted
stm32xx-i2c: fix notification-induced crash
The I2C code uses the kernel timer to attempt to detect cases where the hardware has "gone away" and stopped generating interrupts. (What this _actually_ means is that the software state machine has gotten a transition wrong: we've never found evidence of misbehavior in the hardware block, at least so far. I'm including this caveat for people who find this commit online later.) However, this code has contained two bugs, either of which could crash the driver. First, the race condition. Under high load (say, when a crash dump is being recorded), the I2C driver may not reliably clear accumulated timer-expired notifications. This can happen as follows: 1. The hardware interrupt arrives. The kernel maps it to the I2C driver's notification mask. The notification bit is immediately set, and the driver is returned to "runnable" state. 2. Load continues and the driver does not actually get to run. 3. The driver's timer goes off. The kernel sets the timer notification bit. However, because the driver has already technically been woken up with an atomic snapshot of notification conditions, the bit will not be _observed_ until the next recv in the driver task. 4. Next time the driver wants to impose a timeout, it sets up the timer and listens for timer notifications. It receives one immediately, decides it has "lost an interrupt," and crashes itself. Second, and hopefully less likely, this behavior allows the driver to be killed by any other task using `sys_post` to the driver's timer bit. This is because the I2C driver assumes that any timer notification that happens before a hardware operation complete implies a failure. This is obviously not true if the timer notification is made up. The root cause for both of these is that the I2C driver's time management code was not following best practices for Hubris timers and interrupts, which is to _verify that the condition has actually occurred_ before responding to a notification bit. This prevents notifications "hung over" from a previous run from disturbing the next run, and blocks other tasks from disrupting execution as a side effect. This commit fixes that, by inserting a timer check loop around the recv.
1 parent 73220f0 commit 7aaa0aa

1 file changed

Lines changed: 28 additions & 13 deletions

File tree

drv/stm32xx-i2c/src/lib.rs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,18 +1196,33 @@ fn wfi_raw(event_mask: u32, timeout: I2cTimeout) -> I2cControlResult {
11961196

11971197
sys_set_timer(Some(dead), TIMER_NOTIFICATION);
11981198

1199-
let received = sys_recv_notification(event_mask | TIMER_NOTIFICATION);
1200-
1201-
// If the event arrived _and_ our timer went off, prioritize the event and
1202-
// ignore the timeout.
1203-
if received & event_mask != 0 {
1204-
// Attempt to clear our timer. Note that this does not protect against
1205-
// the race where the kernel decided to wake us up, but the timer went
1206-
// off before we got to this point.
1207-
sys_set_timer(None, TIMER_NOTIFICATION);
1208-
I2cControlResult::Interrupted
1209-
} else {
1210-
// The event_mask bit was not set, so:
1211-
I2cControlResult::TimedOut
1199+
loop {
1200+
let received = sys_recv_notification(event_mask | TIMER_NOTIFICATION);
1201+
1202+
// If the event arrived _and_ our timer went off, prioritize the event and
1203+
// ignore the timeout.
1204+
if received & event_mask != 0 {
1205+
// Attempt to clear our timer. Note that this does not protect against
1206+
// the race where the kernel decided to wake us up, but the timer went
1207+
// off before we got to this point.
1208+
sys_set_timer(None, TIMER_NOTIFICATION);
1209+
break I2cControlResult::Interrupted;
1210+
} else {
1211+
// The timer bit must have been set. Verify that our timer has
1212+
// actually expired:
1213+
if sys_get_timer().now >= dead {
1214+
break I2cControlResult::TimedOut;
1215+
}
1216+
1217+
// Otherwise, one of two things has happened:
1218+
// 1. Some joker has posted to our timer bit. Ha ha very funny.
1219+
// 2. The timer bit was _already set_ on entry to this routine, as a
1220+
// hangover from a previous run.
1221+
//
1222+
// We don't need to re-set our timer here because we sampled
1223+
// sys_get_timer _after_ receiving notifications, meaning it either
1224+
// hasn't gone off, or it has gone off but that fact is still stored
1225+
// in our notification bits.
1226+
}
12121227
}
12131228
}

0 commit comments

Comments
 (0)