tcp: connections are never abandoned when the sender retries a blocked send - #410
Open
tinic wants to merge 1 commit into
Open
tcp: connections are never abandoned when the sender retries a blocked send#410tinic wants to merge 1 commit into
tinic wants to merge 1 commit into
Conversation
… 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) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
NX_TCP_MAXIMUM_RETRIEScannot be reached on a socket whose sender retries a blockedsend. The connection is never abandoned: it retransmits until the interval grows without
bound, and the application waiting on it blocks with no error for as long as it is left
running.
Two changes in two files, 31 insertions and 5 deletions. No API or configuration change.
Symptom
With a router silently dropping oversized datagrams, and
NX_TCP_MAXIMUM_RETRIES 6/NX_TCP_RETRY_SHIFT 1— which should abandon the connection at 127 s — retransmissions ofthe same sequence number were observed at:
The doubling shows
NX_TCP_RETRY_SHIFTtaking effect. The+128shows the limit did not.Root cause
_nx_tcp_fast_periodic_processing()(nx_tcp_fast_periodic_processing.c:129) tests theretry limit against one of two counters, selected by
nx_tcp_socket_zero_window_probe_has_data:Two defects mean neither branch can fire.
1.
nx_tcp_socket_send_internal.carms the probe for any send it cannot queue.There are three reasons data does not go out — the receiver's advertised window, the
congestion window, and the transmit queue depth — and only the first is a zero window.
Both sites test only
zero_window_probe_has_data == NX_FALSEbefore declaring a probe, soa send blocked by congestion or by queue depth marks the socket as being in the persist
state when it is not. That moves the limit onto
zero_window_probe_failure, which theordinary data path never advances.
2.
nx_tcp_socket_retransmit.cclearszero_window_probe_failureon every proberather than when a probe begins, pinning it at 1. So even a genuine persist against a peer
that has stopped answering never reaches the limit either.
Why neither defect alone is sufficient
This is the part that is not visible in either hunk on its own, and it is why the bug
survives casual reading.
A single blocked send does not hide the limit: the next retransmission clears the flag and
the ordinary counter resumes. What hides it is the caller. A blocking sender that
re-enters
nx_tcp_socket_send()afterNX_WINDOW_OVERFLOWorNX_TX_QUEUE_DEPTH— and anon-blocking sender returning on its own select loop — re-arms the flag faster than the
retransmission interval doubles. From the second interval onwards, the flag is set every
time the timer looks, so the limit is tested against the wrong counter every time.
That is the ordinary shape of a blocking socket API, which is why this is reachable in
normal use rather than only under a contrived sequence.
The fix
nx_tcp_socket_send_internal.c, both sites: requirenx_tcp_socket_tx_window_advertised == 0in addition to the existing condition, so theflag means what its name says.
nx_tcp_socket_retransmit.c: clearzero_window_probe_failureonly when a new probestarts, matching the two sites above.
nx_tcp_fast_periodic_processing.cis deliberately untouched — with the flag correct, itscondition is correct.
Why this does not drop peers that are legitimately holding a zero window
The obvious alternative fix — ignoring the probe counter — would abandon a live peer
advertising a zero window, which is exactly what the persist state exists to avoid.
That path is preserved. A peer that answers its probes still clears
zero_window_probe_failureinnx_tcp_socket_state_ack_check.c:555, so a socket in agenuine persist against a responsive peer is never reset however long the window stays
shut. Only a peer that has stopped answering is given up on, after the configured number
of retries.
Reproduction
A regression test drives six of these translation units against a host shim and steps the
timer directly, so 600 simulated seconds run in about 0.3 s with no target hardware:
Before this change the first two cases print
+1 +3 +7 +15 +31 +63 +127 +255 +511 NO RESET. The third case is the guard against over-fixing described above, and is unchangedby the patch.
I am happy to contribute the test in whatever form suits the project's test layout; it is
currently written against a downstream harness rather than this repository's.