From: Stefano Brivio <sbrivio@redhat.com>
To: Lynxes Exe <lynxes.exe@gmail.com>
Cc: passt-dev@passt.top
Subject: Re: tcp: peer stuck in low_rtt_dst table loses window clamping, every new connection stalls on RTOs
Date: Wed, 04 Nov 2026 14:53:22 +0100 (CET) [thread overview]
Message-ID: <20261104145322.2dc73741@elisabeth> (raw)
In-Reply-To: <CAJPNJUuUYSkW776P0hKmweHnKOqXtsXqwm4HXP-+jorXrSfFFg@mail.gmail.com>
Hi Lynxes,
Thanks for the report, and sorry for the delay. Some comments inline:
On Wed, 28 Oct 2026 14:15:59 +0100
Lynxes Exe <lynxes.exe@gmail.com> wrote:
> Hi,
>
> A disclaimer before anything is owed: what you're about to read was
> written by Claude, and is the result of a bug fixing session that occurred
> on my personal home infrastructure. It was not intended as a bug hunt.
> Since it seems like this issue has been unreported and I couldn't find
> anything similar to it, and since what Claude reported seems potentially
> plausible, I'm putting it here.
> If AI assisted/generated bug reports are unwanted, I apologize.
At least as far as I'm concerned, in term of bugs reports, whatever
makes sense and is usable is welcome. This report has a few dubious
aspects (see below) but it might still be valuable, I think.
> Original report
> --------------------------
> I think a peer address can get stuck in pasta's low-RTT destination
> table and then lose window clamping for good.
This part wasn't clearly demonstrated.
> That caused every new
> TCP connection from our LAN router's address to stall on RTOs until
> pasta was restarted. Details below.
This looks plausible and somewhat demonstrated.
> I haven't found an existing report on Bugzilla or this list.
Correct, no known issues in this sense so far.
> Version: pasta 2026_10_02.cba3570-3-g8ba4132 (the logic below is
> unchanged in current master)
> Setup: rootless Docker 29.6.1 via rootlesskit, --net=pasta,
> --port-driver=implicit, --mtu=65520 (pasta --tcp-ports=auto
> --udp-ports=auto --no-map-gw --ipv4-only ...)
> Kernel: 6.18.57 (Arch linux-lts), x86_64
>
> What we think is happening
> --------------------------
> We suspect the low-RTT destination table (low_rtt_dst[],
> tcp_rtt_dst_check() / tcp_rtt_dst_low()). Our LAN router's address,
> 10.0.0.1, ended up in it even though the router is several
> milliseconds away.
Nothing clearly confirms the "ended up in it" part (yet).
> [...]
>
> We could not read the table directly (no ptrace into pasta), so
> "10.0.0.1 is in the table" is inferred from this behaviour and the
> code path.
...I guess the problem here is training a language model about the
difference between "inferred" and "suspected". :)
One could ptrace() as root by the way, but that wouldn't help either.
Could you make sure about this with a print in tcp_rtt_dst_check()? If
that's not the case, you might need to further trace what's going on in
tcp_update_seqack_wnd() instead.
Rebuilding and using a modified version of passt is quick (it just
depends on a C library):
git clone git://passt.top/passt && cd passt
[edit as needed]
make && sudo make install # Or make install as root
Both Podman and Docker / rootlesskit will pick up the local build under
/usr/local/bin. You can cleanly remove it with 'make uninstall' as root
once you're done.
> Why we think 10.0.0.1 got into the table
> ----------------------------------------
> A real RTT of 10 us or less to the router is impossible here
> (measured RTT 2-6 ms). Our suspicion comes from reading the kernel
> source; we have not caught it on a live socket:
>
> - tcp_rtt_dst_check() tests
> (int)tinfo->tcpi_min_rtt > LOW_RTT_THRESHOLD.
> - The kernel initialises rtt_min to ~0U, both in tcp_init_sock()
> (net/ipv4/tcp.c) and for accepted sockets in
> tcp_create_openreq_child() (tcp_minisocks.c). tcpi_min_rtt stays
> ~0U until the first valid RTT sample.
> - tcp_synack_rtt_meas() takes no sample when the SYN-ACK was
> retransmitted (rtt_us = -1 unless !req->num_retrans). The same
> applies on the active side when the SYN was retransmitted (Karn).
> - Cast to int, ~0U becomes -1, which passes the "<= 10 us" test.
> - pasta runs this check in tcp_prepare_flags() when it sends the SYN
> (inbound) or SYN-ACK (outbound) to the guest. That is right after
> the host-side handshake, before any data ACK could provide a
> sample.
...maybe, yes.
> We measured exactly such handshake retransmissions on this
> Wi-Fi/NAT path: 6 of 1,800 fresh connections took ~1.05 s, i.e. one
> 1 s retry. With hundreds of inbound and outbound connections to
> 10.0.0.1 per day, one retried handshake would be enough to insert it
> permanently.
>
> The night the stalls became severe, the published container was also
> restarted about ten times while clients kept connecting, which made
> unusual handshakes more likely. We cannot tell exactly when the
> insertion happened.
>
>
> Suggested fixes
> ---------------
> 1. In tcp_rtt_dst_check(), treat tcpi_min_rtt == ~0U (and 0) as
> "no sample" and skip the insertion. Compare as unsigned, or check
> tcpi_rtt / tcpi_rcv_rtt as well.
I'm not sure if we actually risk hitting 0: strictly speaking that
would mean less than 1 µs which looks unlikely (even for simple veth
pairs or local guests), but in practice there might be rounding errors
somewhere.
Do we need to skip 0? From a quick glance of the kernel paths involved
I'd say we don't.
Could you test this perhaps? Not build tested but I think you could
modify the initial early return condition to:
if (!min_rtt_cap ||
(int)tinfo->tcpi_min_rtt > LOW_RTT_THRESHOLD ||
tinfo->tcpi_min_rtt == ~0U /* not measured yet */)
return;
> 2. Don't trust a single sample forever: give entries a lifetime, or
> evict an address when a later connection to it shows a min RTT
> above the threshold.
Reasonable. I guess we could add a timestamp and check if the entry
expired (minutes?) where we update the RTT in
tcp_update_seqack_wnd()... or even in tcp_rtt_dst_check(). No strong
preference on my side, I didn't really think it through.
> 3. Log (at debug level) when an address enters the table. It was
> invisible here; the only visible signs were retransmissions
> inside the namespace.
Yeah, that would be good. I added this feature before we started
logging anything, I think. Feel free to send a patch for that.
> Workaround: restarting pasta clears the table.
>
> Happy to test a patch or collect more data.
Let me know if you can check those few things I suggested, thanks.
--
Stefano
prev parent reply other threads:[~2026-11-04 13:53 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 13:15 Lynxes Exe
2026-10-29 1:11 ` David Gibson
2026-11-04 13:53 ` Stefano Brivio [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261104145322.2dc73741@elisabeth \
--to=sbrivio@redhat.com \
--cc=lynxes.exe@gmail.com \
--cc=passt-dev@passt.top \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://passt.top/passt
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).