Root Cause Analysis
1. Involved Components and Context
- Source Files:
- Data Structures and Fields:
struct request_queue: Holds the shared timer timeout (struct timer_list).
struct timer_list: Holds expires (unsigned long) representing the absolute expiration timestamp in jiffies.
struct request: Holds deadline (unsigned long) representing the specific request's timeout.
2. Mechanism of the Defect
In the multiqueue block layer (blk-mq), multiple hardware contexts and CPUs submit and start requests concurrently against the same struct request_queue. When a request is started, blk_mq_start_request() calls blk_add_timer(req) without holding any queue-wide lock that serializes timeout modifications across CPUs.
Each request stores its own timeout deadline in req->deadline using WRITE_ONCE(req->deadline, expiry). To wake up the block layer's timeout handling workqueue (q->timeout_work), a single shared timer, q->timeout, is maintained per request_queue.
In blk_add_timer() (block/blk-timeout.c:153-165), the code checks whether q->timeout needs to be armed or updated:
if (!timer_pending(&q->timeout) ||
time_before(expiry, q->timeout.expires)) {
unsigned long diff = q->timeout.expires - expiry;
/*
* Due to added timer slack to group timers, the timer
* will often be a little in front of what we asked for.
* So apply some tolerance here too, otherwise we keep
* modifying the timer because expires for value X
* will be X + something.
*/
if (!timer_pending(&q->timeout) || (diff >= HZ / 2))
mod_timer(&q->timeout, expiry);
}
To avoid calling mod_timer() (which takes the timer wheel's per-CPU spinlock base->lock) on every request start, blk_add_timer() performs lockless reads of q->timeout.expires twice:
time_before(expiry, q->timeout.expires)
diff = q->timeout.expires - expiry
Neither of these reads uses READ_ONCE() or data_race().
Concurrently, another CPU processing another request on the same request_queue in blk_add_timer() can evaluate the condition as true and invoke mod_timer(&q->timeout, expiry) -> __mod_timer(). Inside __mod_timer() (kernel/time/timer.c:1127), after taking the timer base lock (base->lock), the timer's expiration time is updated via:
timer->expires = expires;
This is an unmarked plain write to timer->expires.
3. Invariant Violation and KCSAN Detection
Because blk_add_timer() reads q->timeout.expires without holding base->lock and without using READ_ONCE(), and __mod_timer() writes to timer->expires without WRITE_ONCE(), concurrent execution across two CPUs produces a data race on q->timeout.expires (0xffff888102545e08), triggering the KCSAN report.