Security: OOB write/read in protocol/dns.c dns_name_decode/encode and compression path
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
- Change APIs to pass capacity / remaining wire length, e.g.
dns_name_decode(const char* buf, size_t buflen, char* domain, size_t domaincap) - Reject expanded names
>= 256and individual labels> 63. - On compression pointer, either fully resolve into
name[]with bounds, or setname[0] = '\0'/ explicit flag and never treat as C-string until resolved. - Cap
dns_name_encodeoutput to provided buffer size; return error if domain too long. - Add unit tests + ASan fuzz of
dns_unpack.
Reproduction (lab)
Standalone canary harnesses (same algorithm as tree):
- long wire name →
dns_name_decodesmashes canary after 256-byte domain - compression pointer path →
name[]never written - long domain string →
dns_name_encodesmashes 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.
Source: ithewei/libhv