Skip to content

wire2host: free a replaced TSIG record in ldns_wire2pkt() (#317) - #319

Merged
wtoorop merged 1 commit into
NLnetLabs:developfrom
1820893135-pixel:fix-wire2pkt-tsig-leak
Sep 23, 2026
Merged

wtoorop merged 1 commit into
NLnetLabs:developfrom
1820893135-pixel:fix-wire2pkt-tsig-leak

Conversation

@1820893135-pixel

Copy link
Copy Markdown
Contributor

The ADDITIONAL-section loop in ldns_wire2pkt() calls

ldns_pkt_set_tsig(packet, rr);

for every TSIG (type 250) record it parses (wire2host.c:479). That setter is
a bare assignment (packet.c:691):

void ldns_pkt_set_tsig(ldns_pkt *pkt, ldns_rr *rr)
{
	pkt->_tsig_rr = rr;
}

It neither frees nor takes ownership of the previous _tsig_rr, so a message
carrying more than one parseable TSIG record silently orphans each earlier one.
ldns_pkt_free() (packet.c:917) releases only the last surviving _tsig_rr,
so the overwritten records - and the owner dname allocated by
ldns_wire2dname() - can never be reclaimed.

RFC 8945 allows at most one TSIG per message, but a malformed message is
exactly the input an attacker controls; a long-running parser that consumes
attacker-supplied messages grows without bound (OOM DoS). No memory corruption,
no info leak.

Reproducer

A 34-byte DNS message: 12-byte header with arcount=3, followed by two TSIG RRs
(owner=root, type=0x00FA, class=0x1013, ttl=0, rdlen=0):

00000000: 0000 0000 0000 0000 0000 0003 0000 fa10  ................
00000010: 1300 0000 0000 0000 00fa 1013 0000 0000  ................
00000020: 0000                                     ..

Only the documented public sequence is used, so this is not harness misuse:

ldns_pkt *pkt = NULL;
(void) ldns_wire2pkt(&pkt, buf, sz);
ldns_pkt_free(pkt);

With ASan + LSan:

=================================================================
==2686128==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 48 byte(s) in 1 object(s) allocated from:
    #1 ldns_rr_new rr.c:33:7
    #2 ldns_wire2rr wire2host.c:325:16
    #3 ldns_wire2pkt wire2host.c:459:12

Indirect leak of 24 byte(s) in 1 object(s) allocated from:
    #1 ldns_rdf_new_frm_data rdata.c:205:8
    #2 ldns_wire2dname wire2host.c:143:11
    #3 ldns_wire2rr wire2host.c:328:11
    #4 ldns_wire2pkt wire2host.c:459:12

Indirect leak of 1 byte(s) in 1 object(s) ...
SUMMARY: AddressSanitizer: 73 byte(s) leaked in 3 allocation(s).

Fix

Release the record that is about to be replaced:

} else if (ldns_rr_get_type(rr) == LDNS_RR_TYPE_TSIG) {
	/* RFC 8945 allows at most one TSIG per message, but a
	 * malformed one can carry several.  set_tsig() just
	 * assigns the pointer, so release the record it is about
	 * to replace - otherwise it (and its owner name) leaks.
	 */
	if (ldns_pkt_tsig(packet) != NULL) {
		ldns_rr_free(ldns_pkt_tsig(packet));
	}
	ldns_pkt_set_tsig(packet, rr);
	ldns_pkt_set_arcount(packet, ldns_pkt_arcount(packet) - 1);

ldns_rr_free() is NULL-safe, so the explicit check is mostly for clarity.

Testing

  • The 34-byte reproducer above: 73 byte(s) leaked in 3 allocation(s) before,
    no leak after.
  • A message with four TSIG records: same (one leak before, none after).
  • A message with a single TSIG record: unchanged.

Fixes #317.

The ADDITIONAL-section loop in ldns_wire2pkt() calls

    ldns_pkt_set_tsig(packet, rr);

for every TSIG (type 250) record it parses. That setter is a bare
assignment (packet.c:691) - it neither frees nor takes ownership of the
previous _tsig_rr - so a message that carries more than one parseable
TSIG record silently orphans each earlier one. ldns_pkt_free() only
releases the last surviving _tsig_rr, so the overwritten records (and the
owner dname allocated by ldns_wire2dname()) can never be reclaimed.

RFC 8945 allows at most one TSIG per message, but a malformed message is
exactly the input an attacker controls, and LeakSanitizer reports it
deterministically:

    $ ./repro poc.bin            # 34-byte DNS message, arcount=3, two TSIG RRs
    Direct leak of 48 byte(s) in 1 object(s) allocated from:
        NLnetLabs#1 ldns_rr_new rr.c:33
        NLnetLabs#2 ldns_wire2rr wire2host.c:325
        NLnetLabs#3 ldns_wire2pkt wire2host.c:459
    Indirect leak of 24 byte(s) ...
    Indirect leak of 1 byte(s) ...
    SUMMARY: AddressSanitizer: 73 byte(s) leaked in 3 allocation(s).

Free the record that is about to be replaced. The reproducer above leaks
nothing after the change, and so does a message with four TSIG records; a
message with a single TSIG is unaffected.

@wtoorop wtoorop left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! Thanks!

@wtoorop
wtoorop merged commit 37d3257 into NLnetLabs:develop Sep 23, 2026
wtoorop added a commit that referenced this pull request Sep 23, 2026
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.

[Bug]Unbounded memory leak in ldns_wire2pkt(): a second TSIG record overwrites (and orphans) the first without freeing it (LSan heap leak, CWE-401)

2 participants