From 8a94a47e8068dbc8b308cea0b875c516d2ad13f5 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 6 Jul 2026 12:42:50 +0300 Subject: radius: fix permanent socket leak in Acct-Stop retry path When an accounting Stop request cannot be retransmitted because no server is available (single server inside its fail-timeout window, server removed on config reload, or a transient socket/connect error that marks the server failed), rad_acct_stop_timeout() reset req->try and returned. The retransmit timer is one-shot (no period), so it never fired again: the request leaked forever together with its open UDP socket, epoll registration and timerfd. The same dead end existed in rad_acct_stop_sent(): a deferred Stop request (req->rpd == NULL) whose queued send was cancelled by rad_server_fail() fell through the failure branch without freeing the request or scheduling a retry. With the RADIUS client bound to a source address (bind=/nas-ip-address) every leaked socket pins one ephemeral port. On a busy NAS each session terminating during a short RADIUS outage leaks one socket; after months of uptime the ephemeral port range is exhausted and every new request fails with "radius:bind: Address already in use" followed by "no available servers", requiring a restart. Fix by re-arming the one-shot timer on send failure instead of resetting the try counter, so retries are bounded by max-try and the request is freed cleanly once attempts are exhausted. Both defects date back to the accounting rewrite (62e89248, 2014). Fixes: https://github.com/accel-ppp/accel-ppp/issues/324 Signed-off-by: Denys Fedoryshchenko --- accel-pppd/radius/acct.c | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/accel-pppd/radius/acct.c b/accel-pppd/radius/acct.c index 69bd3d56..d30aae7a 100644 --- a/accel-pppd/radius/acct.c +++ b/accel-pppd/radius/acct.c @@ -373,6 +373,16 @@ static void rad_acct_stop_sent(struct rad_req_t *req, int res) rpd->acct_req = NULL; } else if (req->rpd) rad_acct_stop_defer(req->rpd); + else { + /* deferred request: the timeout timer is one-shot and + * nobody else references this request, re-arm it to + * retry later, otherwise the request and its socket + * leak */ + if (req->timeout.tpd) + triton_timer_mod(&req->timeout, 0); + else + triton_timer_add(NULL, &req->timeout, 0); + } return; } @@ -423,7 +433,7 @@ static void rad_acct_stop_timeout(struct triton_timer_t *t) req->pack->id++; } - if (req->try == conf_max_try) { + if (req->try >= conf_max_try) { if (req->rpd) req->rpd->acct_req = NULL; rad_req_free(req); @@ -437,7 +447,14 @@ static void rad_acct_stop_timeout(struct triton_timer_t *t) rad_req_free(req); return; } - req->try = 0; + /* no server available at the moment; the timeout timer is + * one-shot, re-arm it to retry later, otherwise the request + * and its socket leak; failed attempts count towards + * conf_max_try so the request is freed above eventually */ + if (req->timeout.tpd) + triton_timer_mod(&req->timeout, 0); + else + triton_timer_add(req->rpd ? req->rpd->ses->ctrl->ctx : NULL, &req->timeout, 0); } } -- cgit v1.2.3 From 1a9923a07c1283b30a5b02a1668f57015f26232e Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 6 Jul 2026 12:43:08 +0300 Subject: radius: fix request slot leak when queued request wakeup fails req_wakeup() ignored the return value of req->send(req, 1). When a request dequeued from the req-limit queue failed at socket setup (__rad_req_send returns -2, e.g. under ephemeral port exhaustion or a routing error), the server's req_cnt slot taken in rad_server_req_exit() was never released and the request was orphaned with no callback and no timer. Leaked slots accumulate until req_cnt permanently saturates req-limit, after which every request queues forever and the server is effectively dead until restart. Handle -2 the same way rad_server_req_enter() does: release the slot, mark the server failed and drive the request through the regular failover path so it either retries on another server or reports the failure to its owner. Signed-off-by: Denys Fedoryshchenko --- accel-pppd/radius/serv.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/accel-pppd/radius/serv.c b/accel-pppd/radius/serv.c index b48e53ac..71398c82 100644 --- a/accel-pppd/radius/serv.c +++ b/accel-pppd/radius/serv.c @@ -152,7 +152,20 @@ static void req_wakeup(struct rad_req_t *req) } pthread_mutex_unlock(&req->serv->lock); - req->send(req, 1); + if (req->send(req, 1) == -2) { + /* socket setup failed: release the slot taken in + * rad_server_req_exit() and drive the failover path, + * otherwise the server's req_cnt leaks and the request + * is orphaned */ + req->active = 0; + pthread_mutex_lock(&req->serv->lock); + req->serv->req_cnt--; + pthread_mutex_unlock(&req->serv->lock); + + rad_server_fail(req->serv); + + req->send(req, -1); + } } static void req_wakeup_failed(struct rad_req_t *req) -- cgit v1.2.3