Skip to contentSkip to conversation

tcp: Add TCP ROCCET congestion control module.

PATCH Applied Tim Fuechsel 4 participants 5 messages Oct 8 – Oct 19

Subsystems (4)
lore
Share & tools
lore
#0 lore raw

PATCH v10 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

Changes since v9:
* Don't discard all rtt samples after fast recovery in min RTT probing
* Enter LAUNCH after sufficient idle period
* Fix outdated comments
* Apply snd_cwnd_clamp everywhere
* Remove possibility for double ROCCET CE in ORBITER
* Fix incorrect implicit s32->u64 cast for sent_more_than_acked
* Fix some incorrect parameter resets on roccet_reset

---
 net/ipv4/Kconfig      |   12 +
 net/ipv4/Makefile     |    1 +
 net/ipv4/tcp_roccet.c | 1408 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1421 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 +1408 -0

1,408 lines not on this page · raw message

base-commit: 8df0638138d3e0344fd1fb36cf2d1ca1cf5028f0

Signature · 1 line
2.43.0
#1 lore raw
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] tcp_roccet: struct roccettcp exceeds ICSK_CA_PRIV_SIZE causing BUILD_BUG_ON - [Medium] tcp_roccet…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] tcp_roccet: struct roccettcp exceeds ICSK_CA_PRIV_SIZE causing BUILD_BUG_ON
- [Medium] tcp_roccet: ca->next_min_rtt_probe type confusion (jiffies vs usecs) on LAUNCH transition --

commit f0a2d58d971a3f893ec1f1f112a640b906471634
Author: Tim Fuechsel <[email protected]>
Subject: 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. It relies on two additional metrics (relative increase in RTT from its minimum and acknowledgment arrival rate) to determine the send rate and avoid bufferbloat. It features a modified slow start and specific minimum RTT probing logic.

Quoted 6 lines
net/ipv4/tcp_roccet.c new file +0 -0
new file mode 100644
index 0000000000000..04b9e52466126
@@ -0,0 +1,1408 @@

[ ... ]

Quoted 58 lines
net/ipv4/tcp_roccet.c new file +58 -0
+struct roccettcp {
+	u32 cnt;	/* increase cwnd by 1 after ACKs */
+	u32 last_max_cwnd;	/* last maximum snd_cwnd */
+	u32 last_cwnd;	/* the last snd_cwnd */
+	u32 last_time;	/* time when updated last_cwnd */
+	u32 bic_origin_point;	/* origin point of bic function */
+	u32 bic_K;	/* time to origin point from the
+			 * beginning of the current epoch
+			 */
+	u32 delay_min;	/* min delay (usec) */
+	u32 epoch_start;	/* beginning of an epoch */
+	u32 ack_cnt;	/* number of acks */
+	u32 tcp_cwnd;	/* estimated tcp cwnd */
+	u32 curr_rtt;	/* last sample rtt of current round */
+
+	u32 roccet_last_event_time_us;	/* The last time ROCCET was triggered */
+	u32 curr_min_rtt;	/* The current observed minRTT */
+	u32 next_min_rtt_probe;	/* Next time to probe the minRTT */
+	u32 probe_min_rtt_until;	/* End of minRTT probing period.
+					 * Set while in RTT_PROBE states
+					 */
+	u32 refill_until;	/* End of pipe refill after minRTT probe.
+				 * Set while in RTT_PROBE states
+				 */
+	u32 cwnd_before_min_rtt_probe;	/* cwnd before min RTT probing. */
+	u32 curr_srrtt;	/* srRTT calculated based on the latest ACK */
+	u32 next_srrtt_check_ts;	/* Next check for srRTT */
+	u32 last_rtt;	/* sample rtt of previous round.
+			 * Used for jitter calculation
+			 */
+
+	u32 interval_snd_seq_start;
+	u32 interval_una_seq_start;
+
+	u32 ack_carry_over;	/* Used to carry over leftover acks from
+				 * LAUNCH to ORBITER
+				 */
+
+	u32 last_ack_rate_time;	/* Timestamp of the last ACK-rate */
+	u16 ack_rate_last_rate;	/* Last ACK-rate */
+	u16 curr_ack_rate;	/* Current ACK-rate */
+	u16 ack_rate_cnt;	/* Used for counting acks */
+
+	enum roccet_state state : 3; /* Current operating state of roccet */
+	bool initial_limit_reached: 1;	/* Set to true after the connection
+					 * initially gets cwnd-limited
+					 */
+	bool is_in_initial_launch: 1;	/* true if the connection is in
+					 * the initial launch phase.
+					 */
+	bool last_event_time_set: 1;	/* true if the last event time
+					 * has been set/initialized.
+					 */
+	bool rtt_probe_timers_set: 1;	/* true if refill_until and
+					 * probe_min_rtt_until are currently
+					 * set.
+					 */
+};

