Skip to contentSkip to conversation

tcp: Add TCP ROCCET congestion control module.

PATCH Superseded Tim Fuechsel 3 participants 3 messages Sep 28 – Oct 1

Subsystems (4)

v9 of 10 Superseded Latest is v10 →

lore
Share & tools
lore
#0 lore raw

PATCH v9 1/1 tcp: Add TCP ROCCET congestion control module.

TCP ROCCET is a new congestion control algorithm based on TCP CUBIC that improves its overall performance in cellular networks. By its mode of function, CUBIC causes bufferbloat while it tries to detect the available throughput of a network path. This is particularly a problem with large buffers in mobile networks. A more detailed description and analysis of this problem caused by TCP CUBIC can be found in [1]. TCP ROCCET's main method of addressing the bufferbloat problem is by adding two additional metrics. The first metric is the relative increase in RTT from its minimum, the srRTT. Here, bufferbloat can be detected when RTTs increase due to buffer filling. The second metric is the acknowledgment arrival rate sampled over 100ms intervals and the bytes sent and received over 5 RTTs. If CUBIC increases the send rate or congestion window, and the acknowledgment arrival rate stays at the same level, the connection is limited by the bottleneck link's capacity. In such cases, ROCCET reduces the send rate to prevent bufferbloat. ROCCET uses a modified version of slow start rather than HyStart because HyStart is known to enter the congestion avoidance phase too early when used in cellular networks [2]. Therefore, ROCCET uses a combination of srRTT and the acknowledgment arrival rate to determine when to exit slow start. For the congestion avoidance phase, ROCCET relies on the srRTT and monitoring the sent and received bytes to detect the filling of the bottleneck buffer. To keep the minimum RTT updated, ROCCET performs minimum RTT probing if no new minimum RTT was found in the last 5 seconds. To this end ROCCET halves the cwnd and probes for 200ms but at least for one RTT, for a new minimum RTT. After probing, ROCCET refills the pipe carefully. In addition, ROCCET reacts to packet loss by adapting CUBIC's behavior. ECNs are handled as ROCCET congestion event, where the cwnd gets reduced and ROCCET enters a buffer drain state.

In real-world mobile 5G NR measurements, TCP ROCCET achieves better performance than CUBIC, by maintaining similar throughput while reducing the latency. More information about TCP ROCCET and measurement evaluations can be found here [3].
For the version (2026-07-21), we provide additional performance evaluation regarding throughput, latency, and bandwidth share [4].

[1] https://doi.org/10.1109/VTC2023-Fall60731.2023.10333357
[2] https://doi.org/10.1109/WMNC.2016.7543932
[3] https://doi.org/10.23919/WONS68803.2026.11501781
[4] http://go.lu-h.de/roccet-2026-07-21

---

Changes since v1:
* Adjust some comments & Rework commit message
* Fix Kconfig format
* Add div-by-0 check variables
* Fix ack summation from +=1 to +=acked (counting now ACKs instead of ACK events)
* Fix wrapping in time comparisons
* Remove most module_params, including 'ignore_loss'
* Remove check that always evaluated to 'true' regarding 'bw_limit_detect'

Changes since v2:
* Improve initialization of roccettcp struct
* Always react to ECE bits and reset flag
* Detect plateaus in ACK-rate
* Fix potential overflow in ACK-rate tracking for high-speed connections
* Reset ACK-counter correctly on new interval
* Fix potential integer overflow in rRTT calculation

Changes since v3:
* Changes in comments & Kconfig help text
* Adjust commit message, adding reference to new performance evaluation
* Add minimum RTT probing
* Use cong_control callback instead of cong_avoid
* Move CA_EVENT_TX_START event check to dedicated callback (cwnd_event_tx_start)
* Add monitoring of send and receive rate as mentioned in the original paper
* Refactor of the code
* Remove kfunc exports
* Fix potential overflow in rRTT calculation by using 64-bit arithmetic
* Fix potential division-by-zero in srRTT calculation due to RTT value
* Initialize ack_rate timestamp to current time
* Fix potential double penalty when receiving ECN (ECE) mark
* Reset ack counting when connection goes idle
* Add module-parameter bounds check on register

Changes since v4:
* Remove __always_inline annotations & unused function
* Fix module parameter bounds check desync
* Add idle-check to fix connection freezing
* Exchange .ssthresh callback from recalc_ssthresh to a ssthresh-getter, fixing double penalties
* Remove unnecessary seq-no wrapping check & Remove 0 as 'uninitialized' seq-no value
* Wait for first control round to finish before starting to monitor potential slow-start exit conditions

Changes since v5:
* Complete refactor of the roccet-state-machine, improving maintainability and robustness
* Fix incorrect state transitions and state-exit conditions
* Reconsider & fix handling of RTO, CWR and Recovery states
* Fix module parameter usage & only re-calculate scaling factors on module-init
* Fix usage of .ssthresh callback and use ssthresh more consciously
* Fix incorrect coding-style

Changes since v6:
* Add beta param-setter with bounds check
* Set cwnd on loss to 1 instead of TCP_INIT_CWND
* Fix exceeding of ICSK_CA_PRIV_SIZE size
* Fix DRAIN-state stuck due to idle-periods & wrap arounds
* Fix ack-carry-over applied to wrong metric

Changes since v7:
* Rewrite idle-check in update_ack_rate code
* Add min RTT probe interval to cubic-epoch timer
* Set seq.-no counters on ORBITER enter
* Don't set Wmax when CWR occurs during RTT probing
* Increase cwnd back to TCP_CWND_INIT on RTO

Changes since v8:
* Fix RTT_PROBE_REFILL to DRAIN transition leaving jiffies in next_min_rtt_probe
* Add previously ignored acked value to ack_rate_cnt on reset
* Add upper bound for min RTT probe duration
* Fix more potential time-wrap problems
* Add a tcp_in_slow_start() guard for tcp_slow_start() call in LAUNCH
* Clamp epoch_start so it won't go in the future
* Set snd_ssthresh on RTO & on ROCCET-CE
* Automatically set initial_limit_reached after max 500 ms
* Avoid double-cwnd-reduction via tcp_in_cwnd_reduction check
* Avoid cwnd growth in cwnd-reduction state
* Keep pacing rate att 100% in RTT_PROBE state

---
 net/ipv4/Kconfig      |   12 +
 net/ipv4/Makefile     |    1 +
 net/ipv4/tcp_roccet.c | 1400 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1413 insertions(+)
 create mode 100644 net/ipv4/tcp_roccet.c
net/ipv4/Kconfig +12 -0
index 301b47660305..3545fb0a045b 100644
@@ -663,6 +663,18 @@ config TCP_CONG_CDG
 	    delay gradients." In Networking 2011. Preprint:
 	    http://caia.swin.edu.au/cv/dahayes/content/networking2011-cdg-preprint.pdf
 
+config TCP_CONG_ROCCET
+	tristate "ROCCET TCP"
+	default n
+	help
+	  TCP ROCCET (RTT oriented CUBIC congestion control exTension) is a
+	  sender-side-only congestion control algorithm based on the TCP CUBIC
+	  protocol stack/TCP CUBIC congestion control algorithm that optimizes
+	  its performance. Especially for networks with large buffers
+	  (wireless, cellular networks), TCP ROCCET has improved performance by
+	  maintaining a similar throughput as CUBIC while reducing latency.
+	  For more information, see: https://arxiv.org/abs/2510.25281
+
 config TCP_CONG_BBR
 	tristate "BBR TCP"
 	default n
net/ipv4/Makefile +1 -0
index 06e21c26b76f..69e4baaed122 100644
@@ -45,6 +45,7 @@ obj-$(CONFIG_INET_TCP_DIAG) += tcp_diag.o
 obj-$(CONFIG_INET_UDP_DIAG) += udp_diag.o
 obj-$(CONFIG_INET_RAW_DIAG) += raw_diag.o
 obj-$(CONFIG_TCP_CONG_BBR) += tcp_bbr.o
+obj-$(CONFIG_TCP_CONG_ROCCET) += tcp_roccet.o
 obj-$(CONFIG_TCP_CONG_BIC) += tcp_bic.o
 obj-$(CONFIG_TCP_CONG_CDG) += tcp_cdg.o
 obj-$(CONFIG_TCP_CONG_CUBIC) += tcp_cubic.o
net/ipv4/tcp_roccet.c new file +1400 -0

1,400 lines not on this page · raw message

base-commit: 014d795c73837ea2339a4ea8e8f82c6e959b845d

Signature · 1 line
2.43.0
#1 lore raw
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [High] tcp: ROCCET stalls Fast Retransmit by bypassing PRR in TCP_CA_Recovery - [High] tcp: integer…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] tcp: ROCCET stalls Fast Retransmit by bypassing PRR in TCP_CA_Recovery
- [High] tcp: integer promotion bug falsely triggers congestion events on ROCCET
- [Low] build: struct roccettcp exceeds ICSK_CA_PRIV_SIZE limit
--

commit 1022dd98954ad71bd010ccb13665b66dd4141109
Author: Tim Fuechsel <[email protected]>

tcp: Add TCP ROCCET congestion control module.

TCP ROCCET is a new congestion control algorithm based on TCP CUBIC that improves overall performance in cellular networks. It introduces additional metrics like relative increase in RTT and acknowledgment arrival rate to address bufferbloat and manage window scaling.

Quoted 5 lines
net/ipv4/tcp_roccet.c new file +0 -0
new file mode 100644
@@ -0,0 +1,1400 @@

[ ... ]

