Skip to content

Free the previous nonce before storing a nextnonce - #341

Open
alvm wants to merge 2 commits into
freeswitch:masterfrom
alvm:iptsec-nextnonce-leak
Open

Free the previous nonce before storing a nextnonce#341
alvm wants to merge 2 commits into
freeswitch:masterfrom
alvm:iptsec-nextnonce-leak

Conversation

@alvm

@alvm alvm commented Aug 21, 2026

Copy link
Copy Markdown

auc_digest_info() stores the server-supplied Authentication-Info nextnonce over cda->cda_ac->ac_nonce without releasing what was there. auth_get_params() allocates the new string from home, and the pointer being overwritten is owned by that same home: it came either from the challenge via auth_digest_challenge_get(), or from an earlier Authentication-Info through this same assignment.

The consequence is not "one leak per nonce" but all but the last: the next challenge calls auth_digest_challenge_free_params(), which does release ac_nonce — so of N responses carrying nextnonce between two challenges, N−1 strings are lost. A server that sends Authentication-Info on every 2xx therefore leaks one nonce string per response for as long as the credentials stay valid.

As with the companion PR #340 for the publication ETag, the block is allocated from a su_home and stays reachable through the home's block table, so no leak checker reports it; it shows up only as heap growth proportional to the response rate.

The free follows the discipline the rest of the file already uses — auth_digest_challenge_free_params() at auth_digest.c:126, and the cnonce path in auc_digest_challenge(), both su_free() from ca_home before replacing. su_free() ignores NULL, so the first nextnonce on a fresh client is unaffected.

The test

The second commit adds a case to test_auth_digest.c. It drives the public client API — auc_challenge(), auc_credentials(), auc_info() — and asserts on the authenticator's own allocation statistics rather than on any internal pointer, because what is being tested is a lifetime and not a value.

with the fix     live blocks 0 -> 0        bytes 15 -> 15
without it       live blocks 1 -> 101      bytes 22 -> 2222

One block per update, and 2 200 bytes for a hundred of them — 22 bytes each, which is exactly the nonce string in the fixture plus its terminator.

Three details in the test that are easy to get wrong, and are commented in place:

  • The statistics have to come from the authenticator's own home. ca_create() makes it a su_home_clone() of the home passed to auc_challenge(), and su_home_get_stats() reports only the home it is given — it does not aggregate clones, whatever its include_clones argument says. Reading the parent shows nothing either way.
  • With the fix in place both block counts are zero, so an assertion on their equality alone would also pass if the statistics were not being collected at all. The test first checks that the instrument reads something (hsb_bytes > 0).
  • It then checks that the surviving nonce is the one the server sent last, through auc_authorization_headers(). A free of the wrong pointer, or a missing assignment, would satisfy the block count while losing the value.

Verification

Clean debian:trixie container (gcc 14.2.0, autoconf 2.72, automake 1.17, glibc 2.41), autogen.sh, configure, full build of libsofia:

build make check
with the fix ok, no new warnings PASS
fix reverted, test kept ok, no new warnings FAIL, as it must

The failure is attributed rather than assumed — the test names its own assertion:

test_auth_digest.c:1400: test_auth_digest test_auth_info_nextnonce() FAILED:
  (hs1->hs_blocks.hsb_number == hs0->hs_blocks.hsb_number)

Warnings counted differentially with -Wall -Wextra against the base: auth_client.c 5 → 5 and test_auth_digest.c 5 → 5. The patch and its test add none.

The fix has also been running in production on two instances for several days at the time of writing, against a registrar that sends Authentication-Info on every 2xx.

Oleksii Molchanov added 2 commits August 21, 2026 18:31
auc_digest_info() stores the server-supplied Authentication-Info
nextnonce over cda->cda_ac->ac_nonce without releasing what was there:

	n = auth_get_params(home, info->ai_params, "nextnonce=",
			    &nextnonce, NULL);
	if (n <= 0)
	  return n;
	cda->cda_ac->ac_nonce = nextnonce;

auth_get_params() allocates the new string from `home`, and the pointer
being overwritten is owned by that same home: it came either from the
challenge via auth_digest_challenge_get(), or from an earlier
Authentication-Info through this same assignment.

The consequence is not "one leak per nonce" but "all but the last": the
next challenge calls auth_digest_challenge_free_params(), which does
release ac_nonce - so of N responses carrying nextnonce between two
challenges, N-1 strings are lost. A server that sends
Authentication-Info on every 2xx therefore leaks one nonce string per
response for as long as the credentials stay valid.

Like every other object in this family the block is allocated from a
su_home and stays reachable through the home's block table, so no leak
checker reports it; it shows up only as heap growth proportional to the
response rate.

The free follows the discipline the rest of the file already uses -
auth_digest_challenge_free_params() at auth_digest.c:126, and the
cnonce path in auc_digest_challenge(), both su_free() from ca_home
before replacing. su_free() ignores NULL, so the first nextnonce on a
fresh client is unaffected.
Covers the free added to auc_digest_info(). The test drives the public
client API - auc_challenge(), auc_credentials(), auc_info() - and
asserts on the authenticator's own allocation statistics rather than on
any internal pointer, because what is being tested is a lifetime and
not a value.

One Authentication-Info is fed first, so that whatever the first update
allocates is already accounted for; the assertion is then that a
hundred further updates leave the number of live blocks unchanged.
Measured on this test:

  with the fix     live blocks 0 -> 0        bytes 15 -> 15
  without it       live blocks 1 -> 101      bytes 22 -> 2222

One block per update, and 2 200 bytes for a hundred of them - 22 bytes
each, which is exactly the nonce string in the fixture plus its
terminator.

Three things about the test worth stating, because all three are easy
to get wrong:

The statistics have to be taken from the authenticator's own home, not
from the home handed to auc_challenge(). ca_create() makes the
authenticator a su_home_clone() of that home, and su_home_get_stats()
reports only the home it is given - it does not aggregate clones,
regardless of its include_clones argument. Reading the parent shows
nothing whether the nonce is freed or not.

With the fix in place both block counts are zero, so an assertion on
their equality alone would also hold if the statistics were not being
collected at all. The test therefore checks first that the instrument
reads something (hsb_bytes > 0), and then that the surviving nonce is
the one the server sent last - a free of the wrong pointer, or a
missing assignment, would satisfy the count while losing the value.

And a leak checker cannot see this at all: the strings are allocated
from a su_home and stay reachable through the home's block table until
the home is destroyed, so valgrind and the sanitizers report nothing.
The home's own statistics are the only instrument that sees 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