[ ... ]

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

[ ... ]

Quoted 5 lines
net/ipv4/tcp_roccet.c new file +5 -0
+	/* Enter RTT_PROBE when the "timer" has expired.
+	 *
+	 * Since we are in ORBITER, we should have already received at least
+	 * one RTT sample. However safeguard against it if not.
+	 *

net/ipv4/tcp_roccet.c line 897

Quoted 10 lines
net/ipv4/tcp_roccet.c new file +10 -0
+	 * Use double the probing period as a guard space to avoid
+	 * idle-time locking us out of probing.
+	 */
+	if (!time_between32(now, ca->next_min_rtt_probe
+				 - 2 * ROCCET_NEXT_MIN_RTT_PROBE_MS
+				 * USEC_PER_MSEC, ca->next_min_rtt_probe) &&
+	    ca->curr_rtt != U32_MAX) {
+		ca->state = RTT_PROBE_ENTER;
+		return;
+	}

[Severity: Medium]
Could this condition falsely trigger a premature RTT probe if the connection was previously idle?

In roccet_enter_min_rtt_probe(), ca->next_min_rtt_probe is assigned a raw jiffies timestamp to track the probe epoch. Normal state transitions out of probing explicitly reinitialize next_min_rtt_probe to a usecs timestamp.

If the connection then enters ORBITER from LAUNCH, this block compares a usecs timestamp (now) against the residual jiffies timestamp. Since usecs are vastly larger, won't this check instantly fail and incorrectly re-enter probing?

[ ... ]

Quoted 4 lines
net/ipv4/tcp_roccet.c new file +4 -0
+static void roccet_handle_state_transitions(struct sock *sk,
+					    enum roccet_state prev_state,
+					    u32 now)
+{

[ ... ]

net/ipv4/tcp_roccet.c line 1126

Quoted 11 lines
net/ipv4/tcp_roccet.c new file +11 -0
+	/* Whenever we leave the min RTT probing states (and not just
+	 * transition between them), we want to reset the probing timers.
+	 */
+	if ((prev_state == RTT_PROBE_ENTER || prev_state == RTT_PROBE ||
+	     prev_state == RTT_PROBE_REFILL) &&
+	    (ca->state != RTT_PROBE && ca->state != RTT_PROBE_REFILL)) {
+		ca->probe_min_rtt_until = 0;
+		ca->refill_until = 0;
+		ca->rtt_probe_timers_set = false;
+	}
+}

[Severity: Medium]
When the connection goes idle during probing, roccet_cwnd_event_tx_start() forces an immediate transition to LAUNCH. This cleanup logic handles clearing the probe timers, but does it miss reinitializing next_min_rtt_probe to usecs for that transition?

[ ... ]

net/ipv4/tcp_roccet.c line 1385

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);

[Severity: High]
Does this assertion fail unconditionally because the struct exceeds 104 bytes?

