* [PATCHv5 net-next] tcp: Add TCP ROCCET congestion control module.
@ 2026-08-31 17:03 Tim Fuechsel
2026-08-31 18:57 ` Eric Dumazet
2026-09-04 22:25 ` netdev-bot+sashiko
0 siblings, 2 replies; 3+ messages in thread
From: Tim Fuechsel @ 2026-08-31 17:03 UTC (permalink / raw)
To: David S. Miller, David Ahern, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Neal Cardwell, Kuniyuki Iwashima,
linux-kernel, netdev, fmancera, ebiggers, bpf, Lukas Prause,
Tim Fuechsel
TCP ROCCET is an 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 addresses the bufferbloat problem by adding two additional
metrics to detect bufferbloat. 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. If CUBIC
increases the send rate or congestion window, and the acknowledgment
arrival rate stays on 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 send and received Bytes
to detect the filling of the bottleneck buffer.
In real-world mobile 5G NR measurements, TCP ROCCET achieves better
performance than CUBIC and BBRv3, by maintaining similar throughput
while reducing the latency. In stationary 5G NR scenarios, the performance
is similar to that of BBRv3. 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
Signed-off-by: Lukas Prause <lukas.prause@ikt.uni-hannover.de>
Signed-off-by: Tim Fuechsel <t.fuechsel@gmx.de>
---
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
---
net/ipv4/Kconfig | 12 +
net/ipv4/Makefile | 1 +
net/ipv4/tcp_roccet.c | 1050 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 1063 insertions(+)
create mode 100644 net/ipv4/tcp_roccet.c
diff --git a/net/ipv4/Kconfig b/net/ipv4/Kconfig
index 301b47660305..3545fb0a045b 100644
--- a/net/ipv4/Kconfig
+++ b/net/ipv4/Kconfig
@@ -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
diff --git a/net/ipv4/Makefile b/net/ipv4/Makefile
index 06e21c26b76f..69e4baaed122 100644
--- a/net/ipv4/Makefile
+++ b/net/ipv4/Makefile
@@ -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
diff --git a/net/ipv4/tcp_roccet.c b/net/ipv4/tcp_roccet.c
new file mode 100644
index 000000000000..18925a79bd8d
--- /dev/null
+++ b/net/ipv4/tcp_roccet.c
@@ -0,0 +1,1050 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * TCP ROCCET: An RTT-Oriented CUBIC Congestion Control
+ * Extension for 5G and Beyond Networks
+ *
+ * TCP ROCCET is a new TCP congestion control
+ * algorithm suited for current cellular 5G NR beyond networks.
+ * It extends the kernel default congestion control CUBIC
+ * and improves its performance, and additionally solves an
+ * unwanted side effects of CUBIC’s implementation.
+ * ROCCET uses its own Slow Start, called LAUNCH, where loss
+ * is not considered as a congestion event.
+ * The congestion avoidance phase, called ORBITER, uses
+ * CUBIC's window growth function and adds, based on RTT
+ * and ACK rate, congestion events.
+ *
+ * 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
+ *
+ *
+ * Further information about CUBIC:
+ * TCP CUBIC: Binary Increase Congestion control for TCP v2.3
+ * Home page:
+ * http://netsrv.csc.ncsu.edu/twiki/bin/view/Main/BIC
+ * This is from the implementation of CUBIC TCP in
+ * Sangtae Ha, Injong Rhee and Lisong Xu,
+ * "CUBIC: A New TCP-Friendly High-Speed TCP Variant"
+ * in ACM SIGOPS Operating System Review, July 2008.
+ * Available from:
+ * http://netsrv.csc.ncsu.edu/export/cubic_a_new_tcp_2008.pdf
+ *
+ * CUBIC integrates a new slow start algorithm, called HyStart.
+ * The details of HyStart are presented in
+ * Sangtae Ha and Injong Rhee,
+ * "Taming the Elephants: New TCP Slow Start", NCSU TechReport 2008.
+ * Available from:
+ * http://netsrv.csc.ncsu.edu/export/hystart_techreport_2008.pdf
+ *
+ * All testing results are available from:
+ * http://netsrv.csc.ncsu.edu/wiki/index.php/TCP_Testing
+ *
+ * Unless CUBIC is enabled and congestion window is large
+ * this behaves the same as the original Reno.
+ */
+
+#include <linux/printk.h>
+#include <linux/math64.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <net/tcp.h>
+
+/* Scale factor beta calculation (max_cwnd = snd_cwnd * beta) */
+#define BICTCP_BETA_SCALE 1024
+
+#define BICTCP_HZ 10 /* BIC HZ 2^10 = 1024 */
+
+/* Alpha value for the sRrTT multiplied by 100.
+ * Here 20 represents a value of 0.2
+ */
+#define ROCCET_ALPHA_TIMES_100 20
+
+/* min RTT probe period in ms */
+#define ROCCET_NEXT_MIN_RTT_PROBE 5000
+
+/* State in which roccet currently operates */
+enum roccet_state {
+ LAUNCH,
+ ORBITER,
+ RTT_PROBE,
+ RTT_PROBE_REFILL,
+ DRAIN
+};
+
+/* TCP ROCCET struct based on the original BICTCP struct with
+ * additions specific to the ROCCET-Algorithm.
+ */
+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 */
+ u32 refill_until; /* End of pipe refill after minRTT probe */
+ u32 cwnd_before_min_rtt_probe; /* cwnd before min RTT probeing */
+ u32 curr_srrtt; /* srRTT calculated based on the latest ACK */
+ u32 next_srrtt_check; /* 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_rate_last_rate_time; /* Timestamp of the last ACK-rate */
+ u16 ack_rate_last_rate; /* Last ACK-rate */
+ u16 ack_rate_curr_rate; /* Current ACK-rate */
+ u16 ack_rate_cnt; /* Used for counting acks */
+
+ bool ece_received : 1; /* Set to true if an ECE bit was received */
+ enum roccet_state state : 3; /* Current operating state of roccet */
+ bool was_idle : 1; /* Tracks whether the connection has was idle
+ * (had no in-flight packets)
+ */
+ bool is_in_recovery: 1; /* Tracks whether the latest state was
+ * TCP_CA_Recovery
+ */
+ bool initial_round_completed: 1; /* Set to true after the initial
+ * roccet-control round has completed
+ */
+};
+
+/* Parameters that are specific to the ROCCET-Algorithm */
+
+// = 717/1024 (BICTCP_BETA_SCALE)
+#define BETA_PARAM_DEFAULT 717
+#define BIC_SCALE_PARAM_DEFAULT 41
+
+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 defer by given amount of segments.");
+
+static int fast_convergence __read_mostly = 1;
+static int beta_param __read_mostly = BETA_PARAM_DEFAULT;
+static int beta __read_mostly;
+static int initial_ssthresh __read_mostly;
+static int bic_scale_param __read_mostly = BIC_SCALE_PARAM_DEFAULT;
+static int bic_scale __read_mostly;
+static int tcp_friendliness __read_mostly = 1;
+
+static u32 cube_rtt_scale __read_mostly;
+static u32 beta_scale __read_mostly;
+static u64 cube_factor __read_mostly;
+
+/* Note parameters that are used for precomputing scale factors are read-only */
+module_param(fast_convergence, int, 0644);
+MODULE_PARM_DESC(fast_convergence, "turn on/off fast convergence");
+module_param(beta_param, int, 0644);
+MODULE_PARM_DESC(beta_param, "beta for multiplicative increase");
+module_param(initial_ssthresh, int, 0644);
+MODULE_PARM_DESC(initial_ssthresh, "initial value of slow start threshold");
+module_param(bic_scale_param, int, 0444);
+MODULE_PARM_DESC(bic_scale_param,
+ "scale (scaled by 1024) value for bic function (bic_scale/1024)");
+module_param(tcp_friendliness, int, 0644);
+MODULE_PARM_DESC(tcp_friendliness, "turn on/off tcp friendliness");
+
+static void roccettcp_reset(struct roccettcp *ca)
+{
+ memset(ca, 0, sizeof(struct roccettcp));
+ ca->next_srrtt_check = 0;
+ ca->curr_min_rtt = ~0U;
+ ca->last_rtt = 0;
+ ca->ece_received = false;
+
+ ca->roccet_last_event_time_us = 0;
+ ca->ack_rate_last_rate = 0;
+ /* Initialize to current time to avoid an
+ * overflow in the ack rate calculation
+ */
+ ca->ack_rate_last_rate_time = jiffies_to_usecs(tcp_jiffies32);
+ ca->ack_rate_curr_rate = 0;
+ ca->ack_rate_cnt = 0;
+
+ /* Start state is LAUNCH */
+ ca->state = LAUNCH;
+
+ ca->initial_round_completed = false;
+}
+
+static void update_min_rtt(struct sock *sk)
+{
+ struct roccettcp *ca = inet_csk_ca(sk);
+
+ /* Check if new lower min RTT was found. If so, set it directly */
+ if (ca->curr_rtt < ca->curr_min_rtt) {
+ ca->curr_min_rtt = max(ca->curr_rtt, 1);
+ /* Probe for the min RTT in ROCCET_NEXT_MIN_RTT_PROBE seconds
+ * if no other update occurs.
+ */
+ ca->next_min_rtt_probe =
+ jiffies_to_usecs(tcp_jiffies32) +
+ ROCCET_NEXT_MIN_RTT_PROBE * USEC_PER_MSEC;
+ }
+}
+
+/* Return difference between last and current ack rate.
+ */
+static s32 get_ack_rate_diff(struct roccettcp *ca)
+{
+ if (ca->ack_rate_curr_rate < ca->ack_rate_last_rate)
+ return 0;
+ return (s32)(ca->ack_rate_curr_rate - ca->ack_rate_last_rate);
+}
+
+/* Update ack rate sampled by 100ms.
+ */
+static void update_ack_rate(struct sock *sk, u32 acked, u32 now)
+{
+ struct roccettcp *ca = inet_csk_ca(sk);
+ s32 interval = USEC_PER_MSEC * 100;
+
+ s32 time_delta = (s32)(ca->ack_rate_last_rate_time - now);
+ const s32 idle_threshold = USEC_PER_SEC * 2;
+
+ /* Check if the time has arrived in the new interval.
+ * Alternatively if the connection was considered to be idle,
+ * also treat as a new interval in order to avoid timing-overflow
+ * problems.
+ */
+ if (time_delta < -interval || ca->was_idle) {
+ /* Check if the connection was idle for X seconds
+ * (e.g. no ACK for X seconds)
+ */
+ if (time_delta < -idle_threshold) {
+ /* Reset ack counting as if a new
+ * connection was created
+ */
+ ca->ack_rate_last_rate = 0;
+ ca->ack_rate_last_rate_time =
+ jiffies_to_usecs(tcp_jiffies32);
+ ca->ack_rate_curr_rate = 0;
+ ca->ack_rate_cnt = 0;
+ } else {
+ ca->ack_rate_last_rate_time = now;
+ ca->ack_rate_last_rate = ca->ack_rate_curr_rate;
+ ca->ack_rate_curr_rate = ca->ack_rate_cnt;
+ ca->ack_rate_cnt =
+ acked; // start counting for the new interval
+ }
+
+ ca->was_idle = false;
+ } else {
+ // Cap the ack count to avoid overflow
+ ca->ack_rate_cnt = min_t(u32, ca->ack_rate_cnt + acked,
+ U16_MAX);
+ }
+}
+
+/* Compute srRTT.
+ */
+static void update_srrtt(struct sock *sk)
+{
+ struct roccettcp *ca = inet_csk_ca(sk);
+
+ /* Avoid integer overflow in the calculation below.
+ * This could occur in cases where we have not yet
+ * received an RTT sample. In these cases, set the
+ * rtt to a safe value.
+ */
+ if (ca->curr_rtt < ca->curr_min_rtt) {
+ ca->curr_rtt = max(ca->curr_rtt, 1);
+ ca->curr_min_rtt = ca->curr_rtt;
+ }
+
+ /* Avoid division by zero */
+ if (ca->curr_min_rtt == 0) {
+ ca->curr_min_rtt = max(ca->curr_min_rtt, 1);
+ return; // skip srRTT update
+ }
+
+ /* Calculate the new rRTT (Scaled by 100).
+ * 100 * ((sRTT - sRTT_min) / sRTT_min).
+ *
+ * curr_min_rtt_timed.rtt is always <= than curr_rtt,
+ * since this is the minimum of the rtt.
+ *
+ * 0 is a valid value for rrtt.
+ */
+ u32 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) * ca->curr_srrtt +
+ ROCCET_ALPHA_TIMES_100 * rrtt) /
+ 100;
+}
+
+/* Do a ROCCET congestion event.
+ */
+static void roccet_congestion_event(struct sock *sk, u32 now)
+{
+ struct tcp_sock *tp = tcp_sk(sk);
+ struct roccettcp *ca = inet_csk_ca(sk);
+
+ ca->epoch_start = 0;
+ ca->roccet_last_event_time_us = now;
+ ca->cnt = 100 * tcp_snd_cwnd(tp);
+ /*Set W_max only if the current cwnd is larger */
+ if (tcp_snd_cwnd(tp) > ca->last_max_cwnd)
+ ca->last_max_cwnd = tcp_snd_cwnd(tp);
+ tcp_snd_cwnd_set(tp,
+ min(tp->snd_cwnd_clamp,
+ max((tcp_snd_cwnd(tp) * beta)
+ / BICTCP_BETA_SCALE, 2U)));
+ tp->snd_ssthresh = tcp_snd_cwnd(tp);
+}
+
+/* Do minimum RTT probing.
+ */
+static void roccet_min_rtt_probe(struct sock *sk, u32 now)
+{
+ struct tcp_sock *tp = tcp_sk(sk);
+ struct roccettcp *ca = inet_csk_ca(sk);
+ u32 interval, probe_cwnd;
+
+ /* Do nothing if we are probing */
+ if (before(now, ca->probe_min_rtt_until) && ca->probe_min_rtt_until > 0)
+ return;
+
+ /* Start of min RTT probing*/
+ if (ca->probe_min_rtt_until == 0) {
+ /* Probe 1*RTT or at least 200ms */
+ interval = max(200 * USEC_PER_MSEC, ca->curr_rtt);
+
+ /* This is to handle deep shared buffers with loss-based
+ * congestion control like CUBIC. If the cwnd is not limited
+ * by the application but falsely detected (see ROCCET paper),
+ * we have to empty the pipe more.
+ * If the limit detection is correct this will cause no harm
+ * to the tcp flow because the cwnd is not fully utilized and
+ * we set the cwnd to its previous value after probing.
+ */
+ probe_cwnd = max(tcp_snd_cwnd(tp) / 2, TCP_INIT_CWND);
+ if (!tcp_is_cwnd_limited(sk))
+ probe_cwnd = max(tcp_snd_cwnd(tp) / 3, TCP_INIT_CWND);
+
+ ca->probe_min_rtt_until = now + interval;
+ ca->cwnd_before_min_rtt_probe = tcp_snd_cwnd(tp);
+
+ /* Half the cwnd to drain the buffer for probing.
+ * Set the ssthresh to the probing cwnd otherwise
+ * the TCP state machine is in slow start.
+ */
+ tcp_snd_cwnd_set(tp, probe_cwnd);
+ tcp_sk(sk)->snd_ssthresh = tcp_snd_cwnd(tp);
+
+ /* Reset current min RTT to allow probing for
+ * a new lower and higher minimum RTT.
+ */
+ ca->curr_min_rtt = ~0U;
+
+ /* Refill the pipe after probing.
+ * To this end we need the previous cwnd over
+ * the probing interval.
+ */
+ ca->refill_until = ca->probe_min_rtt_until + interval;
+ } else if (before(now, ca->refill_until)) {
+ /* Reset cwnd and refill the pipe. */
+ if (ca->state != RTT_PROBE_REFILL) {
+ tcp_snd_cwnd_set(tp, ca->cwnd_before_min_rtt_probe);
+ tcp_sk(sk)->snd_ssthresh = tcp_snd_cwnd(tp);
+ ca->state = RTT_PROBE_REFILL;
+ }
+ } else {
+ /* End min RTT probing phase. */
+ ca->probe_min_rtt_until = 0;
+ ca->state = ORBITER;
+ }
+}
+
+/* Used to check the roccet params in advance in order to avoid
+ * invalid-param-attacks. This validates the provided params and
+ * then saves valid copies of the params.
+ */
+static int param_check(bool use_defaults)
+{
+ int ret = 0;
+
+ /*
+ * Validate parameters to avoid division by zero errors.
+ */
+ if (beta_param <= 0 || beta_param >= BICTCP_BETA_SCALE) {
+ pr_err("roccet: beta must be between 0 and %d\n",
+ BICTCP_BETA_SCALE);
+
+ if (use_defaults) {
+ pr_info("Using default value of %d for beta.\n",
+ BETA_PARAM_DEFAULT);
+ beta = BETA_PARAM_DEFAULT;
+ } else {
+ ret = -EINVAL;
+ }
+ } else {
+ beta = beta_param;
+ }
+
+ if (bic_scale_param <= 0) {
+ pr_err("roccet: bic_scale must be positive\n");
+
+ if (use_defaults) {
+ pr_info("Using default value of %d for bic_scale.\n",
+ BIC_SCALE_PARAM_DEFAULT);
+ bic_scale = BIC_SCALE_PARAM_DEFAULT;
+ } else {
+ ret = -EINVAL;
+ }
+ } else {
+ bic_scale = bic_scale_param;
+ }
+
+ return ret;
+}
+
+/* Precompute some values based on the provided params.
+ */
+static void param_precompute(void)
+{
+ /* Precompute a bunch of the scaling factors that are used per-packet
+ * based on SRTT of 100ms
+ */
+ beta_scale =
+ 8 * (BICTCP_BETA_SCALE + beta) / 3 / (BICTCP_BETA_SCALE - beta);
+
+ cube_rtt_scale = (bic_scale * 10); /* 1024*c/rtt */
+
+ /* calculate the "K" for (wmax-cwnd) = c/rtt * K^3
+ * so K = cubic_root( (wmax-cwnd)*rtt/c )
+ * the unit of K is bictcp_HZ=2^10, not HZ
+ *
+ * c = bic_scale >> 10
+ * rtt = 100ms
+ *
+ * the following code has been designed and tested for
+ * cwnd < 1 million packets
+ * RTT < 100 seconds
+ * HZ < 1,000,00 (corresponding to 10 nano-second)
+ */
+
+ /* 1/c * 2^2*bictcp_HZ * srtt */
+ cube_factor = 1ull << (10 + 3 * BICTCP_HZ); /* 2^40 */
+
+ /* divide by bic_scale and by constant Srtt (100ms) */
+ do_div(cube_factor, bic_scale * 10);
+}
+
+static void roccettcp_init(struct sock *sk)
+{
+ /* Check & precompute on `init` in order to use the newest
+ * available params.
+ */
+ param_check(true);
+ param_precompute();
+
+ struct roccettcp *ca = inet_csk_ca(sk);
+
+ roccettcp_reset(ca);
+
+ if (initial_ssthresh)
+ tcp_sk(sk)->snd_ssthresh = initial_ssthresh;
+
+ ca->interval_snd_seq_start = tcp_sk(sk)->snd_nxt;
+ ca->interval_una_seq_start = tcp_sk(sk)->snd_una;
+
+ cmpxchg(&sk->sk_pacing_status, SK_PACING_NONE, SK_PACING_NEEDED);
+ //WRITE_ONCE(sk->sk_pacing_rate, 0);
+}
+
+static void roccettcp_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;
+
+ /* 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;
+ }
+
+ ca->was_idle = true;
+}
+
+/* calculate the cubic root of x using a table lookup followed by one
+ * Newton-Raphson iteration.
+ * Avg err ~= 0.195%
+ */
+static u32 cubic_root(u64 a)
+{
+ u32 x, b, shift;
+ /* cbrt(x) MSB values for x MSB values in [0..63].
+ * Precomputed then refined by hand - Willy Tarreau
+ *
+ * For x in [0..63],
+ * v = cbrt(x << 18) - 1
+ * cbrt(x) = (v[x] + 10) >> 6
+ */
+ static const u8 v[] = {
+ /* 0x00 */ 0, 54, 54, 54, 118, 118, 118, 118,
+ /* 0x08 */ 123, 129, 134, 138, 143, 147, 151, 156,
+ /* 0x10 */ 157, 161, 164, 168, 170, 173, 176, 179,
+ /* 0x18 */ 181, 185, 187, 190, 192, 194, 197, 199,
+ /* 0x20 */ 200, 202, 204, 206, 209, 211, 213, 215,
+ /* 0x28 */ 217, 219, 221, 222, 224, 225, 227, 229,
+ /* 0x30 */ 231, 232, 234, 236, 237, 239, 240, 242,
+ /* 0x38 */ 244, 245, 246, 248, 250, 251, 252, 254,
+ };
+
+ b = fls64(a);
+ if (b < 7) {
+ /* a in [0..63] */
+ return ((u32)v[(u32)a] + 35) >> 6;
+ }
+
+ b = ((b * 84) >> 8) - 1;
+ shift = (a >> (b * 3));
+
+ x = ((u32)(((u32)v[shift] + 10) << b)) >> 6;
+
+ /* Newton-Raphson iteration
+ * 2
+ * x = ( 2 * x + a / x ) / 3
+ * k+1 k k
+ */
+ x = (2 * x + (u32)div64_u64(a, (u64)x * (u64)(x - 1)));
+ x = ((x * 341) >> 10);
+ return x;
+}
+
+/* Compute congestion window to use.
+ */
+static void bictcp_update(struct roccettcp *ca, u32 cwnd,
+ u32 acked)
+{
+ u32 delta, bic_target, max_cnt;
+ u64 offs, t;
+
+ ca->ack_cnt += acked; /* count the number of ACKed packets */
+
+ if (ca->last_cwnd == cwnd &&
+ (s32)(tcp_jiffies32 - ca->last_time) <= HZ / 32)
+ return;
+
+ /* The CUBIC function can update ca->cnt at most once per jiffy.
+ * On all cwnd reduction events, ca->epoch_start is set to 0,
+ * which will force a recalculation of ca->cnt.
+ */
+ if (ca->epoch_start && tcp_jiffies32 == ca->last_time)
+ goto tcp_friendliness;
+
+ ca->last_cwnd = cwnd;
+ ca->last_time = tcp_jiffies32;
+
+ if (ca->epoch_start == 0) {
+ ca->epoch_start = tcp_jiffies32; /* record beginning */
+ ca->ack_cnt = acked; /* start counting */
+ ca->tcp_cwnd = cwnd; /* syn with cubic */
+
+ if (ca->last_max_cwnd <= cwnd) {
+ ca->bic_K = 0;
+ ca->bic_origin_point = cwnd;
+ } else {
+ /* Compute new K based on
+ * (wmax-cwnd) * (srtt>>3 / HZ) / c * 2^(3*bictcp_HZ)
+ */
+ ca->bic_K = cubic_root(cube_factor *
+ (ca->last_max_cwnd - cwnd));
+ ca->bic_origin_point = ca->last_max_cwnd;
+ }
+ }
+
+ /* cubic function - calc */
+ /* calculate c * time^3 / rtt,
+ * while considering overflow in calculation of time^3
+ * (so time^3 is done by using 64 bit)
+ * and without the support of division of 64bit numbers
+ * (so all divisions are done by using 32 bit)
+ * also NOTE the unit of those variables
+ * time = (t - K) / 2^bictcp_HZ
+ * c = bic_scale >> 10
+ * rtt = (srtt >> 3) / HZ
+ * !!! The following code does not have overflow problems,
+ * if the cwnd < 1 million packets !!!
+ */
+
+ t = (s32)(tcp_jiffies32 - ca->epoch_start);
+ t += usecs_to_jiffies(ca->delay_min);
+
+ /* change the unit from HZ to bictcp_HZ */
+ t <<= BICTCP_HZ;
+ do_div(t, HZ);
+
+ if (t < ca->bic_K) /* t - K */
+ offs = ca->bic_K - t;
+ else
+ offs = t - ca->bic_K;
+
+ /* c/rtt * (t-K)^3 */
+ delta = (cube_rtt_scale * offs * offs * offs) >> (10 + 3 * BICTCP_HZ);
+ if (t < ca->bic_K) /* below origin*/
+ bic_target = ca->bic_origin_point - delta;
+ else /* above origin*/
+ bic_target = ca->bic_origin_point + delta;
+
+ /* cubic function - calc bictcp_cnt*/
+ if (bic_target > cwnd)
+ ca->cnt = cwnd / (bic_target - cwnd);
+ else
+ ca->cnt = 100 * cwnd; /* very small increment*/
+
+ /* The initial growth of cubic function may be too conservative
+ * when the available bandwidth is still unknown.
+ */
+ if (ca->last_max_cwnd == 0 && ca->cnt > 20)
+ ca->cnt = 20; /* increase cwnd 5% per RTT */
+
+tcp_friendliness:
+ /* TCP Friendly */
+ if (tcp_friendliness) {
+ u32 scale = beta_scale;
+
+ delta = (cwnd * scale) >> 3;
+ while (ca->ack_cnt > delta) { /* update tcp cwnd */
+ ca->ack_cnt -= delta;
+ ca->tcp_cwnd++;
+ }
+
+ if (ca->tcp_cwnd > cwnd) { /* if bic is slower than tcp */
+ delta = ca->tcp_cwnd - cwnd;
+ max_cnt = cwnd / delta;
+ if (ca->cnt > max_cnt)
+ ca->cnt = max_cnt;
+ }
+ }
+
+ /* The maximum rate of cwnd increase CUBIC allows is 1 packet per
+ * 2 packets ACKed, meaning cwnd grows at 1.5x per RTT.
+ */
+ ca->cnt = max(ca->cnt, 2U);
+}
+
+static void roccettcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
+{
+ struct tcp_sock *tp = tcp_sk(sk);
+ struct roccettcp *ca = inet_csk_ca(sk);
+
+ u32 now = jiffies_to_usecs(tcp_jiffies32);
+ bool evaluate_srrtt = false;
+ bool send_more_than_acked = false;
+ u32 roccet_xj;
+ u32 jitter;
+ u32 send, received;
+
+ if (ca->state == LAUNCH) {
+ /* LAUNCH: Detect an exit point for tcp slow start
+ * in networks with large buffers of multiple BDP
+ * Like in cellular networks (5G, ...).
+ *
+ * Or exit LAUNCH if cwnd is too large for application layer
+ * data rate (tcp cwnd validation).
+ * This condition is checked only after the initial round of
+ * LAUNCH has completed. This is done in order to avoid false
+ * positives, which can occur when the tracked outstanding
+ * packets have not yet caught up to the initial cwnd.
+ */
+ if ((ca->curr_srrtt > sr_rtt_upper_bound &&
+ get_ack_rate_diff(ca) <= ack_rate_diff_ss) ||
+ (!tcp_is_cwnd_limited(sk) &&
+ ca->initial_round_completed)) {
+ ca->epoch_start = 0;
+
+ /* Handle initial slow start.
+ * Most bufferbloat occurs here
+ */
+ if (tp->snd_ssthresh == TCP_INFINITE_SSTHRESH) {
+ tcp_sk(sk)->snd_ssthresh = tcp_snd_cwnd(tp)
+ / 2;
+ /* since this is the initial slow start,
+ * the min cwnd won't be 1, so the window
+ * can't be set to 0 by accident.
+ * Halfing the cwnd will undo the previous step
+ * of slow start. Which is fine since the pipe
+ * is already full.
+ */
+ tcp_snd_cwnd_set(tp, max(tcp_snd_cwnd(tp) / 2,
+ TCP_INIT_CWND));
+ } else {
+ tcp_sk(sk)->snd_ssthresh =
+ tcp_snd_cwnd(tp) -
+ (tcp_snd_cwnd(tp) / 3);
+ tcp_snd_cwnd_set(tp, tcp_snd_cwnd(tp) -
+ (tcp_snd_cwnd(tp) / 3));
+ }
+ ca->roccet_last_event_time_us = now;
+ return;
+ }
+
+ acked = tcp_slow_start(tp, acked);
+ if (!acked)
+ return;
+
+ } else if (ca->state == ORBITER) {
+ /* ORBITER: Increase the cwnd by using the CUBIC
+ * cwnd growth function, if no roccet congestion
+ * event is detected.
+ */
+
+ /* Calculate jitter */
+ if ((s32)(ca->curr_rtt - ca->last_rtt) < 0)
+ jitter = ca->last_rtt - ca->curr_rtt;
+ else
+ jitter = ca->curr_rtt - ca->last_rtt;
+
+ if (ca->next_srrtt_check == 0)
+ ca->next_srrtt_check = now + 5 * ca->curr_rtt;
+
+ /* Calculate if more bytes was send than received
+ * in the time interval.
+ *
+ * Handle wrap arounds by relying on unsigned subtraction.
+ * e.g. if snd_nxt wraps to 10 and seq_start is U32_MAX - 10,
+ * the subtraction will result in the value of 21.
+ */
+ send = tp->snd_nxt - ca->interval_snd_seq_start;
+ received = tp->snd_una - ca->interval_una_seq_start;
+
+ /* 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.
+ */
+ send_more_than_acked =
+ send >
+ received + ((tcp_snd_cwnd(tp) * tp->mss_cache) / 100);
+
+ /* Check if it's time to evaluate the srRTT */
+ if ((s32)(ca->next_srrtt_check - now) < 0) {
+ evaluate_srrtt = true;
+
+ /* reset struct and set next end of period */
+ ca->next_srrtt_check = now + 5 * ca->curr_rtt;
+
+ /* Reset Rate calculation */
+ ca->interval_snd_seq_start = tp->snd_nxt;
+ ca->interval_una_seq_start = tp->snd_una;
+ }
+
+ /* Respects the jitter of the connection and add it on top of
+ * the upper bound for the srRTT.
+ */
+ roccet_xj = div_u64((u64)jitter * 100, ca->curr_min_rtt) +
+ sr_rtt_upper_bound;
+ if (roccet_xj < sr_rtt_upper_bound)
+ roccet_xj = sr_rtt_upper_bound;
+
+ /* 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 &&
+ send_more_than_acked) {
+ roccet_congestion_event(sk, now);
+ return;
+ }
+
+ /* 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.
+ */
+ if (!tcp_is_cwnd_limited(sk) || send_more_than_acked)
+ return;
+
+ bictcp_update(ca, tcp_snd_cwnd(tp), acked);
+ tcp_cong_avoid_ai(tp, max(1, ca->cnt), acked);
+ }
+}
+
+static u32 roccettcp_ssthresh(struct sock *sk)
+{
+ return tcp_sk(sk)->snd_ssthresh;
+}
+
+static u32 roccettcp_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);
+
+ /* If a loss/ECN occurs in the refill phase of min RTT probing
+ * we reduce the cwnd and abort the refill.
+ */
+ if (ca->state == RTT_PROBE_REFILL)
+ ca->state = ORBITER;
+
+ /* If ROCCET is in min RTT probing and a loss/ECN occurs,
+ * we use the cwnd before the probing interval to
+ * calculate the cwnd reduction and continue probing.
+ * After min RTT probing the cwnd is set to the reduced
+ * value. During min RTT probing it is very likely that
+ * congestion was caused by the cwnd value before min
+ * RTT probing.
+ */
+ if (ca->state == RTT_PROBE) {
+ /* Handle ECN as cubic congestion event in min
+ * RTT probe.
+ */
+ ca->ece_received = false;
+
+ ca->epoch_start = 0; /* end of epoch */
+
+ /* Wmax and fast convergence */
+ if (cwnd < ca->last_max_cwnd && fast_convergence)
+ ca->last_max_cwnd =
+ (cwnd * (BICTCP_BETA_SCALE + beta)) /
+ (2 * BICTCP_BETA_SCALE);
+ else
+ ca->last_max_cwnd = cwnd;
+
+ cwnd = ca->cwnd_before_min_rtt_probe;
+ ca->cwnd_before_min_rtt_probe =
+ max((cwnd * beta) / BICTCP_BETA_SCALE, 2U);
+
+ return tcp_snd_cwnd(tp);
+ }
+
+ /* Handle ECN as ROCCET congestion event. */
+ if (ca->ece_received) {
+ ca->ece_received = false;
+ roccet_congestion_event(sk, jiffies_to_usecs(tcp_jiffies32));
+ return tcp_snd_cwnd(tp);
+ }
+
+ /* On loss in slow start enter congestion avoidance
+ * without a cwnd reduction. Additional slow start
+ * exit conditions with a cwnd reduction are handled
+ * in roccettcp_cong_avoid.
+ */
+ if (tcp_in_slow_start(tp))
+ return tcp_snd_cwnd(tp);
+
+ /* CUBIC congestion event */
+ ca->epoch_start = 0; /* end of epoch */
+
+ /* Wmax and fast convergence */
+ if (tcp_snd_cwnd(tp) < ca->last_max_cwnd && fast_convergence)
+ ca->last_max_cwnd =
+ (tcp_snd_cwnd(tp) * (BICTCP_BETA_SCALE + beta)) /
+ (2 * BICTCP_BETA_SCALE);
+ else
+ ca->last_max_cwnd = tcp_snd_cwnd(tp);
+
+ return max((tcp_snd_cwnd(tp) * beta) / BICTCP_BETA_SCALE, 2U);
+}
+
+static void roccettcp_state(struct sock *sk, u8 new_state)
+{
+ struct roccettcp *ca = inet_csk_ca(sk);
+ struct tcp_sock *tp = tcp_sk(sk);
+
+ ca->is_in_recovery = false;
+
+ if (new_state == TCP_CA_Loss) {
+ roccettcp_reset(ca);
+ } else if (new_state == TCP_CA_Recovery) {
+ ca->is_in_recovery = true;
+
+ /* Here we set the cwnd and ssthresh to the same value so
+ * the TCP state machine knows we are in cong. avoid and
+ * not in slow start.
+ */
+ tcp_sk(sk)->snd_ssthresh = roccettcp_recalc_ssthresh(sk);
+ tcp_snd_cwnd_set(tp, tcp_sk(sk)->snd_ssthresh);
+ }
+}
+
+static void roccettcp_acked(struct sock *sk, const struct ack_sample *sample)
+{
+ struct roccettcp *ca = inet_csk_ca(sk);
+
+ /* Some calls are for duplicates without timestamps */
+ if (sample->rtt_us < 0)
+ return;
+
+ /* Discard delay samples right after fast recovery */
+ if (ca->epoch_start && (s32)(tcp_jiffies32 - ca->epoch_start) < HZ)
+ return;
+
+ u32 delay = sample->rtt_us;
+
+ if (delay == 0)
+ delay = 1;
+
+ /* first time call or link delay decreases */
+ if (ca->delay_min == 0 || (s32)(delay - ca->delay_min) < 0)
+ ca->delay_min = delay;
+
+ /* Get valid sample for roccet */
+ if (sample->rtt_us > 0) {
+ ca->last_rtt = ca->curr_rtt;
+ ca->curr_rtt = sample->rtt_us;
+ }
+}
+
+static void roccet_in_ack_event(struct sock *sk, u32 flags)
+{
+ struct roccettcp *ca = inet_csk_ca(sk);
+
+ /* Handle ECE bit.
+ * Processing of ECE events is done in roccettcp_recalc_ssthresh()
+ */
+ if (flags & CA_ACK_ECE)
+ ca->ece_received = true;
+}
+
+static void roccet_control(struct sock *sk, u32 ack, int flag,
+ const struct rate_sample *rs)
+{
+ struct tcp_sock *tp = tcp_sk(sk);
+ struct roccettcp *ca = inet_csk_ca(sk);
+
+ u32 now = jiffies_to_usecs(tcp_jiffies32);
+ u64 rate;
+
+ /* Update roccet parameters */
+ update_ack_rate(sk, rs->acked_sacked, now);
+ update_min_rtt(sk);
+ update_srrtt(sk);
+
+ /* Update roccet state */
+ if (tcp_in_slow_start(tp)) {
+ ca->state = LAUNCH;
+ } else if ((s32)now - ca->roccet_last_event_time_us <=
+ 100 * USEC_PER_MSEC) {
+ ca->state = DRAIN;
+ } else if (after(now, ca->next_min_rtt_probe) ||
+ ca->state == RTT_PROBE || ca->state == RTT_PROBE_REFILL) {
+ if (ca->state != RTT_PROBE_REFILL)
+ ca->state = RTT_PROBE;
+ roccet_min_rtt_probe(sk, now);
+ } else {
+ ca->state = ORBITER;
+ }
+
+ /* If nothing was fully acked do not increase the cwnd */
+ if (!rs->acked_sacked)
+ return;
+
+ /* Increase the cwnd.
+ * Loss recovery is handled in roccettcp_state()
+ */
+ if (!ca->is_in_recovery)
+ roccettcp_cong_avoid(sk, ack, rs->acked_sacked);
+
+ /* Adjust pacing rate. The code here is similar to the
+ * pacing rate adjustments in tcp_input.c tcp_cong_control().
+ * In LAUNCH (slow start) we want a pacing of 200% and
+ * in ORBITER (congestion avoidance) we adjust the pacing
+ * to 100% and do not use the sysctl_tcp_pacing_ca_ratio.
+ */
+
+ /* set sk_pacing_rate to 200 % of current rate (mss * cwnd / srtt) */
+ rate = (u64)tp->mss_cache * ((USEC_PER_SEC / 100) << 3);
+
+ /* current rate is (cwnd * mss) / srtt
+ * In Slow Start [1], set sk_pacing_rate to 200 % the current rate.
+ * In Congestion Avoidance phase, set it to 120 % the current rate.
+ *
+ * [1]: Normal Slow Start cond is (tp->snd_cwnd < tp->snd_ssthresh)
+ * If snd_cwnd >= (tp->snd_ssthresh / 2), we are approaching
+ * end of slow start and should slow down.
+ */
+ if (tcp_snd_cwnd(tp) < tp->snd_ssthresh / 2)
+ rate *= READ_ONCE
+ (sock_net(sk)->ipv4.sysctl_tcp_pacing_ss_ratio);
+ else
+ /* Pacing rate of 100%
+ * (instead of ipv4.sysctl_tcp_pacing_ca_ratio)
+ */
+ rate *= 100;
+
+ rate *= max(tcp_snd_cwnd(tp), tp->packets_out);
+
+ if (likely(tp->srtt_us))
+ do_div(rate, tp->srtt_us);
+
+ /* WRITE_ONCE() is needed because sch_fq fetches sk_pacing_rate
+ * without any lock. We want to make sure compiler won't store
+ * intermediate values in this location.
+ */
+ WRITE_ONCE(sk->sk_pacing_rate,
+ min_t(u64, rate, READ_ONCE(sk->sk_max_pacing_rate)));
+
+ ca->initial_round_completed = true;
+}
+
+static struct tcp_congestion_ops roccet_tcp __read_mostly = {
+ .init = roccettcp_init,
+ .ssthresh = roccettcp_ssthresh,
+ .set_state = roccettcp_state,
+ .undo_cwnd = tcp_reno_undo_cwnd,
+ .cwnd_event_tx_start = roccettcp_cwnd_event_tx_start,
+ .pkts_acked = roccettcp_acked,
+ .in_ack_event = roccet_in_ack_event,
+ .cong_control = roccet_control,
+ .owner = THIS_MODULE,
+ .name = "roccet",
+};
+
+static int __init roccettcp_register(void)
+{
+ BUILD_BUG_ON(sizeof(struct roccettcp) > ICSK_CA_PRIV_SIZE);
+
+ int param_err = param_check(false);
+
+ if (param_err)
+ return param_err;
+
+ param_precompute();
+
+ return tcp_register_congestion_control(&roccet_tcp);
+}
+
+static void __exit roccettcp_unregister(void)
+{
+ tcp_unregister_congestion_control(&roccet_tcp);
+}
+
+module_init(roccettcp_register);
+module_exit(roccettcp_unregister);
+
+MODULE_AUTHOR("Lukas Prause, Tim Füchsel");
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("ROCCET TCP");
base-commit: 1bb784eb6e38fd73143f021608e4ef3095d0c0d7
--
2.43.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCHv5 net-next] tcp: Add TCP ROCCET congestion control module.
2026-08-31 17:03 [PATCHv5 net-next] tcp: Add TCP ROCCET congestion control module Tim Fuechsel
@ 2026-08-31 18:57 ` Eric Dumazet
2026-09-04 22:25 ` netdev-bot+sashiko
1 sibling, 0 replies; 3+ messages in thread
From: Eric Dumazet @ 2026-08-31 18:57 UTC (permalink / raw)
To: Tim Fuechsel
Cc: David S. Miller, David Ahern, Jakub Kicinski, Paolo Abeni,
Simon Horman, Neal Cardwell, Kuniyuki Iwashima, linux-kernel,
netdev, fmancera, ebiggers, bpf, Lukas Prause
On Mon, Aug 31, 2026 at 7:04 PM Tim Fuechsel <t.fuechsel@gmx.de> wrote:
>
> TCP ROCCET is an 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 addresses the bufferbloat problem by adding two additional
> metrics to detect bufferbloat. 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. If CUBIC
> increases the send rate or congestion window, and the acknowledgment
> arrival rate stays on 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 send and received Bytes
> to detect the filling of the bottleneck buffer.
>
> In real-world mobile 5G NR measurements, TCP ROCCET achieves better
> performance than CUBIC and BBRv3, by maintaining similar throughput
> while reducing the latency. In stationary 5G NR scenarios, the performance
> is similar to that of BBRv3. 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
>
> Signed-off-by: Lukas Prause <lukas.prause@ikt.uni-hannover.de>
> Signed-off-by: Tim Fuechsel <t.fuechsel@gmx.de>
Hi Lukas, Tim,
Here is a AI review of your patch. There are several architectural issues,
race conditions, and logic bugs that need to be addressed.
1. Concurrency data race on module parameters in socket init
------------------------------------------------------------
In roccettcp_init():
param_check(true);
param_precompute();
Both functions mutate global static variables (beta, bic_scale, beta_scale,
cube_rtt_scale, cube_factor) that are marked __read_mostly.
Calling this in roccettcp_init() on every socket creation causes unsynchronized
concurrent writes across CPUs. Furthermore, param_check() calls pr_err() /
pr_info(), which will spam the kernel log on every connection creation if an
invalid parameter was configured.
Global scale factors should only be precomputed at module init time
(roccettcp_register), or validated using kernel_param_ops.
2. Bypassing PRR and broken loss recovery via .cong_control
----------------------------------------------------------
By defining .cong_control = roccet_control, TCP core completely delegates
congestion control and skips PRR (tcp_cwnd_reduction()).
However, in roccet_control():
if (!ca->is_in_recovery)
roccettcp_cong_avoid(sk, ack, rs->acked_sacked);
During recovery (TCP_CA_Recovery), ROCCET does nothing to regulate cwnd or
clock out retransmissions according to RFC 6937. Unless you have a full
custom recovery engine (like BBR), you should stick to .cong_avoid so the
stack handles PRR and loss recovery properly.
3. Broken .ssthresh callback and ignored ECN (TCP_CA_CWR)
---------------------------------------------------------
roccettcp_ssthresh() simply returns tp->snd_ssthresh without recalculation:
static u32 roccettcp_ssthresh(struct sock *sk)
{
return tcp_sk(sk)->snd_ssthresh;
}
The recalculation was moved to roccettcp_state(..., TCP_CA_Recovery).
This breaks the TCP stack contract:
a) tcp_init_cwnd_reduction() initializes PRR state using the return value of
icsk_ca_ops->ssthresh(sk). Returning the unreduced ssthresh corrupts PRR.
b) When ECN CE marks arrive, tcp_enter_cwr() transitions to TCP_CA_CWR and calls
icsk_ca_ops->ssthresh(sk). Since roccettcp_state() only checks for Loss and
Recovery, TCP_CA_CWR is completely ignored. Consequently, neither ssthresh
nor cwnd is ever reduced on ECN.
4. Congestion avoidance starvation in ORBITER
---------------------------------------------
In roccettcp_cong_avoid():
send = tp->snd_nxt - ca->interval_snd_seq_start;
received = tp->snd_una - ca->interval_una_seq_start;
send_more_than_acked =
send > received + ((tcp_snd_cwnd(tp) * tp->mss_cache) / 100);
...
if (!tcp_is_cwnd_limited(sk) || send_more_than_acked)
return;
During steady-state transmission, the in-flight byte count (send - received)
is approximately cwnd * mss. This is ~100x larger than the 1% guard space
((cwnd * mss) / 100). Thus, send_more_than_acked evaluates to true on almost
every ACK, returning before bictcp_update() or tcp_cong_avoid_ai() can run.
Window growth in congestion avoidance is completely starved.
5. curr_min_rtt poisoning and premature slow-start exit
-------------------------------------------------------
In roccettcp_reset(), ca->curr_rtt is 0 and ca->curr_min_rtt is ~0U.
In update_min_rtt():
if (ca->curr_rtt < ca->curr_min_rtt) {
ca->curr_min_rtt = max(ca->curr_rtt, 1);
...
If an ACK arrives before an RTT sample is collected (e.g. sample->rtt_us <= 0),
ca->curr_rtt is 0. ca->curr_min_rtt is set to max(0, 1) = 1 us.
Because 1 us is smaller than any real path RTT, ca->curr_min_rtt remains stuck
at 1 us.
Then in update_srrtt():
u32 rrtt = div_u64(100 * (u64)(ca->curr_rtt - ca->curr_min_rtt),
ca->curr_min_rtt);
For a 20 ms RTT (20000 us), rrtt evaluates to 1,999,900. curr_srrtt immediately
exceeds sr_rtt_upper_bound and terminates slow start on the very first sample.
6. Perpetual min-RTT probe loop
-------------------------------
In roccet_min_rtt_probe(), when the probe finishes:
ca->probe_min_rtt_until = 0;
ca->state = ORBITER;
ca->next_min_rtt_probe is not updated when exiting the probe. If no new lower
RTT was found during the probe, next_min_rtt_probe remains in the past.
On the next ACK, after(now, ca->next_min_rtt_probe) immediately evaluates to
true, trapping the flow in a continuous probe loop and repeatedly halving cwnd
down to TCP_INIT_CWND.
7. 71-minute timestamp wrap and permanent DRAIN state
-----------------------------------------------------
now is calculated via jiffies_to_usecs(tcp_jiffies32), which wraps every
~71.5 minutes (2^32 us).
In roccet_control():
else if ((s32)now - ca->roccet_last_event_time_us <= 100 * USEC_PER_MSEC)
ca->state = DRAIN;
ca->roccet_last_event_time_us is initialized to 0. When bit 31 of now is set
(for 35.7 minutes out of every 71.5 minutes), (s32)now is negative, so
(s32)now - 0 is negative and always <= 100000. Newly created connections during
that 35-minute window will immediately enter and remain permanently stuck in
DRAIN state.
Also, before() and after() from <net/tcp.h> are sequence number comparison
macros (32-bit TCP seqno), not time comparison macros.
8. Re-implementing tcp_update_pacing_rate()
-------------------------------------------
roccet_control() duplicates the pacing rate calculation from tcp_input.c and
hardcodes 100% pacing instead of honoring sysctl_tcp_pacing_ca_ratio. If you
switch to .cong_avoid, the core stack will manage pacing updates automatically.
9. Coding style & formatting
----------------------------
- Declarations after statements (ISO C90 violations) in roccettcp_init(),
update_srrtt(), roccettcp_acked(), and roccettcp_register().
- C++ style comments (//) instead of /* ... */ in multiple places.
- Commented-out dead code: //WRITE_ONCE(sk->sk_pacing_rate, 0);
- Typos in comments ("sRrTT", "probeing", "has was idle", "defer", etc.)
and references to non-existent struct fields ("curr_min_rtt_timed.rtt").
Thanks.
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCHv5 net-next] tcp: Add TCP ROCCET congestion control module.
2026-08-31 17:03 [PATCHv5 net-next] tcp: Add TCP ROCCET congestion control module Tim Fuechsel
2026-08-31 18:57 ` Eric Dumazet
@ 2026-09-04 22:25 ` netdev-bot+sashiko
1 sibling, 0 replies; 3+ messages in thread
From: netdev-bot+sashiko @ 2026-09-04 22:25 UTC (permalink / raw)
To: t.fuechsel
Cc: davem, dsahern, edumazet, kuba, pabeni, horms, ncardwell, kuniyu,
linux-kernel, netdev, fmancera, ebiggers, bpf, lukas.prause
Thank you for your contribution! Sashiko AI review found 21 potential
issue(s) to consider:
Critical: 0 · High: 5 · Medium: 8 · Low: 8
- [High] ROCCET performs no congestion response to ECN.
- [High] ROCCET removes the standard congestion response to loss/RTO, and
the commit message does not disclose it.
- [High] `update_min_rtt()` (net/ipv4/tcp_roccet.c:191-206) treats an
absent RTT sample (`ca->curr_rtt == 0`) as a valid measurement and…
- [High] `param_check()` only rejects `beta_param` outside (0, 1024).
- [High] `roccettcp_init()` (net/ipv4/tcp_roccet.c:457-463) — called for
every new TCP socket that selects ROCCET, i.e. by any unprivileged…
- [Medium] `param_check(true)` is invoked from `roccettcp_init()`
(net/ipv4/tcp_roccet.c:462) for every new socket, and it emits…
- [Medium] The writable `beta_param` interface
(net/ipv4/tcp_roccet.c:144, `module_param(beta_param, int, 0644)` at…
- [Medium] `roccettcp_reset()` (net/ipv4/tcp_roccet.c:168) memsets the
whole per-socket CA state, zeroing…
- [Medium] `initial_round_completed` is documented as a full-round guard
('Set to true after the initial roccet-control round has completed',…
- [Medium] The LAUNCH exit path in `roccettcp_cong_avoid()` computes
`tcp_sk(sk)->snd_ssthresh = tcp_snd_cwnd(tp) / 2`…
- [Medium] `roccet_min_rtt_probe()` reduces cwnd and relies on a later
call, while `ca->state` is still RTT_PROBE/RTT_PROBE_REFILL and while…
- [Medium] In `roccettcp_recalc_ssthresh()`, the RTT_PROBE branch updates
`ca->last_max_cwnd` from the local `cwnd`, which at that point still…
- [Medium] `update_srrtt()` truncates the 64-bit rRTT result into a u32
and then evaluates the EWMA entirely in 32-bit arithmetic: `u32 rrtt =…
- [Low] Unfinished-submission artifacts in the new file: leftover
commented-out code `//WRITE_ONCE(sk->sk_pacing_rate, 0);` in…
- [Low] The main commit description omits ROCCET's periodic forced
congestion-window reduction for minimum-RTT probing.
- [Low] Two comments describe artifacts that do not exist or state the
wrong unit: `update_srrtt()` explains its invariant in terms of…
- [Low] `param_check()`'s comment (net/ipv4/tcp_roccet.c:382-385) claims
it exists 'to check the roccet params in advance in order to avoid…
- [Low] In `update_ack_rate()`, the 'new interval' branch assigns the u32
parameter `acked` directly into the u16 field `ca->ack_rate_cnt`…
- [Low] In `roccettcp_cong_avoid()`, the LAUNCH branch does `acked =
tcp_slow_start(tp, acked); if (!acked) return;`…
- [Low] The ORBITER send/received monitoring interval cannot account for
more than 4 GiB of data: `send = tp->snd_nxt -…
- [Low] `sr_rtt_upper_bound` and `ack_rate_diff_ss` are
`module_param(..., 0644)` and are read from the packet-processing hot…
--
tcp: Add TCP ROCCET congestion control module.
This adds a new loadable congestion control module, ROCCET, which builds on
the CUBIC window growth function and adds an srRTT metric, an ACK arrival
rate metric and a send/receive byte comparison to detect bufferbloat. It
replaces HyStart with its own slow start (LAUNCH) and drives everything from
a .cong_control callback.
A few questions about the new file follow.
The commit body describes the congestion avoidance phase as purely passive:
"For the congestion avoidance phase, ROCCET relies on the srRTT and
monitoring the send and received Bytes to detect the filling of the
bottleneck buffer."
[Severity: Low]
Could the commit message also describe the active minimum RTT probing?
roccet_min_rtt_probe() cuts cwnd to max(cwnd/2, TCP_INIT_CWND), or
max(cwnd/3, TCP_INIT_CWND) when tcp_is_cwnd_limited() is false, pins
snd_ssthresh to that value for at least 200ms or one RTT, resets
curr_min_rtt and later restores the previous window. It is armed every time
the five second deadline in ca->next_min_rtt_probe expires. Only the
revision history mentions it ("Add minimum RTT probing"), while the body
reads as if no forced window reduction happens.
> diff --git a/net/ipv4/tcp_roccet.c b/net/ipv4/tcp_roccet.c
> new file mode 100644
> index 0000000000000..18925a79bd8d3
> --- /dev/null
> +++ b/net/ipv4/tcp_roccet.c
> @@ -0,0 +1,1050 @@
[ ... ]
> +/* min RTT probe period in ms */
> +#define ROCCET_NEXT_MIN_RTT_PROBE 5000
[ ... ]
> +/* Parameters that are specific to the ROCCET-Algorithm */
> +
> +// = 717/1024 (BICTCP_BETA_SCALE)
> +#define BETA_PARAM_DEFAULT 717
> +#define BIC_SCALE_PARAM_DEFAULT 41
> +
> +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 defer by given amount of segments.");
[ ... ]
> +/* Note parameters that are used for precomputing scale factors are read-only */
> +module_param(fast_convergence, int, 0644);
> +MODULE_PARM_DESC(fast_convergence, "turn on/off fast convergence");
> +module_param(beta_param, int, 0644);
> +MODULE_PARM_DESC(beta_param, "beta for multiplicative increase");
[Severity: Medium]
The comment above says precompute parameters are read-only, but beta_param
is registered with mode 0644 while bic_scale_param uses 0444. Since there
is no module_param_cb setter, a write to beta_param goes straight into the
global with no validation and no recomputation of beta, beta_scale or
cube_factor. Those are refreshed only later, when some unrelated socket
runs roccettcp_init().
If the written value is out of range, param_check(true) puts the default
into the hidden beta but leaves beta_param reporting the rejected value, so
/sys/module/tcp_roccet/parameters/beta_param permanently disagrees with the
beta actually in use. Should beta_param be 0444 like bic_scale_param, or
use a setter that validates and recomputes at write time?
[ ... ]
> +static void roccettcp_reset(struct roccettcp *ca)
> +{
> + memset(ca, 0, sizeof(struct roccettcp));
[Severity: Medium]
The memset also zeroes ca->interval_snd_seq_start and
ca->interval_una_seq_start. roccettcp_init() seeds them explicitly:
ca->interval_snd_seq_start = tcp_sk(sk)->snd_nxt;
ca->interval_una_seq_start = tcp_sk(sk)->snd_una;
but the TCP_CA_Loss branch of roccettcp_state() calls roccettcp_reset()
without re-seeding them. Does ORBITER then compare absolute sequence
numbers?
send = tp->snd_nxt - ca->interval_snd_seq_start;
received = tp->snd_una - ca->interval_una_seq_start;
With both baselines at 0 this reduces to "bytes in flight > 1% of
cwnd * mss", which looks true for as long as the flow is sending, so the
later
if (!tcp_is_cwnd_limited(sk) || send_more_than_acked)
return;
blocks all window growth until the first srRTT evaluation re-seeds the
baselines, and that same evaluation can authorize a congestion event.
> +static void update_min_rtt(struct sock *sk)
> +{
> + struct roccettcp *ca = inet_csk_ca(sk);
> +
> + /* Check if new lower min RTT was found. If so, set it directly */
> + if (ca->curr_rtt < ca->curr_min_rtt) {
> + ca->curr_min_rtt = max(ca->curr_rtt, 1);
[Severity: High]
Can this latch a 1us minimum RTT when no RTT sample has been taken yet?
After roccettcp_reset() or roccettcp_init(), ca->curr_rtt is 0 and
ca->curr_min_rtt is ~0U. roccettcp_acked() returns early for invalid
samples:
/* Some calls are for duplicates without timestamps */
if (sample->rtt_us < 0)
return;
and tcp_clean_rtx_queue() calls pkts_acked unconditionally with
sample.rtt_us taken from rate->rtt_us, which is -1 when no RTT could be
measured. roccet_control() then still runs update_min_rtt() and
update_srrtt() with curr_rtt == 0, so 0 < ~0U is true and curr_min_rtt
becomes max(0, 1) == 1.
Reachable cases would be a SACK-only or duplicate first ACK when the first
packet of the initial window is lost, and the first ACK after an RTO on a
connection without TCP timestamps.
Once curr_min_rtt is 1, update_srrtt() computes
rrtt = 100 * (curr_rtt - 1) / 1, i.e. millions, so curr_srrtt stays far
above sr_rtt_upper_bound (default 100) and the bufferbloat detector fires
continuously until the next minimum RTT probe five seconds later. Would it
help to distinguish "no sample yet" from a real 1us RTT here?
> + /* Probe for the min RTT in ROCCET_NEXT_MIN_RTT_PROBE seconds
> + * if no other update occurs.
> + */
[ ... ]
> + ca->ack_rate_curr_rate = ca->ack_rate_cnt;
> + ca->ack_rate_cnt =
> + acked; // start counting for the new interval
> + }
> +
> + ca->was_idle = false;
> + } else {
> + // Cap the ack count to avoid overflow
> + ca->ack_rate_cnt = min_t(u32, ca->ack_rate_cnt + acked,
> + U16_MAX);
> + }
[Severity: Low]
The new-interval branch assigns the u32 argument acked straight into the u16
ca->ack_rate_cnt with no clamp, while the sibling branch a few lines below
saturates with min_t(u32, ..., U16_MAX). Should the first assignment
saturate too? An ACK covering more than 65535 segments at an interval
boundary truncates (70000 becomes 4464) and feeds a wrong value into
get_ack_rate_diff().
[ ... ]
> + /* Calculate the new rRTT (Scaled by 100).
> + * 100 * ((sRTT - sRTT_min) / sRTT_min).
> + *
> + * curr_min_rtt_timed.rtt is always <= than curr_rtt,
> + * since this is the minimum of the rtt.
[Severity: Low]
There is no curr_min_rtt_timed member in struct roccettcp; the field is the
flat u32 curr_min_rtt. Same kind of thing in update_min_rtt(), where the
comment says the next probe happens "in ROCCET_NEXT_MIN_RTT_PROBE seconds"
while the macro is documented as milliseconds and is multiplied by
USEC_PER_MSEC, i.e. five seconds and not 5000 seconds.
> + *
> + * 0 is a valid value for rrtt.
> + */
> + u32 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) * ca->curr_srrtt +
> + ROCCET_ALPHA_TIMES_100 * rrtt) /
> + 100;
> +}
[Severity: Medium]
The div_u64() numerator is widened to 64 bits, but the result is stored in a
u32 and the EWMA below is evaluated entirely in 32-bit arithmetic. Does
80 * ca->curr_srrtt wrap once curr_srrtt exceeds roughly 53.6M?
With curr_min_rtt floored to 1us (see the update_min_rtt() question above),
a 540ms RTT sample already gives rrtt above 53.6M, and curr_srrtt converges
toward it. curr_srrtt is the only bufferbloat signal gating
roccet_congestion_event() and the LAUNCH exit, so a wrap there either
defeats the reaction or fires it spuriously.
The v2/v3 changelog says "Fix potential integer overflow in rRTT
calculation" and "Fix potential overflow in rRTT calculation by using 64-bit
arithmetic", but only the numerator was widened.
[ ... ]
> + probe_cwnd = max(tcp_snd_cwnd(tp) / 2, TCP_INIT_CWND);
> + if (!tcp_is_cwnd_limited(sk))
> + probe_cwnd = max(tcp_snd_cwnd(tp) / 3, TCP_INIT_CWND);
[ ... ]
> + ca->refill_until = ca->probe_min_rtt_until + interval;
> + } else if (before(now, ca->refill_until)) {
> + /* Reset cwnd and refill the pipe. */
> + if (ca->state != RTT_PROBE_REFILL) {
> + tcp_snd_cwnd_set(tp, ca->cwnd_before_min_rtt_probe);
> + tcp_sk(sk)->snd_ssthresh = tcp_snd_cwnd(tp);
> + ca->state = RTT_PROBE_REFILL;
> + }
> + } else {
> + /* End min RTT probing phase. */
> + ca->probe_min_rtt_until = 0;
> + ca->state = ORBITER;
> + }
> +}
[Severity: Medium]
The cwnd restore depends on ca->state still being RTT_PROBE or
RTT_PROBE_REFILL and on an ACK landing inside the refill window. Can other
code paths clear the state and leave the reduced window in place?
roccettcp_recalc_ssthresh() starts with:
if (ca->state == RTT_PROBE_REFILL)
ca->state = ORBITER;
and roccet_control() overwrites ca->state in the two higher priority
branches:
if (tcp_in_slow_start(tp)) {
ca->state = LAUNCH;
} else if ((s32)now - ca->roccet_last_event_time_us <=
100 * USEC_PER_MSEC) {
ca->state = DRAIN;
In all of these, probe_min_rtt_until and refill_until stay non-zero, so the
restore branch is never taken. The sender then keeps the 2x-3x reduced cwnd
with snd_ssthresh pinned to it, and when the deadline finally passes the
terminal else only clears probe_min_rtt_until, making that probe round a
no-op. The same skip happens if no ACK arrives inside the refill window.
> +/* Used to check the roccet params in advance in order to avoid
> + * invalid-param-attacks. This validates the provided params and
> + * then saves valid copies of the params.
> + */
[Severity: Low]
This describes all of the parameters, but only beta_param and
bic_scale_param are validated and copied. sr_rtt_upper_bound,
ack_rate_diff_ss, initial_ssthresh, fast_convergence and tcp_friendliness
are all 0644 and are read live with no bounds check and no validated copy,
including initial_ssthresh which is assigned directly to tp->snd_ssthresh in
roccettcp_init(). Could the comment be narrowed to what is actually
checked?
> +static int param_check(bool use_defaults)
> +{
> + int ret = 0;
> +
> + /*
> + * Validate parameters to avoid division by zero errors.
> + */
> + if (beta_param <= 0 || beta_param >= BICTCP_BETA_SCALE) {
> + pr_err("roccet: beta must be between 0 and %d\n",
> + BICTCP_BETA_SCALE);
> +
> + if (use_defaults) {
> + pr_info("Using default value of %d for beta.\n",
> + BETA_PARAM_DEFAULT);
> + beta = BETA_PARAM_DEFAULT;
> + } else {
> + ret = -EINVAL;
> + }
> + } else {
> + beta = beta_param;
> + }
[Severity: Medium]
param_check(true) is called from roccettcp_init(), i.e. once per socket.
Can this flood the log? The out-of-range path never rewrites beta_param
itself, only the hidden beta, so once an invalid value is stored every
subsequent connection that selects roccet prints two to four lines,
indefinitely. Would pr_*_once() or ratelimited variants, or moving the
diagnostics to module init and a param setter, avoid that?
[ ... ]
> +static void param_precompute(void)
> +{
> + /* Precompute a bunch of the scaling factors that are used per-packet
> + * based on SRTT of 100ms
> + */
> + beta_scale =
> + 8 * (BICTCP_BETA_SCALE + beta) / 3 / (BICTCP_BETA_SCALE - beta);
> +
> + cube_rtt_scale = (bic_scale * 10); /* 1024*c/rtt */
[ ... ]
> + /* 1/c * 2^2*bictcp_HZ * srtt */
> + cube_factor = 1ull << (10 + 3 * BICTCP_HZ); /* 2^40 */
> +
> + /* divide by bic_scale and by constant Srtt (100ms) */
> + do_div(cube_factor, bic_scale * 10);
> +}
> +
> +static void roccettcp_init(struct sock *sk)
> +{
> + /* Check & precompute on `init` in order to use the newest
> + * available params.
> + */
> + param_check(true);
> + param_precompute();
[Severity: High]
Is it safe to write the module-wide globals from a per-socket callback?
roccettcp_init() runs for every new socket that selects roccet, and
param_check()/param_precompute() write beta, bic_scale, beta_scale,
cube_rtt_scale and cube_factor with no lock, RCU or atomic annotation, while
every other live flow reads them locklessly from the ACK path
(bictcp_update(), roccet_congestion_event(), roccettcp_recalc_ssthresh()).
cube_factor is also published in an intermediate state:
cube_factor = 1ull << (10 + 3 * BICTCP_HZ); /* 2^40 */
do_div(cube_factor, bic_scale * 10);
so a concurrent bictcp_update() can multiply the undivided 2^40, and two
initializers racing here can apply do_div() twice to the same variable,
shrinking cube_factor by another factor of ~410 for the rest of the module's
life.
param_check() also reads beta_param twice, once for the range test and once
for "beta = beta_param;". If the second read picks up a concurrently
written 1024, param_precompute() divides by (BICTCP_BETA_SCALE - beta) == 0.
Upstream CUBIC does this precomputation once in __init cubictcp_register()
and keeps bic_scale read-only. Could ROCCET do the same?
> +
> + struct roccettcp *ca = inet_csk_ca(sk);
> +
> + roccettcp_reset(ca);
[ ... ]
> + cmpxchg(&sk->sk_pacing_status, SK_PACING_NONE, SK_PACING_NEEDED);
> + //WRITE_ONCE(sk->sk_pacing_rate, 0);
> +}
[Severity: Low]
This isn't a bug, but the commented-out WRITE_ONCE() looks like leftover
debug code and should probably be dropped. There are also several C99 //
comments in the file (next to BETA_PARAM_DEFAULT, in update_ack_rate() and
in update_srrtt()) which checkpatch flags as errors.
And in roccet_control() the pacing comment contradicts both the code and the
comment right below it:
* In Congestion Avoidance phase, set it to 120 % the current rate.
...
/* Pacing rate of 100%
* (instead of ipv4.sysctl_tcp_pacing_ca_ratio)
*/
rate *= 100;
[ ... ]
> +tcp_friendliness:
> + /* TCP Friendly */
> + if (tcp_friendliness) {
> + u32 scale = beta_scale;
> +
> + delta = (cwnd * scale) >> 3;
> + while (ca->ack_cnt > delta) { /* update tcp cwnd */
> + ca->ack_cnt -= delta;
> + ca->tcp_cwnd++;
> + }
[Severity: High]
Can this loop spin forever when delta is 0? ca->ack_cnt is non-zero here
because "ca->ack_cnt += acked;" runs at the top of bictcp_update(), and
subtracting 0 never changes it, so there is no exit condition.
param_check() only rejects beta_param outside (0, 1024), and the surviving
values truncate beta_scale down far enough that (cwnd * scale) >> 3 is 0 for
small cwnd:
beta_param = 300 -> beta_scale 4 -> delta 0 at cwnd 1
beta_param = 200 -> beta_scale 3 -> delta 0 at cwnd <= 2
beta_param = 1 -> beta_scale 2 -> delta 0 at cwnd <= 3
cwnd of 2 is reachable because roccet_congestion_event() clamps to a floor
of 2. This runs in softirq context on the ACK path, so the CPU would stop
processing softirqs.
Unlike upstream CUBIC, which computes beta_scale once in
__init cubictcp_register(), this module recomputes it from beta_param on
every new socket, so a runtime write to the 0644 parameter reaches live
traffic. Should the accepted range be tightened, or delta == 0 guarded?
[ ... ]
> + if ((ca->curr_srrtt > sr_rtt_upper_bound &&
> + get_ack_rate_diff(ca) <= ack_rate_diff_ss) ||
> + (!tcp_is_cwnd_limited(sk) &&
> + ca->initial_round_completed)) {
> + ca->epoch_start = 0;
> +
> + /* Handle initial slow start.
> + * Most bufferbloat occurs here
> + */
> + if (tp->snd_ssthresh == TCP_INFINITE_SSTHRESH) {
> + tcp_sk(sk)->snd_ssthresh = tcp_snd_cwnd(tp)
> + / 2;
> + /* since this is the initial slow start,
> + * the min cwnd won't be 1, so the window
> + * can't be set to 0 by accident.
> + * Halfing the cwnd will undo the previous step
> + * of slow start. Which is fine since the pipe
> + * is already full.
> + */
> + tcp_snd_cwnd_set(tp, max(tcp_snd_cwnd(tp) / 2,
> + TCP_INIT_CWND));
[Severity: Medium]
Is the assumption in that comment always true? cwnd == 1 together with
snd_ssthresh == TCP_INFINITE_SSTHRESH looks reachable:
tcp_enter_loss()
WRITE_ONCE(tp->snd_ssthresh, icsk->icsk_ca_ops->ssthresh(sk));
-> roccettcp_ssthresh() returns the unchanged TCP_INFINITE_SSTHRESH
tcp_snd_cwnd_set(tp, tcp_packets_in_flight(tp) + 1); /* == 1 */
tcp_set_ca_state(sk, TCP_CA_Loss)
roccettcp_state() -> roccettcp_reset() -> state = LAUNCH
Taking this branch with cwnd == 1 stores snd_ssthresh = 0 while cwnd is
raised to TCP_INIT_CWND. After that tcp_in_slow_start() can never be true
again (cwnd is never < 0), so roccet_control() never selects LAUNCH,
tcp_slow_start() is unreachable for the rest of the connection, and the
pacing branch "tcp_snd_cwnd(tp) < tp->snd_ssthresh / 2" is dead. cwnd of 2
or 3 gives snd_ssthresh 1. Should there be a floor here?
> + } else {
[ ... ]
> + acked = tcp_slow_start(tp, acked);
> + if (!acked)
> + return;
> +
> + } else if (ca->state == ORBITER) {
[Severity: Low]
The non-zero leftover returned by tcp_slow_start() means cwnd just reached
snd_ssthresh and the remaining credit should be applied in congestion
avoidance, which is what stock CUBIC does with bictcp_update() plus
tcp_cong_avoid_ai(). Here the LAUNCH branch just falls out of the if/else
chain, and ca->state only becomes ORBITER on the next roccet_control() call,
so the credit is dropped. Was that intended?
[ ... ]
> + send = tp->snd_nxt - ca->interval_snd_seq_start;
> + received = tp->snd_una - ca->interval_una_seq_start;
[Severity: Low]
These are 32-bit sequence deltas, and the interval is five RTTs
(ca->next_srrtt_check = now + 5 * ca->curr_rtt). If a flow advances more
than 4GiB within that interval the modulo totals invert the
send_more_than_acked comparison. That needs roughly 34Gbit/s at a 200ms
RTT, so it is an extreme case, but should the totals be accumulated in u64?
[ ... ]
> + roccet_xj = div_u64((u64)jitter * 100, ca->curr_min_rtt) +
> + sr_rtt_upper_bound;
> + if (roccet_xj < sr_rtt_upper_bound)
> + roccet_xj = sr_rtt_upper_bound;
[Severity: Low]
sr_rtt_upper_bound is a 0644 module parameter read here twice as a plain
load, and the second read is the wrap guard for the first. Since sysfs
writes only take the module param mutex, which this softirq-context reader
does not hold, the compiler is free to reload it and defeat the guard.
Would a single READ_ONCE() snapshot into a local be better? The LAUNCH exit
test reads sr_rtt_upper_bound and ack_rate_diff_ss the same way.
[ ... ]
> +static u32 roccettcp_ssthresh(struct sock *sk)
> +{
> + return tcp_sk(sk)->snd_ssthresh;
> +}
[Severity: High]
With .ssthresh pointing at this getter, is there any multiplicative decrease
left on loss or RTO?
tcp_enter_loss() does:
WRITE_ONCE(tp->snd_ssthresh, icsk->icsk_ca_ops->ssthresh(sk));
which stores the same value back, so snd_ssthresh is never reduced on RTO.
roccettcp_state() then calls roccettcp_reset() for TCP_CA_Loss, which
memsets the state, wiping ca->last_max_cwnd (CUBIC's W_max) and returning
the flow to LAUNCH. roccettcp_recalc_ssthresh() returns cwnd unchanged
while tcp_in_slow_start(), and because .cong_control is provided
tcp_cong_control() returns before tcp_cwnd_reduction(), so PRR does not run
either.
After an RTO the flow slow-starts straight back to the same unreduced
ssthresh. On a lossy or shallow-buffered path, where RTT never inflates and
the srRTT heuristic never fires, that leaves no back-off on loss at all.
Only the file header mentions that LAUNCH ignores loss; could the commit
message describe the RTO and ORBITER behaviour as well?
> +static u32 roccettcp_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);
[ ... ]
> + if (ca->state == RTT_PROBE) {
[ ... ]
> + /* Wmax and fast convergence */
> + if (cwnd < ca->last_max_cwnd && fast_convergence)
> + ca->last_max_cwnd =
> + (cwnd * (BICTCP_BETA_SCALE + beta)) /
> + (2 * BICTCP_BETA_SCALE);
> + else
> + ca->last_max_cwnd = cwnd;
> +
> + cwnd = ca->cwnd_before_min_rtt_probe;
[Severity: Medium]
At this point cwnd still holds tcp_snd_cwnd(tp), which during a probe is the
deliberately deflated probe window; the assignment from
ca->cwnd_before_min_rtt_probe happens only afterwards and is used just to
recompute cwnd_before_min_rtt_probe. Doesn't that contradict the comment
above ("we use the cwnd before the probing interval to calculate the cwnd
reduction ... it is very likely that congestion was caused by the cwnd value
before min RTT probing")?
With last_max_cwnd taken from the small probe window, once probing ends and
cwnd is restored to the larger pre-probe value, bictcp_update() sees
ca->last_max_cwnd <= cwnd and takes the
ca->bic_K = 0;
ca->bic_origin_point = cwnd;
path, skipping the concave region and growing convexly right after a
congestion event.
[ ... ]
> +static void roccet_in_ack_event(struct sock *sk, u32 flags)
> +{
> + struct roccettcp *ca = inet_csk_ca(sk);
> +
> + /* Handle ECE bit.
> + * Processing of ECE events is done in roccettcp_recalc_ssthresh()
> + */
> + if (flags & CA_ACK_ECE)
> + ca->ece_received = true;
> +}
[Severity: High]
Is roccettcp_recalc_ssthresh() actually reachable from the ECE path? It is
only called from the TCP_CA_Recovery branch of roccettcp_state(), while a
pure ECN mark goes through:
tcp_try_to_open()
tcp_enter_cwr()
tcp_init_cwnd_reduction()
WRITE_ONCE(tp->snd_ssthresh, ca_ops->ssthresh(sk));
tcp_set_ca_state(sk, TCP_CA_CWR);
ca_ops->ssthresh is roccettcp_ssthresh(), which returns snd_ssthresh
unchanged, and roccettcp_state() has no TCP_CA_CWR branch. Since
.cong_control is set, tcp_cong_control() also returns before
tcp_cwnd_reduction(), so PRR does not reduce cwnd either.
On an ECN-marked path without loss, does anything reduce ssthresh or cwnd?
The latched ece_received then appears to be applied much later as a stale
reduction at an unrelated Recovery event. The v3 changelog says "Always
react to ECE bits and reset flag".
[ ... ]
> + /* current rate is (cwnd * mss) / srtt
> + * In Slow Start [1], set sk_pacing_rate to 200 % the current rate.
> + * In Congestion Avoidance phase, set it to 120 % the current rate.
[ ... ]
> + ca->initial_round_completed = true;
> +}
[Severity: Medium]
This is set unconditionally at the end of the first roccet_control() call
that has rs->acked_sacked != 0, but the field is documented as "Set to true
after the initial roccet-control round has completed" and
roccettcp_cong_avoid() relies on that meaning:
* This condition is checked only after the initial round of
* LAUNCH has completed. This is done in order to avoid false
* positives, which can occur when the tracked outstanding
* packets have not yet caught up to the initial cwnd.
Since there is no RTT or sequence-space round boundary test, the
"!tcp_is_cwnd_limited(sk) && ca->initial_round_completed" term can fire from
the second ACK onwards. With cwnd 10 and only three packets in flight,
tcp_is_cwnd_limited() is false in slow start (10 < 2 * 3), so the exit runs
and leaves the flow permanently out of slow start at cwnd 10, growing only
via the cubic path afterwards. Should the flag be set on a round boundary
instead?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/apWz9rPcGMgYCKOR%40volt-roccet-vm
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-04 22:25 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-31 17:03 [PATCHv5 net-next] tcp: Add TCP ROCCET congestion control module Tim Fuechsel
2026-08-31 18:57 ` Eric Dumazet
2026-09-04 22:25 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®