Get in touch

OFFENSIVE LAB / PUBLICATION · DNSMASQ

An out-of-bounds read in dnsmasq,found at the lab, fixed upstream.

A single crafted DNS record could crash dnsmasq while it validated DNSSEC.
OffensiveLab traced the flaw to get_rdata(); the upstream fix is a few lines of bounds-checking.

NAMETYPECLASSTTLRDLEN12RDATAembedded name, longer than RDLENrecord ends hereRDLEN = 12 bytes declaredread runs past the buffer → SIGSEGVIllustration: DNS resource record, not to scale.Fixed upstream · dnsmasq

Find it, prove it, help fix it, write it down.

Project
dnsmasq

DNS forwarder & DHCP server

Where
dnssec.c

function get_rdata()

Weakness
OOB read

CWE-125, out-of-bounds read

Precondition
DNSSEC

only when validation is on

Impact
Crash

remote denial of service

Patch
+17−13

one file · commit d823d8e9

Fixed upstream by Simon Kelley in dnsmasq. The commit message credits OffensiveLab's Tristan G. for spotting the problem. Commit d823d8e9 →

THE FINDING

One record, one crash.

dnsmasq parses every answer it validates. A single resource record, built to lie about its own length, was enough to take the process down.

THE BUG

What happens when a DNS record claims to be shorter than the domain name packed inside it?

Read the fix

A false length

A resource record can claim a data length, its RDLEN, shorter than the domain name packed inside it.

Only with DNSSEC

Reachable only when DNSSEC validation is on. The malicious packets do not even need a valid signature.

A negative gone huge

The bytes left after the name turn negative, wrap to a giant unsigned value, and the parser reads off the end.

Disclosure

dnsmasq · 2026
01SpottedOffensiveLab traces a reproducible crash in get_rdata() to a record whose name overruns its RDLEN.Found
02ReportedThe finding reaches the dnsmasq maintainer, Simon Kelley, with the case that triggers it.Sent
03Fixed upstreamA bounds check lands in src/dnssec.c, commit d823d8e9, 3 June 2026.Patched

MECHANISM

How a lie about length becomes a crash.

No exploit code here, just the shape of the bug and the check that now stops it.

  1. A record declares a length

    Every DNS resource record carries RDLEN, the number of bytes of record data that follow it. Some record types also pack a domain name into that data.

  2. A crafted record lies

    A malicious server sends a record whose embedded name is longer than the RDLEN it claims: the name alone already runs past where the record is supposed to end.

  3. The leftover count goes negative

    After reading the name, get_rdata() works out how many bytes are left for the fields behind it: end minus position. Here that is negative, and it is held in an unsigned counter, so it wraps to a very large positive number.

  4. The read runs off the buffer

    The parser then copies that many bytes, reaching far past the record and the packet itself. dnsmasq hits unmapped memory and crashes with SIGSEGV.

Attack surface

Vector
A malicious or compromised upstream DNS server
Precondition
DNSSEC validation enabled in dnsmasq
Signature
Not required, unsigned attack packets work
Result
Out-of-bounds read, then a process crash (DoS)
src/dnssec.c · get_rdata()upstream fix
189 if (d == (u16)0)
190 {
191 /* domain-name, canonicalise */
- int len;
- if (!extract_name(…) || (len = to_wire(buff)) == 0)
- continue;
- state->c = len;
+ /* malformed, or name runs past the RR end: abort,
+ so a negative length never wraps to a huge one */
+ if (!extract_name(…) || state->ip > state->end)
+ return 0;
+ state->c = to_wire(buff); /* always > 0 */
state->op = (unsigned char *)buff;
}
else
{
+ /* short RR: clamp to bytes left, finish if none */
+ if ((state->end - state->ip) < d &&
+ (d = state->end - state->ip) == 0)
+ return 0;
state->op = state->ip;

THE FIX

A few lines of bounds-checking.

The patch makes get_rdata() refuse a record whose name does not fit, instead of trusting the arithmetic that follows.

The patch

src/dnssec.c · +17 −13
ChangeIn
Guard the name branchReturn instead of continuing when the name is malformed or reaches past the end of the record, so the leftover length can never go negative.get_rdata()
Clamp the plain dataWhen fewer bytes remain than the record declares, clamp to what is left and return if nothing is, covering fixed-size fields a short record claims as zero bytes.get_rdata()
Tidy a stray statementRemove a duplicated semicolon left over from earlier code.dnssec.c

Reported and fixed.

Independent research, disclosed upstream and merged into dnsmasq. That is the loop the lab runs: find it, prove it, help fix it, publish it.