struct roccettcp contains 24 u32 fields (96 bytes) and 3 u16 fields (6 bytes), consuming 102 bytes. The next field is the enum roccet_state bitfield, which forces 2 bytes of padding after the u16 fields, placing the bitfield block at offset 104, pushing the total struct size to 108 bytes.

Since ICSK_CA_PRIV_SIZE is exactly 104 bytes, won't this break compilation on standard architectures?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/asdfdDIQb7Nhm692@volt-roccet-vm?part=1
#2 lore raw

Thanks for the review. We have some comments about the two mentioned issues and don't think any code changes are necessary.

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

[ ... ]

Quoted 7 lines

+    /* Enter RTT_PROBE when the "timer" has expired.
+     *
+     * Since we are in ORBITER, we should have already received at least +     * one RTT sample. However safeguard against it if not. +     *
+     * Use double the probing period as a guard space to avoid +     * idle-time locking us out of probing.

Quoted 8 lines

+     */
+    if (!time_between32(now, ca->next_min_rtt_probe +                 - 2 * ROCCET_NEXT_MIN_RTT_PROBE_MS +                 * USEC_PER_MSEC, ca->next_min_rtt_probe) && +        ca->curr_rtt != U32_MAX) {
+        ca->state = RTT_PROBE_ENTER;
+        return;
+    }

[Severity: Medium]
Could this condition falsely trigger a premature RTT probe if the connection was previously idle?

Quoted 7 lines

In roccet_enter_min_rtt_probe(), ca->next_min_rtt_probe is assigned a raw jiffies timestamp to track the probe epoch. Normal state transitions out of probing explicitly reinitialize next_min_rtt_probe to a usecs timestamp.

If the connection then enters ORBITER from LAUNCH, this block compares a usecs timestamp (now) against the residual jiffies timestamp. Since usecs are vastly larger, won't this check instantly fail and incorrectly re-enter probing?

This is true, however, it won't cause any problems.
Since it makes sense to initiate a min RTT probe after an idle period, this behavior is consistent with how the algorithm was designed.

Quoted 18 lines

[ ... ]

Quoted 4 lines
net/ipv4/tcp_roccet.c new file +4 -0
+static void roccet_handle_state_transitions(struct sock *sk,
+                        enum roccet_state prev_state,
+                        u32 now)
+{

[ ... ]

Quoted 11 lines

+    /* Whenever we leave the min RTT probing states (and not just +     * transition between them), we want to reset the probing timers. +     */
+    if ((prev_state == RTT_PROBE_ENTER || prev_state == RTT_PROBE || +         prev_state == RTT_PROBE_REFILL) &&
+        (ca->state != RTT_PROBE && ca->state != RTT_PROBE_REFILL)) { +        ca->probe_min_rtt_until = 0;
+        ca->refill_until = 0;
+        ca->rtt_probe_timers_set = false;
+    }
+}

Quoted 5 lines

[Severity: Medium]
When the connection goes idle during probing, roccet_cwnd_event_tx_start() forces an immediate transition to LAUNCH. This cleanup logic handles clearing the probe timers, but does it miss reinitializing next_min_rtt_probe to usecs for that transition?

Same as above. This will lead to a new min RTT probe after the LAUNCH phase, which is a desirable behaviour.

Quoted 10 lines

[ ... ]

net/ipv4/tcp_roccet.c line 1385

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);

[Severity: High]
Does this assertion fail unconditionally because the struct exceeds 104 bytes?

Quoted 7 lines

struct roccettcp contains 24 u32 fields (96 bytes) and 3 u16 fields (6 bytes), consuming 102 bytes. The next field is the enum roccet_state bitfield, which forces 2 bytes of padding after the u16 fields, placing the bitfield block at offset 104, pushing the total struct size to 108 bytes.

Since ICSK_CA_PRIV_SIZE is exactly 104 bytes, won't this break compilation on standard architectures?

