From: Ondřej Surý Date: Thu, 30 Apr 2026 05:39:23 +0000 (+0200) Subject: Dispatch ratelimiter events under the lock X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9a15d2c9730176c97945f0e8d40187d4243bd8aa;p=thirdparty%2Fbind9.git Dispatch ratelimiter events under the lock isc__ratelimiter_tick() and isc_ratelimiter_shutdown() each pulled events out of rl->pending into a function-local list, dropped the mutex, and then iterated. ISC_LIST_APPEND leaves the link in the LINKED state, so a concurrent isc_ratelimiter_dequeue() saw an event as still queued, called ISC_LIST_UNLINK against rl->pending — which patched the prev/next of the local list — and freed the event before dispatch finished, producing either an INSIST in the unlink macro or a use-after-free in the dispatch loop. isc_async_run() is a non-blocking wfcq enqueue, so there is no benefit to dropping the mutex around it. Unlink each event and hand it to isc_async_run() while still holding rl->lock; the existing ISC_LINK_LINKED check in dequeue then correctly distinguishes "still queued and cancellable" from "already taken". Assisted-by: Claude:claude-opus-4-7 (cherry picked from commit 4d465f4fa58abb79b90d9019900e7d543fb511ea) --- diff --git a/lib/isc/ratelimiter.c b/lib/isc/ratelimiter.c index 5cb83f273c0..3f7819fbf01 100644 --- a/lib/isc/ratelimiter.c +++ b/lib/isc/ratelimiter.c @@ -218,14 +218,10 @@ isc_ratelimiter_dequeue(isc_ratelimiter_t *restrict rl, isc_rlevent_t **rlep) { static void isc__ratelimiter_tick(void *arg) { isc_ratelimiter_t *rl = (isc_ratelimiter_t *)arg; - isc_rlevent_t *rle = NULL; uint32_t pertic; - ISC_LIST(isc_rlevent_t) pending; REQUIRE(VALID_RATELIMITER(rl)); - ISC_LIST_INIT(pending); - LOCK(&rl->lock); REQUIRE(rl->timer != NULL); @@ -237,12 +233,8 @@ isc__ratelimiter_tick(void *arg) { pertic = rl->pertic; while (pertic != 0) { - rle = ISC_LIST_HEAD(rl->pending); - if (rle != NULL) { - /* There is work to do. Let's do it after unlocking. */ - ISC_LIST_UNLINK(rl->pending, rle, link); - ISC_LIST_APPEND(pending, rle, link); - } else { + isc_rlevent_t *rle = ISC_LIST_HEAD(rl->pending); + if (rle == NULL) { /* * We processed all the scheduled work, but there's a * room for at least one more event (we haven't consumed @@ -253,6 +245,15 @@ isc__ratelimiter_tick(void *arg) { rl->state = isc_ratelimiter_idle; break; } + /* + * Unlink and dispatch under the lock: isc_async_run() is a + * non-blocking enqueue, so this stays cheap, and once the + * link is TOMBSTONEd a concurrent isc_ratelimiter_dequeue() + * sees ISC_LINK_LINKED == false and returns ISC_R_NOTFOUND + * cleanly instead of racing with our dispatch. + */ + ISC_LIST_UNLINK(rl->pending, rle, link); + isc_async_run(rle->loop, rle->cb, rle->arg); pertic--; } @@ -262,11 +263,6 @@ isc__ratelimiter_tick(void *arg) { } unlock: UNLOCK(&rl->lock); - - while ((rle = ISC_LIST_HEAD(pending)) != NULL) { - ISC_LIST_UNLINK(pending, rle, link); - isc_async_run(rle->loop, rle->cb, rle->arg); - } } void @@ -288,27 +284,21 @@ isc__ratelimiter_doshutdown(void *arg) { void isc_ratelimiter_shutdown(isc_ratelimiter_t *restrict rl) { - isc_rlevent_t *rle = NULL; - ISC_LIST(isc_rlevent_t) pending; - REQUIRE(VALID_RATELIMITER(rl)); - ISC_LIST_INIT(pending); - LOCK(&rl->lock); if (rl->state != isc_ratelimiter_shuttingdown) { + isc_rlevent_t *rle = NULL; rl->state = isc_ratelimiter_shuttingdown; - ISC_LIST_MOVE(pending, rl->pending); + while ((rle = ISC_LIST_HEAD(rl->pending)) != NULL) { + ISC_LIST_UNLINK(rl->pending, rle, link); + rle->canceled = true; + isc_async_run(rl->loop, rle->cb, rle->arg); + } isc_ratelimiter_ref(rl); isc_async_run(rl->loop, isc__ratelimiter_doshutdown, rl); } UNLOCK(&rl->lock); - - while ((rle = ISC_LIST_HEAD(pending)) != NULL) { - ISC_LIST_UNLINK(pending, rle, link); - rle->canceled = true; - isc_async_run(rl->loop, rle->cb, rle->arg); - } } static void