From 7fe6bacd584602eead298c3c0abf3d33d579bcf6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 07:51:18 +0200 Subject: [PATCH 01/15] F-5792: wake non-loopback UDP senders after wolfIP_poll txbuf drain wolfIP_poll's UDP TX drain loop popped descriptors from the udp.txbuf but never raised CB_EVENT_WRITABLE. wolfIP_notify_loopback_space_available() only covers loopback-interface sockets, so on a non-loopback UDP socket a sender that filled the txbuf (wolfIP_sock_sendto returns -WOLFIP_EAGAIN) and blocked waiting for CB_EVENT_WRITABLE was never woken once poll drained the queue (e.g. the FreeRTOS BSD shim sendto() blocking on xSemaphoreTake) -- a permanent deadlock. Set CB_EVENT_WRITABLE when the drain frees txbuf space, mirroring the loopback notify and the TCP drain path. Add a unit test that fills a non-loopback UDP txbuf to EAGAIN and asserts the bit is raised after poll. The ICMP drain loop has the same structural omission but is out of scope for this finding. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_poll_dispatcher.c | 50 ++++++++++++++++++++++ src/wolfip.c | 8 ++++ 3 files changed, 59 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 71d3d134..4ea43b59 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1283,6 +1283,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_poll_tx_udp_sends_on_arp_hit); tcase_add_test(tc_core, test_poll_tx_udp_filter_ip_blocks_send); tcase_add_test(tc_core, test_poll_tx_udp_eagain_retains_queue); + tcase_add_test(tc_core, test_poll_tx_udp_drain_sets_writable); tcase_add_test(tc_core, test_poll_tx_udp_broadcast_sets_ff_mac); tcase_add_test(tc_core, test_poll_tx_udp_loopback_path_no_crash); tcase_add_test(tc_core, test_poll_tx_icmp_sends_on_arp_hit); diff --git a/src/test/unit/unit_tests_poll_dispatcher.c b/src/test/unit/unit_tests_poll_dispatcher.c index 13769e52..dc169bd8 100644 --- a/src/test/unit/unit_tests_poll_dispatcher.c +++ b/src/test/unit/unit_tests_poll_dispatcher.c @@ -865,6 +865,56 @@ START_TEST(test_poll_tx_udp_eagain_retains_queue) } END_TEST +START_TEST(test_poll_tx_udp_drain_sets_writable) +{ + struct wolfIP s; + int udp_sd; + struct tsocket *ts; + struct wolfIP_sockaddr_in sin; + uint8_t payload[1400]; + uint8_t peer_mac[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x99}; + int rc; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, 0x0A000001U, 0xFFFFFF00U, 0); + wolfIP_filter_set_callback(NULL, NULL); + + s.arp.neighbors[0].ip = 0x0A000002U; + s.arp.neighbors[0].if_idx = TEST_PRIMARY_IF; + memcpy(s.arp.neighbors[0].mac, peer_mac, 6); + + memset(payload, 0xAB, sizeof(payload)); + udp_sd = wolfIP_sock_socket(&s, AF_INET, IPSTACK_SOCK_DGRAM, WI_IPPROTO_UDP); + ck_assert_int_gt(udp_sd, 0); + ts = &s.udpsockets[SOCKET_UNMARK(udp_sd)]; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = ee16(5004); + sin.sin_addr.s_addr = ee32(0x0A000002U); + + /* Fill the UDP txbuf until sendto() reports the buffer full. */ + do { + rc = wolfIP_sock_sendto(&s, udp_sd, payload, sizeof(payload), 0, + (struct wolfIP_sockaddr *)&sin, sizeof(sin)); + } while (rc > 0); + ck_assert_int_eq(rc, -WOLFIP_EAGAIN); + ts->if_idx = TEST_PRIMARY_IF; + + /* A blocked sendto() would now be waiting for CB_EVENT_WRITABLE. */ + ts->events = 0; + + /* Poll drains the queue over the wire (mock_send succeeds). */ + (void)wolfIP_poll(&s, 200); + + /* Draining freed txbuf space, so the drain must raise CB_EVENT_WRITABLE to + * wake a blocked sender. Before the fix this bit was never set for + * non-loopback UDP sockets and the sender deadlocked. */ + ck_assert_uint_ne((unsigned)(ts->events & CB_EVENT_WRITABLE), 0U); +} +END_TEST + START_TEST(test_poll_tx_udp_broadcast_sets_ff_mac) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index df45e0e9..70a226aa 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -10063,6 +10063,7 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) for (i = 0; i < MAX_UDPSOCKETS; i++) { struct tsocket *t = &s->udpsockets[i]; struct pkt_desc *desc = fifo_peek(&t->sock.udp.txbuf); + int tx_drained = 0; while (desc) { struct wolfIP_udp_datagram *udp = (struct wolfIP_udp_datagram *)(t->txmem + desc->pos + sizeof(*desc)); unsigned int tx_if = wolfIP_socket_if_idx(t); @@ -10135,8 +10136,15 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) } #endif fifo_pop(&t->sock.udp.txbuf); + tx_drained = 1; desc = fifo_peek(&t->sock.udp.txbuf); } + /* Draining the txbuf frees space; raise CB_EVENT_WRITABLE so a sender + * blocked on a full buffer (e.g. the FreeRTOS BSD shim's sendto()) is + * woken. The loopback path is handled separately via + * wolfIP_notify_loopback_space_available(). */ + if (tx_drained && tx_has_writable_space(t)) + t->events |= CB_EVENT_WRITABLE; } for (i = 0; i < MAX_ICMPSOCKETS; i++) { struct tsocket *t = &s->icmpsockets[i]; From 9182261e3d69c9dd0a56629142681f42b729daad Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 07:59:31 +0200 Subject: [PATCH 02/15] F-5788: deliver CB_EVENT_CLOSED on LAST_ACK teardown When a peer's final ACK acknowledges our FIN in TCP_LAST_ACK, tcp_ack() transitions to TCP_CLOSED and calls close_socket(), which memsets the tsocket (clearing ts->callback and ts->events) during packet processing, before wolfIP_poll() Step 3 dispatches socket callbacks. The pending CB_EVENT_CLOSED was therefore never delivered, so a caller blocked on the socket close (notably the FreeRTOS BSD close() waiting on xSemaphoreTake(portMAX_DELAY)) was never woken and deadlocked. Deliver CB_EVENT_CLOSED to a registered callback immediately before close_socket() destroys the socket, mirroring the poll Step 3 dispatch. Add a unit test that registers a callback on a LAST_ACK socket, injects the final ACK, and asserts CB_EVENT_CLOSED is delivered before teardown. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_tcp_ack.c | 47 ++++++++++++++++++++++++++++++ src/wolfip.c | 10 +++++++ 3 files changed, 58 insertions(+) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 4ea43b59..49a605fe 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -597,6 +597,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_utils, test_tcp_rst_syn_rcvd_returns_to_listen); tcase_add_test(tc_utils, test_tcp_fin_wait_1_to_closing); tcase_add_test(tc_utils, test_tcp_last_ack_closes_socket); + tcase_add_test(tc_utils, test_tcp_last_ack_closes_socket_delivers_closed_event); tcase_add_test(tc_utils, test_tcp_last_ack_partial_ack_keeps_socket_and_timer); tcase_add_test(tc_utils, test_tcp_ack_acks_data_and_sets_writable); tcase_add_test(tc_utils, test_tcp_ack_duplicate_resend_clears_sent); diff --git a/src/test/unit/unit_tests_tcp_ack.c b/src/test/unit/unit_tests_tcp_ack.c index 21785a37..e29f5e6e 100644 --- a/src/test/unit/unit_tests_tcp_ack.c +++ b/src/test/unit/unit_tests_tcp_ack.c @@ -3826,6 +3826,53 @@ START_TEST(test_tcp_last_ack_closes_socket) } END_TEST +/* LAST_ACK final ACK must deliver CB_EVENT_CLOSED to a registered callback + * before close_socket() memsets it away, otherwise a caller blocked on the + * socket close (e.g. the FreeRTOS BSD close()) deadlocks. */ +START_TEST(test_tcp_last_ack_closes_socket_delivers_closed_event) +{ + struct wolfIP s; + struct tsocket *ts; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A0000E3U; + uint16_t local_port = 6668; + uint16_t remote_port = 7779; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_LAST_ACK; + ts->sock.tcp.last = 9; + ts->local_ip = local_ip; + ts->remote_ip = remote_ip; + ts->src_port = local_port; + ts->dst_port = remote_port; + ts->sock.tcp.ack = 10; + ts->callback = test_socket_cb; + ts->callback_arg = NULL; + queue_init(&ts->sock.tcp.rxbuf, ts->rxmem, RXBUF_SIZE, ts->sock.tcp.ack); + + socket_cb_calls = 0; + socket_cb_last_fd = -1; + socket_cb_last_events = 0; + + inject_tcp_segment(&s, TEST_PRIMARY_IF, remote_ip, local_ip, remote_port, local_port, + 10, 10, TCP_FLAG_ACK); + + /* Socket torn down ... */ + ck_assert_int_eq(ts->proto, 0); + /* ... but only after CB_EVENT_CLOSED was delivered to the waiter. */ + ck_assert_int_eq(socket_cb_calls, 1); + ck_assert_int_eq(socket_cb_last_fd, 0 | MARK_TCP_SOCKET); + ck_assert_uint_eq(socket_cb_last_events & CB_EVENT_CLOSED, CB_EVENT_CLOSED); +} +END_TEST + START_TEST(test_tcp_last_ack_partial_ack_keeps_socket_and_timer) { struct wolfIP s; diff --git a/src/wolfip.c b/src/wolfip.c index 70a226aa..0dc579ea 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -4641,6 +4641,16 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) if (t->sock.tcp.state == TCP_LAST_ACK && tcp_seq_leq(fin_acked, ack)) { tcp_ctrl_rto_stop(t); t->sock.tcp.state = TCP_CLOSED; + /* The peer's final ACK tears the socket down synchronously here, + * during packet processing, before wolfIP_poll() Step 3 dispatches + * socket callbacks. close_socket() memsets the callback and event + * mask away, so deliver CB_EVENT_CLOSED now; otherwise a caller + * blocked on the socket close (e.g. the FreeRTOS BSD close()) would + * never be woken. */ + if (t->callback) { + int sock_fd = (int)(t - t->S->tcpsockets) | MARK_TCP_SOCKET; + t->callback(sock_fd, CB_EVENT_CLOSED, t->callback_arg); + } close_socket(t); return; } From 4accd9b81ad77c06620b18997c2147ce105dcd58 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 08:06:38 +0200 Subject: [PATCH 03/15] F-5785: close accepted socket on RST in SYN_RCVD instead of reverting to LISTEN A peer RST on a half-open connection in TCP_SYN_RCVD unconditionally reverted the socket to TCP_LISTEN. That is correct only for a real listening socket (is_listener=1) caught between SYN and accept(); an accepted clone produced by wolfIP_sock_accept() has is_listener=0 and is a distinct connection. Reverting it to LISTEN turned it into a phantom listener, cleared CB_EVENT_READABLE, and fired no callback, so a FreeRTOS BSD consumer blocked in recv() (state != ESTABLISHED -> -1 -> wait) never woke: the task deadlocked and the fd slot leaked, exhausting WOLFIP_FREERTOS_BSD_MAX_FDS after repetition. Gate the LISTEN revert on is_listener, mirroring the existing ctrl-RTO recovery path; otherwise deliver CB_EVENT_CLOSED synchronously (as the LAST_ACK teardown does, since close_socket() wipes the callback before poll Step 3) and close the socket. A subsequent recv() then observes TCP_CLOSED, wolfIP_sock_can_read() returns 1, and recv() returns an error instead of blocking forever. --- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_tcp_state.c | 47 +++++++++++++++++++++++++++- src/wolfip.c | 27 ++++++++++++---- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 49a605fe..2ac915c9 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -1201,6 +1201,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_core, test_tcp_parse_options_sack_permitted_parsed); tcase_add_test(tc_core, test_tcp_input_syn_rcvd_rst_bad_seq_ignored); tcase_add_test(tc_core, test_tcp_input_syn_rcvd_rst_good_seq_reverts_to_listen); + tcase_add_test(tc_core, test_tcp_input_syn_rcvd_rst_good_seq_nonlistener_closes); tcase_add_test(tc_core, test_tcp_input_time_wait_sends_ack_on_any_segment); tcase_add_test(tc_core, test_tcp_input_last_ack_unacceptable_sends_ack); tcase_add_test(tc_core, test_tcp_input_last_ack_syn_sends_challenge_ack); diff --git a/src/test/unit/unit_tests_tcp_state.c b/src/test/unit/unit_tests_tcp_state.c index bf273b47..832a320c 100644 --- a/src/test/unit/unit_tests_tcp_state.c +++ b/src/test/unit/unit_tests_tcp_state.c @@ -535,7 +535,7 @@ START_TEST(test_tcp_input_syn_rcvd_rst_bad_seq_ignored) } END_TEST -/* RST in SYN_RCVD with matching seq → revert to LISTEN */ +/* RST in SYN_RCVD with matching seq on a listener → revert to LISTEN */ START_TEST(test_tcp_input_syn_rcvd_rst_good_seq_reverts_to_listen) { struct wolfIP s; @@ -553,6 +553,7 @@ START_TEST(test_tcp_input_syn_rcvd_rst_good_seq_reverts_to_listen) ts->proto = WI_IPPROTO_TCP; ts->S = &s; ts->sock.tcp.state = TCP_SYN_RCVD; + ts->sock.tcp.is_listener = 1; /* listening socket: must revert, not close */ ts->sock.tcp.ack = 2; /* rcv_nxt = 2 */ ts->local_ip = local_ip; ts->remote_ip = remote_ip; @@ -570,6 +571,50 @@ START_TEST(test_tcp_input_syn_rcvd_rst_good_seq_reverts_to_listen) } END_TEST +/* RST in SYN_RCVD with matching seq on an accepted (non-listener) socket → + * the half-open connection is torn down and CB_EVENT_CLOSED is delivered, so a + * blocked consumer wakes instead of waiting on a phantom LISTEN socket. */ +START_TEST(test_tcp_input_syn_rcvd_rst_good_seq_nonlistener_closes) +{ + struct wolfIP s; + struct tsocket *ts; + ip4 local_ip = 0x0A000001U; + ip4 remote_ip = 0x0A0000A1U; + uint16_t lport = 8080, rport = 40000; + + wolfIP_init(&s); + mock_link_init(&s); + wolfIP_ipconfig_set(&s, local_ip, 0xFFFFFF00U, 0); + + ts = &s.tcpsockets[0]; + memset(ts, 0, sizeof(*ts)); + ts->proto = WI_IPPROTO_TCP; + ts->S = &s; + ts->sock.tcp.state = TCP_SYN_RCVD; + ts->sock.tcp.is_listener = 0; /* accepted clone: must close, not revert */ + ts->sock.tcp.ack = 2; /* rcv_nxt = 2 */ + ts->local_ip = local_ip; + ts->remote_ip = remote_ip; + ts->src_port = lport; + ts->dst_port = rport; + ts->sock.tcp.tmr_rto = NO_TIMER; + ts->callback = test_socket_cb; + ts->callback_arg = NULL; + fifo_init(&ts->sock.tcp.txbuf, ts->txmem, TXBUF_SIZE); + + socket_cb_calls = 0; + socket_cb_last_events = 0; + + /* RST with seq == rcv_nxt: socket must be destroyed, not reverted */ + inject_tcp_segment(&s, TEST_PRIMARY_IF, remote_ip, local_ip, + rport, lport, 2, 0, TCP_FLAG_RST); + + ck_assert_int_eq(ts->proto, 0); /* socket destroyed */ + ck_assert_int_eq(socket_cb_calls, 1); + ck_assert_uint_ne(socket_cb_last_events & CB_EVENT_CLOSED, 0); +} +END_TEST + /* Time-wait state re-ACKs any incoming segment */ START_TEST(test_tcp_input_time_wait_sends_ack_on_any_segment) { diff --git a/src/wolfip.c b/src/wolfip.c index 0dc579ea..fdaa5628 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -5008,12 +5008,27 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, /* RFC 9293: only accept RST if SEQ matches rcv_nxt */ if (seg_seq != rcv_nxt) continue; - /* RST on a half-open connection: fall back to listening state. */ - t->sock.tcp.state = TCP_LISTEN; - t->events &= ~CB_EVENT_READABLE; - t->remote_ip = IPADDR_ANY; - t->dst_port = 0; - t->sock.tcp.ack = 0; + if (t->sock.tcp.is_listener) { + /* RST on a half-open connection of a listening socket: + * fall back to LISTEN to keep the server open. */ + t->sock.tcp.state = TCP_LISTEN; + t->events &= ~CB_EVENT_READABLE; + t->remote_ip = IPADDR_ANY; + t->dst_port = 0; + t->sock.tcp.ack = 0; + continue; + } + /* An accepted (cloned) connection has no listen role: a peer + * RST before the handshake completed must tear it down, not + * resurrect it as a phantom listener. Deliver CB_EVENT_CLOSED + * synchronously because close_socket() wipes the callback + * before wolfIP_poll() Step 3 dispatches events; otherwise a + * consumer blocked on the new socket would never wake. */ + if (t->callback) { + int sock_fd = (int)(t - S->tcpsockets) | MARK_TCP_SOCKET; + t->callback(sock_fd, CB_EVENT_CLOSED, t->callback_arg); + } + close_socket(t); continue; } if (t->sock.tcp.state == TCP_SYN_SENT) { From d52f8e12e3298ac6934dec599645207434f092d8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 08:16:36 +0200 Subject: [PATCH 04/15] F-5781: free wolfSSL io_descs slot on TLS teardown and treat -1 as fatal The wolfSSL <-> wolfIP IO glue allocates a session from a static io_descs[MAX_WOLFIP_CTX] pool in wolfSSL_SetIO_wolfIP() but never released it: no cleanup function existed and httpd's teardown paths only freed the WOLFSSL object. Every TLS connection therefore leaked one slot, and after MAX_WOLFIP_CTX (8) connections every subsequent handshake failed until process restart - an unauthenticated DoS. The sibling wolfSSH/wolfMQTT ports already free their descriptors on teardown (io_desc_free / *_CleanupIO_*); the wolfSSL port did not. In addition, wolfIP_io_recv/wolfIP_io_send mapped a -1 return (the "not established" / torn-down case from wolfIP_sock_recvfrom / wolfIP_sock_sendto) to WANT_READ / WANT_WRITE, the same as the genuine would-block code -WOLFIP_EAGAIN. A reset connection was thus reported as retryable, so wolfSSL spun on a dead socket and the owning session was never closed (so its slot never reclaimed). Only -WOLFIP_EAGAIN is "would block"; -1 must be a fatal close. Add wolfSSL_CleanupIO_wolfIP() to release the slot, call it on every httpd TLS teardown path before wolfSSL_free(), and map -1 to a fatal close in both IO callbacks. Update the IO behavior unit tests for the new mapping and add test_wolfssl_io_cleanup_frees_slot proving a torn down session's slot becomes reusable. --- src/http/httpd.c | 5 ++++ src/port/wolfssl_io.c | 34 +++++++++++++++++++++-- src/test/test_http_arg_oob.c | 2 ++ src/test/test_http_smuggle.c | 2 ++ src/test/unit/mocks/wolfssl/ssl.h | 1 + src/test/unit/unit.c | 1 + src/test/unit/unit_shared.c | 7 +++++ src/test/unit/unit_tests_proto.c | 45 +++++++++++++++++++++++++++++-- wolfip.h | 1 + 9 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/http/httpd.c b/src/http/httpd.c index 19b36e7f..6f864271 100644 --- a/src/http/httpd.c +++ b/src/http/httpd.c @@ -133,6 +133,7 @@ void http_send_response_headers(struct http_client *hc, int status_code, const c } if (rc <= 0) { /* Error – close connection */ + wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; wolfIP_sock_close(hc->httpd->ipstack, hc->client_sd); @@ -150,6 +151,7 @@ void http_send_response_body(struct http_client *hc, const void *body, size_t le rc = wolfIP_sock_send(hc->httpd->ipstack, hc->client_sd, body, len, 0); if (rc <= 0) { + wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; wolfIP_sock_close(hc->httpd->ipstack, hc->client_sd); @@ -178,6 +180,7 @@ void http_send_response_chunk(struct http_client *hc, const void *chunk, size_t if ((http_write_response(hc, txt_chunk, strlen(txt_chunk)) <= 0) || (http_write_response(hc, chunk, len) <= 0) || (http_write_response(hc, "\r\n", 2) <= 0)) { + wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; wolfIP_sock_close(hc->httpd->ipstack, hc->client_sd); @@ -194,6 +197,7 @@ void http_send_response_chunk_end(struct http_client *hc) { else rc = wolfIP_sock_send(hc->httpd->ipstack, hc->client_sd, "0\r\n\r\n", 5, 0); if (rc <= 0) { + wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; wolfIP_sock_close(hc->httpd->ipstack, hc->client_sd); @@ -484,6 +488,7 @@ static void http_recv_cb(int sd, uint16_t event, void *arg) { fail_close: if (hc->ssl) { + wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; } diff --git a/src/port/wolfssl_io.c b/src/port/wolfssl_io.c index 0a3bfab0..e8a0512c 100644 --- a/src/port/wolfssl_io.c +++ b/src/port/wolfssl_io.c @@ -72,7 +72,12 @@ static int wolfIP_io_recv(WOLFSSL* ssl, char* buf, int sz, void* ctx) return WOLFSSL_CBIO_ERR_GENERAL; ret = wolfIP_sock_recv(desc->stack, desc->fd, buf, sz, 0); - if (ret == -WOLFIP_EAGAIN || ret == -1) + /* Only -WOLFIP_EAGAIN means "would block": wolfIP_sock_recvfrom returns it + * (via queue_pop) for an established socket with an empty RX queue. A -1 is + * the "not established" / torn-down case and must be reported as a fatal + * close, otherwise wolfSSL keeps retrying a dead connection forever and the + * owning session is never released. */ + if (ret == -WOLFIP_EAGAIN) return WOLFSSL_CBIO_ERR_WANT_READ; if (ret <= 0) return WOLFSSL_CBIO_ERR_CONN_CLOSE; @@ -89,7 +94,11 @@ static int wolfIP_io_send(WOLFSSL* ssl, char* buf, int sz, void* ctx) return WOLFSSL_CBIO_ERR_GENERAL; ret = wolfIP_sock_send(desc->stack, desc->fd, buf, sz, 0); - if (ret == -WOLFIP_EAGAIN || ret == -1) + /* Only -WOLFIP_EAGAIN means "would block" (TX buffer full, nothing queued). + * A -1 is the "not established" / torn-down case from wolfIP_sock_sendto and + * must be reported as a fatal close, otherwise wolfSSL retries the dead + * connection forever and its session is never released. */ + if (ret == -WOLFIP_EAGAIN) return WOLFSSL_CBIO_ERR_WANT_WRITE; if (ret <= 0) return WOLFSSL_CBIO_ERR_CONN_CLOSE; @@ -135,3 +144,24 @@ int wolfSSL_SetIO_wolfIP(WOLFSSL* ssl, int fd) } return -1; } + +/* Release the io_descs[] slot allocated by wolfSSL_SetIO_wolfIP() for this + * session. Must be called on every TLS teardown path (before wolfSSL_free) + * or the static pool leaks one slot per connection and is exhausted after + * MAX_WOLFIP_CTX sessions. */ +void wolfSSL_CleanupIO_wolfIP(WOLFSSL* ssl) +{ + struct wolfip_io_desc *desc; + + if (!ssl) + return; + + desc = (struct wolfip_io_desc *)wolfSSL_GetIOReadCtx(ssl); + if (!desc) + return; + + wolfSSL_SetIOReadCtx(ssl, NULL); + wolfSSL_SetIOWriteCtx(ssl, NULL); + desc->fd = 0; + desc->stack = NULL; +} diff --git a/src/test/test_http_arg_oob.c b/src/test/test_http_arg_oob.c index e037c965..c5e2df03 100644 --- a/src/test/test_http_arg_oob.c +++ b/src/test/test_http_arg_oob.c @@ -56,6 +56,8 @@ int wolfSSL_SetIO_wolfIP(WOLFSSL *ssl, int fd) { (void)ssl; (void)fd; return 0; } int wolfSSL_SetIO_wolfIP_CTX(WOLFSSL_CTX *ctx, struct wolfIP *s) { (void)ctx; (void)s; return 0; } +void wolfSSL_CleanupIO_wolfIP(WOLFSSL *ssl) +{ (void)ssl; } #define CHECK(cond) do { if (!(cond)) { \ printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); failures++; } } while (0) diff --git a/src/test/test_http_smuggle.c b/src/test/test_http_smuggle.c index 3ec238cd..aa668f4f 100644 --- a/src/test/test_http_smuggle.c +++ b/src/test/test_http_smuggle.c @@ -54,6 +54,8 @@ int wolfSSL_SetIO_wolfIP(WOLFSSL *ssl, int fd) { (void)ssl; (void)fd; return 0; } int wolfSSL_SetIO_wolfIP_CTX(WOLFSSL_CTX *ctx, struct wolfIP *s) { (void)ctx; (void)s; return 0; } +void wolfSSL_CleanupIO_wolfIP(WOLFSSL *ssl) +{ (void)ssl; } /* --- test harness --------------------------------------------------------- */ static int handler_calls; diff --git a/src/test/unit/mocks/wolfssl/ssl.h b/src/test/unit/mocks/wolfssl/ssl.h index f1545323..021d98e2 100644 --- a/src/test/unit/mocks/wolfssl/ssl.h +++ b/src/test/unit/mocks/wolfssl/ssl.h @@ -30,6 +30,7 @@ int wolfSSL_SetIORecv(WOLFSSL_CTX *ctx, CallbackIORecv cb); int wolfSSL_SetIOSend(WOLFSSL_CTX *ctx, CallbackIOSend cb); int wolfSSL_SetIOReadCtx(WOLFSSL *ssl, void *ctx); int wolfSSL_SetIOWriteCtx(WOLFSSL *ssl, void *ctx); +void *wolfSSL_GetIOReadCtx(WOLFSSL *ssl); WOLFSSL_CTX *wolfSSL_get_SSL_CTX(WOLFSSL *ssl); #ifdef __cplusplus diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 2ac915c9..de5cbed5 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -950,6 +950,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_wolfssl, test_wolfssl_io_setio_invalid_ctx); tcase_add_test(tc_wolfssl, test_wolfssl_io_setio_invalid_fd); tcase_add_test(tc_wolfssl, test_wolfssl_io_setio_no_stack); + tcase_add_test(tc_wolfssl, test_wolfssl_io_cleanup_frees_slot); tcase_add_test(tc_wolfssl, test_wolfssl_io_recv_behaviors); tcase_add_test(tc_wolfssl, test_wolfssl_io_recv_invalid_desc); tcase_add_test(tc_wolfssl, test_wolfssl_io_recv_fragmented_sequence); diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index 2fcda51c..b562b2d7 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -176,6 +176,13 @@ int wolfSSL_SetIOWriteCtx(WOLFSSL *ssl, void *ctx) return 0; } +void *wolfSSL_GetIOReadCtx(WOLFSSL *ssl) +{ + if (!ssl) + return NULL; + return ssl->rctx; +} + WOLFSSL_CTX *wolfSSL_get_SSL_CTX(WOLFSSL *ssl) { if (!ssl) diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index f700e468..9a3e5ff0 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -4200,6 +4200,43 @@ START_TEST(test_wolfssl_io_setio_no_stack) } END_TEST +/* Regression for F-5781: io_descs[] slots must be released on teardown, or the + * static pool is exhausted after MAX_WOLFIP_CTX sessions and every later TLS + * handshake fails (unauthenticated DoS). */ +START_TEST(test_wolfssl_io_cleanup_frees_slot) +{ + struct wolfIP s; + WOLFSSL_CTX ctx; + WOLFSSL ssl[MAX_WOLFIP_CTX]; + WOLFSSL extra; + int i; + + memset(&ctx, 0, sizeof(ctx)); + memset(ssl, 0, sizeof(ssl)); + memset(&extra, 0, sizeof(extra)); + reset_wolfssl_io_state(); + wolfSSL_SetIO_wolfIP_CTX(&ctx, &s); + + /* Fill every slot in the pool. */ + for (i = 0; i < MAX_WOLFIP_CTX; i++) { + ssl[i].ctx = &ctx; + ck_assert_int_eq(wolfSSL_SetIO_wolfIP(&ssl[i], 10 + i), 0); + } + + /* Pool exhausted: a new session cannot be set up. */ + extra.ctx = &ctx; + ck_assert_int_eq(wolfSSL_SetIO_wolfIP(&extra, 99), -1); + + /* Tearing down one session must release its slot and clear its IO ctx. */ + wolfSSL_CleanupIO_wolfIP(&ssl[0]); + ck_assert_ptr_null(ssl[0].rctx); + ck_assert_ptr_null(ssl[0].wctx); + + /* The freed slot is now reusable. */ + ck_assert_int_eq(wolfSSL_SetIO_wolfIP(&extra, 99), 0); +} +END_TEST + START_TEST(test_wolfssl_io_recv_behaviors) { struct wolfIP s; @@ -4215,9 +4252,11 @@ START_TEST(test_wolfssl_io_recv_behaviors) ret = wolfIP_io_recv(NULL, buf, sizeof(buf), &desc); ck_assert_int_eq(ret, WOLFSSL_CBIO_ERR_WANT_READ); + /* -1 is the torn-down ("not established") case: it must be a fatal close, + * not a retryable WANT_READ, or wolfSSL spins on a dead connection. */ test_recv_ret = -1; ret = wolfIP_io_recv(NULL, buf, sizeof(buf), &desc); - ck_assert_int_eq(ret, WOLFSSL_CBIO_ERR_WANT_READ); + ck_assert_int_eq(ret, WOLFSSL_CBIO_ERR_CONN_CLOSE); test_recv_ret = 0; ret = wolfIP_io_recv(NULL, buf, sizeof(buf), &desc); @@ -4380,9 +4419,11 @@ START_TEST(test_wolfssl_io_send_behaviors) ret = wolfIP_io_send(NULL, buf, 4, &desc); ck_assert_int_eq(ret, WOLFSSL_CBIO_ERR_WANT_WRITE); + /* -1 is the torn-down ("not established") case: it must be a fatal close, + * not a retryable WANT_WRITE, or wolfSSL spins on a dead connection. */ test_send_ret = -1; ret = wolfIP_io_send(NULL, buf, 4, &desc); - ck_assert_int_eq(ret, WOLFSSL_CBIO_ERR_WANT_WRITE); + ck_assert_int_eq(ret, WOLFSSL_CBIO_ERR_CONN_CLOSE); test_send_ret = 0; ret = wolfIP_io_send(NULL, buf, 4, &desc); diff --git a/wolfip.h b/wolfip.h index 55c0a24d..1a5b8e7c 100644 --- a/wolfip.h +++ b/wolfip.h @@ -526,6 +526,7 @@ static inline void iptoa(ip4 ip, char *buf) #include int wolfSSL_SetIO_wolfIP(WOLFSSL* ssl, int fd); int wolfSSL_SetIO_wolfIP_CTX(WOLFSSL_CTX *ctx, struct wolfIP *s); + void wolfSSL_CleanupIO_wolfIP(WOLFSSL* ssl); #ifdef WOLFIP_ESP #include From 5328f514ceda970120cbe102a973b7b674b82991 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 08:22:01 +0200 Subject: [PATCH 05/15] F-5780: treat wolfIP_sock_send -1 as fatal close in wolfssh_io_send wolfssh_io_send mapped both -WOLFIP_EAGAIN (TX buffer full, retryable) and -1 to WS_CBIO_ERR_WANT_WRITE. wolfIP_sock_sendto returns -1 when the TCP socket is no longer in ESTABLISHED/CLOSE_WAIT - the torn-down case after a peer RST - which is fatal, not "would block". Reporting it as WANT_WRITE made wolfSSH retry the dead connection forever, so the SSH state machine never transitioned to closing and the io_desc slot was never released via wolfSSH_CleanupIO_wolfIP. A handful of unauthenticated RSTs would exhaust the static io_descs[MAX_WOLFSSH_CTX] pool and permanently deny SSH service. This is the send-path sibling of the wolfSSL fix in F-5781. Map only -WOLFIP_EAGAIN to WS_CBIO_ERR_WANT_WRITE and route 0/-1 to WS_CBIO_ERR_CONN_CLOSE so the session is closed and its slot reclaimed. Add a wolfSSH IO unit harness (mock wolfssh/ssh.h plus stubs in unit_shared.c that include wolfssh_io.c with renamed static helpers) and test_wolfssh_io_send_behaviors, which asserts the -1 case now yields a fatal close. The test fails before the fix and passes after. --- src/port/wolfssh_io.c | 9 ++++- src/test/unit/mocks/wolfssh/ssh.h | 47 +++++++++++++++++++++++ src/test/unit/unit.c | 1 + src/test/unit/unit_shared.c | 62 +++++++++++++++++++++++++++++++ src/test/unit/unit_tests_proto.c | 35 +++++++++++++++++ 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 src/test/unit/mocks/wolfssh/ssh.h diff --git a/src/port/wolfssh_io.c b/src/port/wolfssh_io.c index 38a9db7a..f02c91c9 100644 --- a/src/port/wolfssh_io.c +++ b/src/port/wolfssh_io.c @@ -94,10 +94,15 @@ static int wolfssh_io_send(WOLFSSH *ssh, void *buf, word32 sz, void *ctx) } ret = wolfIP_sock_send(desc->stack, desc->fd, buf, (int)sz, 0); - if (ret == -WOLFIP_EAGAIN || ret == -1) { + /* Only -WOLFIP_EAGAIN means "would block" (TX buffer full, nothing + * queued). A -1 is the "not established" / torn-down case from + * wolfIP_sock_sendto (peer RST) and must be reported as a fatal close, + * otherwise wolfSSH retries the dead connection forever and its io_desc + * slot is never released. */ + if (ret == -WOLFIP_EAGAIN) { return WS_CBIO_ERR_WANT_WRITE; } - if (ret == 0) { + if (ret == 0 || ret == -1) { return WS_CBIO_ERR_CONN_CLOSE; } if (ret < 0) { diff --git a/src/test/unit/mocks/wolfssh/ssh.h b/src/test/unit/mocks/wolfssh/ssh.h new file mode 100644 index 00000000..ab8fc009 --- /dev/null +++ b/src/test/unit/mocks/wolfssh/ssh.h @@ -0,0 +1,47 @@ +/* Mock wolfssh/ssh.h for unit tests. + * Only includes pieces needed by src/port/wolfssh_io.c tests. + */ +#ifndef WOLFSSH_SSH_H +#define WOLFSSH_SSH_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned int word32; + +typedef struct WOLFSSH_CTX { + int id; +} WOLFSSH_CTX; + +typedef struct WOLFSSH { + WOLFSSH_CTX *ctx; + void *rctx; + void *wctx; +} WOLFSSH; + +typedef int (*WS_CallbackIORecv)(WOLFSSH *ssh, void *buf, word32 sz, void *ctx); +typedef int (*WS_CallbackIOSend)(WOLFSSH *ssh, void *buf, word32 sz, void *ctx); + +/* Return codes (subset, matching real wolfSSH values). */ +#define WS_SUCCESS (0) +#define WS_BAD_ARGUMENT (-2) +#define WS_MEMORY_E (-6) + +/* Custom IO callback error codes (subset, matching real wolfSSH values). */ +#define WS_CBIO_ERR_GENERAL (-1) +#define WS_CBIO_ERR_WANT_READ (-2) +#define WS_CBIO_ERR_WANT_WRITE (-3) +#define WS_CBIO_ERR_CONN_CLOSE (-6) + +void wolfSSH_SetIORecv(WOLFSSH_CTX *ctx, WS_CallbackIORecv cb); +void wolfSSH_SetIOSend(WOLFSSH_CTX *ctx, WS_CallbackIOSend cb); +void wolfSSH_SetIOReadCtx(WOLFSSH *ssh, void *ctx); +void wolfSSH_SetIOWriteCtx(WOLFSSH *ssh, void *ctx); +void *wolfSSH_GetIOReadCtx(WOLFSSH *ssh); + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFSSH_SSH_H */ diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index de5cbed5..a43c5815 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -960,6 +960,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_wolfssl, test_wolfssl_io_send_behaviors); tcase_add_test(tc_wolfssl, test_wolfssl_io_send_invalid_desc); tcase_add_test(tc_wolfssl, test_wolfssl_io_send_want_write_keeps_buffer); + tcase_add_test(tc_wolfssl, test_wolfssh_io_send_behaviors); /* Branch-coverage tests backported from the trimmed wolfIP suite. */ tcase_add_test(tc_core, test_socket_from_fd_invalid_inputs); diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index b562b2d7..690d8742 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -247,6 +247,68 @@ static int test_wolfIP_sock_send(struct wolfIP *s, int fd, const void *buf, int #undef wolfIP_sock_recv #undef wolfIP_sock_send +/* wolfSSH IO glue mocks. */ +#include + +void wolfSSH_SetIORecv(WOLFSSH_CTX *ctx, WS_CallbackIORecv cb) +{ + (void)ctx; + (void)cb; +} + +void wolfSSH_SetIOSend(WOLFSSH_CTX *ctx, WS_CallbackIOSend cb) +{ + (void)ctx; + (void)cb; +} + +void wolfSSH_SetIOReadCtx(WOLFSSH *ssh, void *ctx) +{ + if (ssh) + ssh->rctx = ctx; +} + +void wolfSSH_SetIOWriteCtx(WOLFSSH *ssh, void *ctx) +{ + if (ssh) + ssh->wctx = ctx; +} + +void *wolfSSH_GetIOReadCtx(WOLFSSH *ssh) +{ + if (!ssh) + return NULL; + return ssh->rctx; +} + +/* wolfssh_io.c reuses the same static names (io_descs / io_desc_alloc / + * io_desc_free) as wolfssl_io.c above; rename them to avoid a collision in + * this single translation unit, and route its socket IO to the same mocks. */ +#define io_descs wolfssh_io_descs +#define io_desc_alloc wolfssh_io_desc_alloc +#define io_desc_free wolfssh_io_desc_free +#define wolfIP_sock_recv test_wolfIP_sock_recv +#define wolfIP_sock_send test_wolfIP_sock_send +#include "../../port/wolfssh_io.c" +#undef wolfIP_sock_recv +#undef wolfIP_sock_send +#undef io_descs +#undef io_desc_alloc +#undef io_desc_free + +static void reset_wolfssh_io_state(void) +{ + memset(wolfssh_io_descs, 0, sizeof(wolfssh_io_descs)); + test_recv_ret = 0; + test_send_ret = 0; + test_recv_fill_len = 0; + test_send_capture_len = 0; + test_send_last_len = 0; + test_recv_steps_len = 0; + test_recv_step = 0; + test_recv_step_total = 0; +} + static void reset_wolfssl_io_state(void) { memset(ctx_map, 0, sizeof(ctx_map)); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 9a3e5ff0..5d86220b 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -4468,6 +4468,41 @@ START_TEST(test_wolfssl_io_send_invalid_desc) } END_TEST +START_TEST(test_wolfssh_io_send_behaviors) +{ + struct wolfIP s; + struct wolfssh_io_desc desc; + char buf[4] = {0x11, 0x22, 0x33, 0x44}; + int ret; + + memset(&desc, 0, sizeof(desc)); + desc.stack = &s; + desc.fd = 4; + desc.in_use = 1; + + reset_wolfssh_io_state(); + test_send_ret = -WOLFIP_EAGAIN; + ret = wolfssh_io_send(NULL, buf, sizeof(buf), &desc); + ck_assert_int_eq(ret, WS_CBIO_ERR_WANT_WRITE); + + /* -1 is the torn-down ("not established", peer RST) case from + * wolfIP_sock_sendto: it must be a fatal close, not a retryable + * WANT_WRITE, or wolfSSH spins on a dead connection and never frees the + * io_desc slot (unauthenticated DoS). */ + test_send_ret = -1; + ret = wolfssh_io_send(NULL, buf, sizeof(buf), &desc); + ck_assert_int_eq(ret, WS_CBIO_ERR_CONN_CLOSE); + + test_send_ret = 0; + ret = wolfssh_io_send(NULL, buf, sizeof(buf), &desc); + ck_assert_int_eq(ret, WS_CBIO_ERR_CONN_CLOSE); + + test_send_ret = 4; + ret = wolfssh_io_send(NULL, buf, sizeof(buf), &desc); + ck_assert_int_eq(ret, 4); +} +END_TEST + START_TEST(test_tcp_listen_rejects_wrong_interface) { struct wolfIP s; From 0ce610baca3af5106f549f42735afd53c0b90695 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 08:24:52 +0200 Subject: [PATCH 06/15] F-5736: treat wolfIP_sock_recv -1 as fatal close in wolfssh_io_recv wolfssh_io_recv mapped both -WOLFIP_EAGAIN (no data queued, retryable) and -1 to WS_CBIO_ERR_WANT_READ. wolfIP_sock_recvfrom returns -1 when the TCP socket is no longer in ESTABLISHED/FIN_WAIT/CLOSE_WAIT (wolfip.c:6430-6431) - the torn-down case after a peer RST drives the socket to TCP_CLOSED - which is fatal, not "would block". Reporting it as WANT_READ propagates as WS_WANT_READ, and the SSH_STATE_KEY_EXCHANGE handler in ssh_server.c stays in KEY_EXCHANGE on WANT_READ/WANT_WRITE, so the handshake state machine is wedged forever. Because server.ssh is a single global and new connections are only accepted in SSH_STATE_LISTENING, one unauthenticated RST during key exchange permanently denies SSH service until reboot. This is the recv-path sibling of the send-path fix in F-5780. Map only -WOLFIP_EAGAIN to WS_CBIO_ERR_WANT_READ and route 0/-1 to WS_CBIO_ERR_CONN_CLOSE so the dead session is closed and its io_desc slot reclaimed, mirroring wolfssh_io_send. Add test_wolfssh_io_recv_behaviors, which asserts the -1 case now yields a fatal close. The test fails before the fix and passes after. --- src/port/wolfssh_io.c | 9 ++++++-- src/test/unit/unit.c | 1 + src/test/unit/unit_tests_proto.c | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/port/wolfssh_io.c b/src/port/wolfssh_io.c index f02c91c9..133c9ea4 100644 --- a/src/port/wolfssh_io.c +++ b/src/port/wolfssh_io.c @@ -70,10 +70,15 @@ static int wolfssh_io_recv(WOLFSSH *ssh, void *buf, word32 sz, void *ctx) } ret = wolfIP_sock_recv(desc->stack, desc->fd, buf, (int)sz, 0); - if (ret == -WOLFIP_EAGAIN || ret == -1) { + /* Only -WOLFIP_EAGAIN means "would block" (no data queued yet). A -1 is + * the "not established" / torn-down case from wolfIP_sock_recvfrom (peer + * RST drove the socket to TCP_CLOSED) and must be reported as a fatal + * close, otherwise wolfSSH retries the dead connection forever and the + * SSH handshake state machine is wedged in KEY_EXCHANGE indefinitely. */ + if (ret == -WOLFIP_EAGAIN) { return WS_CBIO_ERR_WANT_READ; } - if (ret == 0) { + if (ret == 0 || ret == -1) { return WS_CBIO_ERR_CONN_CLOSE; } if (ret < 0) { diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index a43c5815..3abcf1ce 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -961,6 +961,7 @@ Suite *wolf_suite(void) tcase_add_test(tc_wolfssl, test_wolfssl_io_send_invalid_desc); tcase_add_test(tc_wolfssl, test_wolfssl_io_send_want_write_keeps_buffer); tcase_add_test(tc_wolfssl, test_wolfssh_io_send_behaviors); + tcase_add_test(tc_wolfssl, test_wolfssh_io_recv_behaviors); /* Branch-coverage tests backported from the trimmed wolfIP suite. */ tcase_add_test(tc_core, test_socket_from_fd_invalid_inputs); diff --git a/src/test/unit/unit_tests_proto.c b/src/test/unit/unit_tests_proto.c index 5d86220b..4cc2ee76 100644 --- a/src/test/unit/unit_tests_proto.c +++ b/src/test/unit/unit_tests_proto.c @@ -4503,6 +4503,41 @@ START_TEST(test_wolfssh_io_send_behaviors) } END_TEST +START_TEST(test_wolfssh_io_recv_behaviors) +{ + struct wolfIP s; + struct wolfssh_io_desc desc; + char buf[4]; + int ret; + + memset(&desc, 0, sizeof(desc)); + desc.stack = &s; + desc.fd = 4; + desc.in_use = 1; + + reset_wolfssh_io_state(); + test_recv_ret = -WOLFIP_EAGAIN; + ret = wolfssh_io_recv(NULL, buf, sizeof(buf), &desc); + ck_assert_int_eq(ret, WS_CBIO_ERR_WANT_READ); + + /* -1 is the torn-down ("not established", peer RST) case from + * wolfIP_sock_recvfrom: it must be a fatal close, not a retryable + * WANT_READ, or wolfSSH spins on a dead connection and the SSH handshake + * state machine is wedged in KEY_EXCHANGE forever (unauthenticated DoS). */ + test_recv_ret = -1; + ret = wolfssh_io_recv(NULL, buf, sizeof(buf), &desc); + ck_assert_int_eq(ret, WS_CBIO_ERR_CONN_CLOSE); + + test_recv_ret = 0; + ret = wolfssh_io_recv(NULL, buf, sizeof(buf), &desc); + ck_assert_int_eq(ret, WS_CBIO_ERR_CONN_CLOSE); + + test_recv_ret = 4; + ret = wolfssh_io_recv(NULL, buf, sizeof(buf), &desc); + ck_assert_int_eq(ret, 4); +} +END_TEST + START_TEST(test_tcp_listen_rejects_wrong_interface) { struct wolfIP s; From f180cf6edbe35dba5f1169119fb99d8891199848 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 08:26:30 +0200 Subject: [PATCH 07/15] F-5735: reclaim wolfSSL io_descs slot on stm32h563 TLS teardown The stm32h563 tls_server.c example allocated a session from the static io_descs[MAX_WOLFIP_CTX] pool via wolfSSL_SetIO_wolfIP() but never released it: tls_client_free() called wolfSSL_shutdown/wolfSSL_free without first calling wolfSSL_CleanupIO_wolfIP(), so io_descs[i].stack stayed non-NULL and the slot was permanently marked in use. After MAX_WOLFIP_CTX (8) connect-then-abort cycles the pool was exhausted and every subsequent accept got a NULL IOCtx, disabling the TLS service until reboot - an unauthenticated DoS needing only a TCP handshake. F-5781 added wolfSSL_CleanupIO_wolfIP() and wired it into httpd's teardown paths, and ssh_server.c already frees its descriptor on teardown, but the stm32h563 tls_server.c call site was missed. Call wolfSSL_CleanupIO_wolfIP() before wolfSSL_free() in tls_client_free, and check the wolfSSL_SetIO_wolfIP() return value in tls_listen_cb, rejecting the accept (free SSL, close fd, release the client slot) when the pool is exhausted instead of proceeding with a NULL IOCtx. --- src/port/stm32h563/tls_server.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/port/stm32h563/tls_server.c b/src/port/stm32h563/tls_server.c index 1bb6886b..421b1595 100644 --- a/src/port/stm32h563/tls_server.c +++ b/src/port/stm32h563/tls_server.c @@ -78,6 +78,7 @@ static void tls_client_handle_data(tls_client_t *client, uint16_t event); /* External functions from wolfssl_io.c */ extern int wolfSSL_SetIO_wolfIP_CTX(WOLFSSL_CTX *ctx, struct wolfIP *s); extern int wolfSSL_SetIO_wolfIP(WOLFSSL *ssl, int fd); +extern void wolfSSL_CleanupIO_wolfIP(WOLFSSL *ssl); /* Debug output helper */ static void debug_print(const char *msg) @@ -237,6 +238,7 @@ static void tls_client_free(tls_client_t *client) { if (client->ssl) { wolfSSL_shutdown(client->ssl); + wolfSSL_CleanupIO_wolfIP(client->ssl); wolfSSL_free(client->ssl); client->ssl = NULL; } @@ -319,7 +321,14 @@ static void tls_listen_cb(int fd, uint16_t event, void *arg) } /* Associate SSL with socket */ - wolfSSL_SetIO_wolfIP(client->ssl, client_fd); + if (wolfSSL_SetIO_wolfIP(client->ssl, client_fd) != 0) { + debug_print("TLS: SetIO failed\n"); + wolfSSL_free(client->ssl); + client->ssl = NULL; + wolfIP_sock_close(server.stack, client_fd); + client->state = TLS_CLIENT_STATE_FREE; + return; + } client->fd = client_fd; client->state = TLS_CLIENT_STATE_HANDSHAKE; From ca0f88b90318709b92265ab1dc9437ff8c567fbe Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 08:29:49 +0200 Subject: [PATCH 08/15] F-5695: seed STM32H753 RNG fallback LFSR from runtime entropy When the hardware RNG fails, wolfIP_getrandom() fell back to an xorshift LFSR seeded from the compile-time constant 0x1A2B3C4D and never re-seeded, so every STM32H753 unit in the degraded state emitted the same globally known sequence (0xC9F6F11C, 0xED7A2436, ...). That makes TCP ISNs, ephemeral ports, DHCP xids and DNS ids predictable on any affected device. Seed the fallback LFSR from runtime state (DWT cycle counter plus any residual RNG_DR bits) on first use and mix the cycle counter in on each call, matching the convention already used by the lpc54s018 and va416xx ports (which use 0x1A2B3C4D only as a non-zero guard). Still not a cryptographic RNG, but no longer globally identical across devices. --- src/port/stm32h753/main.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/port/stm32h753/main.c b/src/port/stm32h753/main.c index d183e354..b75dc475 100644 --- a/src/port/stm32h753/main.c +++ b/src/port/stm32h753/main.c @@ -204,6 +204,14 @@ static int https_status_handler(struct httpd *httpd, struct http_client *hc, #define RNG_SR_CECS (1u << 1) #define RNG_SR_SECS (1u << 2) +/* Cortex-M7 DWT cycle counter (used to seed the RNG fallback with runtime + * entropy so a degraded device does not emit a globally-identical sequence) */ +#define DWT_CTRL (*(volatile uint32_t *)0xE0001000UL) +#define DWT_CYCCNT (*(volatile uint32_t *)0xE0001004UL) +#define DWT_CTRL_CYCCNTENA (1u << 0) +#define CM_DEMCR (*(volatile uint32_t *)0xE000EDFCUL) +#define CM_DEMCR_TRCENA (1u << 24) + /* USART3 for debug output (ST-Link VCP on NUCLEO-H753ZI: PD8=TX, PD9=RX) */ #define USART3_BASE 0x40004800UL #define USART3_CR1 (*(volatile uint32_t *)(USART3_BASE + 0x00)) @@ -369,8 +377,21 @@ uint32_t wolfIP_getrandom(void) uint32_t val; if (rng_get_word(&val) == 0) return val; - /* Fallback LFSR if HW RNG fails */ - static uint32_t lfsr = 0x1A2B3C4DU; + /* HW RNG failed: fall back to an xorshift LFSR seeded from runtime state + * rather than a compile-time constant, so a degraded device does not emit + * the same globally-known sequence (and thus predictable TCP ISNs) on + * every unit. The DWT cycle counter (CPU clock, free running once the + * stack is up) and any residual RNG_DR bits vary per device and power-up. + * Not a cryptographic RNG. */ + static uint32_t lfsr = 0u; + if (lfsr == 0u) { + CM_DEMCR |= CM_DEMCR_TRCENA; + DWT_CTRL |= DWT_CTRL_CYCCNTENA; + lfsr = DWT_CYCCNT ^ RNG_DR ^ 0x1A2B3C4DU; + if (lfsr == 0u) + lfsr = 0x1A2B3C4DU; /* LFSR seed must never be zero */ + } + lfsr ^= DWT_CYCCNT; /* mix in timing jitter on each call */ lfsr ^= lfsr << 13; lfsr ^= lfsr >> 17; lfsr ^= lfsr << 5; From ba3ec3e0bc4cd2eb7a35e00eea3753fcf87b32cc Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 08:35:05 +0200 Subject: [PATCH 09/15] F-5693: mix SysTick jitter into VA416xx wolfIP_getrandom wolfIP_getrandom() on the VA416xx port seeded a bijective xorshift32 LFSR once from HAL_time_ms (milliseconds since power-on) and mixed in no further entropy. The seed was therefore confined to the trivially-enumerable boot window, and because xorshift32 is bijective an on-path observer of a single output could invert the state and predict every subsequent value, including TCP ISNs, ephemeral source ports, DHCP xids and DNS ids. Mix the SysTick current-value register (a 24-bit free-running down-counter reloaded every 1ms) into both the initial seed and every call, matching the convention already used by the lpc54s018 and stm32h753 ports. This widens the initial state beyond the boot window and feeds fresh timing jitter on each call so a single observed output no longer determines the sequence. Still not a cryptographic RNG, but no longer enumerable or invertible from one observation. --- src/port/va416xx/main.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/port/va416xx/main.c b/src/port/va416xx/main.c index 568b627b..4d94e03e 100644 --- a/src/port/va416xx/main.c +++ b/src/port/va416xx/main.c @@ -83,20 +83,31 @@ static int client_fd = -1; /* wolfIP random number generator (required by stack) */ /* ========================================================================= */ +/* SysTick Current Value Register: 24-bit free-running down-counter reloaded + * every 1ms (reload = SYSCLK/1000 = 100000 at 100MHz), so each read yields + * ~17 bits of fast-varying timing jitter. */ +#define SYST_CVR (*(volatile uint32_t *)0xE000E018UL) + uint32_t wolfIP_getrandom(void) { static uint32_t lfsr; static int seeded = 0; if (!seeded) { - /* Seed from boot time so ISNs and ephemeral ports vary per power-up. - * HAL_time_ms at first wolfIP call is typically 1-5 s into boot. - * Note: not cryptographically secure; suitable for embedded demo use. */ - lfsr = (uint32_t)HAL_time_ms; + /* Seed from boot time mixed with the SysTick current value so the + * initial state is not confined to the trivially-enumerable boot + * window (HAL_time_ms at first wolfIP call is typically 1-5 s into + * boot). Note: not cryptographically secure; suitable for embedded + * demo use. */ + lfsr = (uint32_t)HAL_time_ms ^ (SYST_CVR << 8); if (lfsr == 0U) lfsr = 0x1A2B3C4DU; /* LFSR must never be zero */ seeded = 1; } + /* Mix in SysTick timing jitter each call so a single observed output + * cannot be inverted to predict subsequent ISNs/ports/xids (xorshift32 + * on its own is bijective). */ + lfsr ^= SYST_CVR; lfsr ^= lfsr << 13; lfsr ^= lfsr >> 17; lfsr ^= lfsr << 5; From 71091fcdd8762bb952ce9f58e76ae5734d81b971 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 08:41:40 +0200 Subject: [PATCH 10/15] F-5793: honour actual IHL in esp_transport_wrap for IP options esp_transport_wrap computed the payload length and ESP insertion point from the compile-time constant IP_HEADER_LEN (20), ignoring ver_ihl. For packets carrying IP options (IHL>5) -- e.g. igmp_send_report builds a Router-Alert packet with ver_ihl=0x46, and the forwarding path relays received optioned packets -- this over-counted orig_payload_len by the option length and inserted the ESP header at offset 20, overwriting the option bytes and dragging them into the ciphertext. The resulting frame is malformed (ver_ihl still claims IHL=6 while ESP starts after the fixed header) and fails AEAD/HMAC verification at a standards-compliant peer. Derive the real header length from ver_ihl and insert the ESP header after the IP header and its options (rfc4303 sec 3.1.1), keeping option bytes in the clear. Behaviour for the common IHL=5 case is unchanged. Add unit_esp regression test test_wrap_preserves_ip_options. --- src/test/unit/unit_esp.c | 73 ++++++++++++++++++++++++++++++++++++++++ src/wolfesp.c | 38 +++++++++++++-------- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/src/test/unit/unit_esp.c b/src/test/unit/unit_esp.c index 8f95e427..1e09c7d8 100644 --- a/src/test/unit/unit_esp.c +++ b/src/test/unit/unit_esp.c @@ -1505,6 +1505,78 @@ START_TEST(test_wrap_rejects_ip_len_below_header) } END_TEST +/* + * esp_transport_wrap must honour the actual IHL for packets carrying IP + * options (IHL > 5), e.g. the Router-Alert option emitted by + * igmp_send_report (ver_ihl = 0x46). rfc4303 sec 3.1.1 inserts the ESP + * header after the IP header AND its options, so option bytes must stay in + * the clear in front of the ESP header and must not be counted as payload. + * + * Regression for the IP_HEADER_LEN=20 constant: with the bug the SPI is + * written over the option bytes at ip->data[0] and the 4 option bytes are + * dragged into the (over-counted) ESP payload. + */ +START_TEST(test_wrap_preserves_ip_options) +{ + static uint8_t buf[LINK_MTU + 256]; + /* IPv4 Router-Alert option (type 0x94, len 4). */ + static const uint8_t opt[4] = { 0x94, 0x04, 0x00, 0x00 }; + uint8_t ref[32]; + struct wolfIP_ip_packet *ip = (struct wolfIP_ip_packet *)buf; + uint16_t ip_len; + uint16_t orig_ip_len; + uint32_t i; + int ret; + + for (i = 0U; i < sizeof(ref); i++) + ref[i] = (uint8_t)(0x40U + (i & 0x3FU)); + + esp_setup(); + + /* HMAC-only outbound SA so the wrapped layout stays in cleartext and is + * inspectable (iv_len == 0, no encryption). */ + ret = wolfIP_esp_sa_new_hmac(0, (uint8_t *)spi_rt, + atoip4(T_SRC), atoip4(T_DST), + ESP_AUTH_SHA256_RFC4868, k_auth16, + sizeof(k_auth16), ESP_ICVLEN_HMAC_128); + ck_assert_int_eq(ret, 0); + + /* Build an IHL=6 packet by hand: 20-byte header + 4 option bytes. */ + memset(buf, 0, sizeof(buf)); + ip->eth.type = ee16(0x0800U); + ip->ver_ihl = 0x46U; /* IPv4, 24-byte header */ + ip->tos = 0U; + orig_ip_len = (uint16_t)(IP_HEADER_LEN + sizeof(opt) + sizeof(ref)); + ip->len = ee16(orig_ip_len); + ip->ttl = 64U; + ip->proto = WI_IPPROTO_UDP; + ip->src = ee32(atoip4(T_SRC)); + ip->dst = ee32(atoip4(T_DST)); + memcpy(ip->data, opt, sizeof(opt)); + memcpy(ip->data + sizeof(opt), ref, sizeof(ref)); + ip->csum = 0U; + iphdr_set_checksum(ip); + + ip_len = orig_ip_len; + ret = esp_transport_wrap(ip, &ip_len); + ck_assert_int_eq(ret, 0); + + /* IHL must be unchanged: the IP header (incl. options) is preserved. */ + ck_assert_uint_eq(ip->ver_ihl, 0x46U); + + /* Option bytes must remain in the clear, in front of the ESP header. */ + ck_assert_mem_eq(ip->data, opt, sizeof(opt)); + + /* The ESP SPI must follow the options, not overwrite them. */ + ck_assert_mem_eq(ip->data + sizeof(opt), spi_rt, ESP_SPI_LEN); + + /* ip->len / *ip_len must be measured from the real header length. */ + ck_assert_uint_eq((uint16_t)(ip_len - (IP_HEADER_LEN + sizeof(opt))), + (uint16_t)(ee16(ip->len) - (IP_HEADER_LEN + sizeof(opt)))); + ck_assert_uint_eq(ee16(ip->len), ip_len); +} +END_TEST + START_TEST(test_ip_recv_esp_transport_delivers_udp_payload) { static uint8_t buf[LINK_MTU + 256]; @@ -2031,6 +2103,7 @@ static Suite *esp_suite(void) tc = tcase_create("no_sa"); tcase_add_test(tc, test_wrap_no_matching_sa); tcase_add_test(tc, test_wrap_rejects_ip_len_below_header); + tcase_add_test(tc, test_wrap_preserves_ip_options); suite_add_tcase(s, tc); /* TCP immediate-send ESP regression */ diff --git a/src/wolfesp.c b/src/wolfesp.c index e5e221e0..61226504 100644 --- a/src/wolfesp.c +++ b/src/wolfesp.c @@ -1581,20 +1581,30 @@ esp_transport_wrap(struct wolfIP_ip_packet *ip, uint16_t * ip_len) { uint8_t block_len = 0; uint16_t orig_ip_len = *ip_len; - uint16_t orig_payload_len = orig_ip_len - IP_HEADER_LEN; + uint16_t ip_hdr_len = (uint16_t)((ip->ver_ihl & 0x0f) * 4); + uint16_t orig_payload_len; uint16_t payload_len = 0; - uint8_t * payload = ip->data; + uint8_t * esp_base; + uint8_t * payload; uint8_t pad_len = 0; uint32_t seq_n = 0; /* sequence num in network order */ uint16_t icv_offset = 0; wolfIP_esp_sa * esp_sa = NULL; uint8_t iv_len = 0; - if (orig_ip_len < IP_HEADER_LEN) { + if (ip_hdr_len < IP_HEADER_LEN || orig_ip_len < ip_hdr_len) { ESP_LOG("error: ip_len below header: %u\n", orig_ip_len); return -1; } + /* rfc4303 sec 3.1.1: in transport mode the ESP header is inserted after + * the IP header AND any options it carries. Use the actual IHL so option + * bytes (e.g. igmp_send_report's Router-Alert, ver_ihl=0x46) stay in the + * clear in front of the ESP header and are not counted as payload. */ + orig_payload_len = (uint16_t)(orig_ip_len - ip_hdr_len); + esp_base = ip->data + (ip_hdr_len - IP_HEADER_LEN); + payload = esp_base; + /* todo: priority, proto / port filtering. currently this grabs * the first dst and src match. */ for (size_t i = 0; i < out_sa_num; ++i) { @@ -1629,8 +1639,8 @@ esp_transport_wrap(struct wolfIP_ip_packet *ip, uint16_t * ip_len) iv_len = esp_iv_len_from_enc(esp_sa->enc); /* move ip payload back to make room for ESP header (SPI, SEQ) + IV. */ - memmove(ip->data + ESP_SPI_LEN + ESP_SEQ_LEN + iv_len, - ip->data, orig_payload_len); + memmove(esp_base + ESP_SPI_LEN + ESP_SEQ_LEN + iv_len, + esp_base, orig_payload_len); /* Copy in SPI and sequence number fields. */ memcpy(payload, esp_sa->spi, sizeof(esp_sa->spi)); @@ -1704,7 +1714,7 @@ esp_transport_wrap(struct wolfIP_ip_packet *ip, uint16_t * ip_len) payload += ESP_NEXT_HEADER_LEN; /* calculate final esp payload length. */ - payload_len = orig_ip_len - IP_HEADER_LEN; + payload_len = orig_payload_len; payload_len += ESP_SPI_LEN + ESP_SEQ_LEN + iv_len + pad_len + ESP_PADDING_LEN + ESP_NEXT_HEADER_LEN + esp_sa->icv_len; @@ -1716,22 +1726,22 @@ esp_transport_wrap(struct wolfIP_ip_packet *ip, uint16_t * ip_len) switch(esp_sa->enc) { #ifndef NO_DES3 case ESP_ENC_CBC_DES3: - err = esp_des3_rfc2451_enc(esp_sa, ip->data, payload_len); + err = esp_des3_rfc2451_enc(esp_sa, esp_base, payload_len); break; #endif /* !NO_DES3 */ case ESP_ENC_CBC_AES: - err = esp_aes_rfc3602_enc(esp_sa, ip->data, payload_len); + err = esp_aes_rfc3602_enc(esp_sa, esp_base, payload_len); break; #if defined(WOLFSSL_AESGCM_STREAM) case ESP_ENC_GCM_RFC4106: - err = esp_aes_rfc4106_enc(esp_sa, ip->data, payload_len); + err = esp_aes_rfc4106_enc(esp_sa, esp_base, payload_len); break; #endif /*WOLFSSL_AESGCM_STREAM */ case ESP_ENC_GCM_RFC4543: - err = esp_aes_rfc4543_enc(esp_sa, ip->data, payload_len); + err = esp_aes_rfc4543_enc(esp_sa, esp_base, payload_len); break; case ESP_ENC_NONE: @@ -1759,8 +1769,8 @@ esp_transport_wrap(struct wolfIP_ip_packet *ip, uint16_t * ip_len) uint8_t * icv = NULL; byte hash[WC_SHA256_DIGEST_SIZE]; memset(hash, 0, sizeof(hash)); - icv = ip->data + icv_offset; - err = esp_calc_icv_hmac(hash, esp_sa, ip->data, payload_len); + icv = esp_base + icv_offset; + err = esp_calc_icv_hmac(hash, esp_sa, esp_base, payload_len); if (err == 0) { memcpy(icv, hash, esp_sa->icv_len); } @@ -1785,10 +1795,10 @@ esp_transport_wrap(struct wolfIP_ip_packet *ip, uint16_t * ip_len) } } - *ip_len = payload_len + IP_HEADER_LEN; + *ip_len = payload_len + ip_hdr_len; #ifdef DEBUG_ESP - wolfIP_print_esp(esp_sa, ip->data, payload_len, pad_len, ip->proto); + wolfIP_print_esp(esp_sa, esp_base, payload_len, pad_len, ip->proto); #endif /* DEBUG_ESP */ /* update len, set proto to ESP 0x32 (50), recalculate iphdr checksum. */ From f1cae4ab20b84a2eb2fc11ad4dfc932b692fb013 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 08:46:57 +0200 Subject: [PATCH 11/15] F-5732: send TLS close_notify before tearing down httpd sessions Every connection-close path in httpd.c freed the wolfSSL session without a preceding wolfSSL_shutdown(), so the close_notify alert required by RFC 5246 7.2.1 was never emitted, leaving HTTPS responses open to truncation attacks (CWE-325). Add wolfSSL_shutdown() before wolfSSL_CleanupIO_wolfIP()/wolfSSL_free() at all five close sites, matching the teardown sequence already used in tls_server.c, tls_client.c, mqtt_client.c and https_server_freertos.c. Add test-http-close-notify, which #includes httpd.c with stubbed wolfSSL teardown calls and asserts close_notify is sent before IO cleanup/free on each close path. --- Makefile | 10 ++ src/http/httpd.c | 5 + src/test/test_http_close_notify.c | 157 ++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 src/test/test_http_close_notify.c diff --git a/Makefile b/Makefile index f19704e6..96fe791f 100644 --- a/Makefile +++ b/Makefile @@ -183,6 +183,7 @@ EXE=build/tcpecho build/tcp_netcat_poll build/tcp_netcat_select \ build/test-evloop build/test-dns build/test-wolfssl-forwarding \ build/test-ttl-expired build/test-wolfssl build/test-httpd \ build/test-http-smuggle build/test-http-arg-oob \ + build/test-http-close-notify \ build/test-posix-errno \ build/ipfilter-logger \ build/test-esp build/esp-server @@ -423,6 +424,15 @@ build/test-http-arg-oob: src/test/test_http_arg_oob.c src/http/httpd.c @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ src/test/test_http_arg_oob.c $(LDFLAGS) -lwolfssl +# Standalone regression test for TLS close_notify on every close path (F-5732). +# It #includes httpd.c directly and stubs the wolfSSL teardown calls to record +# their order, so it does not link the real wolfSSL library. +build/test-http-close-notify:CFLAGS+=-Wno-cpp -DWOLFSSL_DEBUG -DWOLFSSL_WOLFIP -DWOLFIP_ENABLE_HTTP -Isrc/http +build/test-http-close-notify: src/test/test_http_close_notify.c src/http/httpd.c + @mkdir -p build || true + @echo "[LD] $@" + @$(CC) $(CFLAGS) -o $@ src/test/test_http_close_notify.c $(LDFLAGS) + build/%.o: src/%.c @mkdir -p `dirname $@` || true @echo "[CC] $<" diff --git a/src/http/httpd.c b/src/http/httpd.c index 6f864271..58dd0bcf 100644 --- a/src/http/httpd.c +++ b/src/http/httpd.c @@ -133,6 +133,7 @@ void http_send_response_headers(struct http_client *hc, int status_code, const c } if (rc <= 0) { /* Error – close connection */ + wolfSSL_shutdown(hc->ssl); wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; @@ -151,6 +152,7 @@ void http_send_response_body(struct http_client *hc, const void *body, size_t le rc = wolfIP_sock_send(hc->httpd->ipstack, hc->client_sd, body, len, 0); if (rc <= 0) { + wolfSSL_shutdown(hc->ssl); wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; @@ -180,6 +182,7 @@ void http_send_response_chunk(struct http_client *hc, const void *chunk, size_t if ((http_write_response(hc, txt_chunk, strlen(txt_chunk)) <= 0) || (http_write_response(hc, chunk, len) <= 0) || (http_write_response(hc, "\r\n", 2) <= 0)) { + wolfSSL_shutdown(hc->ssl); wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; @@ -197,6 +200,7 @@ void http_send_response_chunk_end(struct http_client *hc) { else rc = wolfIP_sock_send(hc->httpd->ipstack, hc->client_sd, "0\r\n\r\n", 5, 0); if (rc <= 0) { + wolfSSL_shutdown(hc->ssl); wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; @@ -488,6 +492,7 @@ static void http_recv_cb(int sd, uint16_t event, void *arg) { fail_close: if (hc->ssl) { + wolfSSL_shutdown(hc->ssl); wolfSSL_CleanupIO_wolfIP(hc->ssl); wolfSSL_free(hc->ssl); hc->ssl = NULL; diff --git a/src/test/test_http_close_notify.c b/src/test/test_http_close_notify.c new file mode 100644 index 00000000..c46a89cb --- /dev/null +++ b/src/test/test_http_close_notify.c @@ -0,0 +1,157 @@ +/* test_http_close_notify.c + * + * Copyright (C) 2024 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + * + * + * Regression test for F-5732: every TLS connection-close path in httpd.c must + * send the close_notify alert (wolfSSL_shutdown) before tearing the session + * down (wolfSSL_CleanupIO_wolfIP / wolfSSL_free). Skipping close_notify lets a + * network-adjacent attacker truncate an HTTPS response indistinguishably from a + * legitimate close (RFC 5246 7.2.1 / CWE-325). These stubs record call order so + * the test fails if any close site frees the session without a prior shutdown. + */ + +#include +#include +#include + +/* Pull in the unit under test directly; the close paths are not exported. */ +#include "httpd.c" + +/* --- ordered call tracking ----------------------------------------------- */ +static int seq; +static int shutdown_seq; +static int cleanup_seq; +static int free_seq; + +static void reset_tracking(void) +{ + seq = 0; + shutdown_seq = 0; + cleanup_seq = 0; + free_seq = 0; +} + +/* --- stubs for the wolfIP symbols referenced by httpd.c ------------------ */ +int wolfIP_sock_socket(struct wolfIP *s, int d, int t, int p) +{ (void)s; (void)d; (void)t; (void)p; return -1; } +int wolfIP_sock_bind(struct wolfIP *s, int fd, const struct wolfIP_sockaddr *a, socklen_t l) +{ (void)s; (void)fd; (void)a; (void)l; return -1; } +int wolfIP_sock_listen(struct wolfIP *s, int fd, int b) +{ (void)s; (void)fd; (void)b; return -1; } +int wolfIP_sock_accept(struct wolfIP *s, int fd, struct wolfIP_sockaddr *a, socklen_t *l) +{ (void)s; (void)fd; (void)a; (void)l; return -1; } +int wolfIP_sock_send(struct wolfIP *s, int fd, const void *b, size_t l, int f) +{ (void)s; (void)fd; (void)b; (void)f; return (int)l; } +int wolfIP_sock_recv(struct wolfIP *s, int fd, void *b, size_t l, int f) +{ (void)s; (void)fd; (void)b; (void)l; (void)f; return -1; } +int wolfIP_sock_close(struct wolfIP *s, int fd) +{ (void)s; (void)fd; return 0; } +void wolfIP_register_callback(struct wolfIP *s, int fd, tsocket_cb cb, void *arg) +{ (void)s; (void)fd; (void)cb; (void)arg; } + +/* --- stubs for the wolfSSL symbols referenced by httpd.c ----------------- */ +WOLFSSL *wolfSSL_new(WOLFSSL_CTX *ctx) +{ (void)ctx; return NULL; } +int wolfSSL_SetIO_wolfIP(WOLFSSL *ssl, int fd) +{ (void)ssl; (void)fd; return 0; } +int wolfSSL_SetIO_wolfIP_CTX(WOLFSSL_CTX *ctx, struct wolfIP *s) +{ (void)ctx; (void)s; return 0; } +/* All I/O fails, forcing every send/recv onto its error-close path. */ +int wolfSSL_write(WOLFSSL *ssl, const void *data, int sz) +{ (void)ssl; (void)data; (void)sz; return -1; } +int wolfSSL_read(WOLFSSL *ssl, void *data, int sz) +{ (void)ssl; (void)data; (void)sz; return -1; } +int wolfSSL_get_error(WOLFSSL *ssl, int ret) +{ (void)ssl; (void)ret; return 0; /* not WANT_READ -> close */ } +int wolfSSL_shutdown(WOLFSSL *ssl) +{ (void)ssl; shutdown_seq = ++seq; return 0; } +void wolfSSL_CleanupIO_wolfIP(WOLFSSL *ssl) +{ (void)ssl; cleanup_seq = ++seq; } +void wolfSSL_free(WOLFSSL *ssl) +{ (void)ssl; free_seq = ++seq; } + +/* --- test harness -------------------------------------------------------- */ +#define CHECK(cond) do { if (!(cond)) { \ + printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); failures++; } } while (0) + +static int failures; +static int ssl_marker; /* opaque non-NULL session handle */ + +static void check_close_emitted_close_notify(const char *site) +{ + /* close_notify must precede session/IO teardown on every close path. */ + if (shutdown_seq == 0) { + printf("FAIL %s: wolfSSL_shutdown (close_notify) never called\n", site); + failures++; + return; + } + CHECK(shutdown_seq < cleanup_seq); /* shutdown before IO cleanup */ + CHECK(shutdown_seq < free_seq); /* shutdown before free */ +} + +int main(void) +{ + struct httpd httpd; + struct http_client hc; + + failures = 0; + memset(&httpd, 0, sizeof(httpd)); + + /* http_send_response_headers error close */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = (WOLFSSL *)&ssl_marker; + http_send_response_headers(&hc, 200, "OK", "text/plain", 0); + check_close_emitted_close_notify("http_send_response_headers"); + + /* http_send_response_body error close */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = (WOLFSSL *)&ssl_marker; + http_send_response_body(&hc, "x", 1); + check_close_emitted_close_notify("http_send_response_body"); + + /* http_send_response_chunk error close */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = (WOLFSSL *)&ssl_marker; + http_send_response_chunk(&hc, "x", 1); + check_close_emitted_close_notify("http_send_response_chunk"); + + /* http_send_response_chunk_end error close */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = (WOLFSSL *)&ssl_marker; + http_send_response_chunk_end(&hc); + check_close_emitted_close_notify("http_send_response_chunk_end"); + + /* http_recv_cb fail_close (read error) */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = (WOLFSSL *)&ssl_marker; + http_recv_cb(1, 0, &hc); + check_close_emitted_close_notify("http_recv_cb fail_close"); + + if (failures == 0) + printf("test_http_close_notify: all checks passed\n"); + else + printf("test_http_close_notify: %d check(s) failed\n", failures); + return failures ? 1 : 0; +} From 88d76e7314e13ede06ad372f335d82eeddaa20f9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 09:03:21 +0200 Subject: [PATCH 12/15] Addressed copilot comments (same cause as failing http test) --- src/http/httpd.c | 54 ++++++++++++---------------- src/test/test_http_close_notify.c | 58 ++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 36 deletions(-) diff --git a/src/http/httpd.c b/src/http/httpd.c index 58dd0bcf..47c1b4b2 100644 --- a/src/http/httpd.c +++ b/src/http/httpd.c @@ -106,6 +106,8 @@ static struct http_url *http_find_url(struct httpd *httpd, const char *path) { return NULL; } +static void http_close_client(struct http_client *hc); + void http_send_response_headers(struct http_client *hc, int status_code, const char *status_text, const char *content_type, size_t content_length) { char txt_response[HTTP_TX_BUF_LEN]; @@ -133,12 +135,7 @@ void http_send_response_headers(struct http_client *hc, int status_code, const c } if (rc <= 0) { /* Error – close connection */ - wolfSSL_shutdown(hc->ssl); - wolfSSL_CleanupIO_wolfIP(hc->ssl); - wolfSSL_free(hc->ssl); - hc->ssl = NULL; - wolfIP_sock_close(hc->httpd->ipstack, hc->client_sd); - hc->client_sd = 0; + http_close_client(hc); } } @@ -152,12 +149,7 @@ void http_send_response_body(struct http_client *hc, const void *body, size_t le rc = wolfIP_sock_send(hc->httpd->ipstack, hc->client_sd, body, len, 0); if (rc <= 0) { - wolfSSL_shutdown(hc->ssl); - wolfSSL_CleanupIO_wolfIP(hc->ssl); - wolfSSL_free(hc->ssl); - hc->ssl = NULL; - wolfIP_sock_close(hc->httpd->ipstack, hc->client_sd); - hc->client_sd = 0; + http_close_client(hc); } } @@ -173,6 +165,21 @@ static int http_write_response(struct http_client *hc, const void *buf, size_t l return wolfIP_sock_send(s, hc->client_sd, buf, len, 0); } +static void http_close_client(struct http_client *hc) +{ + if (!hc) + return; + + if (hc->ssl) { + wolfSSL_shutdown(hc->ssl); + wolfSSL_CleanupIO_wolfIP(hc->ssl); + wolfSSL_free(hc->ssl); + hc->ssl = NULL; + } + wolfIP_sock_close(hc->httpd->ipstack, hc->client_sd); + hc->client_sd = 0; +} + void http_send_response_chunk(struct http_client *hc, const void *chunk, size_t len) { char txt_chunk[8]; memset(txt_chunk, 0, sizeof(txt_chunk)); @@ -182,12 +189,7 @@ void http_send_response_chunk(struct http_client *hc, const void *chunk, size_t if ((http_write_response(hc, txt_chunk, strlen(txt_chunk)) <= 0) || (http_write_response(hc, chunk, len) <= 0) || (http_write_response(hc, "\r\n", 2) <= 0)) { - wolfSSL_shutdown(hc->ssl); - wolfSSL_CleanupIO_wolfIP(hc->ssl); - wolfSSL_free(hc->ssl); - hc->ssl = NULL; - wolfIP_sock_close(hc->httpd->ipstack, hc->client_sd); - hc->client_sd = 0; + http_close_client(hc); } } @@ -200,12 +202,7 @@ void http_send_response_chunk_end(struct http_client *hc) { else rc = wolfIP_sock_send(hc->httpd->ipstack, hc->client_sd, "0\r\n\r\n", 5, 0); if (rc <= 0) { - wolfSSL_shutdown(hc->ssl); - wolfSSL_CleanupIO_wolfIP(hc->ssl); - wolfSSL_free(hc->ssl); - hc->ssl = NULL; - wolfIP_sock_close(hc->httpd->ipstack, hc->client_sd); - hc->client_sd = 0; + http_close_client(hc); } } @@ -491,13 +488,7 @@ static void http_recv_cb(int sd, uint16_t event, void *arg) { return; fail_close: - if (hc->ssl) { - wolfSSL_shutdown(hc->ssl); - wolfSSL_CleanupIO_wolfIP(hc->ssl); - wolfSSL_free(hc->ssl); - hc->ssl = NULL; - } - wolfIP_sock_close(hc->httpd->ipstack, sd); + http_close_client(hc); /* wolfIP_sock_close on an ESTABLISHED socket only starts the active close * (FIN_WAIT_1) and returns -EAGAIN; the socket lingers, still carrying this * callback and its arg. Once we zero client_sd the slot is reused by the @@ -505,7 +496,6 @@ static void http_recv_cb(int sd, uint16_t event, void *arg) { * different live connection's state. Deregister it here so a late segment * on the half-closed socket can no longer fire http_recv_cb. */ wolfIP_register_callback(hc->httpd->ipstack, sd, NULL, NULL); - hc->client_sd = 0; } static void http_accept_cb(int sd, uint16_t event, void *arg) { diff --git a/src/test/test_http_close_notify.c b/src/test/test_http_close_notify.c index c46a89cb..c506001f 100644 --- a/src/test/test_http_close_notify.c +++ b/src/test/test_http_close_notify.c @@ -39,6 +39,8 @@ static int seq; static int shutdown_seq; static int cleanup_seq; static int free_seq; +static int tls_teardown_calls; +static int sock_send_result; static void reset_tracking(void) { @@ -46,6 +48,8 @@ static void reset_tracking(void) shutdown_seq = 0; cleanup_seq = 0; free_seq = 0; + tls_teardown_calls = 0; + sock_send_result = -1; } /* --- stubs for the wolfIP symbols referenced by httpd.c ------------------ */ @@ -58,7 +62,7 @@ int wolfIP_sock_listen(struct wolfIP *s, int fd, int b) int wolfIP_sock_accept(struct wolfIP *s, int fd, struct wolfIP_sockaddr *a, socklen_t *l) { (void)s; (void)fd; (void)a; (void)l; return -1; } int wolfIP_sock_send(struct wolfIP *s, int fd, const void *b, size_t l, int f) -{ (void)s; (void)fd; (void)b; (void)f; return (int)l; } +{ (void)s; (void)fd; (void)b; (void)l; (void)f; return sock_send_result; } int wolfIP_sock_recv(struct wolfIP *s, int fd, void *b, size_t l, int f) { (void)s; (void)fd; (void)b; (void)l; (void)f; return -1; } int wolfIP_sock_close(struct wolfIP *s, int fd) @@ -81,11 +85,11 @@ int wolfSSL_read(WOLFSSL *ssl, void *data, int sz) int wolfSSL_get_error(WOLFSSL *ssl, int ret) { (void)ssl; (void)ret; return 0; /* not WANT_READ -> close */ } int wolfSSL_shutdown(WOLFSSL *ssl) -{ (void)ssl; shutdown_seq = ++seq; return 0; } +{ (void)ssl; tls_teardown_calls++; shutdown_seq = ++seq; return 0; } void wolfSSL_CleanupIO_wolfIP(WOLFSSL *ssl) -{ (void)ssl; cleanup_seq = ++seq; } +{ (void)ssl; tls_teardown_calls++; cleanup_seq = ++seq; } void wolfSSL_free(WOLFSSL *ssl) -{ (void)ssl; free_seq = ++seq; } +{ (void)ssl; tls_teardown_calls++; free_seq = ++seq; } /* --- test harness -------------------------------------------------------- */ #define CHECK(cond) do { if (!(cond)) { \ @@ -106,6 +110,17 @@ static void check_close_emitted_close_notify(const char *site) CHECK(shutdown_seq < free_seq); /* shutdown before free */ } +static void check_plain_http_close_skips_tls(const char *site, + struct http_client *hc) +{ + if (tls_teardown_calls != 0) { + printf("FAIL %s: plain HTTP close called wolfSSL teardown\n", site); + failures++; + } + CHECK(hc->ssl == NULL); + CHECK(hc->client_sd == 0); +} + int main(void) { struct httpd httpd; @@ -149,6 +164,41 @@ int main(void) http_recv_cb(1, 0, &hc); check_close_emitted_close_notify("http_recv_cb fail_close"); + /* http_send_response_headers plain HTTP close must not touch wolfSSL */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = NULL; + http_send_response_headers(&hc, 200, "OK", "text/plain", 0); + check_plain_http_close_skips_tls("http_send_response_headers plain", &hc); + + /* http_send_response_body plain HTTP close must not touch wolfSSL */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = NULL; + http_send_response_body(&hc, "x", 1); + check_plain_http_close_skips_tls("http_send_response_body plain", &hc); + + /* http_send_response_chunk plain HTTP close must not touch wolfSSL */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = NULL; + http_send_response_chunk(&hc, "x", 1); + check_plain_http_close_skips_tls("http_send_response_chunk plain", &hc); + + /* http_send_response_chunk_end plain HTTP close must not touch wolfSSL */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = NULL; + http_send_response_chunk_end(&hc); + check_plain_http_close_skips_tls("http_send_response_chunk_end plain", &hc); + + /* http_recv_cb plain HTTP close must not touch wolfSSL */ + reset_tracking(); + memset(&hc, 0, sizeof(hc)); + hc.httpd = &httpd; hc.client_sd = 1; hc.ssl = NULL; + http_recv_cb(1, 0, &hc); + check_plain_http_close_skips_tls("http_recv_cb fail_close plain", &hc); + if (failures == 0) printf("test_http_close_notify: all checks passed\n"); else From 26c5f5c83c8c688f0e4c74002e5fa2278b917d17 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 09:32:41 +0200 Subject: [PATCH 13/15] Fix regression: hardfault on close --- Makefile | 8 ++++++++ src/port/freeRTOS/bsd_socket.c | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/Makefile b/Makefile index 96fe791f..77057614 100644 --- a/Makefile +++ b/Makefile @@ -184,6 +184,7 @@ EXE=build/tcpecho build/tcp_netcat_poll build/tcp_netcat_select \ build/test-ttl-expired build/test-wolfssl build/test-httpd \ build/test-http-smuggle build/test-http-arg-oob \ build/test-http-close-notify \ + build/test-freertos-close-last-ack \ build/test-posix-errno \ build/ipfilter-logger \ build/test-esp build/esp-server @@ -433,6 +434,13 @@ build/test-http-close-notify: src/test/test_http_close_notify.c src/http/httpd.c @echo "[LD] $@" @$(CC) $(CFLAGS) -o $@ src/test/test_http_close_notify.c $(LDFLAGS) +# Standalone regression test for the FreeRTOS BSD close() wrapper when +# CB_EVENT_CLOSED is delivered synchronously during LAST_ACK teardown. +build/test-freertos-close-last-ack: src/test/test_freertos_close_last_ack.c src/port/freeRTOS/bsd_socket.c + @mkdir -p build || true + @echo "[LD] $@" + @$(CC) -Isrc/test/freertos_mocks $(CFLAGS) -o $@ src/test/test_freertos_close_last_ack.c $(LDFLAGS) + build/%.o: src/%.c @mkdir -p `dirname $@` || true @echo "[CC] $<" diff --git a/src/port/freeRTOS/bsd_socket.c b/src/port/freeRTOS/bsd_socket.c index 60151b4c..1b455b22 100644 --- a/src/port/freeRTOS/bsd_socket.c +++ b/src/port/freeRTOS/bsd_socket.c @@ -50,6 +50,7 @@ typedef struct { int internal_fd; SemaphoreHandle_t ready_sem; volatile uint16_t wait_events; + volatile uint16_t seen_events; } wolfip_bsd_fd_entry; static struct wolfIP *g_ipstack; @@ -131,6 +132,7 @@ static int wolfip_bsd_fd_alloc(int internal_fd) g_fds[i].internal_fd = internal_fd; g_fds[i].ready_sem = sem; g_fds[i].wait_events = 0; + g_fds[i].seen_events = 0; return i; } } @@ -148,6 +150,7 @@ static void wolfip_bsd_fd_free(int public_fd) g_fds[public_fd].internal_fd = -1; g_fds[public_fd].ready_sem = NULL; g_fds[public_fd].wait_events = 0; + g_fds[public_fd].seen_events = 0; } static void wolfip_bsd_socket_cb(int internal_fd, uint16_t events, void *arg) @@ -158,6 +161,7 @@ static void wolfip_bsd_socket_cb(int internal_fd, uint16_t events, void *arg) if (entry == NULL) { return; } + entry->seen_events |= events; g_cb_log_count++; if ((events & CB_EVENT_CLOSED) != 0u || (g_cb_log_count & 0x1Fu) == 0u) { printf("[sock_cb] ifd=%d events=0x%04x wait=0x%04x cb_count=%lu\n", @@ -173,6 +177,7 @@ static void wolfip_bsd_socket_cb(int internal_fd, uint16_t events, void *arg) static void wolfip_bsd_prepare_wait_locked(wolfip_bsd_fd_entry *entry, uint16_t wait_events) { + entry->seen_events = 0; entry->wait_events = wait_events; while (xSemaphoreTake(entry->ready_sem, 0) == pdTRUE) { } @@ -655,6 +660,17 @@ int close(int sockfd) xSemaphoreGive(g_lock); return ret; } + if ((ret == -1) && IS_SOCKET_TCP(entry->internal_fd) && + ((entry->seen_events & CB_EVENT_CLOSED) != 0u)) { + /* The TCP core can destroy the socket immediately after delivering + * CB_EVENT_CLOSED (e.g. final ACK in LAST_ACK), so the retry sees + * the already-zeroed descriptor and wolfIP_sock_close() returns -1. + * Treat that as a completed close and release the wrapper slot. */ + wolfIP_register_callback(g_ipstack, entry->internal_fd, NULL, NULL); + wolfip_bsd_fd_free(sockfd); + xSemaphoreGive(g_lock); + return 0; + } if (ret != -WOLFIP_EAGAIN) { xSemaphoreGive(g_lock); wolfip_bsd_set_error(ret); From 7a2c157a002cc93a6ee9c2f05059f12ba4d76687 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 09:33:27 +0200 Subject: [PATCH 14/15] Added freeRTOS mocks for standalone test --- src/test/freertos_mocks/FreeRTOS.h | 17 ++ src/test/freertos_mocks/config.h | 3 + src/test/freertos_mocks/semphr.h | 14 ++ src/test/freertos_mocks/task.h | 15 ++ src/test/freertos_mocks/wolfip.h | 58 ++++++ src/test/test_freertos_close_last_ack.c | 256 ++++++++++++++++++++++++ 6 files changed, 363 insertions(+) create mode 100644 src/test/freertos_mocks/FreeRTOS.h create mode 100644 src/test/freertos_mocks/config.h create mode 100644 src/test/freertos_mocks/semphr.h create mode 100644 src/test/freertos_mocks/task.h create mode 100644 src/test/freertos_mocks/wolfip.h create mode 100644 src/test/test_freertos_close_last_ack.c diff --git a/src/test/freertos_mocks/FreeRTOS.h b/src/test/freertos_mocks/FreeRTOS.h new file mode 100644 index 00000000..5c5bf8de --- /dev/null +++ b/src/test/freertos_mocks/FreeRTOS.h @@ -0,0 +1,17 @@ +#ifndef TEST_FREERTOS_H +#define TEST_FREERTOS_H + +#include + +typedef int BaseType_t; +typedef unsigned int UBaseType_t; +typedef uint32_t TickType_t; + +#define pdTRUE 1 +#define pdFALSE 0 +#define pdPASS 1 +#define portMAX_DELAY ((TickType_t)0xffffffffu) +#define portTICK_PERIOD_MS 1u +#define pdMS_TO_TICKS(ms) ((TickType_t)(ms)) + +#endif diff --git a/src/test/freertos_mocks/config.h b/src/test/freertos_mocks/config.h new file mode 100644 index 00000000..8290bb8b --- /dev/null +++ b/src/test/freertos_mocks/config.h @@ -0,0 +1,3 @@ +#ifndef TEST_FREERTOS_CONFIG_H +#define TEST_FREERTOS_CONFIG_H +#endif diff --git a/src/test/freertos_mocks/semphr.h b/src/test/freertos_mocks/semphr.h new file mode 100644 index 00000000..a9d17b85 --- /dev/null +++ b/src/test/freertos_mocks/semphr.h @@ -0,0 +1,14 @@ +#ifndef TEST_SEMPHR_H +#define TEST_SEMPHR_H + +#include "FreeRTOS.h" + +typedef struct MockSemaphore *SemaphoreHandle_t; + +SemaphoreHandle_t xSemaphoreCreateBinary(void); +SemaphoreHandle_t xSemaphoreCreateMutex(void); +BaseType_t xSemaphoreTake(SemaphoreHandle_t sem, TickType_t ticks); +BaseType_t xSemaphoreGive(SemaphoreHandle_t sem); +void vSemaphoreDelete(SemaphoreHandle_t sem); + +#endif diff --git a/src/test/freertos_mocks/task.h b/src/test/freertos_mocks/task.h new file mode 100644 index 00000000..c25d2994 --- /dev/null +++ b/src/test/freertos_mocks/task.h @@ -0,0 +1,15 @@ +#ifndef TEST_TASK_H +#define TEST_TASK_H + +#include "FreeRTOS.h" + +typedef void (*TaskFunction_t)(void *); +typedef void * TaskHandle_t; + +BaseType_t xTaskCreate(TaskFunction_t task, const char *name, + uint16_t stack_words, void *arg, UBaseType_t priority, TaskHandle_t *handle); +void vTaskDelay(TickType_t ticks); +TickType_t xTaskGetTickCount(void); +void vTaskDelete(TaskHandle_t handle); + +#endif diff --git a/src/test/freertos_mocks/wolfip.h b/src/test/freertos_mocks/wolfip.h new file mode 100644 index 00000000..0c71b985 --- /dev/null +++ b/src/test/freertos_mocks/wolfip.h @@ -0,0 +1,58 @@ +#ifndef TEST_FREERTOS_WOLFIP_H +#define TEST_FREERTOS_WOLFIP_H + +#include +#include + +typedef uint32_t socklen_t; + +struct wolfIP { + int dummy; +}; + +struct wolfIP_sockaddr { + uint16_t sa_family; + char sa_data[14]; +}; + +typedef void (*tsocket_cb)(int fd, uint16_t event, void *arg); + +#define AF_INET 2 +#define IPSTACK_SOCK_STREAM 1 +#define IPSTACK_SOCK_DGRAM 2 + +#define WOLFIP_EAGAIN 11 +#define WOLFIP_EINVAL 22 +#define WOLFIP_ENOMEM 12 + +#define MARK_TCP_SOCKET 0x100 +#define IS_SOCKET_TCP(fd) (((fd) & MARK_TCP_SOCKET) == MARK_TCP_SOCKET) + +#define CB_EVENT_READABLE 0x0001 +#define CB_EVENT_WRITABLE 0x0002 +#define CB_EVENT_CLOSED 0x0004 + +int wolfIP_poll(struct wolfIP *ipstack, uint64_t now_ms); +int wolfIP_sock_socket(struct wolfIP *s, int domain, int type, int protocol); +int wolfIP_sock_bind(struct wolfIP *s, int fd, const struct wolfIP_sockaddr *addr, socklen_t len); +int wolfIP_sock_listen(struct wolfIP *s, int fd, int backlog); +int wolfIP_sock_accept(struct wolfIP *s, int fd, struct wolfIP_sockaddr *addr, socklen_t *len); +int wolfIP_sock_connect(struct wolfIP *s, int fd, const struct wolfIP_sockaddr *addr, socklen_t len); +int wolfIP_sock_send(struct wolfIP *s, int fd, const void *buf, size_t len, int flags); +int wolfIP_sock_sendto(struct wolfIP *s, int fd, const void *buf, size_t len, int flags, + const struct wolfIP_sockaddr *dest_addr, socklen_t len2); +int wolfIP_sock_recv(struct wolfIP *s, int fd, void *buf, size_t len, int flags); +int wolfIP_sock_recvfrom(struct wolfIP *s, int fd, void *buf, size_t len, int flags, + struct wolfIP_sockaddr *src_addr, socklen_t *len2); +int wolfIP_sock_setsockopt(struct wolfIP *s, int fd, int level, int optname, + const void *optval, socklen_t optlen); +int wolfIP_sock_getsockopt(struct wolfIP *s, int fd, int level, int optname, + void *optval, socklen_t *optlen); +int wolfIP_sock_getsockname(struct wolfIP *s, int fd, struct wolfIP_sockaddr *addr, socklen_t *len); +int wolfIP_sock_getpeername(struct wolfIP *s, int fd, struct wolfIP_sockaddr *addr, socklen_t *len); +int wolfIP_sock_can_write(struct wolfIP *s, int fd); +int wolfIP_sock_can_read(struct wolfIP *s, int fd); +int wolfIP_sock_close(struct wolfIP *s, int fd); +void wolfIP_register_callback(struct wolfIP *s, int fd, tsocket_cb cb, void *arg); + +#endif diff --git a/src/test/test_freertos_close_last_ack.c b/src/test/test_freertos_close_last_ack.c new file mode 100644 index 00000000..da17c804 --- /dev/null +++ b/src/test/test_freertos_close_last_ack.c @@ -0,0 +1,256 @@ +/* Regression test for the FreeRTOS BSD close() wrapper when the core delivers + * CB_EVENT_CLOSED synchronously during LAST_ACK teardown. */ + +#include +#include +#include + +#include "FreeRTOS.h" +#include "semphr.h" +#include "task.h" +#include "wolfip.h" + +struct MockSemaphore { + int count; +}; + +static int sem_allocs; +static int sem_frees; +static tsocket_cb registered_cb; +static void *registered_arg; +static int fake_internal_fd = MARK_TCP_SOCKET; +static int close_calls; + +SemaphoreHandle_t xSemaphoreCreateBinary(void) +{ + struct MockSemaphore *sem = calloc(1, sizeof(*sem)); + if (sem != NULL) + sem_allocs++; + return sem; +} + +SemaphoreHandle_t xSemaphoreCreateMutex(void) +{ + return xSemaphoreCreateBinary(); +} + +BaseType_t xSemaphoreTake(SemaphoreHandle_t sem, TickType_t ticks) +{ + if (sem == NULL) + return pdFALSE; + if (ticks == portMAX_DELAY && sem->count == 0 && registered_cb != NULL) { + tsocket_cb cb = registered_cb; + void *arg = registered_arg; + + registered_cb = NULL; + registered_arg = NULL; + cb(fake_internal_fd, CB_EVENT_CLOSED, arg); + } + if (sem->count > 0) { + sem->count--; + return pdTRUE; + } + return pdFALSE; +} + +BaseType_t xSemaphoreGive(SemaphoreHandle_t sem) +{ + if (sem == NULL) + return pdFALSE; + sem->count++; + return pdTRUE; +} + +void vSemaphoreDelete(SemaphoreHandle_t sem) +{ + if (sem != NULL) { + sem_frees++; + free(sem); + } +} + +BaseType_t xTaskCreate(TaskFunction_t task, const char *name, + uint16_t stack_words, void *arg, UBaseType_t priority, TaskHandle_t *handle) +{ + (void)task; + (void)name; + (void)stack_words; + (void)arg; + (void)priority; + (void)handle; + return pdPASS; +} + +void vTaskDelay(TickType_t ticks) +{ + (void)ticks; +} + +TickType_t xTaskGetTickCount(void) +{ + return 0; +} + +void vTaskDelete(TaskHandle_t handle) +{ + (void)handle; +} + +int wolfIP_poll(struct wolfIP *ipstack, uint64_t now_ms) +{ + (void)ipstack; + (void)now_ms; + return 10; +} + +int wolfIP_sock_socket(struct wolfIP *s, int domain, int type, int protocol) +{ + (void)s; + (void)domain; + (void)type; + (void)protocol; + return fake_internal_fd; +} + +int wolfIP_sock_bind(struct wolfIP *s, int fd, const struct wolfIP_sockaddr *addr, socklen_t len) +{ + (void)s; (void)fd; (void)addr; (void)len; + return 0; +} + +int wolfIP_sock_listen(struct wolfIP *s, int fd, int backlog) +{ + (void)s; (void)fd; (void)backlog; + return 0; +} + +int wolfIP_sock_accept(struct wolfIP *s, int fd, struct wolfIP_sockaddr *addr, socklen_t *len) +{ + (void)s; (void)fd; (void)addr; (void)len; + return -WOLFIP_EAGAIN; +} + +int wolfIP_sock_connect(struct wolfIP *s, int fd, const struct wolfIP_sockaddr *addr, socklen_t len) +{ + (void)s; (void)fd; (void)addr; (void)len; + return -WOLFIP_EAGAIN; +} + +int wolfIP_sock_send(struct wolfIP *s, int fd, const void *buf, size_t len, int flags) +{ + (void)s; (void)fd; (void)buf; (void)len; (void)flags; + return -WOLFIP_EAGAIN; +} + +int wolfIP_sock_sendto(struct wolfIP *s, int fd, const void *buf, size_t len, int flags, + const struct wolfIP_sockaddr *dest_addr, socklen_t len2) +{ + (void)s; (void)fd; (void)buf; (void)len; (void)flags; (void)dest_addr; (void)len2; + return -WOLFIP_EAGAIN; +} + +int wolfIP_sock_recv(struct wolfIP *s, int fd, void *buf, size_t len, int flags) +{ + (void)s; (void)fd; (void)buf; (void)len; (void)flags; + return -WOLFIP_EAGAIN; +} + +int wolfIP_sock_recvfrom(struct wolfIP *s, int fd, void *buf, size_t len, int flags, + struct wolfIP_sockaddr *src_addr, socklen_t *len2) +{ + (void)s; (void)fd; (void)buf; (void)len; (void)flags; (void)src_addr; (void)len2; + return -WOLFIP_EAGAIN; +} + +int wolfIP_sock_setsockopt(struct wolfIP *s, int fd, int level, int optname, + const void *optval, socklen_t optlen) +{ + (void)s; (void)fd; (void)level; (void)optname; (void)optval; (void)optlen; + return 0; +} + +int wolfIP_sock_getsockopt(struct wolfIP *s, int fd, int level, int optname, + void *optval, socklen_t *optlen) +{ + (void)s; (void)fd; (void)level; (void)optname; (void)optval; (void)optlen; + return 0; +} + +int wolfIP_sock_getsockname(struct wolfIP *s, int fd, struct wolfIP_sockaddr *addr, socklen_t *len) +{ + (void)s; (void)fd; (void)addr; (void)len; + return 0; +} + +int wolfIP_sock_getpeername(struct wolfIP *s, int fd, struct wolfIP_sockaddr *addr, socklen_t *len) +{ + (void)s; (void)fd; (void)addr; (void)len; + return 0; +} + +int wolfIP_sock_can_write(struct wolfIP *s, int fd) +{ + (void)s; (void)fd; + return 0; +} + +int wolfIP_sock_can_read(struct wolfIP *s, int fd) +{ + (void)s; (void)fd; + return 0; +} + +int wolfIP_sock_close(struct wolfIP *s, int fd) +{ + (void)s; + if (fd != fake_internal_fd) + return -WOLFIP_EINVAL; + close_calls++; + if (close_calls == 1) + return -WOLFIP_EAGAIN; + return -1; +} + +void wolfIP_register_callback(struct wolfIP *s, int fd, tsocket_cb cb, void *arg) +{ + (void)s; + (void)fd; + registered_cb = cb; + registered_arg = arg; +} + +#include "../port/freeRTOS/bsd_socket.c" + +int main(void) +{ + struct wolfIP stack; + int fd; + int rc; + + memset(&stack, 0, sizeof(stack)); + if (wolfip_freertos_socket_init(&stack, 1, 128) != 0) { + printf("init failed\n"); + return 1; + } + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + printf("socket failed\n"); + return 1; + } + + rc = close(fd); + if (rc != 0) { + printf("close failed rc=%d close_calls=%d sem_allocs=%d sem_frees=%d\n", + rc, close_calls, sem_allocs, sem_frees); + return 1; + } + if (close_calls != 2 || sem_frees != 1) { + printf("unexpected state close_calls=%d sem_allocs=%d sem_frees=%d\n", + close_calls, sem_allocs, sem_frees); + return 1; + } + + printf("test_freertos_close_last_ack: passed\n"); + return 0; +} From a267f4c1891a823a41357aa10648aa23fc589c3e Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 10:40:40 +0200 Subject: [PATCH 15/15] Fix FreeRTOS poll-task stack overflow: defer close callbacks out of RX path F-5788 (deliver CB_EVENT_CLOSED on LAST_ACK teardown) and F-5785 (close accepted socket on RST in SYN_RCVD) invoke the user socket callback *synchronously from deep inside tcp_input()/tcp_ack()*, i.e. at the bottom of the RX packet-processing call chain. The FreeRTOS BSD layer's callback calls printf(), which pulls in newlib's _vfprintf_r/_malloc_r; those frames on top of the already-deep RX stack overflow the wolfIP poll task's 1024-word stack. The result is an ARMv8-M stack-limit hardfault (CFSR=0x00100000 STKOF) faulting in the printf/malloc prologue, seen intermittently in the stm32h563 m33mu FreeRTOS echo CI job. Before F-5788 the same CB_EVENT_CLOSED was delivered from wolfIP_poll() Step 3, which dispatches socket callbacks from a shallow stack; that is the intended invariant. Restore it: - tcp_ack() LAST_ACK and tcp_input() SYN_RCVD-RST no longer call the callback or close_socket() directly. They set state = TCP_CLOSED and, when a callback is registered, OR in CB_EVENT_CLOSED and leave the socket in place so Step 3 delivers the event on a shallow stack and then reaps it via close_socket(). With no callback, the socket is closed immediately as before. - wolfIP_poll() Step 3 now dispatches a TCP_CLOSED socket only when it carries a deferred CB_EVENT_CLOSED, and reaps it after delivery. Other TCP_CLOSED sockets are still skipped (preserved by test_poll_tcp_cb_not_dispatched_when_closed). - tcp_input() ignores further input for a socket in the deferred-close window (TCP_CLOSED with CB_EVENT_CLOSED pending) so it cannot be matched/torn down before Step 3 delivers the event. Reproduced and verified on m33mu (stm32h563 FreeRTOS echo): with a 1KB frame added to the BSD callback the pre-fix code hardfaults (STKOF) 2/3 runs while the fixed code is clean 3/3; the real CI echo job passes. The two unit tests that asserted synchronous delivery now drive a poll cycle and assert the deferred delivery + teardown. All 1372 unit checks pass. --- src/test/unit/unit_tests_tcp_ack.c | 23 +++++++--- src/test/unit/unit_tests_tcp_state.c | 13 +++++- src/wolfip.c | 66 +++++++++++++++++++--------- 3 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/test/unit/unit_tests_tcp_ack.c b/src/test/unit/unit_tests_tcp_ack.c index e29f5e6e..49717d36 100644 --- a/src/test/unit/unit_tests_tcp_ack.c +++ b/src/test/unit/unit_tests_tcp_ack.c @@ -3826,9 +3826,11 @@ START_TEST(test_tcp_last_ack_closes_socket) } END_TEST -/* LAST_ACK final ACK must deliver CB_EVENT_CLOSED to a registered callback - * before close_socket() memsets it away, otherwise a caller blocked on the - * socket close (e.g. the FreeRTOS BSD close()) deadlocks. */ +/* LAST_ACK final ACK must deliver CB_EVENT_CLOSED to a registered callback so a + * caller blocked on the socket close (e.g. the FreeRTOS BSD close()) is woken. + * Delivery + teardown are deferred from the RX path to wolfIP_poll() Step 3 so + * the callback runs on a shallow stack (a callback that reaches into the C + * library from deep in packet processing overflowed the poll task stack). */ START_TEST(test_tcp_last_ack_closes_socket_delivers_closed_event) { struct wolfIP s; @@ -3864,12 +3866,21 @@ START_TEST(test_tcp_last_ack_closes_socket_delivers_closed_event) inject_tcp_segment(&s, TEST_PRIMARY_IF, remote_ip, local_ip, remote_port, local_port, 10, 10, TCP_FLAG_ACK); - /* Socket torn down ... */ - ck_assert_int_eq(ts->proto, 0); - /* ... but only after CB_EVENT_CLOSED was delivered to the waiter. */ + /* RX path moved it to TCP_CLOSED and queued CB_EVENT_CLOSED, but deferred + * the callback + teardown: the socket still exists and the callback has not + * fired yet. */ + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + ck_assert_int_ne(ts->proto, 0); + ck_assert_uint_eq(ts->events & CB_EVENT_CLOSED, CB_EVENT_CLOSED); + ck_assert_int_eq(socket_cb_calls, 0); + + /* poll Step 3 delivers CB_EVENT_CLOSED on a shallow stack, then reaps. */ + (void)wolfIP_poll(&s, 1); + ck_assert_int_eq(socket_cb_calls, 1); ck_assert_int_eq(socket_cb_last_fd, 0 | MARK_TCP_SOCKET); ck_assert_uint_eq(socket_cb_last_events & CB_EVENT_CLOSED, CB_EVENT_CLOSED); + ck_assert_int_eq(ts->proto, 0); /* torn down after the event was delivered */ } END_TEST diff --git a/src/test/unit/unit_tests_tcp_state.c b/src/test/unit/unit_tests_tcp_state.c index 832a320c..4ab5cc1b 100644 --- a/src/test/unit/unit_tests_tcp_state.c +++ b/src/test/unit/unit_tests_tcp_state.c @@ -605,11 +605,20 @@ START_TEST(test_tcp_input_syn_rcvd_rst_good_seq_nonlistener_closes) socket_cb_calls = 0; socket_cb_last_events = 0; - /* RST with seq == rcv_nxt: socket must be destroyed, not reverted */ + /* RST with seq == rcv_nxt: socket must be destroyed, not reverted. The + * teardown + CB_EVENT_CLOSED are deferred from the RX path to poll Step 3 + * so the callback runs on a shallow stack. */ inject_tcp_segment(&s, TEST_PRIMARY_IF, remote_ip, local_ip, rport, lport, 2, 0, TCP_FLAG_RST); - ck_assert_int_eq(ts->proto, 0); /* socket destroyed */ + ck_assert_int_eq(ts->sock.tcp.state, TCP_CLOSED); + ck_assert_int_ne(ts->proto, 0); /* not reverted to LISTEN, not yet reaped */ + ck_assert_uint_eq(ts->events & CB_EVENT_CLOSED, CB_EVENT_CLOSED); + ck_assert_int_eq(socket_cb_calls, 0); + + (void)wolfIP_poll(&s, 1); + + ck_assert_int_eq(ts->proto, 0); /* socket destroyed after event delivered */ ck_assert_int_eq(socket_cb_calls, 1); ck_assert_uint_ne(socket_cb_last_events & CB_EVENT_CLOSED, 0); } diff --git a/src/wolfip.c b/src/wolfip.c index fdaa5628..153fe375 100644 --- a/src/wolfip.c +++ b/src/wolfip.c @@ -4641,17 +4641,19 @@ static void tcp_ack(struct tsocket *t, const struct wolfIP_tcp_seg *tcp) if (t->sock.tcp.state == TCP_LAST_ACK && tcp_seq_leq(fin_acked, ack)) { tcp_ctrl_rto_stop(t); t->sock.tcp.state = TCP_CLOSED; - /* The peer's final ACK tears the socket down synchronously here, - * during packet processing, before wolfIP_poll() Step 3 dispatches - * socket callbacks. close_socket() memsets the callback and event - * mask away, so deliver CB_EVENT_CLOSED now; otherwise a caller - * blocked on the socket close (e.g. the FreeRTOS BSD close()) would - * never be woken. */ - if (t->callback) { - int sock_fd = (int)(t - t->S->tcpsockets) | MARK_TCP_SOCKET; - t->callback(sock_fd, CB_EVENT_CLOSED, t->callback_arg); - } - close_socket(t); + /* The peer's final ACK tears the socket down here, deep in the RX + * path, before wolfIP_poll() Step 3 dispatches socket callbacks. + * Do NOT invoke the user callback from here: a callback that touches + * the C library (e.g. the FreeRTOS BSD layer's printf -> malloc) adds + * frames at the bottom of the RX call chain and overflows the poll + * task stack. Instead defer CB_EVENT_CLOSED delivery and the final + * close_socket() to Step 3, which dispatches from a shallow stack and + * then reaps TCP_CLOSED sockets. A caller blocked on the socket close + * (e.g. the FreeRTOS BSD close()) is woken from there. */ + if (t->callback) + t->events |= CB_EVENT_CLOSED; + else + close_socket(t); return; } if (t->sock.tcp.state == TCP_FIN_WAIT_1 && tcp_seq_leq(fin_acked, ack)) { @@ -4910,6 +4912,12 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, struct tsocket *t = &S->tcpsockets[i]; if (t->proto == 0 || t->S == NULL) continue; + /* A socket moved to TCP_CLOSED by the RX path with CB_EVENT_CLOSED + * still pending has only deferred its teardown to wolfIP_poll() + * Step 3 (so the close callback runs on a shallow stack). Ignore any + * further input for it until Step 3 delivers the event and reaps it. */ + if (t->sock.tcp.state == TCP_CLOSED && (t->events & CB_EVENT_CLOSED)) + continue; if (t->src_port == ee16(tcp->dst_port)) { /* TCP segment sanity checks */ iplen = ee16(tcp->ip.len); @@ -5020,15 +5028,17 @@ static void tcp_input(struct wolfIP *S, unsigned int if_idx, } /* An accepted (cloned) connection has no listen role: a peer * RST before the handshake completed must tear it down, not - * resurrect it as a phantom listener. Deliver CB_EVENT_CLOSED - * synchronously because close_socket() wipes the callback - * before wolfIP_poll() Step 3 dispatches events; otherwise a - * consumer blocked on the new socket would never wake. */ - if (t->callback) { - int sock_fd = (int)(t - S->tcpsockets) | MARK_TCP_SOCKET; - t->callback(sock_fd, CB_EVENT_CLOSED, t->callback_arg); - } - close_socket(t); + * resurrect it as a phantom listener. Defer CB_EVENT_CLOSED + * delivery and the close_socket() to wolfIP_poll() Step 3 + * (shallow stack) rather than invoking the callback from + * deep in the RX path, which can overflow the poll task + * stack; a consumer blocked on the new socket is woken from + * there. */ + t->sock.tcp.state = TCP_CLOSED; + if (t->callback) + t->events |= CB_EVENT_CLOSED; + else + close_socket(t); continue; } if (t->sock.tcp.state == TCP_SYN_SENT) { @@ -9885,11 +9895,25 @@ int wolfIP_poll(struct wolfIP *s, uint64_t now) /* Step 3: handle DHCP and application callbacks */ for (i = 0; i < MAX_TCPSOCKETS; i++) { struct tsocket *ts = &s->tcpsockets[i]; - if ((ts->sock.tcp.state != TCP_CLOSED) && (ts->callback) && (ts->events)) { + if ((ts->callback == NULL) || (ts->events == 0)) + continue; + /* A socket the RX path moved to TCP_CLOSED is dispatched here only when + * it deferred a CB_EVENT_CLOSED for delivery on this shallow stack + * (LAST_ACK final ACK, or RST on a half-open accepted socket); the + * teardown was deferred too so the callback would not run deep in + * packet processing. Any other TCP_CLOSED socket is left alone. */ + if ((ts->sock.tcp.state == TCP_CLOSED) && !(ts->events & CB_EVENT_CLOSED)) + continue; + { uint16_t events = ts->events; ts->events = 0; ts->callback(i | MARK_TCP_SOCKET, events, ts->callback_arg); } + /* Now that CB_EVENT_CLOSED has been delivered, reap the deferred-close + * socket. A socket closed elsewhere is already memset (callback NULL) + * and never reaches this branch. */ + if (ts->sock.tcp.state == TCP_CLOSED) + close_socket(ts); } for (i = 0; i < MAX_UDPSOCKETS; i++) { struct tsocket *ts = &s->udpsockets[i];