Based on our understanding and compilation results, this is not the case. The structure, including padding, is exactly 104 bytes in size.

#3 lore raw
In reply to Tim Fuechsel
Thank you for your contribution! Sashiko AI review found 11 potential issue(s) to consider.

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

Critical: 0 · High: 0 · Medium: 7 · 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] roccet_acked() copies CUBIC's 'discard delay samples right after fast recovery' test, `ca->epoch_start && (s32)(tcp_jiffies32 -…
- [Medium] tcp_slow_start_after_idle_check() returns early when ca_ops->cong_control is set, so roccet_cwnd_event_tx_start() is the…
- [Medium] roccet_tcp pairs .undo_cwnd = tcp_reno_undo_cwnd with a set_state(TCP_CA_Loss) handler whose effect undo cannot reverse.
- [Medium] roccet_enter_min_rtt_probe() (line 537) stores tcp_jiffies32 in ca->next_min_rtt_probe, a field that otherwise holds a usec…
- [Medium] roccet_cwnd_event_tx_start() treats the RTO retransmission itself as an idle restart and overwrites the ssthresh that…
- [Medium] roccet_enter_min_rtt_probe() computes probe_cwnd = max(cwnd/2 or cwnd/3, TCP_INIT_CWND) and sets cwnd = min(probe_cwnd, clamp), with…
- [Medium] Example: in RTT_PROBE with cwnd_before_min_rtt_probe = 100 and a live cwnd of 50, a fast-recovery event occurs.
- [Low] beta_param_set() validates the string with kstrtoint(val, 10, ...) and then stores it with param_set_int(val, kp).
- [Low] In struct roccettcp, curr_rtt is documented as the 'last sample rtt of current round' and last_rtt as the 'sample rtt of previous…
- [Low] Because ROCCET has .cong_control, the core idle restart in tcp_skb_entail() → tcp_slow_start_after_idle_check() is skipped.
- [Low] roccet_cwnd_event_tx_start() writes tp->snd_ssthresh with a plain store (tp->snd_ssthresh = tcp_snd_cwnd(tp);).

--

tcp: Add TCP ROCCET congestion control module.

This patch adds TCP ROCCET, a congestion control module based on CUBIC. It detects bufferbloat in cellular networks from the relative RTT increase (srRTT) and the ACK arrival rate. It replaces slow start with its own LAUNCH phase, uses CUBIC growth in ORBITER, and periodically probes for a new minimum RTT.

Quoted 6 lines
net/ipv4/tcp_roccet.c new file +0 -0
new file mode 100644
index 0000000000000..04b9e52466126
@@ -0,0 +1,1408 @@

[ ... ]

Quoted 1 line

