Back to Blog

CVE-2026-1678: DNS Parser Overflow in Zephyr

Tobias Jensen

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

medium

dns_unpack_name() grabs net_buf_tailroom(buf) once at function entry and reuses that value through the loop.

Drift between check and state

high

Each iteration appends label bytes and a dot via net_buf_add_*, growing buf->len. The bounds check never refreshes dest_size.

Unchecked final write

critical

After 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:

Vulnerable (<=4.3)Fixed
-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 byte

The 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

FieldValue
Advisory IDGHSA-536f-h63g-hj42
CVECVE-2026-1678
SeverityCritical (CVSS 9.4)
Affected packageZephyr RTOS
Affected versions4.3 and earlier
Patched versionsReleases including PR #99683 (merged 2025-11-21)
CWECWE-787

Further reading