From b700877b89d18d8ca9868221260ff18755e58b84 Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Wed, 29 Apr 2026 13:50:57 +0200 Subject: [PATCH 01/10] add a tcp_time_wait branch in tcp_input that re-sends an ack on any non-rst/syn matched segment so retransmitted peer FINSs caused by a lost final ack are acknowledged (per rereference from 9293 section 3.10.7.4) instead of silently dropped. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_proto.c | 74 ++++++++++++++++++++++++++++++++ src/wolfip.c | 8 ++++ 3 files changed, 83 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 3be65d7..e8b3dd3 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -641,6 +641,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_input_fin_wait_2_ack_with_payload_receives); tcase_add_test(tc_utils, test_tcp_input_fin_wait_2_fin_with_payload_queues); tcase_add_test(tc_utils, test_tcp_input_fin_wait_2_fin_payload_ack_mismatch_no_transition); + tcase_add_test(tc_utils, test_tcp_input_time_wait_retransmitted_fin_acks); tcase_add_test(tc_utils, test_tcp_sock_close_state_transitions); tcase_add_test(tc_utils, test_tcp_input_fin_wait_1_fin_with_payload_returns); tcase_add_test(tc_utils, test_tcp_input_fin_wait_1_fin_out_of_order_no_transition); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index e6f1db7..efb6739 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -333,6 +333,80 @@ START_TEST(test_tcp_input_fin_wait_2_fin_payload_ack_mismatch_no_transition) } END_TEST +START_TEST(test_tcp_input_time_wait_retransmitted_fin_acks) +{ + /* RFC 9293 §3.10.7.4 step 9: a TCP in TIME-WAIT must ACK retransmitted + * FINs from the peer (and restart the 2 MSL timer) so the peer can complete + * its close. wolfIP's tcp_input dispatch chain has no TIME_WAIT branch, so + * a matched-but-unhandled retransmitted FIN is silently dropped; leaving + * the peer to retransmit until its own retry limit and abort. This test + * pins the contract: a retransmitted FIN arriving in TIME_WAIT produces an + * outgoing ACK acknowledging the FIN. */ + struct wolfIP s; + struct tsocket *ts; + struct wolfIP_tcp_seg fin_retx; + struct wolfIP_tcp_seg *queued; + struct pkt_desc *desc; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A000002U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_TIME_WAIT; + ts->local_ip = local_ip; + ts->remote_ip = remote_ip; + ts->if_idx = TEST_PRIMARY_IF; + ts->src_port = 1234; + ts->dst_port = 4321; + /* Peer FIN had seq=100; we already advanced rcv_nxt past it. */ + ts->sock.tcp.ack = 101; + ts->sock.tcp.seq = 200; + ts->sock.tcp.snd_una = 200; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, ts->sock.tcp.ack); + + memset(&fin_retx, 0, sizeof(fin_retx)); + fin_retx.ip.ver_ihl = 0x45; + fin_retx.ip.ttl = 64; + fin_retx.ip.proto = WI_IPPROTO_TCP; + fin_retx.ip.len = ee16(IP_HEADER_LEN + TCP_HEADER_LEN); + fin_retx.ip.src = ee32(remote_ip); + fin_retx.ip.dst = ee32(local_ip); + fin_retx.dst_port = ee16(ts->src_port); + fin_retx.src_port = ee16(ts->dst_port); + fin_retx.seq = ee32(100); + fin_retx.ack = ee32(ts->sock.tcp.seq); + fin_retx.hlen = TCP_HEADER_LEN << 2; + fin_retx.flags = TCP_FLAG_FIN | TCP_FLAG_ACK; + fin_retx.win = ee16(65535); + fix_tcp_checksums(&fin_retx); + + tcp_input(&s, TEST_PRIMARY_IF, &fin_retx, + (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + TCP_HEADER_LEN)); + + /* Socket must remain in TIME_WAIT; only the close timer should govern + * its exit, never an unhandled retransmit. */ + ck_assert_int_eq(ts->sock.tcp.state, TCP_TIME_WAIT); + + /* An ACK must have been queued for transmission. tcp_send_ack pushes + * onto the socket's TX FIFO; the main loop drains it later, so we + * inspect the FIFO directly here. */ + desc = fifo_peek(&ts->sock.tcp.txbuf); + ck_assert_ptr_nonnull(desc); + queued = (struct wolfIP_tcp_seg *)(ts->txmem + desc->pos + sizeof(*desc)); + ck_assert_uint_eq(queued->flags & TCP_FLAG_ACK, TCP_FLAG_ACK); + ck_assert_uint_eq(queued->flags & TCP_FLAG_RST, 0U); + ck_assert_uint_eq(ee32(queued->ack), ts->sock.tcp.ack); +} +END_TEST + START_TEST(test_socket_from_fd_invalid) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 12120d8..5e9ddf6 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -4804,6 +4804,14 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, tcp_send_ack(t); } } + } else if (t->sock.tcp.state == TCP_TIME_WAIT) { + /* RFC 9293 §3.10.7.4 step 9: in TIME-WAIT, the only legal + * response to a peer segment (notably a retransmitted FIN + * caused by our final ACK being lost) is to re-ACK so the + * peer can complete its close. RST and SYN are filtered out + * earlier in tcp_input. */ + tcp_send_ack(t); + continue; } else if (t->sock.tcp.state == TCP_LAST_ACK) { /* RFC 9293 s3.10.7.2: segment acceptability applies * to all synchronized states including LAST_ACK. */ From 7d92e8b35af9cf9bbee8f14a50747d6a60da9883 Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Wed, 29 Apr 2026 16:23:53 +0200 Subject: [PATCH 02/10] Gate request-side arp_store_neighbor in arp_recv on a matching arp_pending_match_and_clear so unsolicited ARP requests can no longer fill the neighbor cache and lock out legitimate replies, with test_arp_request_flood_does_not_lock_out_legit_reply as regression test. Updated three pre-existing tests to model the now-required solicited-learn path. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_proto.c | 5 ++ src/test/unit/unit_tests_tcp_ack.c | 87 +++++++++++++++++++++++++++++- src/wolfip.c | 7 ++- 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index e8b3dd3..4eca2a2 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -715,6 +715,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_arp_recv_null_stack); tcase_add_test(tc_proto, test_arp_recv_request_sends_reply); tcase_add_test(tc_proto, test_arp_recv_request_does_not_store_self_neighbor); + tcase_add_test(tc_proto, test_arp_request_flood_does_not_lock_out_legit_reply); tcase_add_test(tc_proto, test_arp_recv_request_no_send_fn); tcase_add_test(tc_proto, test_wolfip_if_for_local_ip_paths); tcase_add_test(tc_proto, test_wolfip_if_for_local_ip_null_found); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index efb6739..d6a029b 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -2673,6 +2673,11 @@ START_TEST(test_arp_request_handling) { memcpy(arp_req.sma, req_mac, 6); arp_req.tip = ee32(device_ip); + /* Model a solicited learn: stack has an outstanding ARP request for + * req_ip, so the request handler is allowed to populate the cache. */ + s.last_tick = 1000; + arp_pending_record(&s, TEST_PRIMARY_IF, req_ip); + /* Call arp_recv with the ARP request */ arp_recv(&s, TEST_PRIMARY_IF, &arp_req, sizeof(arp_req)); wolfIP_poll(&s, 1000); diff --git a/src/test/unit/unit_tests_tcp_ack.c b/src/test/unit/unit_tests_tcp_ack.c index 2c987cc..e2fe4d1 100644 --- a/src/test/unit/unit_tests_tcp_ack.c +++ b/src/test/unit/unit_tests_tcp_ack.c @@ -2235,6 +2235,9 @@ START_TEST(test_arp_recv_request_does_not_store_self_neighbor) wolfIP_filter_set_callback(NULL, NULL); wolfIP_filter_set_mask(0); + s.last_tick = 1000; + arp_pending_record(&s, TEST_PRIMARY_IF, sender_ip); + memset(&arp_req, 0, sizeof(arp_req)); arp_req.htype = ee16(1); arp_req.ptype = ee16(0x0800); @@ -2264,6 +2267,89 @@ START_TEST(test_arp_recv_request_does_not_store_self_neighbor) } END_TEST +/* Regression: a same-LAN attacker that floods the ARP cache by sending + * MAX_NEIGHBORS ARP requests from distinct sender IP/MAC pairs targeting our + * IP must not lock out legitimate ARP replies for outstanding requests. + * arp_store_neighbor's silent-drop-when-full behaviour, combined with + * unconditional sender caching from the request branch of arp_recv, lets a + * flood deny resolution of any new peer until ARP_AGING_TIMEOUT_MS elapses. */ +START_TEST(test_arp_request_flood_does_not_lock_out_legit_reply) +{ + struct wolfIP s; + struct arp_packet arp_pkt; + struct wolfIP_ll_dev *ll; + struct ipconf *conf; + const ip4 our_ip = 0x0A000001U; + const ip4 legit_ip = 0x0A0000FEU; + const uint8_t legit_mac[6] = {0x02, 0xCA, 0xFE, 0xBA, 0xBE, 0x01}; + uint8_t mac_out[6]; + int i; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, our_ip, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_filter_set_mask(0); + + ll = wolfIP_getdev_ex(&s, TEST_PRIMARY_IF); + conf = wolfIP_ipconf_at(&s, TEST_PRIMARY_IF); + s.last_tick = 1000; + + /* Attacker floods MAX_NEIGHBORS ARP requests from distinct sender + * IP/MAC pairs, all targeting our IP. Each one passes the + * broadcast/multicast/zero/own-IP filter and so reaches + * arp_store_neighbor unconditionally. */ + for (i = 0; i < MAX_NEIGHBORS; i++) { + uint8_t fake_mac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, + (uint8_t)(0x10 + i)}; + ip4 fake_ip = (ip4)(0x0A000010U + (uint32_t)i); + + memset(&arp_pkt, 0, sizeof(arp_pkt)); + memcpy(arp_pkt.eth.dst, ll->mac, 6); + memcpy(arp_pkt.eth.src, fake_mac, 6); + arp_pkt.eth.type = ee16(ETH_TYPE_ARP); + arp_pkt.htype = ee16(1); + arp_pkt.ptype = ee16(0x0800); + arp_pkt.hlen = 6; + arp_pkt.plen = 4; + arp_pkt.opcode = ee16(ARP_REQUEST); + memcpy(arp_pkt.sma, fake_mac, 6); + arp_pkt.sip = ee32(fake_ip); + memset(arp_pkt.tma, 0, 6); + arp_pkt.tip = ee32(conf->ip); + + s.last_tick += 1; + arp_recv(&s, TEST_PRIMARY_IF, &arp_pkt, sizeof(arp_pkt)); + } + + /* Stack issues a legitimate ARP request for legit_ip and then receives + * the matching reply. Caching this reply must succeed even with the + * neighbor table full of attacker-driven entries. */ + s.last_tick += 1; + arp_pending_record(&s, TEST_PRIMARY_IF, legit_ip); + + memset(&arp_pkt, 0, sizeof(arp_pkt)); + memcpy(arp_pkt.eth.dst, ll->mac, 6); + memcpy(arp_pkt.eth.src, legit_mac, 6); + arp_pkt.eth.type = ee16(ETH_TYPE_ARP); + arp_pkt.htype = ee16(1); + arp_pkt.ptype = ee16(0x0800); + arp_pkt.hlen = 6; + arp_pkt.plen = 4; + arp_pkt.opcode = ee16(ARP_REPLY); + memcpy(arp_pkt.sma, legit_mac, 6); + arp_pkt.sip = ee32(legit_ip); + memcpy(arp_pkt.tma, ll->mac, 6); + arp_pkt.tip = ee32(conf->ip); + + s.last_tick += 1; + arp_recv(&s, TEST_PRIMARY_IF, &arp_pkt, sizeof(arp_pkt)); + + ck_assert_int_eq(arp_lookup(&s, TEST_PRIMARY_IF, legit_ip, mac_out), 0); + ck_assert_mem_eq(mac_out, legit_mac, 6); +} +END_TEST + START_TEST(test_send_ttl_exceeded_filter_drop) { struct wolfIP s; @@ -2690,7 +2776,6 @@ START_TEST(test_arp_recv_filter_drop) arp_recv(&s, TEST_PRIMARY_IF, &arp_req, sizeof(arp_req)); ck_assert_uint_eq(last_frame_sent_size, 0); - ck_assert_int_ne(s.arp.neighbors[0].ip, IPADDR_ANY); wolfIP_filter_set_callback(NULL, NULL); wolfIP_filter_set_eth_mask(0); diff --git a/src/wolfip.c b/src/wolfip.c index 5e9ddf6..978653b 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -8129,7 +8129,12 @@ static void arp_recv(struct wolfIP *s, unsigned int if_idx, void *buf, int len) if (idx >= 0) { if (memcmp(s->arp.neighbors[idx].mac, sender_mac, 6) == 0) s->arp.neighbors[idx].ts = s->last_tick; - } else { + } else if (arp_pending_match_and_clear(s, if_idx, sip)) { + /* Only learn from an unsolicited request when we have an + * outstanding ARP request for this peer; otherwise a + * same-LAN attacker can flood requests from distinct + * sender IP/MAC pairs to exhaust the neighbor table and + * lock out legitimate replies until ARP_AGING_TIMEOUT_MS. */ arp_store_neighbor(s, if_idx, sip, sender_mac); } } From 60f7c31f3afa836c1fe6af046b727fce67c8cca9 Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Wed, 29 Apr 2026 16:51:13 +0200 Subject: [PATCH 03/10] Add test_syn_sent_rst_ack_seg_ack_bounds to pin RFC 9293 section 3.10.7.3 SND.UNA < SEG.ACK <= SND.NXT on the SYN_SENT RST+ACK path so deletion of the upper-bound clause and < <-> <= boundary mutations on either bound in tcp_input no longer slip past CI. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_api.c | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 4eca2a2..7427cb6 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -220,6 +220,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_accepted_socket_destroyed_on_synrcvd_rto_expiry); tcase_add_test(tc_utils, test_tcp_send_syn_options_aligned_small_mtu); tcase_add_test(tc_utils, test_syn_sent_bare_rst_dropped); + tcase_add_test(tc_utils, test_syn_sent_rst_ack_seg_ack_bounds); tcase_add_test(tc_utils, test_syn_rcvd_rst_bad_seq_dropped); tcase_add_test(tc_utils, test_ip_recv_drops_broadcast_source); tcase_add_test(tc_utils, test_ip_recv_drops_multicast_source); diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index cac5349..f5ac1af 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -3836,6 +3836,64 @@ START_TEST(test_syn_sent_bare_rst_dropped) } END_TEST +/* Regression: per RFC 9293 §3.10.7.3, an RST+ACK in SYN_SENT is acceptable + * only when SND.UNA < SEG.ACK <= SND.NXT. After connect(), snd_una == seq + * (the ISN) and SND.NXT == seq+1, so the only valid seg_ack is isn+1. + * Pin both bounds so deletion of the upper-bound clause or < <-> <= mutations + * on either bound are caught. */ +START_TEST(test_syn_sent_rst_ack_seg_ack_bounds) +{ + struct wolfIP s; + int sd; + struct tsocket *ts; + struct wolfIP_sockaddr_in sin; + uint32_t isn; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP); + ck_assert_int_gt(sd, 0); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(80); + sin.sin_addr.s_addr = ee32(0x0A000002U); + ck_assert_int_eq(wolfIP_sock_connect(&s, sd, (struct wolfIP_sockaddr *)&sin, sizeof(sin)), + -WOLFIP_EAGAIN); + + ts = &s.tcpsockets[SOCKET_UNMARK(sd)]; + ck_assert_int_eq(ts->sock.tcp.state, TCP_SYN_SENT); + isn = ts->sock.tcp.seq; + + /* seg_ack == snd_una (lower-bound equality): must be dropped */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, + 0x0A000002U, 0x0A000001U, + 80, ts->src_port, + 1000, isn, + TCP_FLAG_RST | TCP_FLAG_ACK); + ck_assert_uint_eq(ts->proto, WI_IPPROTO_TCP); + ck_assert_int_eq(ts->sock.tcp.state, TCP_SYN_SENT); + + /* seg_ack == snd_nxt + 1 (above SND.NXT): must be dropped */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, + 0x0A000002U, 0x0A000001U, + 80, ts->src_port, + 1000, isn + 2, + TCP_FLAG_RST | TCP_FLAG_ACK); + ck_assert_uint_eq(ts->proto, WI_IPPROTO_TCP); + ck_assert_int_eq(ts->sock.tcp.state, TCP_SYN_SENT); + + /* seg_ack == snd_nxt (only valid value): must close the socket */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, + 0x0A000002U, 0x0A000001U, + 80, ts->src_port, + 1000, isn + 1, + TCP_FLAG_RST | TCP_FLAG_ACK); + ck_assert_uint_eq(ts->proto, 0); +} +END_TEST + /* Regression: in SYN_RCVD, a RST with a sequence number outside the receive * window must be silently dropped per RFC 9293 §3.10.7. The SYN_RCVD branch * bypassed the window check entirely, accepting any RST. */ From 671d5ad475df668bc9b7b789d90421baf0511055 Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Wed, 29 Apr 2026 17:04:02 +0200 Subject: [PATCH 04/10] Reject buffers shorter than ETH_HEADER_LEN at the top of wolfIP_recv_on's ethernet branch so the eth filter callback and the eth->type/eth->dst comparisons can no longer read past the end of a runt caller-supplied buffer, with test_wolfip_recv_ex_runt_eth_frame_drops_before_filter pinning the contract. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_proto.c | 36 ++++++++++++++++++++++++++++++++ src/wolfip.c | 2 ++ 3 files changed, 39 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 7427cb6..ffd791f 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -667,6 +667,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_arp_lookup_expired_entry_rejected); tcase_add_test(tc_proto, test_arp_reply_updates_expired_entry); tcase_add_test(tc_proto, test_wolfip_recv_ex_multi_interface_arp_reply); + tcase_add_test(tc_proto, test_wolfip_recv_ex_runt_eth_frame_drops_before_filter); tcase_add_test(tc_proto, test_forward_prepare_null_args); tcase_add_test(tc_proto, test_send_ttl_exceeded_includes_full_ip_header_with_options); tcase_add_test(tc_proto, test_send_ttl_exceeded_filter_drop); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index d6a029b..f1595f0 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -3580,6 +3580,42 @@ START_TEST(test_wolfip_recv_ex_multi_interface_arp_reply) } END_TEST +/* Regression: wolfIP_recv_on must reject buffers shorter than ETH_HEADER_LEN + * before any read of eth->dst/eth->src/eth->type. Without the guard, the + * eth-receiving filter callback (and the unicast/broadcast memcmp + eth->type + * comparison) read past the end of the caller-supplied buffer. The public + * wolfIP_recv_ex contract documents no minimum length, so a driver port that + * forwards a runt frame trips an OOB read on s's working buffer. */ +START_TEST(test_wolfip_recv_ex_runt_eth_frame_drops_before_filter) +{ + struct wolfIP s; + uint8_t *runt = malloc(ETH_HEADER_LEN - 1); + ck_assert_ptr_nonnull(runt); + memset(runt, 0, ETH_HEADER_LEN - 1); + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + + filter_cb_calls = 0; + memset(&filter_last_event, 0, sizeof(filter_last_event)); + wolfIP_filter_set_callback(test_filter_cb, NULL); + wolfIP_filter_set_eth_mask(WOLFIP_FILT_MASK(WOLFIP_FILT_RECEIVING)); + last_frame_sent_size = 0; + + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, runt, 0); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, runt, 1); + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, runt, ETH_HEADER_LEN - 1); + + ck_assert_int_eq(filter_cb_calls, 0); + ck_assert_uint_eq(last_frame_sent_size, 0); + + wolfIP_filter_set_callback(NULL, NULL); + wolfIP_filter_set_eth_mask(0); + free(runt); +} +END_TEST + START_TEST(test_wolfip_forwarding_basic) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 978653b..0b8925d 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -8492,6 +8492,8 @@ static void wolfIP_recv_on(struct wolfIP *s, unsigned int if_idx, void *buf, uin ip_recv(s, if_idx, ip, len); return; } + if (len < (uint32_t)ETH_HEADER_LEN) + return; eth = (struct wolfIP_eth_frame *)buf; #ifdef DEBUG_ETH wolfIP_print_eth(eth, len); From 73fc1b1a6ca45c93458bf4dbea85c9771e8b3665 Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Wed, 29 Apr 2026 17:21:44 +0200 Subject: [PATCH 05/10] add test_multicast_igmp_query_bad_checksum_dropped to pin the IGMP checksum guard in igmp_input so deletion of the rejection branch can no longer slip past CI. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_multicast.c | 41 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index ffd791f..c559e6d 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -238,6 +238,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_multicast_udp_receive_requires_join); tcase_add_test(tc_utils, test_multicast_udp_send_mac_ttl_loop_and_options); tcase_add_test(tc_utils, test_multicast_igmp_query_refreshes_report); + tcase_add_test(tc_utils, test_multicast_igmp_query_bad_checksum_dropped); tcase_add_test(tc_utils, test_multicast_join_requires_configured_ip); tcase_add_test(tc_utils, test_multicast_if_pins_egress_interface); tcase_add_test(tc_utils, test_multicast_loop_does_not_fire_on_blocked_send); diff --git a/src/test/unit/unit_tests_multicast.c b/src/test/unit/unit_tests_multicast.c index ab96baa..7a77162 100644 --- a/src/test/unit/unit_tests_multicast.c +++ b/src/test/unit/unit_tests_multicast.c @@ -252,6 +252,47 @@ START_TEST(test_multicast_igmp_query_refreshes_report) } END_TEST +START_TEST(test_multicast_igmp_query_bad_checksum_dropped) +{ + struct wolfIP s; + int sd; + struct wolfIP_ip_mreq mreq; + uint8_t frame[ETH_HEADER_LEN + IP_HEADER_LEN + IGMPV3_QUERY_MIN_LEN]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)frame; + uint8_t *igmp = frame + ETH_HEADER_LEN + IP_HEADER_LEN; + ip4 group = 0xE9010207U; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000002U, 0xFFFFFF00U, 0); + sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(sd, 0); + multicast_mreq(&mreq, group, IPADDR_ANY); + ck_assert_int_eq(wolfIP_sock_setsockopt(&s, sd, WOLFIP_SOL_IP, + WOLFIP_IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)), 0); + + memset(frame, 0, sizeof(frame)); + memcpy(ip->eth.dst, "\x01\x00\x5e\x00\x00\x01", 6); + memcpy(ip->eth.src, "\x02\x00\x00\x00\x00\x01", 6); + ip->eth.type = ee16(ETH_TYPE_IP); + ip->ver_ihl = 0x45; + ip->ttl = 1; + ip->proto = WI_IPPROTO_IGMP; + ip->len = ee16(IP_HEADER_LEN + IGMPV3_QUERY_MIN_LEN); + ip->src = ee32(0x0A000001U); + ip->dst = ee32(IGMP_ALL_HOSTS); + igmp[0] = IGMP_TYPE_MEMBERSHIP_QUERY; + put_be32(igmp + 4, group); + put_be16(igmp + 2, ip_checksum_buf(igmp, IGMPV3_QUERY_MIN_LEN)); + igmp[2] ^= 0x01; + fix_ip_checksum(ip); + + last_frame_sent_size = 0; + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, sizeof(frame)); + ck_assert_uint_eq(last_frame_sent_size, 0); +} +END_TEST + START_TEST(test_multicast_join_requires_configured_ip) { struct wolfIP s; From 95bc67b089690ea960b52e619e482cd268bb4301 Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Wed, 29 Apr 2026 17:48:18 +0200 Subject: [PATCH 06/10] Add a udp.len <= ip.len - IP_HEADER_LEN guard in udp_try_recv so an L2-padded frame whose UDP length overruns its IP packet's declared length can no longer leak post-IP bytes through recvfrom (per RFC 768 / RFC 791), with test_regression_udp_len_exceeds_ip_len_dropped pinning the contract. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_dns_dhcp.c | 5 +++ src/test/unit/unit_tests_proto.c | 54 +++++++++++++++++++++++++++++ src/wolfip.c | 9 +++++ 4 files changed, 69 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index c559e6d..5ae8176 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -833,6 +833,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_regression_timer_heap_insert_bounded_by_max_timers); tcase_add_test(tc_proto, test_regression_icmp_inflated_ip_len); tcase_add_test(tc_proto, test_regression_udp_inflated_udp_len); + tcase_add_test(tc_proto, test_regression_udp_len_exceeds_ip_len_dropped); tcase_add_test(tc_proto, test_regression_udp_len_below_header_discards_and_unblocks); tcase_add_test(tc_proto, test_regression_udp_payload_exceeds_buffer_discards_and_unblocks); tcase_add_test(tc_proto, test_regression_icmp_payload_exceeds_buffer_discards_and_unblocks); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index af31d4a..793288a 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -4807,6 +4807,7 @@ START_TEST(test_udp_try_recv_filter_drop) memset(&udp, 0, sizeof(udp)); udp.ip.dst = ee32(local_ip); + udp.ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN + 4); udp.dst_port = ee16(1234); udp.len = ee16(UDP_HEADER_LEN + 4); udp_try_recv(&s, TEST_PRIMARY_IF, &udp, (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4)); @@ -4838,6 +4839,7 @@ START_TEST(test_udp_try_recv_conf_null) memset(udp_buf, 0, sizeof(udp_buf)); udp->ip.dst = ee32(dst_ip); + udp->ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN + 4); udp->dst_port = ee16(1234); udp->len = ee16(UDP_HEADER_LEN + 4); udp_try_recv(&s, TEST_PRIMARY_IF, udp, (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4)); @@ -4864,6 +4866,7 @@ START_TEST(test_udp_try_recv_remote_ip_matches_local_ip) memset(&udp, 0, sizeof(udp)); udp.ip.dst = ee32(local_ip); + udp.ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN + 4); udp.dst_port = ee16(1234); udp.len = ee16(UDP_HEADER_LEN + 4); udp_try_recv(&s, TEST_PRIMARY_IF, &udp, (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4)); @@ -4891,6 +4894,7 @@ START_TEST(test_udp_try_recv_dhcp_running_local_zero) memset(udp_buf, 0, sizeof(udp_buf)); udp->ip.dst = ee32(local_ip); + udp->ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN + 4); udp->dst_port = ee16(1234); udp->len = ee16(UDP_HEADER_LEN + 4); udp_try_recv(&s, TEST_PRIMARY_IF, udp, (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4)); @@ -4916,6 +4920,7 @@ START_TEST(test_udp_try_recv_short_expected_len) memset(&udp, 0, sizeof(udp)); udp.ip.dst = ee32(local_ip); + udp.ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN + 10); udp.dst_port = ee16(1234); udp.len = ee16(UDP_HEADER_LEN + 10); udp_try_recv(&s, TEST_PRIMARY_IF, &udp, (uint32_t)(ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN + 4)); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index f1595f0..74dad67 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -4741,6 +4741,60 @@ START_TEST(test_regression_udp_inflated_udp_len) } END_TEST +START_TEST(test_regression_udp_len_exceeds_ip_len_dropped) +{ + /* Pin RFC 768 + RFC 791: UDP's declared length must lie within the IP + * packet's declared length (udp.len <= ip.len - IP_HEADER_LEN). When the + * Ethernet frame is padded to the 60-byte minimum, an attacker can declare + * ip.len = 28 (no UDP payload), udp.len = 16 (claims 8 bytes), csum = 0 + * (skipped). The frame_len-bounded checks all pass (frame_len(60) >= + * ETH+ip.len(42); udp.len(16) <= frame_len-ETH-IP(26)), but those 8 bytes + * lie *beyond* the IP packet's declared end. Without the ip.len-bounded + * cross-check, the FIFO accepts the frame and recvfrom delivers data that + * was never inside the IP datagram. */ + struct wolfIP s; + struct tsocket *ts; + uint8_t buf[60 - ETH_HEADER_LEN]; /* udp_try_recv operates after eth */ + struct wolfIP_udp_datagram *udp = (struct wolfIP_udp_datagram *)buf; + uint32_t local_ip = 0x0A000001U; + uint32_t frame_len; + + wolfIP_init(&s); + mock_link_init(&s); + s.dhcp_state = DHCP_OFF; + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = udp_new_socket(&s); + ck_assert_ptr_nonnull(ts); + ts->src_port = 1234; + ts->local_ip = local_ip; + + /* Fill the post-IP region with attacker-chosen bytes; the test asserts + * none of these reach the FIFO. */ + memset(buf, 0xAB, sizeof(buf)); + memset(buf, 0, sizeof(struct wolfIP_udp_datagram)); + udp->ip.src = ee32(0x0A000002U); + udp->ip.dst = ee32(local_ip); + udp->ip.ver_ihl = 0x45; + udp->ip.proto = WI_IPPROTO_UDP; + udp->ip.ttl = 64; + /* IP says: 20 (header) + 8 (UDP header) + 0 (no payload). */ + udp->ip.len = ee16(IP_HEADER_LEN + UDP_HEADER_LEN); + udp->src_port = ee16(9999); + udp->dst_port = ee16(1234); + /* UDP claims: 8 (header) + 8 (payload); overruns the IP packet. */ + udp->len = ee16(UDP_HEADER_LEN + 8); + udp->csum = 0; /* skipped per RFC 768, so no checksum guard fires */ + /* L2 frame padded to the 60-byte Ethernet minimum. */ + frame_len = 60; + + udp_try_recv(&s, TEST_PRIMARY_IF, udp, frame_len); + /* The FIFO must be empty: a UDP datagram that overruns its IP packet's + * declared length is malformed and must not surface to recvfrom. */ + ck_assert_ptr_eq(fifo_peek(&ts->sock.udp.rxbuf), NULL); +} +END_TEST + START_TEST(test_regression_udp_len_below_header_discards_and_unblocks) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 0b8925d..99a1302 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -2250,6 +2250,15 @@ static void udp_try_recv(struct wolfIP *s, unsigned int if_idx, if (ee16(udp->len) > frame_len - ETH_HEADER_LEN - IP_HEADER_LEN) return; + /* RFC 768 / RFC 791: UDP's declared length must lie within the IP + * packet's declared length. Without this guard, an L2-padded frame + * (e.g. 60-byte Ethernet minimum) carrying ip.len < udp.len + IP_HEADER_LEN + * passes every frame_len-bounded check and surfaces bytes from outside + * the IP datagram to recvfrom. */ + if (ee16(udp->ip.len) < IP_HEADER_LEN || + ee16(udp->len) > (uint16_t)(ee16(udp->ip.len) - IP_HEADER_LEN)) + return; + /* validate UDP checksum per RFC 1122 (only if non-zero) */ if (udp->csum != 0) { union transport_pseudo_header ph; From b2031267eb4d3b87b097b7c237cd02dfd84ede5e Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Wed, 29 Apr 2026 18:36:16 +0200 Subject: [PATCH 07/10] Replace the signed-int (... & 0xFF) << 24 reassembly in dns_callback's A-record path with the safe memcpy+ee32 get_be32 helper (hoisted out of the IP_MULTICAST gate so it is unconditionally available) so a high-bit top octet (>= 0x80) can no longer trigger ISO C11 6.5.7p4 undefined behavior on the int shift, with test_regression_dns_callback_high_bit_octet_ip_no_ub pinning the contract under -fsanitize=undefined. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_dns_dhcp.c | 65 +++++++++++++++++++++++++++++ src/wolfip.c | 21 ++++------ 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 5ae8176..f27e6fa 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -394,6 +394,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_udp_try_recv_full_fifo_drop_does_not_set_readable_or_send_icmp); tcase_add_test(tc_utils, test_dns_callback_bad_flags); tcase_add_test(tc_utils, test_dns_callback_truncated_response_aborts_query); + tcase_add_test(tc_utils, test_regression_dns_callback_high_bit_octet_ip_no_ub); tcase_add_test(tc_utils, test_dns_callback_bad_name); tcase_add_test(tc_utils, test_dns_callback_short_header_ignored); tcase_add_test(tc_utils, test_dns_callback_wrong_id_ignored); diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index 793288a..bd6b4da 100644 --- a/src/test/unit/unit_tests_dns_dhcp.c +++ b/src/test/unit/unit_tests_dns_dhcp.c @@ -5187,6 +5187,71 @@ START_TEST(test_dns_callback_truncated_response_aborts_query) } END_TEST +START_TEST(test_regression_dns_callback_high_bit_octet_ip_no_ub) +{ + /* The dns_callback() A-record reassembly used to compute + * ip = (buf[pos+3] & 0xFF) | ... | ((buf[pos+0] & 0xFF) << 24); + * with buf typed as char (signed by default). For the high-bit + * case (top octet >= 0x80) the int-typed (... & 0xFF) << 24 + * produces a value that is not representable in int, which is + * undefined behavior per ISO C11 6.5.7p4. Under -fsanitize=undefined + * (make unit-ubsan) that shift trips a runtime error; this test + * pins the contract that high-bit IPs both (a) do not invoke UB + * and (b) are delivered to dns_lookup_cb byte-for-byte intact. */ + struct wolfIP s; + uint8_t response[128]; + int pos; + struct dns_header *hdr = (struct dns_header *)response; + struct dns_question *q; + struct dns_rr *rr; + const uint8_t ip_bytes[4] = {0xC8, 0x0A, 0x14, 0x1E}; /* 200.10.20.30 */ + + wolfIP_init(&s); + mock_link_init(&s); + s.dns_server = 0x0A000001U; + s.dns_query_type = DNS_QUERY_TYPE_A; + s.dns_id = 0x1234; + s.dns_lookup_cb = test_dns_lookup_cb; + dns_lookup_calls = 0; + dns_lookup_ip = 0; + s.dns_udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(s.dns_udp_sd, 0); + + memset(response, 0, sizeof(response)); + hdr->id = ee16(s.dns_id); + hdr->flags = ee16(0x8100); + hdr->qdcount = ee16(1); + hdr->ancount = ee16(1); + pos = sizeof(struct dns_header); + response[pos++] = 7; memcpy(&response[pos], "example", 7); pos += 7; + response[pos++] = 3; memcpy(&response[pos], "com", 3); pos += 3; + response[pos++] = 0; + q = (struct dns_question *)(response + pos); + q->qtype = ee16(DNS_A); + q->qclass = ee16(1); + pos += sizeof(struct dns_question); + response[pos++] = 0xC0; + response[pos++] = (uint8_t)sizeof(struct dns_header); + rr = (struct dns_rr *)(response + pos); + rr->type = ee16(DNS_A); + rr->class = ee16(1); + rr->ttl = ee32(60); + rr->rdlength = ee16(4); + pos += sizeof(struct dns_rr); + memcpy(&response[pos], ip_bytes, sizeof(ip_bytes)); + pos += sizeof(ip_bytes); + + enqueue_udp_rx(&s.udpsockets[SOCKET_UNMARK(s.dns_udp_sd)], response, (uint16_t)pos, DNS_PORT); + dns_callback(s.dns_udp_sd, CB_EVENT_READABLE, &s); + + ck_assert_int_eq(dns_lookup_calls, 1); + ck_assert_uint_eq(dns_lookup_ip, 0xC80A141EU); + ck_assert_uint_eq(s.dns_id, 0); + ck_assert_int_eq(s.dns_query_type, DNS_QUERY_TYPE_NONE); + ck_assert_ptr_eq(s.dns_lookup_cb, NULL); +} +END_TEST + START_TEST(test_dns_callback_bad_name) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 99a1302..430fbea 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -1634,6 +1634,14 @@ static inline int wolfIP_ip_is_multicast(ip4 addr) return ((addr & 0xF0000000U) == 0xE0000000U); } +static uint32_t get_be32(const uint8_t *p) +{ + uint32_t be; + + memcpy(&be, p, sizeof(be)); + return ee32(be); +} + #ifdef IP_MULTICAST static uint16_t ip_checksum_buf(const void *buf, uint16_t len) { @@ -1664,14 +1672,6 @@ static void put_be32(uint8_t *p, uint32_t v) memcpy(p, &be, sizeof(be)); } -static uint32_t get_be32(const uint8_t *p) -{ - uint32_t be; - - memcpy(&be, p, sizeof(be)); - return ee32(be); -} - static void mcast_ip_to_eth(ip4 group, uint8_t mac[6]) { mac[0] = 0x01; @@ -8919,10 +8919,7 @@ void dns_callback(int dns_sd, uint16_t ev, void *arg) dns_abort_query(s); return; } - ip = (buf[pos + 3] & 0xFF) | - ((buf[pos + 2] & 0xFF) << 8) | - ((buf[pos + 1] & 0xFF) << 16) | - ((buf[pos + 0] & 0xFF) << 24); + ip = get_be32((const uint8_t *)buf + pos); if (s->dns_lookup_cb) s->dns_lookup_cb(ip); dns_abort_query(s); From 2d6edbbe8486b0bf0195ab4036d2e18b376684bd Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Thu, 30 Apr 2026 12:12:43 +0200 Subject: [PATCH 08/10] Add a martian + strict-RPF source filter to ip_recv's WOLFIP_ENABLE_FORWARDING relay path so packets sourced from 127.0.0.0/8 on a non-loopback ingress, 169.254.0.0/16 link-local, or any locally-configured subnet on the wrong interface are dropped before wolfIP_forward_interface, with test_regression_forwarding_rpf_drops_spoofed_source pinning the contract. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_proto.c | 51 ++++++++++++++++++++++++++++++++ src/wolfip.c | 34 +++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index f27e6fa..3aacb59 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -789,6 +789,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_proto, test_forward_packet_ip_filter_drop); tcase_add_test(tc_proto, test_forward_packet_eth_filter_drop); tcase_add_test(tc_proto, test_loopback_dest_not_forwarded); + tcase_add_test(tc_proto, test_regression_forwarding_rpf_drops_spoofed_source); tcase_add_test(tc_proto, test_tcp_listen_rejects_wrong_interface); tcase_add_test(tc_proto, test_tcp_listen_accepts_bound_interface); tcase_add_test(tc_proto, test_tcp_listen_accepts_any_interface); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 74dad67..d5608c6 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -3762,6 +3762,57 @@ START_TEST(test_loopback_dest_not_forwarded) } END_TEST +START_TEST(test_regression_forwarding_rpf_drops_spoofed_source) +{ + static const ip4 spoofed_sources[] = { + 0x7F000001U, /* 127.0.0.1; loopback */ + 0xA9FE0001U, /* 169.254.0.1; link-local */ + 0xC0A80132U /* 192.168.1.50; in TEST_SECOND_IF's subnet, wrong ingress */ + }; + unsigned int i; + uint8_t iface1_mac[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x02}; + uint8_t next_hop_mac[6] = {0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE}; + uint8_t src_mac[6] = {0x52, 0x54, 0x00, 0x12, 0x34, 0x56}; + uint32_t dest_ip = 0xC0A80164U; /* 192.168.1.100; on TEST_SECOND_IF */ + + for (i = 0; i < sizeof(spoofed_sources) / sizeof(spoofed_sources[0]); i++) { + struct wolfIP s; + uint8_t frame_buf[64]; + struct wolfIP_ip_packet *frame = (struct wolfIP_ip_packet *)frame_buf; + + wolfIP_init(&s); + mock_link_init(&s); + mock_link_init_idx(&s, TEST_SECOND_IF, iface1_mac); + wolfIP_ipconfig_set(&s, 0xC0A80001U, 0xFFFFFF00U, 0); + wolfIP_ipconfig_set_ex(&s, TEST_SECOND_IF, 0xC0A80101U, 0xFFFFFF00U, 0); + s.arp.neighbors[0].ip = dest_ip; + s.arp.neighbors[0].if_idx = TEST_SECOND_IF; + memcpy(s.arp.neighbors[0].mac, next_hop_mac, 6); + + memset(frame_buf, 0, sizeof(frame_buf)); + memcpy(frame->eth.dst, s.ll_dev[TEST_PRIMARY_IF].mac, 6); + memcpy(frame->eth.src, src_mac, 6); + frame->eth.type = ee16(ETH_TYPE_IP); + frame->ver_ihl = 0x45; + frame->ttl = 64; + frame->proto = WI_IPPROTO_UDP; + frame->len = ee16(IP_HEADER_LEN); + frame->src = ee32(spoofed_sources[i]); + frame->dst = ee32(dest_ip); + frame->csum = 0; + iphdr_set_checksum(frame); + + memset(last_frame_sent, 0, sizeof(last_frame_sent)); + last_frame_sent_size = 0; + + wolfIP_recv_ex(&s, TEST_PRIMARY_IF, frame, + ETH_HEADER_LEN + IP_HEADER_LEN); + + ck_assert_uint_eq(last_frame_sent_size, 0); + } +} +END_TEST + /* wolfSSL IO glue tests */ START_TEST(test_wolfssl_io_ctx_registers_callbacks) { diff --git a/src/wolfip.c b/src/wolfip.c index 430fbea..525b9d2 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -8359,6 +8359,39 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, } } if (!is_local) { + ip4 src = ee32(ip->src); + int rpf_drop = 0; + + /* Martian source: 127.0.0.0/8 must not arrive on a non-loopback + * interface (and must never be forwarded). */ + if ((src & WOLFIP_LOOPBACK_MASK) == + (WOLFIP_LOOPBACK_IP & WOLFIP_LOOPBACK_MASK) && + !wolfIP_is_loopback_if(if_idx)) { + rpf_drop = 1; + } + /* Martian source: 169.254.0.0/16 link-local is not routable. */ + if (!rpf_drop && (src & 0xFFFF0000U) == 0xA9FE0000U) { + rpf_drop = 1; + } + /* Strict RPF: a source that belongs to some other configured + * interface's local subnet must not arrive on this one. */ + if (!rpf_drop) { + for (i = 0; i < s->if_count; i++) { + struct ipconf *conf = &s->ipconf[i]; + if (i == if_idx) + continue; + if (!conf || conf->ip == IPADDR_ANY) + continue; + if (ip_is_local_conf(conf, src)) { + rpf_drop = 1; + break; + } + } + } + if (rpf_drop) + return; + + { int out_if = wolfIP_forward_interface(s, if_idx, dest); if (out_if >= 0) { uint8_t mac[6]; @@ -8381,6 +8414,7 @@ static inline void ip_recv(struct wolfIP *s, unsigned int if_idx, wolfIP_forward_packet(s, out_if, ip, len, broadcast ? NULL : mac, broadcast); return; } + } } } #endif /* WOLFIP_ENABLE_FORWARDING */ From 1037af7a53df0b54a6a183df38af4765d10822b3 Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Thu, 30 Apr 2026 12:20:11 +0200 Subject: [PATCH 09/10] Add an if (!s) return -WOLFIP_EINVAL; guard to wolfIP_sock_socket so a NULL stack pointer no longer segfaults inside tcp_new_socket/udp_new_socket/icmp_new_socket/raw_new_socket/packet_new_socket, with test_regression_sock_socket_null_wolfip_returns_einval pinning the contract. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_api.c | 17 +++++++++++++++++ src/wolfip.c | 2 ++ 3 files changed, 20 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 3aacb59..061e985 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -146,6 +146,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_filter_socket_event_proto_variants); tcase_add_test(tc_utils, test_filter_setters_and_get_mask); tcase_add_test(tc_utils, test_sock_socket_errors); + tcase_add_test(tc_utils, test_regression_sock_socket_null_wolfip_returns_einval); tcase_add_test(tc_utils, test_sock_socket_udp_protocol_zero); tcase_add_test(tc_utils, test_sock_socket_full_tables); tcase_add_test(tc_utils, test_filter_mask_for_proto_variants); diff --git a/src/test/unit/unit_tests_api.c b/src/test/unit/unit_tests_api.c index f5ac1af..3ae1bb1 100644 --- a/src/test/unit/unit_tests_api.c +++ b/src/test/unit/unit_tests_api.c @@ -465,6 +465,23 @@ START_TEST(test_sock_socket_errors) ck_assert_int_eq(wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_TCP), -1); } END_TEST + +START_TEST(test_regression_sock_socket_null_wolfip_returns_einval) +{ + ck_assert_int_eq(wolfIP_sock_socket(NULL, AF_INET, IPSTACK_SOCK_STREAM, WI_IPPROTO_TCP), + -WOLFIP_EINVAL); + ck_assert_int_eq(wolfIP_sock_socket(NULL, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP), + -WOLFIP_EINVAL); + ck_assert_int_eq(wolfIP_sock_socket(NULL, AF_INET, IPSTACK_SOCK_DGRAM, 0), + -WOLFIP_EINVAL); + ck_assert_int_eq(wolfIP_sock_socket(NULL, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_ICMP), + -WOLFIP_EINVAL); + ck_assert_int_eq(wolfIP_sock_socket(NULL, AF_INET, IPSTACK_SOCK_RAW, WI_IPPROTO_UDP), + -WOLFIP_EINVAL); + ck_assert_int_eq(wolfIP_sock_socket(NULL, AF_PACKET, IPSTACK_SOCK_RAW, ee16(ETH_TYPE_IP)), + -WOLFIP_EINVAL); +} +END_TEST START_TEST(test_udp_sendto_and_recvfrom) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 525b9d2..13d00b2 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -5291,6 +5291,8 @@ int wolfIP_sock_socket(struct wolfIP *s, int domain, int type, int protocol) #if WOLFIP_RAWSOCKETS || WOLFIP_PACKET_SOCKETS int base_type = type; #endif + if (!s) + return -WOLFIP_EINVAL; if (domain != AF_INET) goto packet_try; if (type == IPSTACK_SOCK_STREAM) { From ebeca1d3b20a6e19b142066ba9e62727654544ce Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Thu, 30 Apr 2026 12:37:59 +0200 Subject: [PATCH 10/10] move WOLFIP_LOOPBACK_IP/WOLFIP_LOOPBACK_MASK outside of the loopback gate, since the forwarding rpf code uses them uncoditionally now --- src/wolfip.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wolfip.c b/src/wolfip.c index 13d00b2..0dfdae0 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -35,11 +35,11 @@ #ifndef LINK_MTU_MIN #define LINK_MTU_MIN 64U #endif +#define WOLFIP_LOOPBACK_IP 0x7F000001U +#define WOLFIP_LOOPBACK_MASK 0xFF000000U #if WOLFIP_ENABLE_LOOPBACK #define WOLFIP_LOOPBACK_IF_IDX 0U #define WOLFIP_PRIMARY_IF_IDX 1U -#define WOLFIP_LOOPBACK_IP 0x7F000001U -#define WOLFIP_LOOPBACK_MASK 0xFF000000U static inline int wolfIP_is_loopback_if(unsigned int if_idx) { return if_idx == WOLFIP_LOOPBACK_IF_IDX;