diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 3be65d7..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); @@ -220,6 +221,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); @@ -237,6 +239,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); @@ -392,6 +395,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); @@ -641,6 +645,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); @@ -665,6 +670,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); @@ -714,6 +720,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); @@ -783,6 +790,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); @@ -828,6 +836,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_api.c b/src/test/unit/unit_tests_api.c index cac5349..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; @@ -3836,6 +3853,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. */ diff --git a/src/test/unit/unit_tests_dns_dhcp.c b/src/test/unit/unit_tests_dns_dhcp.c index af31d4a..bd6b4da 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)); @@ -5182,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/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; diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index e6f1db7..d5608c6 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; @@ -2599,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); @@ -3501,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; @@ -3647,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) { @@ -4626,6 +4792,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/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 12120d8..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; @@ -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; @@ -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; @@ -4804,6 +4813,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. */ @@ -5274,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) { @@ -8121,7 +8140,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); } } @@ -8337,6 +8361,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]; @@ -8359,6 +8416,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 */ @@ -8479,6 +8537,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); @@ -8895,10 +8955,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);