From 20209a62bc8565fc1e040882bc03c71ff0d73076 Mon Sep 17 00:00:00 2001 From: Shahriyar Jalayeri Date: Thu, 6 Aug 2026 20:37:50 +0200 Subject: net: dhcp6: bound received DUID option lengths dhcp6_parse_options() takes the SERVERID and CLIENTID option lengths from the received ADVERTISE/REPLY and uses them unchecked: - the SERVERID length is stored as server_uid_size and later copied into the fixed net_tx_packet buffer in dhcp6_send_request_packet() with no capacity check, so a large SERVERID overruns net_tx_packet (an out-of-bounds write); - the CLIENTID length is passed straight to memcmp() against the fixed sm_params.duid buffer, so a length larger than it reads past the end (an out-of-bounds read). Both are reachable by any on-link attacker that answers a DHCPv6 SOLICIT during netboot; neither length is bounded against anything but the received packet size. Bound the SERVERID length to the RFC 8415 DUID maximum at parse time, which also bounds the malloc() and the copy into the REQUEST for every caller, and only compare a CLIENTID that is exactly the size of the client DUID. Fixes: a0245818f7f8 ("net: dhcp6: Add DHCPv6 (DHCP for IPv6)") Signed-off-by: Shahriyar Jalayeri --- net/dhcpv6.c | 12 ++++++++---- net/dhcpv6.h | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/net/dhcpv6.c b/net/dhcpv6.c index 640f089a2e1..a18e358137c 100644 --- a/net/dhcpv6.c +++ b/net/dhcpv6.c @@ -322,15 +322,19 @@ static void dhcp6_parse_options(uchar *rx_pkt, unsigned int len) switch (ntohs(option_hdr->option_id)) { case DHCP6_OPTION_CLIENTID: - if (memcmp(option_ptr, sm_params.duid, option_len) - != 0) { - debug("CLIENT ID DOESN'T MATCH\n"); - } else { + if (option_len == sizeof(sm_params.duid) && + !memcmp(option_ptr, sm_params.duid, option_len)) { debug("CLIENT ID FOUND and MATCHES\n"); sm_params.rx_status.client_id_match = true; + } else { + debug("CLIENT ID DOESN'T MATCH\n"); } break; case DHCP6_OPTION_SERVERID: + if (option_len > DHCP6_DUID_MAX_LEN) { + debug("SERVER ID too long\n"); + break; + } sm_params.rx_status.server_id_found = true; sm_params.rx_status.server_uid_ptr = (uchar *)option_hdr; sm_params.rx_status.server_uid_size = option_len + diff --git a/net/dhcpv6.h b/net/dhcpv6.h index d41a3c30615..b0207a9f2c9 100644 --- a/net/dhcpv6.h +++ b/net/dhcpv6.h @@ -37,6 +37,9 @@ #define DUID_LL_SIZE (sizeof(struct dhcp6_option_duid_ll) + ETH_ALEN) #define DUID_MAX_SIZE DUID_LL_SIZE /* only supports DUID-LL currently */ +/* RFC 8415 sec 11.1: a DUID is a 2-octet type plus at most 128 octets */ +#define DHCP6_DUID_MAX_LEN 130 + /* vendor-class-data to send in vendor clas option */ #define DHCP6_VCI_STRING "U-Boot" -- cgit v1.2.3 From 5c8ac7ebfcde2e9a40e7ad6d560145842359676f Mon Sep 17 00:00:00 2001 From: Shahriyar Jalayeri Date: Thu, 6 Aug 2026 20:37:51 +0200 Subject: test: dm: eth: add DHCPv6 oversized option regression tests Answer a DHCPv6 SOLICIT from the sandbox eth tx handler with an ADVERTISE that carries an option longer than a valid DUID, and check the client rejects it rather than sending a REQUEST. Two cases cover the option parser: an over-long SERVERID, which a client that trusts the length copies out of bounds while building the REQUEST, and an over-long CLIENTID, which drives a memcmp past the client DUID buffer. Signed-off-by: Shahriyar Jalayeri --- test/dm/eth.c | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/test/dm/eth.c b/test/dm/eth.c index ed0b57d8861..f3a972c5c0a 100644 --- a/test/dm/eth.c +++ b/test/dm/eth.c @@ -621,6 +621,157 @@ static int dm_test_eth_async_ping_reply(struct unit_test_state *uts) return 0; } DM_TEST(dm_test_eth_async_ping_reply, UTF_SCAN_FDT); + +#if IS_ENABLED(CONFIG_CMD_DHCP6) && IS_ENABLED(CONFIG_IPV6) +#define DHCP6_DUID_LL_LEN 10 /* type(2) + hw_type(2) + MAC(6) */ +static bool dhcp6_request_seen; +static bool dhcp6_advertise_sent; +static int dhcp6_clientid_len; +static int dhcp6_serverid_len; + +/* + * Answer a DHCPv6 SOLICIT with an ADVERTISE whose SERVERID option is longer + * than any valid DUID. A correct client rejects it and never sends a REQUEST; + * a client that trusts the length copies it out of bounds while building the + * REQUEST. + */ +static int sb_dhcp6_advertise_handler(struct udevice *dev, void *packet, + unsigned int len) +{ + struct eth_sandbox_priv *priv = dev_get_priv(dev); + struct ethernet_hdr *seth = packet; + struct ethernet_hdr *eth; + struct ip6_hdr *sip6, *ip6; + struct udp_hdr *sudp, *udp; + uchar *sdhcp6, *d, *opt, *rx; + int msglen; + u16 udptot; + + if (ntohs(seth->et_protlen) != PROT_IP6) + return 0; + sip6 = (struct ip6_hdr *)((uchar *)packet + ETHER_HDR_SIZE); + if (sip6->nexthdr != IPPROTO_UDP) + return 0; + sudp = (struct udp_hdr *)((uchar *)sip6 + IP6_HDR_SIZE); + if (ntohs(sudp->udp_dst) != 547 || ntohs(sudp->udp_src) != 546) + return 0; + sdhcp6 = (uchar *)sudp + UDP_HDR_SIZE; + + /* a REQUEST means the client accepted the over-long SERVERID */ + if (sdhcp6[0] == 3) { /* DHCP6_MSG_REQUEST */ + dhcp6_request_seen = true; + net_set_state(NETLOOP_FAIL); + return 0; + } + if (sdhcp6[0] != 1) /* DHCP6_MSG_SOLICIT */ + return 0; + if (dhcp6_advertise_sent) { + /* the client re-solicited, so it rejected the ADVERTISE */ + net_set_state(NETLOOP_FAIL); + return 0; + } + dhcp6_advertise_sent = true; + if (priv->recv_packets >= PKTBUFSRX) + return 0; + + rx = priv->recv_packet_buffer[priv->recv_packets]; + memset(rx, 0, PKTSIZE); + + eth = (struct ethernet_hdr *)rx; + memcpy(eth->et_dest, seth->et_src, ARP_HLEN); + memcpy(eth->et_src, priv->fake_host_hwaddr, ARP_HLEN); + eth->et_protlen = htons(PROT_IP6); + + ip6 = (struct ip6_hdr *)(rx + ETHER_HDR_SIZE); + ip6->version = 6; + ip6->nexthdr = IPPROTO_UDP; + ip6->hop_limit = 255; + memcpy(&ip6->saddr, &sip6->daddr, sizeof(struct in6_addr)); + memcpy(&ip6->daddr, &sip6->saddr, sizeof(struct in6_addr)); + + udp = (struct udp_hdr *)((uchar *)ip6 + IP6_HDR_SIZE); + udp->udp_src = htons(547); + udp->udp_dst = htons(546); + + d = (uchar *)udp + UDP_HDR_SIZE; + opt = d; + /* dhcp6 header: reuse the SOLICIT trans_id, msg_type = ADVERTISE */ + memcpy(opt, sdhcp6, 4); + opt[0] = 2; /* DHCP6_MSG_ADVERTISE */ + opt += 4; + /* CLIENTID: the client DUID from the SOLICIT, padded to the test size */ + opt[0] = 0; opt[1] = 1; + opt[2] = dhcp6_clientid_len >> 8; + opt[3] = dhcp6_clientid_len & 0xff; + memcpy(opt + 4, sdhcp6 + 8, DHCP6_DUID_LL_LEN); + opt += 4 + dhcp6_clientid_len; + /* echo the client's IA_NA (hdr 4 + iaid/t1/t2 12) */ + memcpy(opt, sdhcp6 + 4 + 14 + 6, 16); + opt += 16; + /* PREFERENCE = 255 so the client acts on this ADVERTISE at once */ + opt[0] = 0; opt[1] = 7; opt[2] = 0; opt[3] = 1; opt[4] = 255; + opt += 5; + /* SERVERID of the test size */ + opt[0] = 0; opt[1] = 2; + opt[2] = dhcp6_serverid_len >> 8; + opt[3] = dhcp6_serverid_len & 0xff; + memset(opt + 4, 0x41, dhcp6_serverid_len); + opt += 4 + dhcp6_serverid_len; + + msglen = opt - d; + udptot = UDP_HDR_SIZE + msglen; + ip6->payload_len = htons(udptot); + udp->udp_len = htons(udptot); + udp->udp_xsum = 0; + udp->udp_xsum = csum_ipv6_magic(&ip6->saddr, &ip6->daddr, udptot, + IPPROTO_UDP, + csum_partial((u8 *)udp, udptot, 0)); + + priv->recv_packet_length[priv->recv_packets] = + ETHER_HDR_SIZE + IP6_HDR_SIZE + udptot; + priv->recv_packets++; + + return 0; +} + +static int dhcp6_run_advertise(struct unit_test_state *uts) +{ + dhcp6_request_seen = false; + dhcp6_advertise_sent = false; + sandbox_eth_set_tx_handler(0, sb_dhcp6_advertise_handler); + sandbox_eth_skip_timeout(); + + env_set("ethact", "eth@10002000"); + net_loop(DHCP6); + + sandbox_eth_set_tx_handler(0, NULL); + + /* the malformed ADVERTISE must be rejected: no REQUEST is sent */ + ut_assert(!dhcp6_request_seen); + + return 0; +} + +/* Check the DHCPv6 client rejects an over-long SERVERID option */ +static int dm_test_dhcp6_serverid_reject(struct unit_test_state *uts) +{ + dhcp6_clientid_len = DHCP6_DUID_LL_LEN; + dhcp6_serverid_len = 200; + + return dhcp6_run_advertise(uts); +} +DM_TEST(dm_test_dhcp6_serverid_reject, UTF_SCAN_FDT); + +/* Check the DHCPv6 client rejects an over-long CLIENTID option */ +static int dm_test_dhcp6_clientid_reject(struct unit_test_state *uts) +{ + dhcp6_clientid_len = 20; + dhcp6_serverid_len = DHCP6_DUID_LL_LEN; + + return dhcp6_run_advertise(uts); +} +DM_TEST(dm_test_dhcp6_clientid_reject, UTF_SCAN_FDT); +#endif #endif #if IS_ENABLED(CONFIG_IPV6_ROUTER_DISCOVERY) -- cgit v1.2.3 From 2d94618a58aeb7630f18eee33419ce48d0fd3616 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Tue, 1 Sep 2026 11:08:55 -0600 Subject: net: lwip: return ERR_ABRT after aborting wget receive altcp_abort() deallocates the connection PCB. A raw TCP receive callback which calls it must return ERR_ABRT so the TCP input path does not access the released PCB. The wget receive callback instead returns ERR_BUF when storing received data fails. Return ERR_ABRT after aborting the connection. Fixes: e40c910e88a9 ("net-lwip: wget: add LMB and buffer checks") Signed-off-by: James Hilliard Reviewed-by: Jerome Forissier --- net/lwip/wget.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/lwip/wget.c b/net/lwip/wget.c index 247ece18e2b..e8930da410b 100644 --- a/net/lwip/wget.c +++ b/net/lwip/wget.c @@ -206,7 +206,7 @@ static err_t httpc_recv_cb(void *arg, struct altcp_pcb *pcb, struct pbuf *pbuf, for (buf = pbuf; buf; buf = buf->next) { if (store_block(ctx, buf->payload, buf->len) < 0) { altcp_abort(pcb); - ret = ERR_BUF; + ret = ERR_ABRT; goto out; } } -- cgit v1.2.3 From 6fb684d1a9377c6e87cccfd46125d32f78a12337 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Tue, 1 Sep 2026 11:08:59 -0600 Subject: net: lwip: allow DNS callbacks to be canceled A caller can stop waiting for a DNS result while the resolver keeps the request and callback. If another lwIP client continues polling, that callback can later use state which its owner has already released. Add dns_cancel() to remove matching callbacks without canceling a query shared by other callers. Use it when stopping SNTP so a pending name lookup cannot outlive the client. Signed-off-by: James Hilliard Reviewed-by: Jerome Forissier --- lib/lwip/lwip/src/apps/sntp/sntp.c | 3 +++ lib/lwip/lwip/src/core/dns.c | 26 ++++++++++++++++++++++++++ lib/lwip/lwip/src/include/lwip/dns.h | 1 + 3 files changed, 30 insertions(+) diff --git a/lib/lwip/lwip/src/apps/sntp/sntp.c b/lib/lwip/lwip/src/apps/sntp/sntp.c index 0e7f36520fa..a37699f65b7 100644 --- a/lib/lwip/lwip/src/apps/sntp/sntp.c +++ b/lib/lwip/lwip/src/apps/sntp/sntp.c @@ -711,6 +711,9 @@ void sntp_stop(void) { LWIP_ASSERT_CORE_LOCKED(); +#if SNTP_SERVER_DNS + dns_cancel(sntp_dns_found, NULL); +#endif if (sntp_pcb != NULL) { #if SNTP_MONITOR_SERVER_REACHABILITY u8_t i; diff --git a/lib/lwip/lwip/src/core/dns.c b/lib/lwip/lwip/src/core/dns.c index 6540f143bac..5762647ee27 100644 --- a/lib/lwip/lwip/src/core/dns.c +++ b/lib/lwip/lwip/src/core/dns.c @@ -387,6 +387,32 @@ dns_getserver(u8_t numdns) } } +/** + * @ingroup dns + * Cancel pending callbacks registered by dns_gethostbyname(). + * + * DNS queries shared with other callers continue so their result can still be + * cached and delivered. Only callbacks matching both arguments are removed. + * + * @param found callback passed to dns_gethostbyname() + * @param callback_arg callback argument passed to dns_gethostbyname() + */ +void +dns_cancel(dns_found_callback found, void *callback_arg) +{ + u8_t i; + + LWIP_ASSERT_CORE_LOCKED(); + + for (i = 0; i < DNS_MAX_REQUESTS; i++) { + if ((dns_requests[i].found == found) && + (dns_requests[i].arg == callback_arg)) { + dns_requests[i].found = NULL; + dns_requests[i].arg = NULL; + } + } +} + /** * The DNS resolver client timer - handle retries and timeouts and should * be called every DNS_TMR_INTERVAL milliseconds (every second by default). diff --git a/lib/lwip/lwip/src/include/lwip/dns.h b/lib/lwip/lwip/src/include/lwip/dns.h index 091341544f3..82579f09f35 100644 --- a/lib/lwip/lwip/src/include/lwip/dns.h +++ b/lib/lwip/lwip/src/include/lwip/dns.h @@ -111,6 +111,7 @@ err_t dns_gethostbyname(const char *hostname, ip_addr_t *addr, err_t dns_gethostbyname_addrtype(const char *hostname, ip_addr_t *addr, dns_found_callback found, void *callback_arg, u8_t dns_addrtype); +void dns_cancel(dns_found_callback found, void *callback_arg); #if DNS_LOCAL_HOSTLIST -- cgit v1.2.3 From 0c1130caf1b47d9eadaa6741d1ca1246a8786bc8 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Tue, 1 Sep 2026 11:09:00 -0600 Subject: net: lwip: add an HTTP client abort operation The HTTP client has no public way to cancel a request. Returning from wget while DNS or TCP work remains pending can therefore leave callbacks referencing its released command state. Track pending DNS resolution and add httpc_abort(). The operation removes a pending DNS callback, detaches the HTTP callbacks and immediately aborts the altcp connection. Report a local abort through the normal result callback. Signed-off-by: James Hilliard Reviewed-by: Jerome Forissier --- lib/lwip/lwip/src/apps/http/http_client.c | 71 ++++++++++++++++++++--- lib/lwip/lwip/src/include/lwip/apps/http_client.h | 1 + 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/lib/lwip/lwip/src/apps/http/http_client.c b/lib/lwip/lwip/src/apps/http/http_client.c index 1973e79e723..2bc2a0db97e 100644 --- a/lib/lwip/lwip/src/apps/http/http_client.c +++ b/lib/lwip/lwip/src/apps/http/http_client.c @@ -146,15 +146,23 @@ typedef struct _httpc_state u32_t rx_content_len; u32_t hdr_content_len; httpc_parse_state_t parse_state; +#if LWIP_DNS + u8_t dns_pending; +#endif #if HTTPC_DEBUG_REQUEST char* server_name; char* uri; #endif } httpc_state_t; -/** Free http client state and deallocate all resources within */ -static err_t -httpc_free_state(httpc_state_t* req) +#if LWIP_DNS +static void httpc_dns_found(const char* hostname, const ip_addr_t *ipaddr, + void *arg); +#endif + +/** Free http client state and detach its connection callbacks */ +static struct altcp_pcb * +httpc_detach_state(httpc_state_t* req) { struct altcp_pcb* tpcb; @@ -168,16 +176,26 @@ httpc_free_state(httpc_state_t* req) } tpcb = req->pcb; - mem_free(req); - req = NULL; - if (tpcb != NULL) { - err_t r; altcp_arg(tpcb, NULL); altcp_recv(tpcb, NULL); altcp_err(tpcb, NULL); altcp_poll(tpcb, NULL, 0); altcp_sent(tpcb, NULL); + } + mem_free(req); + return tpcb; +} + +/** Free http client state and deallocate all resources within */ +static err_t +httpc_free_state(httpc_state_t* req) +{ + struct altcp_pcb* tpcb; + + tpcb = httpc_detach_state(req); + if (tpcb != NULL) { + err_t r; r = altcp_close(tpcb); if (r != ERR_OK) { altcp_abort(tpcb); @@ -202,6 +220,38 @@ httpc_close(httpc_state_t* req, httpc_result_t result, u32_t server_response, er return ERR_OK; } +/** Abort an HTTP client request. */ +err_t +httpc_abort(httpc_state_t *req) +{ + struct altcp_pcb *tpcb; + + if (req == NULL) { + return ERR_ARG; + } + +#if LWIP_DNS + if (req->dns_pending) { + dns_cancel(httpc_dns_found, req); + req->dns_pending = 0; + } +#endif + + if (req->conn_settings != NULL) { + if (req->conn_settings->result_fn != NULL) { + req->conn_settings->result_fn(req->callback_arg, + HTTPC_RESULT_LOCAL_ABORT, req->rx_content_len, 0, ERR_ABRT); + } + } + + tpcb = httpc_detach_state(req); + if (tpcb != NULL) { + altcp_abort(tpcb); + } + + return ERR_OK; +} + /** Parse http header response line 1 */ static err_t http_parse_response_status(struct pbuf *p, u16_t *http_version, u16_t *http_status, u16_t *http_status_str_offset) @@ -447,6 +497,7 @@ httpc_dns_found(const char* hostname, const ip_addr_t *ipaddr, void *arg) LWIP_UNUSED_ARG(hostname); + req->dns_pending = 0; if (ipaddr != NULL) { err = httpc_get_internal_addr(req, ipaddr); if (err == ERR_OK) { @@ -479,9 +530,13 @@ httpc_get_internal_dns(httpc_state_t* req, const char* server_name) if (err == ERR_OK) { /* cached or IP-string */ err = httpc_get_internal_addr(req, &req->remote_addr); - } else if (err == ERR_INPROGRESS) { + } +#if LWIP_DNS + else if (err == ERR_INPROGRESS) { + req->dns_pending = 1; return ERR_OK; } +#endif return err; } diff --git a/lib/lwip/lwip/src/include/lwip/apps/http_client.h b/lib/lwip/lwip/src/include/lwip/apps/http_client.h index d39e1bf829a..3f62150dca1 100644 --- a/lib/lwip/lwip/src/include/lwip/apps/http_client.h +++ b/lib/lwip/lwip/src/include/lwip/apps/http_client.h @@ -143,6 +143,7 @@ err_t httpc_get_file(const ip_addr_t* server_addr, u16_t port, const char* uri, altcp_recv_fn recv_fn, void* callback_arg, httpc_state_t **connection); err_t httpc_get_file_dns(const char* server_name, u16_t port, const char* uri, const httpc_connection_t *settings, altcp_recv_fn recv_fn, void* callback_arg, httpc_state_t **connection); +err_t httpc_abort(httpc_state_t *connection); #if LWIP_HTTPC_HAVE_FILE_IO err_t httpc_get_file_to_disk(const ip_addr_t* server_addr, u16_t port, const char* uri, const httpc_connection_t *settings, -- cgit v1.2.3 From cabdcdbbca151a5f7ac98135dd1f995ead7417e0 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Tue, 1 Sep 2026 11:09:01 -0600 Subject: net: lwip: allow DHCP to stop without releasing its lease U-Boot keeps using the address acquired by the dhcp command. The existing lwIP stop operation sends DHCPRELEASE and clears the interface address, which is unsuitable when the interface remains active for another client. Factor the common shutdown path and add dhcp_stop_without_release(). It stops the state machine and drops its UDP PCB reference without sending DHCPRELEASE or clearing the interface address. Signed-off-by: James Hilliard Reviewed-by: Jerome Forissier --- lib/lwip/lwip/src/core/ipv4/dhcp.c | 39 ++++++++++++++++++++++++++--------- lib/lwip/lwip/src/include/lwip/dhcp.h | 1 + 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/lwip/lwip/src/core/ipv4/dhcp.c b/lib/lwip/lwip/src/core/ipv4/dhcp.c index d9ed8b0f37a..bfc6cfe488e 100644 --- a/lib/lwip/lwip/src/core/ipv4/dhcp.c +++ b/lib/lwip/lwip/src/core/ipv4/dhcp.c @@ -1315,20 +1315,15 @@ dhcp_reboot(struct netif *netif) return result; } -/** - * @ingroup dhcp4 - * Release a DHCP lease and stop DHCP statemachine (and AUTOIP if LWIP_DHCP_AUTOIP_COOP). - * - * @param netif network interface - */ -void -dhcp_release_and_stop(struct netif *netif) +/** Stop the DHCP state machine, optionally releasing its lease. */ +static void +dhcp_stop_internal(struct netif *netif, int release) { struct dhcp *dhcp = netif_dhcp_data(netif); ip_addr_t server_ip_addr; LWIP_ASSERT_CORE_LOCKED(); - LWIP_DEBUGF(DHCP_DEBUG | LWIP_DBG_TRACE, ("dhcp_release_and_stop()\n")); + LWIP_DEBUGF(DHCP_DEBUG | LWIP_DBG_TRACE, ("dhcp_stop_internal()\n")); if (dhcp == NULL) { return; } @@ -1352,7 +1347,7 @@ dhcp_release_and_stop(struct netif *netif) dhcp->t1_renew_time = dhcp->t2_rebind_time = dhcp->lease_used = dhcp->t0_timeout = 0; /* send release message when current IP was assigned via DHCP */ - if (dhcp_supplied_address(netif)) { + if (release && dhcp_supplied_address(netif)) { /* create and initialize the DHCP message header */ struct pbuf *p_out; u16_t options_out_len; @@ -1391,6 +1386,30 @@ dhcp_release_and_stop(struct netif *netif) } } +/** + * @ingroup dhcp4 + * Stop DHCP without sending DHCPRELEASE or removing the configured address. + * + * @param netif network interface + */ +void +dhcp_stop_without_release(struct netif *netif) +{ + dhcp_stop_internal(netif, 0); +} + +/** + * @ingroup dhcp4 + * Release a DHCP lease and stop DHCP statemachine (and AUTOIP if LWIP_DHCP_AUTOIP_COOP). + * + * @param netif network interface + */ +void +dhcp_release_and_stop(struct netif *netif) +{ + dhcp_stop_internal(netif, 1); +} + /** * @ingroup dhcp4 * This function calls dhcp_release_and_stop() internally. diff --git a/lib/lwip/lwip/src/include/lwip/dhcp.h b/lib/lwip/lwip/src/include/lwip/dhcp.h index b413fa63f96..e7a79174f54 100644 --- a/lib/lwip/lwip/src/include/lwip/dhcp.h +++ b/lib/lwip/lwip/src/include/lwip/dhcp.h @@ -127,6 +127,7 @@ err_t dhcp_start(struct netif *netif); err_t dhcp_renew(struct netif *netif); err_t dhcp_release(struct netif *netif); void dhcp_stop(struct netif *netif); +void dhcp_stop_without_release(struct netif *netif); void dhcp_release_and_stop(struct netif *netif); void dhcp_inform(struct netif *netif); void dhcp_network_changed_link_up(struct netif *netif); -- cgit v1.2.3 From 179aed54f469f4e88354e0477bea1ab2808f99cf Mon Sep 17 00:00:00 2001 From: Esteban Alba Date: Fri, 21 Aug 2026 02:07:43 -0500 Subject: net: net6: validate IPv6 payload and transport lengths on receive net_ip6_handler() checks the received length only against IP6_HDR_SIZE and then uses length fields from the packet without checking them. The checksum is computed over payload_len, but the UDP handler length comes from udp_len. A sender can keep payload_len correct so the checksum still validates and set udp_len larger than the frame. A handler that trusts that length then reads or writes past the receive buffer. The DHCPv6 client copies the declared number of bytes and can be made to write past the packet buffer from a single link-local ADVERTISE. Validate payload_len against the received length and trim len to it before protocol dispatch. Validate the ICMPv6 and UDP header sizes before either header is dereferenced, and validate udp_len before reading udp_xsum or calling the UDP handler. Fixes: 1feb697830ce ("net: ipv6: Add implementation of main IPv6 functions") Suggested-by: Jerome Forissier Signed-off-by: Esteban Alba Reviewed-by: Jerome Forissier --- net/net6.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/net/net6.c b/net/net6.c index 4cff98df15c..e1a455748e3 100644 --- a/net/net6.c +++ b/net/net6.c @@ -392,11 +392,19 @@ int net_ip6_handler(struct ethernet_hdr *et, struct ip6_hdr *ip6, int len) if (ip6->version != 6) return -EINVAL; + hlen = ntohs(ip6->payload_len); + + if (len < IP6_HDR_SIZE + hlen) + return -EINVAL; + len = IP6_HDR_SIZE + hlen; + switch (ip6->nexthdr) { case PROT_ICMPV6: + if (hlen < sizeof(struct icmp6hdr)) + return -EINVAL; + icmp = (struct icmp6hdr *)(((uchar *)ip6) + IP6_HDR_SIZE); csum = icmp->icmp6_cksum; - hlen = ntohs(ip6->payload_len); icmp->icmp6_cksum = 0; /* checksum */ csum_p = csum_partial((u8 *)icmp, hlen, 0); @@ -421,9 +429,15 @@ int net_ip6_handler(struct ethernet_hdr *et, struct ip6_hdr *ip6, int len) } break; case IPPROTO_UDP: + if (hlen < UDP_HDR_SIZE) + return -EINVAL; + udp = (struct udp_hdr *)(((uchar *)ip6) + IP6_HDR_SIZE); + if (ntohs(udp->udp_len) < UDP_HDR_SIZE || + ntohs(udp->udp_len) > hlen) + return -EINVAL; + csum = udp->udp_xsum; - hlen = ntohs(ip6->payload_len); udp->udp_xsum = 0; /* checksum */ csum_p = csum_partial((u8 *)udp, hlen, 0); -- cgit v1.2.3 From 9e6028eabcfb2d27b8f7005b2d3a6f0cba85b5eb Mon Sep 17 00:00:00 2001 From: Li Xiao Date: Tue, 18 Aug 2026 16:20:10 +0800 Subject: net: airoha: fix PCS calibration retry limit The calibration retry counter starts at zero and is incremented after the limit check. Using a strict greater-than comparison therefore allows one more retry than AIROHA_PCS_MAX_CALIBRATION_TRY specifies. Use a greater-than-or-equal comparison so the function stops after the configured number of retries. Fixes: d4dd6eb29bda ("net: airoha: add support for Airoha PCS driver") Signed-off-by: Li Xiao Reviewed-by: Jerome Forissier --- drivers/net/airoha/pcs-an7581.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/net/airoha/pcs-an7581.c b/drivers/net/airoha/pcs-an7581.c index 746ff55d72f..bc15d9d07c4 100644 --- a/drivers/net/airoha/pcs-an7581.c +++ b/drivers/net/airoha/pcs-an7581.c @@ -1321,7 +1321,7 @@ retry_calibration: */ regmap_read(priv->xfi_pma, AIROHA_PCS_PMA_RX_FREQDET, &val); if (!(val & AIROHA_PCS_PMA_FBCK_LOCK)) { - if (calibration_try > AIROHA_PCS_MAX_CALIBRATION_TRY) { + if (calibration_try >= AIROHA_PCS_MAX_CALIBRATION_TRY) { dev_err(priv->dev, "No FBCK Lock from FreqDet module after %d calibration try. PCS won't work.\n", AIROHA_PCS_MAX_CALIBRATION_TRY); return -EIO; -- cgit v1.2.3 From ff448fa9a1b8f8c539c64c7fc1b17982c23c1545 Mon Sep 17 00:00:00 2001 From: Yuxiao Zhang Date: Tue, 1 Sep 2026 10:51:42 -0700 Subject: u-boot: fix error check in eth init eth_start_udev can fail but errno could still be zero which causes the function ignores the error silently. This fix catches the error properly. Signed-off-by: Yuxiao Zhang Reviewed-by: Tom Rini --- net/eth-uclass.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/net/eth-uclass.c b/net/eth-uclass.c index 5c437143a30..07ec59b97f0 100644 --- a/net/eth-uclass.c +++ b/net/eth-uclass.c @@ -289,8 +289,10 @@ int eth_start_udev(struct udevice *dev) if (priv->running) return 0; - if (!device_active(dev)) + if (!device_active(dev)) { + eth_errno = -EINVAL; return -EINVAL; + } ret = eth_get_ops(dev)->start(dev); if (ret < 0) -- cgit v1.2.3 From 0bbf09859658b8cc9ac13be41af23b516b8ef69a Mon Sep 17 00:00:00 2001 From: Shahriyar Jalayeri Date: Wed, 19 Aug 2026 18:40:24 +0200 Subject: net: nfs: Bound the length of an NFS read reply nfs_read_reply() takes the length of the returned data from the server and hands it to store_block(), which memcpy()s that many bytes out of the 1152-byte rpc_pkt stack buffer to image_load_addr. The length was kept in a signed int and bounded with: if (((uchar *)&rpc_pkt.u.reply.data[0] - (uchar *)&rpc_pkt + rlen) > len) return -9999; On an LP64 target the pointer subtraction is a 64-bit ptrdiff_t, so a length with the top bit set makes rlen negative, the sum stays below len and the check passes. store_block() then takes rlen as an unsigned int, so 0x80000000 becomes a ~2 GB copy that runs off both buffers. The bound is also measured from the reply header rather than from the data, which begins several words later, so a large positive length still reads past the end of rpc_pkt. Read the length into an unsigned int so it can never be negative, reject anything larger than NFS_READ_SIZE (the most a read requests), and bound it against the received packet measured from the start of the data. Take the NFSv3 length from the opaque data_size field that prefixes the returned bytes, which is what store_block() copies. Both the classic and the lwIP NFS clients reach this through nfs_pkt_recv(), so the single check covers both. Fixes: aa207cf3a6d6 ("CVE-2019-14194/CVE-2019-14198: nfs: fix unbounded memcpy with a failed length check at nfs_read_reply") Signed-off-by: Shahriyar Jalayeri Reviewed-by: Jerome Forissier --- net/nfs-common.c | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/net/nfs-common.c b/net/nfs-common.c index 637fcfd9bb8..7c82ab5d3d9 100644 --- a/net/nfs-common.c +++ b/net/nfs-common.c @@ -700,7 +700,8 @@ static int nfs_readlink_reply(uchar *pkt, unsigned int len) static int nfs_read_reply(uchar *pkt, unsigned int len) { struct rpc_t rpc_pkt; - int rlen; + u32 rlen; + size_t data_offset; uchar *data_ptr; memcpy(&rpc_pkt.u.data[0], pkt, sizeof(rpc_pkt.u.reply)); @@ -734,17 +735,19 @@ static int nfs_read_reply(uchar *pkt, unsigned int len) int nfsv3_data_offset = nfs3_get_attributes_offset(rpc_pkt.u.reply.data); - /* count value */ - rlen = ntohl(rpc_pkt.u.reply.data[1 + nfsv3_data_offset]); - /* Skip unused values : - * EOF: 32 bits value, - * data_size: 32 bits value, - */ + /* Skip count and EOF, read data_size from opaque data */ + rlen = ntohl(rpc_pkt.u.reply.data[3 + nfsv3_data_offset]); data_ptr = (uchar *) &rpc_pkt.u.reply.data[4 + nfsv3_data_offset]; } - if (((uchar *)&rpc_pkt.u.reply.data[0] - (uchar *)&rpc_pkt + rlen) > len) + /* reject a length larger than a read requests */ + if (rlen > NFS_READ_SIZE) + return -9999; + + /* reject a length that runs past the received packet */ + data_offset = data_ptr - (uchar *)&rpc_pkt; + if (data_offset > len || rlen > len - data_offset) return -9999; if (store_block(data_ptr, nfs_offset, rlen)) -- cgit v1.2.3 From 1c0aff3a5fbfeee7a8948f624e0b8554e6e0d8fd Mon Sep 17 00:00:00 2001 From: Shahriyar Jalayeri Date: Wed, 19 Aug 2026 18:40:25 +0200 Subject: net: nfs: Bound the length of an NFS readlink reply nfs_readlink_reply() takes the symlink length from the server and memcpy()s that many bytes into nfs_path_buff[]. It was kept in a signed int and bounded with: if (((uchar *)&rpc_pkt.u.reply.data[0] - (uchar *)&rpc_pkt + rlen) > len) return -NFS_RPC_DROP; A negative rlen makes the sum smaller than len, so the check passes; rlen is then used as an unsigned size_t in memcpy(), and in the relative-symlink branch pathlen + rlen also stays below the buffer size, so a length of -1 drives a memcpy() off nfs_path_buff. The bound is also measured from the reply header rather than from the symlink data, which begins a few words later, so a large positive length reads past the end of the received reply. A malicious server answers the READ with an ISDIR status to move the client into the readlink state, then returns such a reply. Read the length into an unsigned int, bound it against the received packet measured from the symlink data, and check it against the destination buffer with the subtraction ordered so it cannot wrap. Fixes: cf3a4f1e86ec ("CVE-2019-14195: nfs: fix unbounded memcpy with unvalidated length at nfs_readlink_reply") Signed-off-by: Shahriyar Jalayeri Reviewed-by: Jerome Forissier [jf: fix conflict with 85d82c523275 ("net: nfs: clean up bounds checks in nfs_readlink_reply()")] Signed-off-by: Jerome Forissier --- net/nfs-common.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/net/nfs-common.c b/net/nfs-common.c index 7c82ab5d3d9..869ba6b9ef9 100644 --- a/net/nfs-common.c +++ b/net/nfs-common.c @@ -642,7 +642,8 @@ static int nfs3_get_attributes_offset(uint32_t *data) static int nfs_readlink_reply(uchar *pkt, unsigned int len) { struct rpc_t rpc_pkt; - int rlen; + u32 rlen; + size_t data_offset; int nfsv3_data_offset = 0; memcpy((unsigned char *)&rpc_pkt, pkt, len); @@ -666,27 +667,33 @@ static int nfs_readlink_reply(uchar *pkt, unsigned int len) /* new path length */ rlen = ntohl(rpc_pkt.u.reply.data[1 + nfsv3_data_offset]); - if (((uchar *)&rpc_pkt.u.reply.data[0] - (uchar *)&rpc_pkt + rlen) > len) + data_offset = (uchar *)&rpc_pkt.u.reply.data[2 + nfsv3_data_offset] - + (uchar *)&rpc_pkt; + + /* reject a length that runs past the received packet */ + if (data_offset > len || rlen > len - data_offset) return -NFS_RPC_DROP; if (*((char *)&rpc_pkt.u.reply.data[2 + nfsv3_data_offset]) != '/') { - int pathlen; - int new_len; + size_t pathlen; + size_t new_len; strcat(nfs_path, "/"); pathlen = strlen(nfs_path); - new_len = pathlen + rlen; - if (new_len >= sizeof(nfs_path_buff)) { - printf("NFS: symlink too long (%d bytes)\n", new_len); + if (pathlen >= sizeof(nfs_path_buff) || + rlen >= sizeof(nfs_path_buff) - pathlen) { + printf("NFS: symlink too long (%zu + %u bytes)\n", + pathlen, rlen); return -NFS_RPC_DROP; } + new_len = pathlen + rlen; memcpy(nfs_path + pathlen, (uchar *)&rpc_pkt.u.reply.data[2 + nfsv3_data_offset], rlen); nfs_path[new_len] = 0; } else { if (rlen >= sizeof(nfs_path_buff)) { - printf("NFS: symlink too long (%d bytes)\n", rlen); + printf("NFS: symlink too long (%u bytes)\n", rlen); return -NFS_RPC_DROP; } memcpy(nfs_path, -- cgit v1.2.3 From a3afed6c8483357b094be6def40fd8d73617d72a Mon Sep 17 00:00:00 2001 From: Shahriyar Jalayeri Date: Wed, 19 Aug 2026 18:40:26 +0200 Subject: test: dm: nfs: Add regression tests for the NFS reply-length checks Add DM tests that feed nfs_pkt_recv() crafted NFSv3 replies with a read and a readlink request outstanding. The READ reply carries a data length with the top bit set; the READLINK reply carries a length of -1 that slips past the destination bound as pathlen - 1. Either would drive a memcpy() out of the reply buffer; the tests assert that nothing is stored and the path buffer is left untouched. Enable CONFIG_CMD_NFS in sandbox_defconfig so the NFS client and these tests are built and run under sandbox. Signed-off-by: Shahriyar Jalayeri Reviewed-by: Jerome Forissier --- configs/sandbox_defconfig | 1 + test/dm/Makefile | 1 + test/dm/nfs.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 test/dm/nfs.c diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig index 79f46317e45..ca73080b06d 100644 --- a/configs/sandbox_defconfig +++ b/configs/sandbox_defconfig @@ -123,6 +123,7 @@ CONFIG_CMD_LINK_LOCAL=y CONFIG_IPV6_ROUTER_DISCOVERY=y CONFIG_CMD_ETHSW=y CONFIG_CMD_DNS=y +CONFIG_CMD_NFS=y CONFIG_CMD_SNTP=y CONFIG_CMD_2048=y CONFIG_CMD_BMP=y diff --git a/test/dm/Makefile b/test/dm/Makefile index fb3e6a7008f..cc51fd33079 100644 --- a/test/dm/Makefile +++ b/test/dm/Makefile @@ -78,6 +78,7 @@ obj-$(CONFIG_MUX_MMIO) += mux-mmio.o obj-y += fdtdec.o obj-$(CONFIG_MTD_RAW_NAND) += nand.o obj-$(CONFIG_IP_DEFRAG) += net_defrag.o +obj-$(CONFIG_CMD_NFS) += nfs.o obj-$(CONFIG_UT_DM) += nop.o obj-y += ofnode.o obj-y += ofread.o diff --git a/test/dm/nfs.c b/test/dm/nfs.c new file mode 100644 index 00000000000..e18e5812440 --- /dev/null +++ b/test/dm/nfs.c @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Regression tests for the NFS reply-length checks. + */ + +#include +#include +#include +#include +#include "../../net/nfs-common.h" + +static int dm_test_nfs_read_oob(struct unit_test_state *uts) +{ + int saved_state = nfs_state; + unsigned long saved_id = rpc_id; + int saved_offset = nfs_offset; + enum nfs_version saved_version = choosen_nfs_version; + u32 saved_size = net_boot_file_size; + struct rpc_t reply; + + /* Pretend a READ request is outstanding (NFSv3). */ + choosen_nfs_version = NFS_V3; + nfs_state = STATE_READ_REQ; + nfs_offset = 0; + rpc_id = 0x11223344; + net_boot_file_size = 0; + + /* Accepted reply, matching xid, READ status OK, no attributes, then a + * count and data length with the top bit set. + */ + memset(&reply, 0, sizeof(reply)); + reply.u.reply.id = htonl((u32)rpc_id); + reply.u.reply.data[0] = 0; /* nfsstat3: OK */ + reply.u.reply.data[1] = 0; /* attributes_follow: no */ + reply.u.reply.data[2] = htonl(0x80000000); /* count */ + reply.u.reply.data[4] = htonl(0x80000000); /* data length */ + + nfs_pkt_recv((uchar *)&reply.u.reply, sizeof(reply.u.reply)); + + /* Rejected: nothing stored. */ + ut_asserteq(0, net_boot_file_size); + + nfs_state = saved_state; + rpc_id = saved_id; + nfs_offset = saved_offset; + choosen_nfs_version = saved_version; + net_boot_file_size = saved_size; + + return 0; +} +DM_TEST(dm_test_nfs_read_oob, 0); + +static int dm_test_nfs_readlink_oob(struct unit_test_state *uts) +{ + int saved_state = nfs_state; + unsigned long saved_id = rpc_id; + enum nfs_version saved_version = choosen_nfs_version; + char *saved_path = nfs_path; + struct rpc_t reply; + + /* Pretend a READLINK request is outstanding (NFSv3). */ + choosen_nfs_version = NFS_V3; + nfs_state = STATE_READLINK_REQ; + rpc_id = 0x11223344; + nfs_path = nfs_path_buff; + strcpy(nfs_path_buff, "dir"); + + /* Accepted reply, matching xid, READLINK status OK, no attributes, a + * length of -1 that slips past the destination bound as pathlen - 1, + * then a relative (non-'/') target. + */ + memset(&reply, 0, sizeof(reply)); + reply.u.reply.id = htonl((u32)rpc_id); + reply.u.reply.data[0] = 0; /* nfsstat3: OK */ + reply.u.reply.data[1] = 0; /* attributes_follow: no */ + reply.u.reply.data[2] = htonl(0xffffffff); /* symlink length -1 */ + reply.u.reply.data[3] = htonl(0x61616161); /* target, not '/' */ + + nfs_pkt_recv((uchar *)&reply.u.reply, sizeof(reply.u.reply)); + + /* Rejected: the path buffer is untouched. */ + ut_asserteq_str("dir", nfs_path_buff); + + nfs_state = saved_state; + rpc_id = saved_id; + choosen_nfs_version = saved_version; + nfs_path = saved_path; + + return 0; +} +DM_TEST(dm_test_nfs_readlink_oob, 0); -- cgit v1.2.3