From 540bc8a2a36df39de649d35bb73915105321954b Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Mon, 27 Jul 2026 18:32:11 -0700 Subject: [PATCH] tcp: a zero-window probe belongs to a zero window, not to any blocked send _nx_tcp_fast_periodic_processing() tests the retransmission retry limit against one of two counters depending on nx_tcp_socket_zero_window_probe_has_data: the ordinary timeout_retries when it is false, and zero_window_probe_failure when it is true. Two defects between them meant NX_TCP_MAXIMUM_RETRIES could not be reached on either path. nx_tcp_socket_send_internal.c armed the probe whenever data could not be queued, and there are three reasons for that -- the receiver's advertised window, the congestion window, and the transmit queue depth. Only the first is a zero window. Setting the flag for the other two describes a socket as being in the persist state when it is not, moving the limit onto a counter the data path never advances. Both sites now require nx_tcp_socket_tx_window_advertised to be zero. nx_tcp_socket_retransmit.c cleared zero_window_probe_failure on every probe rather than when a probe began, pinning it at one, so the second arm could not fire either -- a peer that stopped answering its probes was never given up on. It is now cleared only when a new probe starts, matching the two sites above; a peer that answers still clears it in nx_tcp_socket_state_ack_check.c. Observed with a router silently dropping oversized datagrams: retransmissions of the same sequence at +1, +2, +4, +8, +16, +32, +64 and +128 seconds with no reset, where a limit of 6 with a shift of 1 should abandon the connection at 127 s. One blocked send alone does not hide it -- the next retransmission clears the flag -- but a caller that retries its write re-arms it faster than the interval doubles, so from the second rung on the flag was set every time the timer looked. Co-Authored-By: Claude Opus 5 (1M context) --- common/src/nx_tcp_socket_retransmit.c | 16 ++++++++++++++-- common/src/nx_tcp_socket_send_internal.c | 20 +++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/common/src/nx_tcp_socket_retransmit.c b/common/src/nx_tcp_socket_retransmit.c index a3d964f86..122382f7c 100644 --- a/common/src/nx_tcp_socket_retransmit.c +++ b/common/src/nx_tcp_socket_retransmit.c @@ -110,9 +110,21 @@ ULONG window_size; socket_ptr -> nx_tcp_socket_zero_window_probe_data = *(packet_ptr -> nx_packet_prepend_ptr + ((header_ptr -> nx_tcp_header_word_3 >> 28) << 2)); /* Now set zero window probe started. */ - socket_ptr -> nx_tcp_socket_zero_window_probe_has_data = NX_TRUE; socket_ptr -> nx_tcp_socket_zero_window_probe_sequence = header_ptr -> nx_tcp_sequence_number; - socket_ptr -> nx_tcp_socket_zero_window_probe_failure = 0; + + /* The failure count belongs to the probe, not to each attempt at + it: clear it only when a new probe starts, as the two places in + nx_tcp_socket_send_internal.c that arm one do. Clearing it + here on every attempt pinned it at one, so the retry limit that + _nx_tcp_fast_periodic_processing() tests against it during a + zero window could never be reached and a peer that stopped + answering its probes was never given up on. A peer that does + answer still clears it (nx_tcp_socket_state_ack_check.c). */ + if (socket_ptr -> nx_tcp_socket_zero_window_probe_has_data == NX_FALSE) + { + socket_ptr -> nx_tcp_socket_zero_window_probe_has_data = NX_TRUE; + socket_ptr -> nx_tcp_socket_zero_window_probe_failure = 0; + } NX_CHANGE_ULONG_ENDIAN(header_ptr -> nx_tcp_sequence_number); NX_CHANGE_ULONG_ENDIAN(header_ptr -> nx_tcp_header_word_3); diff --git a/common/src/nx_tcp_socket_send_internal.c b/common/src/nx_tcp_socket_send_internal.c index db48895c1..c98f99251 100644 --- a/common/src/nx_tcp_socket_send_internal.c +++ b/common/src/nx_tcp_socket_send_internal.c @@ -1017,7 +1017,16 @@ UINT compute_checksum = 1; /* Increment the suspended thread count. */ socket_ptr -> nx_tcp_socket_transmit_suspended_count++; - if (socket_ptr -> nx_tcp_socket_zero_window_probe_has_data == NX_FALSE) + /* Only a receiver that advertised a zero window is probed. This + data cannot be sent for one of three reasons -- the receiver's + window, the congestion window, or the transmit queue depth -- + and only the first of them is a zero window. Setting the flag + for the other two describes the socket as being in the persist + state when it is not, which moves the retransmission retry + limit onto the probe failure count (see + nx_tcp_fast_periodic_processing.c) and stops it being reached. */ + if ((socket_ptr -> nx_tcp_socket_tx_window_advertised == 0) && + (socket_ptr -> nx_tcp_socket_zero_window_probe_has_data == NX_FALSE)) { /* Set data for zero window probe. */ @@ -1061,8 +1070,13 @@ UINT compute_checksum = 1; else { - /* Check advertised window. */ - if (socket_ptr -> nx_tcp_socket_zero_window_probe_has_data == NX_FALSE) + /* Check advertised window. As above, a probe belongs to a zero + window and to nothing else: the caller is about to be told + NX_WINDOW_OVERFLOW or NX_TX_QUEUE_DEPTH and will try again, and + each of those attempts would otherwise re-declare a persist + state that hides the retransmission retry limit. */ + if ((socket_ptr -> nx_tcp_socket_tx_window_advertised == 0) && + (socket_ptr -> nx_tcp_socket_zero_window_probe_has_data == NX_FALSE)) { /* Set data for zero window probe. */