Skip to content

Multiple OOB reads in DNS wire format parser — section counts, compression pointers, RDLENGTH not bounds-checked #549

@afldl

Description

@afldl

I found several out-of-bounds read bugs in getdns's DNS message parsing while doing differential fuzzing against Unbound, LDNS, c-ares, and Knot DNS. They're all in the wire-to-dict parsing path. Tested on v1.7.3.

1. Heap-buffer-overflow in gldns_read_uint32() — truncated RR TTL (CWE-125)

When a DNS message header claims more RRs than actually exist in the data (e.g. ANCOUNT=1 but the answer section is truncated before the TTL field), getdns_wire2msg_dict() reads past the buffer in gldns_read_uint32() at gbuffer.h:59.

Call chain: getdns_wire2msg_dict()_getdns_wire2msg_dict_scan()_getdns_rr_iter2rr_dict()_getdns_rr_iter2rr_dict_canonical()gldns_read_uint32() — reads 4 bytes at i->rr_type + 4 which may be past the buffer end.

ASAN trace:

READ of size 4 at 0x...
  gldns_read_uint32 (gbuffer.h:59)
  _getdns_rr_iter2rr_dict_canonical (util-internal.c:230)

2. OOB read via compression pointer to offset 0

When a DNS name contains a compression pointer pointing to offset 0 (the beginning of the message), getdns's dname decompression follows it and can read out of bounds depending on the message structure.

3. RDLENGTH exceeds remaining message — silently accepted

rr_iter_find_nxt() in src/rr-iter.c clamps the next-RR pointer to pkt_end instead of rejecting the parse when rr_type + 10 + RDLENGTH > pkt_end:

i->nxt = ... i->rr_type + 10 + gldns_read_uint16(i->rr_type + 8) > i->pkt_end
       ? i->pkt_end   /* should reject */
       : i->rr_type + 10 + gldns_read_uint16(i->rr_type + 8);

So getdns returns GETDNS_RETURN_GOOD for packets where RDLENGTH claims more data than exists. c-ares, LDNS, Knot, and Unbound all reject the same wire bytes.

4. Label type validation missing

The dname decompression loop doesn't properly validate label type bytes, allowing OOB reads via unexpected label types.

Cross-implementation comparison (across all findings):
Every one of these malformed inputs is rejected by the other 4 implementations I tested. getdns is consistently the only one that accepts them.

Reproduction:

# Build with ASAN
clang -fsanitize=address -g -O0 \
  -I/path/to/getdns-src/build-clangfuzz/include \
  -I/path/to/getdns-src/src \
  h_getdns.c -L/path/to/getdns-src/build -lgetdns -o h_getdns

# Crash on truncated RR
./h_getdns crash_truncated_ttl.bin
# ASAN: heap-buffer-overflow in gldns_read_uint32

# Accept invalid RDLENGTH (other impls reject)
./h_getdns seed_71_ptr_rdlen_oob.bin
# Returns GETDNS_RETURN_GOOD instead of error

Suggested fixes:

  • In rr_iter_find_nxt(): return an error when rr_type + 10 + RDLENGTH > pkt_end instead of clamping
  • In the RR iterator: validate that enough bytes remain before reading TTL/RDLENGTH fields
  • In dname decompression: validate compression pointer targets and label types

Found through differential fuzzing (PathDiff v22-v23) against Unbound, LDNS, c-ares, Knot DNS.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions