Skip to content

Free the previous publication ETag before storing a new one - #340

Open
alvm wants to merge 1 commit into
freeswitch:masterfrom
alvm:publish-etag-refresh-leak
Open

Free the previous publication ETag before storing a new one#340
alvm wants to merge 1 commit into
freeswitch:masterfrom
alvm:publish-etag-refresh-leak

Conversation

@alvm

@alvm alvm commented Aug 21, 2026

Copy link
Copy Markdown

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 in 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 — valgrind and the sanitizers see a live allocation, because it is one. 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.

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. su_free() ignores NULL, so the first PUBLISH on a fresh usage is unaffected.

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.

No unit test, and why

The scaffolding for this path is check_nua, which drives a whole stack; a test there is a considerably larger job than the four-line fix, and I would rather offer it separately than hold this up. I see from #282 that you ask contributors for tests, so I want to be explicit rather than quiet about it — the companion PR #341 for the Authentication-Info nonce leak does come with one.

Verification

Built and checked in a clean debian:trixie container (gcc 14.2.0, autoconf 2.72, automake 1.17, glibc 2.41): autogen.sh, configure, full build of libsofia, make check in libsofia-sip-ua/iptsec passes. nua_publish.c compiles to the same 9 warnings with -Wall -Wextra before and after the patch, so this change adds none.

The fix has also been running in production on two instances for several days at the time of writing, in a deployment where PUBLISH refreshes are the dominant traffic.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant