From 98236ddf3cb6e45a3c3f5fc27e20d221ea08f0f1 Mon Sep 17 00:00:00 2001 From: Oleksii Molchanov Date: Tue, 18 Aug 2026 00:39:29 +0300 Subject: [PATCH] Free the previous publication ETag before storing a new one. An application that refreshes a publication by handing back the ETag it received - the documented way to use PUBLISH - leaks one sip_etag_t per refresh, for the whole life of the handle. nua_publish_client_init() stores the application-supplied SIPTAG_IF_MATCH into the publish usage with sip_etag_dup() but never releases the tag that is already there. Every other assignment to pu_etag does: the 2xx handler in nua_publish_client_response() and the 412 path in nua_publish_client_check_restart() both su_free() first (nua_publish.c:381, 403-404), and nua_publish_usage_remove() frees on teardown (line 115). The claim turns on whether the usage can already hold an ETag when this runs, and it can: nua_dialog_usage_add() returns the usage that is already there rather than creating a second one ("Already exists" branch, nua_dialog.c), so on every refresh after the first pu_etag is non-NULL and the block it points at is lost. The block is allocated from nh_home and therefore stays reachable through the su_home block table, so no leak checker reports it; it shows up only as heap growth proportional to the publication refresh rate. Measured with massif on a driver sending exactly 2.00 If-Match publishes per second: 99.5% of all heap growth attributed to sip_etag_dup, at 138.0 B/s with an interquartile range of 138.0-138.0 - exactly 2.00 blocks/s at 69 B per sip_etag_t, one block per operation. su_free() ignores a NULL pointer, so the first PUBLISH on a fresh usage is unaffected. The duplicate is made before the old tag is released, so a failed allocation leaves the usage holding the tag it already had, and the source can never alias the block being freed. The code has never been touched since it was written: `git log -L 305,320:libsofia-sip-ua/nua/nua_publish.c` returns 044ac9d of 2007-04-15 as the last commit to reach it. --- libsofia-sip-ua/nua/nua_publish.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libsofia-sip-ua/nua/nua_publish.c b/libsofia-sip-ua/nua/nua_publish.c index 2a55c345a..bbdff2ddb 100644 --- a/libsofia-sip-ua/nua/nua_publish.c +++ b/libsofia-sip-ua/nua/nua_publish.c @@ -303,6 +303,7 @@ static int nua_publish_client_init(nua_client_request_t *cr, nua_handle_t *nh = cr->cr_owner; nua_dialog_usage_t *du; struct publish_usage *pu; + sip_etag_t *etag; if (cr->cr_event == nua_r_publish) { du = nua_dialog_usage_add(nh, nh->nh_ds, nua_publish_usage, NULL); @@ -311,9 +312,14 @@ static int nua_publish_client_init(nua_client_request_t *cr, pu = nua_dialog_usage_private(du); pu->pu_published = 0; if (sip->sip_if_match) { - pu->pu_etag = sip_etag_dup(nh->nh_home, sip->sip_if_match); - if (!pu->pu_etag) + /* nua_dialog_usage_add() returns the usage that is already there, so on + every refresh after the first pu_etag is set. Duplicate first and free + afterwards, so a failed allocation leaves the old tag in place. */ + etag = sip_etag_dup(nh->nh_home, sip->sip_if_match); + if (!etag) return -1; + su_free(nh->nh_home, pu->pu_etag); + pu->pu_etag = etag; sip_header_remove(msg, sip, (sip_header_t *)sip->sip_if_match); } }