+struct roccettcp {

[ ... ]

Quoted 1 line
net/ipv4/tcp_roccet.c new file +1 -0
+	u32 curr_rtt;	/* last sample rtt of current round */

[ ... ]

net/ipv4/tcp_roccet.c line 98

Quoted 3 lines
net/ipv4/tcp_roccet.c new file +3 -0
+	u32 last_rtt;	/* sample rtt of previous round.
+			 * Used for jitter calculation
+			 */

[Severity: Low]
This isn't a bug, but these comments describe per-round samples. roccet_acked() actually shifts curr_rtt into last_rtt on every accepted ACK sample. It does not look for round boundaries:

	ca->last_rtt = ca->curr_rtt;
	ca->curr_rtt = sample->rtt_us;

So the jitter that roccet_orbiter_update() uses for roccet_xj is the difference between consecutive ACK samples, not between rounds.

Should the comments be updated, or was a per-round sample intended?

[ ... ]

Quoted 7 lines
net/ipv4/tcp_roccet.c new file +7 -0
+static int beta_param_set(const char *val, const struct kernel_param *kp)
+{
+	int beta_candidate;
+	int ret = kstrtoint(val, 10, &beta_candidate);
+
+	if (ret)
+		return ret;

net/ipv4/tcp_roccet.c line 184

Quoted 10 lines
net/ipv4/tcp_roccet.c new file +10 -0
+
+	if (beta_candidate <= 0 || beta_candidate >= BICTCP_BETA_SCALE) {
+		pr_err_once("TCP ROCCET: beta must be between 0 and %d\n",
+			    BICTCP_BETA_SCALE);
+
+		return -EINVAL;
+	}
+
+	return param_set_int(val, kp);
+}

[Severity: Low]
Do the two parses here agree? The candidate is checked with kstrtoint(val, 10, ...). param_set_int() comes from STANDARD_PARAM_DEF() in kernel/params.c and parses with base 0:

	return strtolfn(val, 0, (type *)kp->arg);

So beta=0717 passes the check as decimal 717 but is stored as octal 463. beta=0x2cd is rejected, even though param_set_int() would accept it.

The stored value stays in range, but it can differ from the value that was checked. Could this store beta_candidate directly, or parse with the same base in both places?

[ ... ]

Quoted 2 lines
net/ipv4/tcp_roccet.c new file +2 -0
+static void roccet_min_rtt_probe_ce(struct roccettcp *ca, u32 cwnd)
+{

[ ... ]

net/ipv4/tcp_roccet.c line 462

Quoted 8 lines
net/ipv4/tcp_roccet.c new file +8 -0
+	if (ca->cwnd_before_min_rtt_probe == 0)
+		pr_warn_once("TCP ROCCET: cwnd_before_min_rtt_probe is 0 during RTT_PROBE. This should not happen.\n");
+	else
+		cwnd = ca->cwnd_before_min_rtt_probe;
+
+	ca->cwnd_before_min_rtt_probe =
+		max((cwnd * beta) / BICTCP_BETA_SCALE, 2U);
+}

[Severity: Medium]
Can a spurious recovery during RTT_PROBE leave a lasting reduction through this saved window?

Take cwnd_before_min_rtt_probe = 100 and a live cwnd of 50 during the probe. Entering fast recovery then does:

tcp_enter_recovery()

    tcp_init_cwnd_reduction()
        tp->prior_cwnd = 50
        roccet_recalc_ssthresh()   <- uses cwnd_before_min_rtt_probe (100)
    tcp_set_ca_state(sk, TCP_CA_Recovery)
        roccet_state()
            roccet_handle_recovery()
                roccet_min_rtt_probe_ce()
                    cwnd_before_min_rtt_probe = 100 * 717 / 1024 = 70

Suppose DSACK later shows the recovery was spurious. tcp_undo_cwnd_reduction() calls tcp_reno_undo_cwnd(), which returns max(cwnd, prior_cwnd) = 50. It never touches cwnd_before_min_rtt_probe.

When the probe ends, roccet_min_rtt_probe() installs the reduced value:

	tcp_snd_cwnd_set(tp, min(ca->cwnd_before_min_rtt_probe,
				 tp->snd_cwnd_clamp));

So cwnd becomes 70 instead of 100. An undo after the probe ends can't get back to 100 either, because prior_cwnd holds 50.

Should the undo path also restore the saved probe window?

[ ... ]

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 537

Quoted 4 lines
net/ipv4/tcp_roccet.c new file +4 -0
+	/* Use field to store when we entered min RTT probing. Here we use
+	 * jiffies to stay consistent with the epoch_start format.
+	 */
+	ca->next_min_rtt_probe = tcp_jiffies32;

[Severity: Medium]
This stores a jiffies value in a field that otherwise holds a usec deadline. roccet_set_next_min_rtt_probe_ts() won't update it while the state is RTT_PROBE or RTT_PROBE_REFILL.

roccet_handle_state_transitions() converts it back to a usec deadline in only two cases: RTT_PROBE or RTT_PROBE_REFILL to ORBITER, and RTT_PROBE_REFILL to DRAIN.

What happens if the application is idle for longer than an RTO during the probe or refill window? roccet_cwnd_event_tx_start() then moves straight to LAUNCH:

		ca->state = LAUNCH;

Only the probe timers are cleared on that transition, so next_min_rtt_probe keeps the jiffies value.

Later LAUNCH to ORBITER or DRAIN to ORBITER transitions don't take the prev_state == RTT_PROBE* branch. roccet_orbiter_update() then compares a usec now against a jiffies deadline:

	if (!time_between32(now, ca->next_min_rtt_probe
				 - 2 * ROCCET_NEXT_MIN_RTT_PROBE_MS
				 * USEC_PER_MSEC, ca->next_min_rtt_probe) &&
	    ca->curr_rtt != U32_MAX) {
		ca->state = RTT_PROBE_ENTER;

This is true for nearly every value of now. Wouldn't ROCCET then enter RTT_PROBE_ENTER right away and cut cwnd for at least 200 ms with no need to probe? The only way out seems to be update_min_rtt() finding a new minimum first.

On the same RTT_PROBE to LAUNCH path, ssthresh is also set from the reduced probe cwnd, and cwnd_before_min_rtt_probe is never applied. Is that intended?

[ ... ]

net/ipv4/tcp_roccet.c line 565

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, min(probe_cwnd, tp->snd_cwnd_clamp));

[Severity: Medium]
Can this raise cwnd during the drain probe?

In ORBITER, cwnd can be anywhere from 2 to 9 after roccet_congestion_event() or roccet_handle_recovery() reduce it to max(cwnd * beta / 1024, 2). probe_cwnd is not limited by the current cwnd, so it becomes TCP_INIT_CWND (10). The probe then sends more than before for at least 200 ms.

curr_min_rtt has just been reset to U32_MAX, so the new minimum is measured with more data in flight than before the probe. That can inflate curr_min_rtt and lower srRTT for the following probe period.

For 10 < cwnd < 20, the reduction is also smaller than the halving described in the commit message.

[ ... ]

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

[ ... ]

net/ipv4/tcp_roccet.c line 1153

Quoted 7 lines
net/ipv4/tcp_roccet.c new file +7 -0
+	/* After idle periods, go into LAUNCH with slow start */
+	if (READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_slow_start_after_idle) &&
+	    delta >= inet_csk(sk)->icsk_rto) {
+		tp->snd_ssthresh = tcp_snd_cwnd(tp);
+		tcp_snd_cwnd_set(tp, min(TCP_INIT_CWND, tp->snd_cwnd_clamp));
+		ca->state = LAUNCH;
+	}