Quoted 2 lines
net/ipv4/tcp_roccet.c new file +2 -0
+struct roccettcp {
+	u32 cnt;	/* increase cwnd by 1 after ACKs */

[ ... ]

net/ipv4/tcp_roccet.c line 134

Quoted 3 lines
net/ipv4/tcp_roccet.c new file +3 -0
+	u16 ack_rate_cnt;	/* Used for counting acks */
+
+	enum roccet_state state : 3; /* Current operating state of roccet */

[Severity: Low]
Will this bitfield layout cause a compile-time failure?

Because the preceding field is u16 (which ends at offset 102), the compiler inserts 2 bytes of padding to align the enum allocation unit, which needs 4-byte alignment, at offset 104. This expands the total struct size to 108 bytes.

Quoted 1 line
net/ipv4/tcp_roccet.c new file +1 -0
+	bool initial_limit_reached: 1;	/* Set to true after the connection

[ ... ]

net/ipv4/tcp_roccet.c line 1377

Quoted 5 lines
net/ipv4/tcp_roccet.c new file +5 -0
+static int __init roccet_register(void)
+{
+	int param_err;
+
+	BUILD_BUG_ON(sizeof(struct roccettcp) > ICSK_CA_PRIV_SIZE);

Since ICSK_CA_PRIV_SIZE is statically defined as 104 bytes in include/net/inet_connection_sock.h, it seems this check will unconditionally fail and break the module build.

[ ... ]

Quoted 2 lines
net/ipv4/tcp_roccet.c new file +2 -0
+static void roccet_orbiter_update(struct sock *sk, u32 acked)
+{

[ ... ]

net/ipv4/tcp_roccet.c line 965

Quoted 8 lines
net/ipv4/tcp_roccet.c new file +8 -0
+	/* Check sent and received bytes from the previous interval.
+	 * Here we use a guard space of 1% of the current cwnd.
+	 * We do this to avoid a false positive evaluation due
+	 * to delays caused by jitter or scheduling.
+	 */
+	sent_more_than_acked = (s32)(sent - received) >
+			       div_u64((u64)tcp_snd_cwnd(tp) *
+			       (u64)tp->mss_cache, 100);

[Severity: High]
Does this comparison trigger false congestion events when ACKs outpace outgoing data?

If received exceeds sent, sent - received wraps to a very large u32. When cast to s32, it correctly represents a negative difference.

However, because div_u64() returns a u64, standard C arithmetic conversions cause the negative s32 to be sign-extended and promoted to u64.

This results in a huge positive number, making the condition incorrectly evaluate to true, which freezes congestion window growth.

[ ... ]

net/ipv4/tcp_roccet.c line 1008

Quoted 13 lines
net/ipv4/tcp_roccet.c new file +13 -0
+	/* Terminates this function if cwnd is not fully utilized.
+	 * In mobile networks like 5G, this termination causes the
+	 * cwnd to be frozen at an excessively high value. This is
+	 * because slow start or HyStart massively exceed the available
+	 * bandwidth and leave the cwnd at an excessively high value.
+	 * The cwnd cannot therefore be fully utilized because it is
+	 * limited by the connection capacity.
+	 *
+	 * Also avoid cwnd-growth if we are in cwnd reduction phase.
+	 */
+	if (!tcp_is_cwnd_limited(sk) || sent_more_than_acked ||
+	    tcp_in_cwnd_reduction(sk))
+		return;

[Severity: High]
Can returning early here during TCP_CA_Recovery stall Fast Retransmit?

When a socket enters loss recovery, the TCP core delegates all congestion control processing to the cong_control callback, bypassing the standard tcp_cwnd_reduction() PRR routine.

If roccet_orbiter_update() always returns early without inflating snd_cwnd for incoming dupACKs:

[ ... ]

Quoted 2 lines
net/ipv4/tcp_roccet.c new file +2 -0
+static void roccet_state(struct sock *sk, u8 new_state)
+{

[ ... ]

net/ipv4/tcp_roccet.c line 1193

Quoted 5 lines
net/ipv4/tcp_roccet.c new file +5 -0
+	} else if (new_state == TCP_CA_Recovery) {
+		/* Directly reduce cwnd and rely on pacing */
+		cwnd = roccet_handle_recovery(sk);
+		tcp_snd_cwnd_set(tp, cwnd);
+	}

The snd_cwnd is statically reduced upon entering recovery. Because it is never inflated by dupACKs, tcp_packets_in_flight() will remain above the reduced snd_cwnd.

When tcp_xmit_retransmit_queue() attempts to fast retransmit, it calculates available segments as tcp_snd_cwnd(tp) - tcp_packets_in_flight(tp) which evaluates to <= 0, completely preventing retransmissions until an RTO fires.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/arqDuiD4Q4GWOFBc@volt-roccet-vm?part=1
#2 lore raw
In reply to Tim Fuechsel
Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider.

Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider.

Critical: 0 · High: 0 · Medium: 8 · Low: 4

If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough).

- [Medium] The commit message says only that ROCCET "reacts to packet loss by adapting CUBIC's behavior".
- [Medium] roccet_acked() keeps CUBIC's early return if (ca->epoch_start && (s32)(tcp_jiffies32 - ca->epoch_start) < HZ) return;, and it sits…
- [Medium] Spurious-RTO undo is partly reversed.
- [Medium] ROCCET uses .cong_control, so the core skips tcp_cwnd_reduction() (PRR).
- [Medium] ROCCET's only idle handling is roccet_cwnd_event_tx_start(), copied from CUBIC, which just shifts epoch_start.
- [Medium] In roccet_orbiter_update(), `sent_more_than_acked = (s32)(sent
- received) > div_u64((u64)tcp_snd_cwnd(tp) * (u64)tp->mss_cache,…
- [Medium] Several places intended to reduce cwnd apply a TCP_INIT_CWND floor, which can raise cwnd instead and also ignores…
- [Medium] A loss-detecting ACK, or any later ACK during Recovery, can reduce cwnd twice.
- [Low] Several in-code comments are stale or contradict the code next to them.
- [Low] Two module parameter descriptions shown by modinfo/sysfs do not match the code.
- [Low] Every ROCCET 'now' timestamp comes from jiffies_to_usecs(tcp_jiffies32) and is compared with u32 modular…
- [Low] update_srrtt() does a plain C division of a u64 by a constant:
`ca->curr_srrtt = ((100 - ROCCET_ALPHA_TIMES_100) *…

--

tcp: Add TCP ROCCET congestion control module.

This adds ROCCET, a new TCP congestion control module based on CUBIC. It detects bufferbloat from the relative RTT increase (srRTT), the ACK arrival rate and the sent/received bytes. It replaces HyStart with its own slow start (LAUNCH), adds periodic min RTT probing, and uses the cong_control callback instead of cong_avoid.

Quoted 10 lines
net/ipv4/tcp_roccet.c new file +4 -0
new file mode 100644
index 000000000000..cd8c44490117
@@ -0,0 +1,1400 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * TCP ROCCET: An RTT-Oriented CUBIC Congestion Control
+ * Extension for 5G and Beyond Networks

[ ... ]

Quoted 4 lines

+ * A peer-reviewed paper on TCP ROCCET will be presented
+ * at the WONS 2026 conference.
+ * A draft of the paper is available here:
+ * https://arxiv.org/abs/2510.25281

[ ... ]

Quoted 3 lines

+ * Unless CUBIC is enabled and congestion window is large
+ * this behaves the same as the original Reno.
+ */

[Severity: Low]
This isn't a bug, but some comments in this file don't match the code.

The header says the paper "will be presented at the WONS 2026 conference", but the commit message already cites the published WONS 2026 DOI.

The sentence "Unless CUBIC is enabled and congestion window is large this behaves the same as the original Reno" was copied from tcp_cubic.c. It doesn't describe ROCCET.

In roccet_handle_state_transitions(), the comment says the LAUNCH bookkeeping also happens "Even on LAUNCH to LAUNCH transitions (on RTO)". The function returns before it gets there:

	if (ca->state == prev_state)
		return;

In roccet_init(), the comment says initial_limit_reached and is_in_initial_launch are reset "so they only get set during init". However, roccet_reset() memsets them again on every TCP_CA_Loss. After an RTO, initial_limit_reached is false again. That turns off the !tcp_is_cwnd_limited() LAUNCH exit until roccet_control() sets the flag again.

In roccet_min_rtt_probe(), the comment refers to "the time_before32 check", but the code uses time_between32().

The comment above roccet_update_pacing_rate() says the code is similar to tcp_cong_control() and paces at 200% in slow start. The logic actually follows tcp_update_pacing_rate(), and the slow start ratio comes from sysctl_tcp_pacing_ss_ratio.

[ ... ]

Quoted 9 lines
net/ipv4/tcp_roccet.c new file +9 -0
+/* Parameters that are specific to the ROCCET-Algorithm */
+static uint sr_rtt_upper_bound __read_mostly = 100;
+static int ack_rate_diff_ss __read_mostly = 10;
+
+module_param(sr_rtt_upper_bound, uint, 0644);
+MODULE_PARM_DESC(sr_rtt_upper_bound, "ROCCET's upper bound for srRTT.");
+module_param(ack_rate_diff_ss, int, 0644);
+MODULE_PARM_DESC(ack_rate_diff_ss,
+		 "ROCCET's threshold to exit slow start if ACK-rate differs by given amount of segments.");

[ ... ]

net/ipv4/tcp_roccet.c line 176

Quoted 2 lines
net/ipv4/tcp_roccet.c new file +2 -0
+module_param_cb(beta, &beta_param_ops, &beta, 0644);
+MODULE_PARM_DESC(beta, "beta for multiplicative increase");

[Severity: Low]
This isn't a bug, but both of these parameter descriptions disagree with how the parameters are used.

beta is only used as the multiplicative decrease factor
(cwnd * beta / BICTCP_BETA_SCALE) in roccet_congestion_event(), roccet_recalc_ssthresh() and roccet_handle_recovery(). Should the text say "multiplicative decrease"?

For ack_rate_diff_ss, roccet_launch_update() exits LAUNCH when the ACK rate increase is at most this value, and only if srRTT is also above sr_rtt_upper_bound:

	if ((ca->curr_srrtt > sr_rtt_upper_bound &&
	     get_ack_rate_diff(ca) <= ack_rate_diff_ss) ||

get_ack_rate_diff() clamps decreases to 0. A negative value written to this 0644 int therefore permanently disables the srRTT based LAUNCH exit. Should it be bounds checked the same way beta is?

[ ... ]

Quoted 5 lines
net/ipv4/tcp_roccet.c new file +5 -0
+/* Compute srRTT.
+ */
+static void update_srrtt(struct roccettcp *ca)
+{
+	u64 rrtt;

[ ... ]

net/ipv4/tcp_roccet.c line 457

Quoted 8 lines
net/ipv4/tcp_roccet.c new file +8 -0
+	rrtt = div_u64(100 * (u64)(ca->curr_rtt - ca->curr_min_rtt),
+		       ca->curr_min_rtt);
+
+	/* (1 - alpha) * srRTT + alpha * rRTT */
+	ca->curr_srrtt = ((100 - ROCCET_ALPHA_TIMES_100) * (u64)ca->curr_srrtt +
+			  ROCCET_ALPHA_TIMES_100 * rrtt) /
+			 100;
+}

[Severity: Low]
Will this link on 32-bit architectures? This divides a u64 by 100 with a plain '/'. The compiler turns that into a libgcc call (__udivdi3 on i386, __aeabi_uldivmod on arm), and the kernel doesn't provide those. The module link would then fail with an undefined reference.

TCP_CONG_ROCCET has no architecture dependency, so 32-bit allmodconfig builds will enable it. The rest of the file already uses div_u64() and do_div(). Would div_u64(..., 100) work here?

[ ... ]

Quoted 2 lines
net/ipv4/tcp_roccet.c new file +2 -0
+static void roccet_enter_min_rtt_probe(struct sock *sk, u32 now)
+{

[ ... ]

net/ipv4/tcp_roccet.c line 586

Quoted 10 lines
net/ipv4/tcp_roccet.c new file +10 -0
+	if (!tcp_is_cwnd_limited(sk))
+		probe_cwnd = max(tcp_snd_cwnd(tp) / 3, TCP_INIT_CWND);
+	else
+		probe_cwnd = max(tcp_snd_cwnd(tp) / 2, TCP_INIT_CWND);
+
+	ca->probe_min_rtt_until = now + interval;
+	ca->cwnd_before_min_rtt_probe = tcp_snd_cwnd(tp);
+
+	/* Reduce the cwnd to drain the buffer for probing. */
+	tcp_snd_cwnd_set(tp, probe_cwnd);

[Severity: Medium]
Can this raise cwnd instead of lowering it?

roccet_congestion_event() and roccet_handle_recovery() let cwnd drop as low as 2, so a cwnd between 2 and 9 is reachable in ORBITER. With cwnd 4, probe_cwnd becomes TCP_INIT_CWND (10). That adds queueing right after curr_min_rtt has been reset to U32_MAX, while the new minimum is being measured.

The same TCP_INIT_CWND floor is used for the LAUNCH exit in roccet_launch_update():

	tcp_snd_cwnd_set(tp, max(tcp_snd_cwnd(tp) / 2,
				 TCP_INIT_CWND));

and in roccet_state() for TCP_CA_Loss:

	tcp_snd_cwnd_set(tp, TCP_INIT_CWND);

None of these writes applies tp->snd_cwnd_clamp, unlike roccet_congestion_event() and tcp_cong_avoid_ai(). A locked route cwnd metric can set the clamp below 10 in tcp_init_metrics():

	if (tcp_metric_locked(tm, TCP_METRIC_CWND))
		tp->snd_cwnd_clamp = tcp_metric_get(tm, TCP_METRIC_CWND);

BPF can do the same. Is that clamp then exceeded on probe entry, LAUNCH exit and RTO?

[ ... ]

Quoted 7 lines
net/ipv4/tcp_roccet.c new file +7 -0
+static void roccet_cwnd_event_tx_start(struct sock *sk)
+{
+	struct roccettcp *ca = inet_csk_ca(sk);
+	u32 now = tcp_jiffies32;
+	s32 delta;
+
+	delta = now - tcp_sk(sk)->lsndtime;

net/ipv4/tcp_roccet.c line 682

Quoted 10 lines
net/ipv4/tcp_roccet.c new file +10 -0
+
+	/* We were application limited (idle) for a while.
+	 * Shift epoch_start to keep cwnd growth to cubic curve.
+	 */
+	if (ca->epoch_start && delta > 0) {
+		ca->epoch_start += delta;
+		if (after(ca->epoch_start, now))
+			ca->epoch_start = now;
+	}
+}

[Severity: Medium]
Is this enough idle handling for a .cong_control module? CUBIC can get away with only shifting epoch_start, because the core restarts cwnd after idle and applies RFC 7661 validation for it. Both of those core paths are skipped when cong_control is set:

include/net/tcp.h:tcp_slow_start_after_idle_check() {
	if (!READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_slow_start_after_idle) ||
	    tp->packets_out || ca_ops->cong_control)
		return;
	...
}
net/ipv4/tcp_output.c:tcp_cwnd_validate() {
	...
		if (READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_slow_start_after_idle) &&
		    (s32)(tcp_jiffies32 - tp->snd_cwnd_stamp) >= inet_csk(sk)->icsk_rto &&
		    !ca_ops->cong_control)
			tcp_cwnd_application_limited(sk);
	...
}

As a result, net.ipv4.tcp_slow_start_after_idle has no effect for ROCCET. After an idle period longer than RTO, the connection resumes with its full stale cwnd, paced only over the old srtt.

No other ROCCET path lowers cwnd based on idle time. The idle reset in update_ack_rate() only clears counters.

[ ... ]

Quoted 2 lines
net/ipv4/tcp_roccet.c new file +2 -0
+static void roccet_orbiter_update(struct sock *sk, u32 acked)
+{

[ ... ]

net/ipv4/tcp_roccet.c line 965

Quoted 11 lines
net/ipv4/tcp_roccet.c new file +11 -0
+	sent = tp->snd_nxt - ca->interval_snd_seq_start;
+	received = tp->snd_una - ca->interval_una_seq_start;
+
+	/* Check sent and received bytes from the previous interval.
+	 * Here we use a guard space of 1% of the current cwnd.
+	 * We do this to avoid a false positive evaluation due
+	 * to delays caused by jitter or scheduling.
+	 */
+	sent_more_than_acked = (s32)(sent - received) >
+			       div_u64((u64)tcp_snd_cwnd(tp) *
+			       (u64)tp->mss_cache, 100);

[Severity: Medium]
Does this comparison do what's intended? (s32)(sent - received) is compared with the u64 that div_u64() returns. The usual arithmetic conversions turn the s32 into a u64, so any negative difference becomes a value near 2^64 and sent_more_than_acked comes out true.

sent - received is the in-flight bytes now minus the in-flight bytes at the start of the interval. It goes negative whenever the flight shrinks:

- after a cwnd reduction (DRAIN lasts only 100ms, but the flight keeps shrinking for about an RTT)
- during Recovery, when snd_nxt stalls
- when the sender is application limited

In those cases, can the srRTT branch below fire another roccet_congestion_event() while the queue is actually draining?

The growth check has the same input:

	if (!tcp_is_cwnd_limited(sk) || sent_more_than_acked ||
	    tcp_in_cwnd_reduction(sk))
		return;

Does it then block CUBIC growth right when growth should resume?

[ ... ]

net/ipv4/tcp_roccet.c line 994

Quoted 8 lines
net/ipv4/tcp_roccet.c new file +8 -0
+	/* The srRTT exceeds the upper bound if bufferbloat happens.
+	 * Here, we want to reduce the cwnd and drain the buffer.
+	 */
+	if (ca->curr_srrtt > roccet_xj && evaluate_srrtt &&
+	    sent_more_than_acked) {
+		roccet_congestion_event(sk, now);
+		return;
+	}

[Severity: Medium]
Can a single ACK reduce cwnd twice when Recovery starts? tcp_ack() runs tcp_fastretrans_alert() before tcp_cong_control(). For a flow in ORBITER:

tcp_fastretrans_alert()
tcp_enter_recovery()
tcp_set_ca_state(sk, TCP_CA_Recovery)
roccet_state()
roccet_handle_recovery() /* cwnd = beta * cwnd */
tcp_cong_control()
roccet_control()
roccet_orbiter_update()
roccet_congestion_event() /* cwnd = beta * cwnd again */

roccet_state() makes the first cut. tcp_set_ca_state() calls set_state before it updates icsk_ca_state, so tcp_in_cwnd_reduction() is still false there. The Recovery path doesn't change the ROCCET state, set roccet_last_event_time_us, or push out next_srrtt_check_ts.

Suppose the srRTT deadline has passed, curr_srrtt is above roccet_xj, and sent_more_than_acked is true. It is often true in Recovery because of the signed/unsigned comparison above. Then roccet_congestion_event() runs here. The tcp_in_cwnd_reduction() check in roccet_orbiter_update() comes after this branch and only guards growth. Later ACKs during Recovery can hit the same branch.

With the default beta, cwnd 100 would go to 70 and then to 49. The v9 changelog item "Avoid double-cwnd-reduction via tcp_in_cwnd_reduction check" covers the roccet_state() path, but not this one.

[ ... ]

Quoted 11 lines
net/ipv4/tcp_roccet.c new file +11 -0
+static u32 roccet_recalc_ssthresh(struct sock *sk)
+{
+	const struct tcp_sock *tp = tcp_sk(sk);
+	struct roccettcp *ca = inet_csk_ca(sk);
+	u32 cwnd = tcp_snd_cwnd(tp);
+
+	/* In LAUNCH, we want no reduction on loss/ECN.
+	 * On ECN this is set later on in roccet_state()
+	 */
+	if (ca->state == LAUNCH)
+		return cwnd;

[ ... ]

Quoted 2 lines
net/ipv4/tcp_roccet.c new file +2 -0
+static u32 roccet_handle_recovery(struct sock *sk)
+{

[ ... ]

net/ipv4/tcp_roccet.c line 1080

Quoted 5 lines
net/ipv4/tcp_roccet.c new file +5 -0
+	/* On loss in LAUNCH, enter ORBITER without a cwnd reduction. */
+	if (ca->state == LAUNCH) {
+		ca->state = ORBITER;
+		return cwnd;
+	}

[Severity: Medium]
On loss, the commit message only says that ROCCET "reacts to packet loss by adapting CUBIC's behavior". Is it intended that a loss in LAUNCH never reduces cwnd?

When Recovery starts in LAUNCH, roccet_recalc_ssthresh() returns the unreduced cwnd. roccet_handle_recovery() then switches to ORBITER and keeps the same cwnd. roccet_last_event_time_us isn't set, so no DRAIN follows. roccet_orbiter_update() only blocks growth during Recovery, so the slow start overshoot window survives the first recovery episode. Since .cong_control is set, tcp_cong_control() returns before the core reduction runs, so nothing else lowers it either.

On RTO, tcp_enter_loss() sets cwnd to packets_in_flight + 1, normally 1, which is the RFC 5681 loss window. roccet_state(TCP_CA_Loss) then overwrites it with TCP_INIT_CWND. If the RTO happens in LAUNCH, ssthresh also stays at the full overshoot cwnd. roccet_recalc_ssthresh() returned cwnd, and roccet_state() only applies max(ssthresh, TCP_INIT_CWND). Does the flow then slow start straight back to the window that caused the timeout?

These deviations are only described in two places. One is the file header ("LAUNCH, where loss is not considered as a congestion event"). The other is the changelog below the --- line ("Increase cwnd back to TCP_CWND_INIT on RTO"), which is dropped when the patch is applied. These are departures from RFC 5681 and RFC 9438. Could the commit message describe them?

[ ... ]

net/ipv4/tcp_roccet.c line 1176

Quoted 13 lines
net/ipv4/tcp_roccet.c new file +13 -0
+static void roccet_state(struct sock *sk, u8 new_state)
+{
+	struct roccettcp *ca = inet_csk_ca(sk);
+	struct tcp_sock *tp = tcp_sk(sk);
+	u32 cwnd;
+	u32 now = jiffies_to_usecs(tcp_jiffies32);
+	enum roccet_state prev_state = ca->state;
+
+	if (new_state == TCP_CA_Loss) {
+		roccet_reset(sk, ca);
+		tcp_snd_cwnd_set(tp, TCP_INIT_CWND);
+		WRITE_ONCE(tp->snd_ssthresh, max(tp->snd_ssthresh,
+						 TCP_INIT_CWND));

[Severity: Medium]
What happens here if the RTO turns out to be spurious? roccet_reset() puts ROCCET into LAUNCH and clears last_max_cwnd and delay_min. Suppose F-RTO later undoes the loss:

tcp_process_loss()
tcp_try_undo_loss()
tcp_undo_cwnd_reduction() /* cwnd and ssthresh restored */
tcp_set_ca_state(sk, TCP_CA_Open) /* ignored by roccet_state() */

tcp_reno_undo_cwnd() restores cwnd to max(cwnd, prior_cwnd), and ssthresh goes back to prior_ssthresh. In congestion avoidance that leaves cwnd >= ssthresh while ROCCET is still in LAUNCH.

On the same ACK, roccet_control()->roccet_launch_update() sees !tcp_in_slow_start(tp) and takes the exit branch:

	tcp_snd_cwnd_set(tp, max(tcp_snd_cwnd(tp) -
				 (tcp_snd_cwnd(tp) / 3),
				 TCP_INIT_CWND));
	WRITE_ONCE(tp->snd_ssthresh, tcp_snd_cwnd(tp));

Does this cut the restored window by about a third, and lose the CUBIC Wmax and delay history, even though the RTO was spurious?

Quoted 1 line
net/ipv4/tcp_roccet.c new file +1 -0
+	} else if (tcp_in_cwnd_reduction(sk)) {

[ ... ]

net/ipv4/tcp_roccet.c line 1193

Quoted 5 lines
net/ipv4/tcp_roccet.c new file +5 -0
+	} else if (new_state == TCP_CA_Recovery) {
+		/* Directly reduce cwnd and rely on pacing */
+		cwnd = roccet_handle_recovery(sk);
+		tcp_snd_cwnd_set(tp, cwnd);
+	}

[Severity: Medium]
Because .cong_control is set, the core skips tcp_cwnd_reduction() (PRR). That includes the fast retransmit PRR forces when recovery starts. tcp_rack_reo_timeout() also leaves this to the module:

	if (!inet_csk(sk)->icsk_ca_ops->cong_control)
		tcp_cwnd_reduction(sk, 1, tp->lost - lost, 0);

Here cwnd is set straight to about 0.7 * cwnd, with no packet conservation step (BBR uses in_flight + acked). roccet_orbiter_update() then blocks growth while tcp_in_cwnd_reduction() is true. Can this delay, or stall, the first retransmission?

tcp_xmit_retransmit_queue() stops while in_flight >= cwnd:

	segs = tcp_snd_cwnd(tp) - tcp_packets_in_flight(tp);
	if (segs <= 0)
		break;

tcp_schedule_loss_probe() only arms TLP in Open or CWR.

With a single loss, the first retransmit waits until SACKs bring in_flight below 0.7 * cwnd, roughly 30% of an RTT.

With heavy tail loss that isn't marked lost yet, it stalls completely. For example: cwnd 100, packet 10 lost, 11..30 SACKed, 31..100 lost, so in_flight == 70 == cwnd. Then no retransmission or probe goes out until the RTO, which also resets all ROCCET state. PRR would have sent one forced retransmit in this case.

Quoted 8 lines
net/ipv4/tcp_roccet.c new file +8 -0
+
+	roccet_handle_state_transitions(sk, prev_state, now);
+}
+
+/* Update RTT samples and min RTT.
+ */
+static void roccet_acked(struct sock *sk, const struct ack_sample *sample)
+{

[ ... ]

Quoted 3 lines
net/ipv4/tcp_roccet.c new file +3 -0
+	/* Discard delay samples right after fast recovery */
+	if (ca->epoch_start && (s32)(tcp_jiffies32 - ca->epoch_start) < HZ)
+		return;

[ ... ]

net/ipv4/tcp_roccet.c line 1227

Quoted 6 lines
net/ipv4/tcp_roccet.c new file +6 -0
+	/* Get valid sample for roccet */
+	if (sample->rtt_us > 0) {
+		ca->last_rtt = ca->curr_rtt;
+		ca->curr_rtt = sample->rtt_us;
+	}
+}

[Severity: Medium]
This early return comes from CUBIC, but here it also skips the curr_rtt and last_rtt updates below. Is that intended?

bictcp_update() starts a new epoch on the first ORBITER growth after any
reduction: DRAIN to ORBITER, Recovery, CWR, Loss, or LAUNCH exit. roccet_cwnd_event_tx_start() can also clamp epoch_start to now after idle. For one second after that, curr_rtt and last_rtt stay frozen.

Meanwhile, roccet_control() keeps calling update_min_rtt() and update_srrtt() on every ACK with acked_sacked > 0. The srRTT EWMA and the jitter term then converge on RTT data up to a second old. That could trigger or suppress srRTT congestion events.

The min RTT probe looks affected too. roccet_enter_min_rtt_probe() resets curr_min_rtt to U32_MAX but leaves epoch_start alone. If the probe starts less than a second after an epoch start, every probe sample is discarded. update_min_rtt() then copies the stale, pre-probe curr_rtt into curr_min_rtt. Would the probe record an inflated minimum RTT in that case?

[ ... ]

net/ipv4/tcp_roccet.c line 1303

Quoted 6 lines
net/ipv4/tcp_roccet.c new file +6 -0
+static void roccet_control(struct sock *sk, u32 ack, int flag,
+			   const struct rate_sample *rs)
+{
+	struct roccettcp *ca = inet_csk_ca(sk);
+
+	u32 now = jiffies_to_usecs(tcp_jiffies32);

[Severity: Low]
Is jiffies_to_usecs(tcp_jiffies32) safe as a wrapping timestamp for time_between32() and time_after32()? It is only linear mod 2^32 when HZ divides USEC_PER_SEC. For HZ values such as 300, it uses the out-of-line version in kernel/time/time.c:

#if BITS_PER_LONG == 32
	return (HZ_TO_USEC_MUL32 * j) >> HZ_TO_USEC_SHR32;
#else
	return (j * HZ_TO_USEC_NUM) / HZ_TO_USEC_DEN;

#endif

That result is not congruent mod 2^32. tcp_jiffies32 wraps 5 minutes after boot (because of INITIAL_JIFFIES) and then every 2^32 jiffies. At each wrap, now jumps by about 1.43e9 us.

Wouldn't all of ROCCET's time windows then misfire at once, for every socket? That covers the DRAIN exit, the ack rate idle reset, the next_min_rtt_probe expiry (a spurious probe that halves cwnd), the next_srrtt_check_ts evaluation, the probe and refill timers, and the 500ms initial limit check.

BBR avoids this by using tp->tcp_mstamp.

[ ... ]

Signature · 1 line
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/arqDuiD4Q4GWOFBc%40volt-roccet-vm