Merge branch 'fix/lwip_dhcps_oob_read_v6.0' into 'release/v6.0'

fix(lwip): Fix DHCP servver OOB read issue (v6.0)

See merge request espressif/esp-idf!48049
This commit is contained in:
Euripedes Rocha
2026-04-30 07:42:35 +02:00
6 changed files with 211 additions and 20 deletions

View File

@@ -452,6 +452,14 @@ menu "LWIP"
Enabling this option allows DHCP server to support temporary static ARP entries
for DHCP Client. This will help the DHCP server to send the DHCP OFFER and DHCP ACK using IP unicast.
config LWIP_DHCPS_TEST_PARSE_OPTIONS
bool "Expose parse_options() for unit testing"
default n
depends on LWIP_DHCPS
help
Enables a non-static wrapper around the internal parse_options() function
so it can be called from test code. Only enable this for testing builds.
endmenu # DHCPS
menuconfig LWIP_AUTOIP

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -49,6 +49,7 @@
#define DHCPNAK 6
#define DHCPRELEASE 7
#define DHCP_OPTION_PAD 0
#define DHCP_OPTION_SUBNET_MASK 1
#define DHCP_OPTION_HOST_NAME 12
#define DHCP_OPTION_ROUTER 3
@@ -917,7 +918,6 @@ static void send_ack(dhcps_t *dhcps, struct dhcps_msg *m, u16_t len)
static u8_t parse_options(dhcps_t *dhcps, u8_t *optptr, s16_t len)
{
ip4_addr_t client;
bool is_dhcp_parse_end = false;
struct dhcps_state s;
client.addr = *((uint32_t *) &dhcps->client_address);
@@ -932,10 +932,31 @@ static u8_t parse_options(dhcps_t *dhcps, u8_t *optptr, s16_t len)
DHCPS_LOG("dhcps: (s16_t)*optptr = %d\n", (s16_t)*optptr);
#endif
if (*optptr == DHCP_OPTION_PAD) {
optptr++;
continue;
}
if (*optptr == DHCP_OPTION_END) {
break;
}
if (optptr + 1 >= end) {
break;
}
u8_t opt_len = optptr[1];
if (optptr + 2 + opt_len > end) {
break;
}
switch ((s16_t) *optptr) {
case DHCP_OPTION_MSG_TYPE: //53
type = *(optptr + 2);
if (opt_len >= 1) {
type = optptr[2];
}
break;
#if CONFIG_LWIP_DHCPS_REPORT_CLIENT_HOSTNAME
@@ -966,31 +987,24 @@ static u8_t parse_options(dhcps_t *dhcps, u8_t *optptr, s16_t len)
#endif
case DHCP_OPTION_REQ_IPADDR://50
if (memcmp((char *) &client.addr, (char *) optptr + 2, 4) == 0) {
if (opt_len >= 4) {
if (memcmp((char *) &client.addr, (char *) optptr + 2, 4) == 0) {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR = 0 ok\n");
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR = 0 ok\n");
#endif
s.state = DHCPS_STATE_ACK;
} else {
s.state = DHCPS_STATE_ACK;
} else {
#if DHCPS_DEBUG
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR != 0 err\n");
DHCPS_LOG("dhcps: DHCP_OPTION_REQ_IPADDR != 0 err\n");
#endif
s.state = DHCPS_STATE_NAK;
s.state = DHCPS_STATE_NAK;
}
}
break;
case DHCP_OPTION_END: {
is_dhcp_parse_end = true;
}
break;
}
if (is_dhcp_parse_end) {
break;
}
optptr += optptr[1] + 2;
optptr += opt_len + 2;
}
switch (type) {
@@ -1712,4 +1726,18 @@ bool dhcps_get_hostname_on_mac(dhcps_t *dhcps, const u8_t *mac, char *out, size_
return false;
}
#endif
#ifdef CONFIG_LWIP_DHCPS_TEST_PARSE_OPTIONS
u8_t dhcps_test_parse_options(u8_t *optptr, s16_t len)
{
dhcps_t *dhcps = dhcps_new();
if (dhcps == NULL) {
return 0;
}
IP4_ADDR(&dhcps->client_address, 192, 168, 4, 2);
u8_t result = parse_options(dhcps, optptr, len);
mem_free(dhcps);
return result;
}
#endif
#endif // ESP_DHCPS

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -484,6 +484,134 @@ TEST(lwip, sntp_client_time_2048)
test_sntp_timestamps(2048, false); // NTP timestamp MSB is cleared for time after 2036
}
/*
* DHCP option parser tests
*
* These exercise the bounds-checking in parse_options() by constructing raw
* option byte arrays and verifying correct behaviour for well-formed,
* truncated, and malformed inputs.
*/
#ifdef CONFIG_LWIP_DHCPS_TEST_PARSE_OPTIONS
extern u8_t dhcps_test_parse_options(u8_t *optptr, s16_t len);
#define TEST_OPT_PAD 0
#define TEST_OPT_MSG_TYPE 53
#define TEST_OPT_REQ_IPADDR 50
#define TEST_OPT_END 255
#define TEST_STATE_OFFER 1
#define TEST_STATE_ACK 3
#define TEST_STATE_IDLE 5
TEST(lwip, dhcps_parse_options_well_formed_discover)
{
u8_t opts[] = {
TEST_OPT_MSG_TYPE, 1, 1, /* DHCPDISCOVER */
TEST_OPT_END
};
u8_t state = dhcps_test_parse_options(opts, sizeof(opts));
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_OFFER, state);
}
TEST(lwip, dhcps_parse_options_well_formed_request_ack)
{
u8_t opts[] = {
TEST_OPT_MSG_TYPE, 1, 3, /* DHCPREQUEST */
TEST_OPT_REQ_IPADDR, 4, 192, 168, 4, 2, /* matches client_address */
TEST_OPT_END
};
u8_t state = dhcps_test_parse_options(opts, sizeof(opts));
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_ACK, state);
}
TEST(lwip, dhcps_parse_options_pad_bytes_skipped)
{
u8_t opts[] = {
TEST_OPT_PAD,
TEST_OPT_PAD,
TEST_OPT_PAD,
TEST_OPT_MSG_TYPE, 1, 1, /* DHCPDISCOVER */
TEST_OPT_END
};
u8_t state = dhcps_test_parse_options(opts, sizeof(opts));
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_OFFER, state);
}
TEST(lwip, dhcps_parse_options_truncated_length_byte)
{
/* Option code at the last byte with no room for the length byte.
* Before the fix this would read 1 byte OOB. */
u8_t opts[] = { TEST_OPT_MSG_TYPE };
u8_t state = dhcps_test_parse_options(opts, sizeof(opts));
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_IDLE, state);
}
TEST(lwip, dhcps_parse_options_truncated_option_data)
{
/* MSG_TYPE says length=1 but the data byte is outside the buffer.
* Before the fix this would read 1 byte OOB via optptr[2]. */
u8_t opts[] = { TEST_OPT_MSG_TYPE, 1 };
u8_t state = dhcps_test_parse_options(opts, sizeof(opts));
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_IDLE, state);
}
TEST(lwip, dhcps_parse_options_oversized_length)
{
/* Option declares a length far exceeding the buffer. */
u8_t opts[] = { TEST_OPT_MSG_TYPE, 200, 1, TEST_OPT_END };
u8_t state = dhcps_test_parse_options(opts, (s16_t)sizeof(opts));
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_IDLE, state);
}
TEST(lwip, dhcps_parse_options_no_end_marker)
{
/* Well-formed option but no END marker — parser must stop at buffer end. */
u8_t opts[] = { TEST_OPT_MSG_TYPE, 1, 1 };
u8_t state = dhcps_test_parse_options(opts, sizeof(opts));
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_OFFER, state);
}
TEST(lwip, dhcps_parse_options_empty)
{
u8_t opts[] = { 0 };
u8_t state = dhcps_test_parse_options(opts, 0);
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_IDLE, state);
}
TEST(lwip, dhcps_parse_options_only_pads)
{
u8_t opts[] = { TEST_OPT_PAD, TEST_OPT_PAD, TEST_OPT_PAD };
u8_t state = dhcps_test_parse_options(opts, sizeof(opts));
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_IDLE, state);
}
TEST(lwip, dhcps_parse_options_unknown_option_skipped)
{
/* Unknown option 99 with length 3 should be skipped, then MSG_TYPE parsed. */
u8_t opts[] = {
99, 3, 0xAA, 0xBB, 0xCC,
TEST_OPT_MSG_TYPE, 1, 1, /* DHCPDISCOVER */
TEST_OPT_END
};
u8_t state = dhcps_test_parse_options(opts, sizeof(opts));
TEST_ASSERT_EQUAL_UINT8(TEST_STATE_OFFER, state);
}
TEST(lwip, dhcps_parse_options_req_ipaddr_truncated)
{
/* REQ_IPADDR claims 4 bytes but buffer only has room for 2. */
u8_t opts[] = {
TEST_OPT_MSG_TYPE, 1, 3,
TEST_OPT_REQ_IPADDR, 4, 192, 168
};
u8_t state = dhcps_test_parse_options(opts, sizeof(opts));
/* Parser should stop before the truncated REQ_IPADDR, return based on type alone. */
TEST_ASSERT(state != 0);
}
#endif /* CONFIG_LWIP_DHCPS_TEST_PARSE_OPTIONS */
TEST_GROUP_RUNNER(lwip)
{
RUN_TEST_CASE(lwip, localhost_ping_test)
@@ -493,6 +621,19 @@ TEST_GROUP_RUNNER(lwip)
RUN_TEST_CASE(lwip, sntp_client_time_2015)
RUN_TEST_CASE(lwip, sntp_client_time_2048)
RUN_TEST_CASE(lwip, dhcp_arp_probe_self_mac_is_ok)
#ifdef CONFIG_LWIP_DHCPS_TEST_PARSE_OPTIONS
RUN_TEST_CASE(lwip, dhcps_parse_options_well_formed_discover)
RUN_TEST_CASE(lwip, dhcps_parse_options_well_formed_request_ack)
RUN_TEST_CASE(lwip, dhcps_parse_options_pad_bytes_skipped)
RUN_TEST_CASE(lwip, dhcps_parse_options_truncated_length_byte)
RUN_TEST_CASE(lwip, dhcps_parse_options_truncated_option_data)
RUN_TEST_CASE(lwip, dhcps_parse_options_oversized_length)
RUN_TEST_CASE(lwip, dhcps_parse_options_no_end_marker)
RUN_TEST_CASE(lwip, dhcps_parse_options_empty)
RUN_TEST_CASE(lwip, dhcps_parse_options_only_pads)
RUN_TEST_CASE(lwip, dhcps_parse_options_unknown_option_skipped)
RUN_TEST_CASE(lwip, dhcps_parse_options_req_ipaddr_truncated)
#endif
}
void app_main(void)

View File

@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.generic
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_lwip(dut: Dut) -> None:
dut.expect_unity_test_output()

View File

View File

@@ -3,3 +3,6 @@ CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=n
# Added to enable compilation of DHCP last IP restore feature
CONFIG_LWIP_DHCP_RESTORE_LAST_IP=y
# Expose DHCP server option parser for unit testing
CONFIG_LWIP_DHCPS_TEST_PARSE_OPTIONS=y