[Severity: Medium]
ROCCET sets .cong_control, so tcp_slow_start_after_idle_check() returns early:

	if (!READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_slow_start_after_idle) ||
	    tp->packets_out || ca_ops->cong_control)
		return;

That makes this the only restart-after-idle logic that runs. Should it follow what tcp_cwnd_restart() does?

	u32 restart_cwnd = tcp_init_cwnd(tp, __sk_dst_get(sk));
	...
	WRITE_ONCE(tp->snd_ssthresh, tcp_current_ssthresh(sk));
	restart_cwnd = min(restart_cwnd, cwnd);

Compared to that, this code differs in a few ways:

There is no min() against the current cwnd. A cwnd of 2 to 9, which is common after the beta reductions, is raised to 10 after idle.

The per-route initcwnd from tcp_init_cwnd() is ignored.

ssthresh is set to the old cwnd instead of tcp_current_ssthresh().

The trigger is delta >= icsk_rto, while the core uses delta > icsk_rto.

The comment says this goes into LAUNCH with slow start. But if the old cwnd is at most 10, ssthresh ends up at or below the new cwnd. Then tcp_in_slow_start() is false, and roccet_launch_update() leaves LAUNCH right away through its !tcp_in_slow_start(tp) branch. Is that intended?

[Severity: Medium]
Can this also fire on the RTO retransmission itself, and not only after a real idle period?

tcp_retransmit_timer()
tcp_enter_loss()
tcp_timeout_mark_lost() <- in_flight drops to 0
->ssthresh()
tcp_set_ca_state(sk, TCP_CA_Loss)
roccet_state() <- cwnd = 10, ssthresh = max(ssthresh, 10)
tcp_retransmit_skb()
__tcp_transmit_skb()
tcp_event_data_sent()
tcp_ca_event(sk, CA_EVENT_TX_START)
roccet_cwnd_event_tx_start()

tcp_event_data_sent() fires CA_EVENT_TX_START before it updates lsndtime:

	if (tcp_packets_in_flight(tp) == 0)
		tcp_ca_event(sk, CA_EVENT_TX_START);

	tp->lsndtime = now;

At that point lsndtime is the previous send, for example the TLP probe or the previous retransmission. The RTO timer was armed for icsk_rto from about that time, so delta >= icsk_rto holds.

With the default slow_start_after_idle=1, this replaces the ssthresh that roccet_state(TCP_CA_Loss) just kept with 10, and sets LAUNCH.

On the next ACK, roccet_launch_update() sees cwnd == ssthresh == 10 and leaves LAUNCH at once. CUBIC then grows again from 10 with last_max_cwnd = 0 (cleared by roccet_reset()), instead of slow starting back toward the old ssthresh.

The core tcp_slow_start_after_idle_check() avoids this case with its tp->packets_out test. Should this path also check packets_out or icsk_ca_state?

[Severity: Low]
Does the restart window apply too late for the first skb after idle?

This runs from CA_EVENT_TX_START inside __tcp_transmit_skb(). By that point, tcp_write_xmit() has already sized the skb using the pre-idle cwnd:

tcp_write_xmit()

    cwnd_quota = tcp_cwnd_test(tp);
    ...
    limit = tcp_mss_split_point(sk, skb, mss_now, cwnd_quota, nonagle);
    ...
    tcp_transmit_skb(sk, skb, 1, gfp)
        tcp_event_data_sent()
            roccet_cwnd_event_tx_start()   <- cwnd cut to 10 here

The core restart, tcp_skb_entail()->tcp_slow_start_after_idle_check(), runs before transmission, but it is skipped for cong_control modules.

So the first GSO packet after idle can carry far more than 10 segments. It is bounded only by tcp_tso_autosize() at the old pacing rate and by gso_max_segs. The pacing rate is not updated until the next ACK either.

[Severity: Low]
Should the ssthresh store here be
WRITE_ONCE(tp->snd_ssthresh, tcp_snd_cwnd(tp))?

tcp_get_timestamping_opt_stats() reads the field without a lock:

	nla_put_u32(stats, TCP_NLA_SND_SSTHRESH, READ_ONCE(tp->snd_ssthresh));

Every other snd_ssthresh store in this module uses WRITE_ONCE(), and KCSAN would likely report this plain store.

[ ... ]

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 1172

