Skip to content

Commit d452383

Browse files
kkdwivediAlexei Starovoitov
authored andcommitted
bpf: Fail bpf_timer_cancel when callback is being cancelled
Given a schedule: timer1 cb timer2 cb bpf_timer_cancel(timer2); bpf_timer_cancel(timer1); Both bpf_timer_cancel calls would wait for the other callback to finish executing, introducing a lockup. Add an atomic_t count named 'cancelling' in bpf_hrtimer. This keeps track of all in-flight cancellation requests for a given BPF timer. Whenever cancelling a BPF timer, we must check if we have outstanding cancellation requests, and if so, we must fail the operation with an error (-EDEADLK) since cancellation is synchronous and waits for the callback to finish executing. This implies that we can enter a deadlock situation involving two or more timer callbacks executing in parallel and attempting to cancel one another. Note that we avoid incrementing the cancelling counter for the target timer (the one being cancelled) if bpf_timer_cancel is not invoked from a callback, to avoid spurious errors. The whole point of detecting cur->cancelling and returning -EDEADLK is to not enter a busy wait loop (which may or may not lead to a lockup). This does not apply in case the caller is in a non-callback context, the other side can continue to cancel as it sees fit without running into errors. Background on prior attempts: Earlier versions of this patch used a bool 'cancelling' bit and used the following pattern under timer->lock to publish cancellation status. lock(t->lock); t->cancelling = true; mb(); if (cur->cancelling) return -EDEADLK; unlock(t->lock); hrtimer_cancel(t->timer); t->cancelling = false; The store outside the critical section could overwrite a parallel requests t->cancelling assignment to true, to ensure the parallely executing callback observes its cancellation status. It would be necessary to clear this cancelling bit once hrtimer_cancel is done, but lack of serialization introduced races. Another option was explored where bpf_timer_start would clear the bit when (re)starting the timer under timer->lock. This would ensure serialized access to the cancelling bit, but may allow it to be cleared before in-flight hrtimer_cancel has finished executing, such that lockups can occur again. Thus, we choose an atomic counter to keep track of all outstanding cancellation requests and use it to prevent lockups in case callbacks attempt to cancel each other while executing in parallel. Reported-by: Dohyun Kim <[email protected]> Reported-by: Neel Natu <[email protected]> Fixes: b00628b ("bpf: Introduce bpf timers.") Signed-off-by: Kumar Kartikeya Dwivedi <[email protected]> Link: https://lore.kernel.org/r/[email protected] Signed-off-by: Alexei Starovoitov <[email protected]>
1 parent af253ae commit d452383

File tree

1 file changed

+35
-3
lines changed

1 file changed

+35
-3
lines changed

kernel/bpf/helpers.c

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,7 @@ struct bpf_async_cb {
11071107
struct bpf_hrtimer {
11081108
struct bpf_async_cb cb;
11091109
struct hrtimer timer;
1110+
atomic_t cancelling;
11101111
};
11111112

11121113
struct bpf_work {
@@ -1262,6 +1263,7 @@ static int __bpf_async_init(struct bpf_async_kern *async, struct bpf_map *map, u
12621263
clockid = flags & (MAX_CLOCKS - 1);
12631264
t = (struct bpf_hrtimer *)cb;
12641265

1266+
atomic_set(&t->cancelling, 0);
12651267
hrtimer_init(&t->timer, clockid, HRTIMER_MODE_REL_SOFT);
12661268
t->timer.function = bpf_timer_cb;
12671269
cb->value = (void *)async - map->record->timer_off;
@@ -1440,7 +1442,8 @@ static void drop_prog_refcnt(struct bpf_async_cb *async)
14401442

14411443
BPF_CALL_1(bpf_timer_cancel, struct bpf_async_kern *, timer)
14421444
{
1443-
struct bpf_hrtimer *t;
1445+
struct bpf_hrtimer *t, *cur_t;
1446+
bool inc = false;
14441447
int ret = 0;
14451448

14461449
if (in_nmi())
@@ -1452,21 +1455,50 @@ BPF_CALL_1(bpf_timer_cancel, struct bpf_async_kern *, timer)
14521455
ret = -EINVAL;
14531456
goto out;
14541457
}
1455-
if (this_cpu_read(hrtimer_running) == t) {
1458+
1459+
cur_t = this_cpu_read(hrtimer_running);
1460+
if (cur_t == t) {
14561461
/* If bpf callback_fn is trying to bpf_timer_cancel()
14571462
* its own timer the hrtimer_cancel() will deadlock
1458-
* since it waits for callback_fn to finish
1463+
* since it waits for callback_fn to finish.
1464+
*/
1465+
ret = -EDEADLK;
1466+
goto out;
1467+
}
1468+
1469+
/* Only account in-flight cancellations when invoked from a timer
1470+
* callback, since we want to avoid waiting only if other _callbacks_
1471+
* are waiting on us, to avoid introducing lockups. Non-callback paths
1472+
* are ok, since nobody would synchronously wait for their completion.
1473+
*/
1474+
if (!cur_t)
1475+
goto drop;
1476+
atomic_inc(&t->cancelling);
1477+
/* Need full barrier after relaxed atomic_inc */
1478+
smp_mb__after_atomic();
1479+
inc = true;
1480+
if (atomic_read(&cur_t->cancelling)) {
1481+
/* We're cancelling timer t, while some other timer callback is
1482+
* attempting to cancel us. In such a case, it might be possible
1483+
* that timer t belongs to the other callback, or some other
1484+
* callback waiting upon it (creating transitive dependencies
1485+
* upon us), and we will enter a deadlock if we continue
1486+
* cancelling and waiting for it synchronously, since it might
1487+
* do the same. Bail!
14591488
*/
14601489
ret = -EDEADLK;
14611490
goto out;
14621491
}
1492+
drop:
14631493
drop_prog_refcnt(&t->cb);
14641494
out:
14651495
__bpf_spin_unlock_irqrestore(&timer->lock);
14661496
/* Cancel the timer and wait for associated callback to finish
14671497
* if it was running.
14681498
*/
14691499
ret = ret ?: hrtimer_cancel(&t->timer);
1500+
if (inc)
1501+
atomic_dec(&t->cancelling);
14701502
rcu_read_unlock();
14711503
return ret;
14721504
}

0 commit comments

Comments
 (0)