CVE-2026-1678: DNS Parser Overflow in Zephyr
- 6 minsCVE-2026-1678 - DNS Parser Overflow in Zephyr
CVE-2026-1678 / GHSA-536f-h63g-hj42 is a critical out-of-bounds write in Zephyr’s DNS name parser, dns_unpack_name(), affecting versions <= 4.3 with a 9.4 CVSS score.
Before I go into the details, I want to shout out the Zephyr security team. They were hands down the best team I have worked with on a vulnerability disclosure. I submitted the report, they confirmed and fixed it within days, and published the advisory after the embargo. The whole process was clean and simple from start to finish.
Why I looked at the DNS parsers
Anywhere code translates between two different formats is a good place to look for memory safety bugs. DNS parsers are a great example of this. Over the wire a name like www.apple.com is not stored as a dot-separated string. The name is split into chunks, each prefixed with a single byte that tells you how many bytes that chunk is, and the whole thing ends with a zero byte. Each chunk can be at most 63 bytes, the full name can be 255 bytes, and a classic UDP message can be up to 512 bytes. The parser has to take that and turn it into a normal C string with dots and a null terminator. Those two formats count space differently, and that mismatch is where things tend to break.
That is what drew me to dns_unpack_name(). The issue I found explains the problem pretty plainly: very long queries could overflow the result buffer because of an incorrect bounds check. What caught my attention was that this was not a case of “there was no check.” There was one. The function grabbed the remaining buffer space once at the top, kept reusing that value while it appended labels, and then wrote the final null terminator without checking again. The buffer was filling up the whole time, but the check never knew.
The vulnerable code
Here is the actual code from subsys/net/lib/dns/dns_pack.c before the fix. I trimmed the compression pointer handling to keep the focus on the overflow path:
int dns_unpack_name(const uint8_t *msg, int maxlen, const uint8_t *src,
struct net_buf *buf, const uint8_t **eol)
{
int dest_size = net_buf_tailroom(buf); // captured ONCE here
...
while ((val = *curr_src++)) {
...
} else {
label_len = val;
if (label_len > 63) {
return -EMSGSIZE;
}
// this check uses dest_size, which never updates
if (((buf->data + label_len + 1) >=
(buf->data + dest_size)) ||
((curr_src + label_len) >= (msg + maxlen))) {
return -EMSGSIZE;
}
// but these calls keep growing buf->len
if (buf->len > 0) {
net_buf_add_u8(buf, '.');
}
net_buf_add_mem(buf, curr_src, label_len);
curr_src += label_len;
}
}
buf->data[buf->len] = '\0'; // no bounds check at all
...
}
At first glance, this feels safe (without the comments saying that its wrong :)). There is a length check, and the code is clearly trying to reject oversized input. But dest_size is frozen at its initial value while the buffer keeps filling up. Every dot and every label makes the real free space smaller, but the check never notices. Think of it like measuring the empty space on a bookshelf once, then continuing to add books while still trusting that first measurement.
Triggering the overflow
The spec stops each label from being larger than 63 bytes, so that is naturally what you throw at it. If you pack five 63-byte labels into a DNS message, it still fits within the 512-byte message limit. But once the parser expands those into a dotted string, you blow right by the default 255-byte destination buffer. Depending on how many labels you use, you can overflow by anywhere from 65 to 200 bytes.
Assertions
Zephyr has a config option called CONFIG_ASSERT, and when it is off, which is the default for production builds, all assertion checks get compiled away entirely. The net_buf append helpers that this function uses rely only on assertions for bounds protection. So when assertions are off, those helpers just write wherever you tell them to, no questions asked.
With assertions on, the overflow hits a check and crashes. Still a denial-of-service, but at least it stops the corruption. With assertions off, the overflow is completely silent. No crash, no warning, just memory corruption from attacker-controlled data over the network, no authentication required.
When you see code that is only “safe” because debug checks are enabled, keep looking. Assertions help developers catch mistakes during testing. They are not a security boundary.
The fix
The fix was introduced in PR #99683, which merged on November 21, 2025. It moves dest_size = net_buf_tailroom(buf) from the top of the function into the loop body so it gets refreshed every time, and replaced the old bounds check with label_len + 1 >= dest_size. The patch also added tests to verify that dns_unpack_name() accepts valid names and returns an error on overflow.
The PR discussion pointed out that buf->data does not actually move while labels are appended, so the old check was conceptually awkward on top of being wrong. The new version is both correct and easier to reason about.
To the few researchers that still look manualy
Don’t disregard places that have a bounce check and search for places where the check and the thing it protects have drifted apart. A stale size or stale “remaining bytes” value is just as dangerous as no check at all.
Pay attention to translation layers. Wire format to string. Compressed to expanded. Length-prefixed to null-terminated. Every time code crosses representations, the sizes change, and that is where things go wrong.
And remember that standards compliance is not a security boundary. Attackers do not need to send well-formed input. If the parser mishandles bad input on the way to rejecting it, the parser has already lost.
Feel free to reach out if you have any questions, and I’ll be happy to elaborate on anything!
Feedback is extremely welcomed! You can reach out to me on X @0xkato