Skip to content

Security: OOB write/read in protocol/dns.c dns_name_decode/encode and compression path #851

Open

Description

@LL-V

Summary

protocol/dns.c has multiple memory-safety bugs in DNS name encode/decode used by dns_rr_unpack / dns_unpack / nslookup / dns_query.

I am reporting these as original findings against current libhv master (lab verification 2026-07-22). I did not find a prior issue/CVE specifically for dns_name_decode / dns_name_encode bounds.

Bug 1 — dns_name_decode unbounded write (CWE-120 / CWE-787)

// protocol/dns.c
int dns_name_decode(const char* buf, char* domain) {
    const char* p = buf;
    int len = *p++;
    int buflen = 1;
    while (*p != '\0') {
        if (len-- == 0) {
            len = *p;
            *domain = '.';
        } else {
            *domain = *p;
        }
        ++p;
        ++domain;   // never checks remaining space in domain[]
        ++buflen;
    }
    *domain = '\0';
    ...
}

Callers pass rr->name which is only DNS_NAME_MAXLEN (256) bytes (dns_rr_t.name[256]), but the decoder never enforces that limit. A crafted wire name that expands to >255 bytes overwrites past name[].

There is also no remaining-input length parameter: a missing terminating 0x00 causes unbounded read of buf.

Local confirmation: canary after a 256-byte domain buffer is smashed (buflen returned 513).

Bug 2 — compression-pointer path leaves name[] uninitialized (CWE-457 / CWE-125)

int dns_rr_unpack(...) {
    if (*(uint8_t*)p >= 192) {
        namelen = 2;
        // name is never written / NUL-terminated
    } else {
        namelen = dns_name_decode(buf, rr->name);
    }
    ...
}

Any RR using a compression pointer leaves rr->name untouched. Later strlen / %s use is an OOB read.

Bug 3 — dns_name_encode unbounded write (CWE-787)

int dns_name_encode(const char* domain, char* buf) {
    ...
    while (*p != '\0') {
        ...
        ++buf;  // no capacity check
        ++buflen;
    }

dns_rr_pack uses a fixed char encoded_name[256] and calls encode without checking domain length. A long rr->name overruns encoded_name.

Local confirmation: encode of a long name returned length 502 past a 256-byte buffer canary.

Attack surface

Any application using libhv DNS helpers (dns_unpack, dns_query, nslookup, examples under protocol DNS) that processes untrusted DNS responses (or packs attacker-influenced names) can hit these paths.

Not the default HTTP-only build path, but enabled with protocol/DNS (WITH_PROTOCOL / equivalent).

Suggested fix

  1. Change APIs to pass capacity / remaining wire length, e.g.
    dns_name_decode(const char* buf, size_t buflen, char* domain, size_t domaincap)
  2. Reject expanded names >= 256 and individual labels > 63.
  3. On compression pointer, either fully resolve into name[] with bounds, or set name[0] = '\0' / explicit flag and never treat as C-string until resolved.
  4. Cap dns_name_encode output to provided buffer size; return error if domain too long.
  5. Add unit tests + ASan fuzz of dns_unpack.

Reproduction (lab)

Standalone canary harnesses (same algorithm as tree):

  • long wire name → dns_name_decode smashes canary after 256-byte domain
  • compression pointer path → name[] never written
  • long domain string → dns_name_encode smashes 256-byte encode buffer

Happy to attach minimal C files or open a PR with a patch if useful.

Researcher

LL-V
GitHub: https://github.com/LL-V

Please credit if a CVE is requested. Testing was local-only against source / standalone reimplementation of the vulnerable functions.

Activity

  1. router0mail commented on Sep 17, 2026

    @router0mail

    Adding an ASan-confirmed repro of Bug 1 through the actual in-tree code path (not a reimplementation), for anyone following up:

    ==pid==ERROR: AddressSanitizer: heap-buffer-overflow
    WRITE of size 1 at 0x... thread T0
        #0 dns_name_decode  protocol/dns.c:57
        #1 dns_rr_unpack    protocol/dns.c:110
        #2 dns_unpack       protocol/dns.c:204   (heap-allocates the RR array, dns.c:202)
    0 bytes to the right of 280-byte region [the dns_rr_t]
    

    Minimal trigger — 1 answer RR whose name is three 120-byte labels
    (\x78 <120×A> \x78 <120×B> \x78 <120×C> \x00): decodes to 363 bytes into name[256].

    One subtlety worth flagging for the fix: the trigger requires the first name byte < 192,
    otherwise dns_rr_unpack (dns.c:104) takes the name-compression branch
    (if (*(uint8_t*)p >= 192) namelen = 2;) and dns_name_decode is never called —
    so a name whose first label is ≥ 192 bytes instead falls into Bug 2 (uninitialized name[]).
    The two bugs are complementary branches on that first byte.

    Full driver + ASan build line are short if useful to paste into a PR/test.

  2. turky-1 commented on Oct 25, 2026

    @turky-1

    Adding the escalation path for anyone following this: this OOB write can
    be driven to full remote code execution, not just a crash.

    Using chained 63-byte labels (max legal length, so the ≥192 compression-
    pointer check in dns_rr_unpack is never triggered) the overflow can be
    grown past adjacent heap chunks. With controlled allocation order, the
    overflow lands on a function pointer in a neighboring chunk, and since
    dns_name_decode can't emit a NUL byte mid-write, a 3-byte partial
    overwrite of a non-PIE code pointer's low bytes is enough to redirect
    execution — no need for a full 8-byte write.

    Full writeup + 4 runnable PoCs (raw overflow, OOB read, pointer hijack,
    and end-to-end code exec) here: https://github.com/turky-1/libhv-rce

    @ithewei given this is remote, unauthenticated RCE and #854 has been open
    since July, is there a timeline for review/merge? Happy to help coordinate
    a security advisory + CVE once a fix is in — this currently has no CVE
    assigned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions