CVE-2026-1678: DNS Parser Overflow in Zephyr
A critical vulnerability (CVSS 9.4) in Zephyr RTOS lets a remote attacker overflow a fixed-size buffer by sending a DNS response with long labels. The parser captures the destination buffer's remaining space once, reuses that stale value through the entire label loop, and writes a null terminator past the end of the buffer with no bounds check at all.
The check is not missing. It is frozen in time while the buffer it guards keeps growing.
The bug
Stale bounds capture
mediumdns_unpack_name() grabs net_buf_tailroom(buf) once at function entry and reuses that value through the loop.
Drift between check and state
highEach iteration appends label bytes and a dot via net_buf_add_*, growing buf->len. The bounds check never refreshes dest_size.
Unchecked final write
criticalAfter the loop, buf->data[buf->len] = '\0' writes the terminator with no bounds check. In production builds with CONFIG_ASSERT off, net_buf helper assertions compile away and the overflow is silent.
Why DNS parsers
Anywhere code translates between two formats is a good place to look for memory safety bugs. DNS names on the wire use length-prefixed labels. Each label caps at 63 bytes. The full encoded name caps at 255 bytes. The parser converts that wire format into a C string with dots between labels and a null terminator at the end.
The translation is where the sizes diverge. Five 63-byte labels fit in a DNS message. Expanded into dotted notation with separators and a terminator, they do not fit in a 255-byte destination buffer. The parser has to know that. In Zephyr 4.3 and earlier, it did not.
Where
The vulnerability lives in dns_unpack_name() in subsys/net/lib/dns/dns_pack.c. The function reads a length-prefixed DNS name from a source buffer and writes the dotted form into a net_buf destination.
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
...
}
Three patterns combine to make this exploitable. First, dest_size is a snapshot. It reflects the tailroom at entry and never updates. Second, net_buf_add_u8 and net_buf_add_mem advance buf->len with every call, so the actual free space shrinks while dest_size stays put. Third, the final buf->data[buf->len] = '\0' has no guard at all. If the loop already wrote past the buffer, the terminator goes even further past.
The drift
The check (buf->data + label_len + 1) >= (buf->data + dest_size) simplifies to label_len + 1 >= dest_size. Once the first label fits, dest_size is still the original tailroom. The check now compares the next label's size against the full original capacity, not what is left.
Triggering the overflow
DNS permits labels up to 63 bytes and total encoded names up to 255 bytes. Five 63-byte labels encode to 5 * (1 + 63) = 320 bytes on the wire, which sits above the 255-byte name limit but fits comfortably in a 512-byte UDP DNS message.
Expanded to dotted notation those same labels produce roughly 5 * 63 + 4 separators + 1 terminator = 320 bytes. The default destination buffer holds 255. Depending on label count and sizes, an attacker can push the overflow anywhere from 65 to 200 bytes past the end of the buffer.
Attack sequence
Attacker sends a DNS response with 5 oversized labels
Labels at the 63-byte maximum fit the wire format
dns_unpack_name captures dest_size = 255
Single snapshot at function entry
Loop appends each label via net_buf_add_mem
buf->len grows; dest_size never does
Bounds check uses stale dest_size
label_len + 1 >= 255 holds for all 63-byte labels
buf->data[buf->len] = '\\0' writes past buffer end
Final terminator has no bounds check at all
The CONFIG_ASSERT trap
Zephyr ships a build option called CONFIG_ASSERT. Developer and debug builds turn it on. Production builds, by default, turn it off. When it is off, every __ASSERT macro in the tree compiles to nothing.
The net_buf_add_u8 and net_buf_add_mem helpers rely on assertions for their tailroom checks. With CONFIG_ASSERT off, those helpers do not verify available space. They advance buf->len and write. The overflow is silent.
Safe only with a debug flag is not safe
When code is only "safe" because debug checks are enabled, keep looking. Debug assertions are diagnostics. They are not a security boundary. In production builds they do not exist.
With assertions on, the overflow becomes a crash. A remote attacker gets denial of service instead of memory corruption. With assertions off, the write succeeds and the corruption reaches adjacent memory.
The fix
PR #99683 landed on November 21, 2025. Two changes close the gap:
-int dest_size = net_buf_tailroom(buf);- while ((val = *curr_src++)) {+ int dest_size = net_buf_tailroom(buf); // refreshed every iteration ...- if (((buf->data + label_len + 1) >=- (buf->data + dest_size)) ||+ if ((label_len + 1 >= dest_size) || ((curr_src + label_len) >= (msg + maxlen))) { return -EMSGSIZE; } if (buf->len > 0) { net_buf_add_u8(buf, '.'); } net_buf_add_mem(buf, curr_src, label_len); ... } -buf->data[buf->len] = '\0';+// dest_size refresh before terminator write covers the final byteThe refreshed dest_size reflects actual remaining tailroom at each iteration. The simplified label_len + 1 >= dest_size drops the pointer arithmetic and compares sizes directly. The patch also adds test coverage: valid names still parse, and crafted overflow payloads return -EMSGSIZE instead of corrupting memory.
Impact
Who is affected
Any Zephyr RTOS deployment running 4.3 or earlier with the DNS resolver enabled. Zephyr ships on microcontrollers, IoT devices, industrial sensors, wearables, and increasingly in vehicle and medical device firmware. DNS resolution runs as part of almost every network-connected application.
An attacker who can return DNS responses to a target, whether by running an authoritative server the target queries, positioning on the network path, or poisoning a cache, can reach dns_unpack_name with controlled label data.
Exploitability
CVSS 9.4. Network attack vector. No authentication required. The vulnerable path is the default DNS response handling, not an opt-in feature. Devices configured for DHCP-provided DNS servers on untrusted networks are reachable without any additional access.
Recommendations
Mitigations
Upgrade to Zephyr past PR #99683. The fix merged on November 21, 2025. Any release cut after that date carries the patch.
Enable CONFIG_ASSERT in production where the attack surface and resource budget allow. Assertions downgrade silent overflows to crashes. A crash is not a fix, but it is a loud failure instead of a corrupted neighbor.
Rebuild and reflash affected devices. DNS resolution is usually statically linked into the firmware image. A patched library does not help until the image on the device is replaced.
Monitor DNS responses on managed networks. Long label counts and oversized dotted names are fingerprints for this class of payload.
Lessons
A bounds check that drifts apart from the state it guards is as dangerous as no check at all. The vulnerable code had a check. The check read a variable. The variable was initialized once and never refreshed. Every subsequent iteration validated against a lie.
Translation layers are where this pattern hides. Wire format to in-memory representation, compression to expansion, length-prefixed to null-terminated. The input size passes one set of limits. The output size exceeds another. Reviewers who count bytes on the input side and stop looking miss the gap.
Standards compliance is not a security boundary either. A 63-byte label is perfectly legal DNS. Five of them is perfectly legal DNS. The wire format admits no attack. The parser's translation into a 255-byte C string is where the bug lives.
CWE classification
CWE-787: Out-of-bounds Write. dns_unpack_name writes past the end of a fixed-size destination buffer because the bounds check references a stale size snapshot and the final terminator write is unchecked.
Advisory details
| Field | Value |
|---|---|
| Advisory ID | GHSA-536f-h63g-hj42 |
| CVE | CVE-2026-1678 |
| Severity | Critical (CVSS 9.4) |
| Affected package | Zephyr RTOS |
| Affected versions | 4.3 and earlier |
| Patched versions | Releases including PR #99683 (merged 2025-11-21) |
| CWE | CWE-787 |