Quoted 5 lines
net/ipv4/tcp_roccet.c new file +5 -0
+	if (new_state == TCP_CA_Loss) {
+		roccet_reset(sk, ca);
+		tcp_snd_cwnd_set(tp, min(TCP_INIT_CWND, tp->snd_cwnd_clamp));
+		WRITE_ONCE(tp->snd_ssthresh, max(tp->snd_ssthresh,
+						 tcp_snd_cwnd(tp)));

[Severity: Medium]
What happens to the ROCCET state if this RTO later turns out to be spurious?

roccet_reset() clears the private state, including the last_max_cwnd that roccet_recalc_ssthresh() just computed, and forces state = LAUNCH.

.undo_cwnd is tcp_reno_undo_cwnd. So after F-RTO or tcp_try_undo_loss(), tcp_undo_cwnd_reduction() restores only cwnd and ssthresh:

	tcp_snd_cwnd_set(tp, icsk->icsk_ca_ops->undo_cwnd(sk));
	if (tp->prior_ssthresh > tp->snd_ssthresh) {
		WRITE_ONCE(tp->snd_ssthresh, tp->prior_ssthresh);

The next set_state(TCP_CA_Open) matches none of the branches in roccet_state(), so ROCCET stays in LAUNCH.

tcp_fastretrans_alert() runs before tcp_cong_control() in tcp_ack(). So on the same ACK, roccet_launch_update() sees cwnd >= ssthresh. For a flow that was in ORBITER, prior_ssthresh is max(ssthresh, 3/4 cwnd), which is at most cwnd. It then takes the !tcp_in_slow_start(tp) exit:

		new_cwnd = tcp_snd_cwnd(tp) - (tcp_snd_cwnd(tp) / 3);

Doesn't this cut cwnd by a third right after the core undid the reduction?

[ ... ]

Quoted 2 lines
net/ipv4/tcp_roccet.c new file +2 -0
+static void roccet_acked(struct sock *sk, const struct ack_sample *sample)
+{

[ ... ]

Quoted 4 lines
net/ipv4/tcp_roccet.c new file +4 -0
+	/* Check if we are right after fast recovery. */
+	after_fast_recovery = ca->epoch_start &&
+			      (s32)(tcp_jiffies32 - ca->epoch_start) < HZ;
+

net/ipv4/tcp_roccet.c line 1219

Quoted 10 lines
net/ipv4/tcp_roccet.c new file +10 -0
+	/* If we are not after fast recovery or we are in RTT_PROBE however,
+	 * we want to record every new min-sample.
+	 */
+	if (!after_fast_recovery || ca->state == RTT_PROBE) {
+		/* Get valid sample for roccet */
+		if (sample->rtt_us > 0) {
+			ca->last_rtt = ca->curr_rtt;
+			ca->curr_rtt = sample->rtt_us;
+		}
+	}

[Severity: Medium]
Does this freeze curr_rtt and last_rtt for one second after every epoch start, not just after fast recovery?

In cubictcp_acked(), the same test only gates delay_min and HyStart:

	if (ca->epoch_start && (s32)(tcp_jiffies32 - ca->epoch_start) < HZ)
		return;

Here it also gates the curr_rtt and last_rtt updates. Only RTT_PROBE is exempt.

epoch_start is set to 0 on every LAUNCH exit, ROCCET congestion event, CWR and Recovery. The next bictcp_update() in ORBITER then sets it to tcp_jiffies32. roccet_cwnd_event_tx_start() also clamps it to now after idle. Each of these starts a one second window with no new RTT samples.

During that window, roccet_control() still runs the EWMA on every ACK using the same stale curr_rtt:

	if (rs->acked_sacked > 0) {
		update_min_rtt(ca);
		update_srrtt(ca);
	}

srRTT then converges toward the rRTT of a single stale sample, often the inflated RTT that caused the reduction. Jitter is frozen as well.

next_srrtt_check_ts is at most one second away. Wouldn't the next srRTT evaluation and roccet_xj in roccet_orbiter_update() be computed from this frozen data?

[ ... ]

Signature · 1 line
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/asdfdDIQb7Nhm692%40volt-roccet-vm
#4 lore raw
In reply to Tim Fuechsel
This patch was applied to netdev/net-next.git (main) by Jakub Kicinski <[email protected]>:

Hello:

This patch was applied to netdev/net-next.git (main) by Jakub Kicinski <[email protected]>:

Quoted 29 lines · On Thu, 8 Oct 2026 11:16:36 +0200 you wrote:

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.

Quoted 1 line

[...]

Here is the summary with links:
- [PATCHv10,net-next] tcp: Add TCP ROCCET congestion control module.

    https://git.kernel.org/netdev/net-next/c/819d59638796

You are awesome, thank you!

Signature · 2 lines
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html