Re: [PATCHv3 net-next] tcp: Add TCP ROCCET congestion control module.
From: Jakub Kicinski <kuba@kernel.org>
Date: 2026-06-09 02:06:23
Also in:
bpf, lkml
This is an AI-generated review of your patch. The human sending this email has considered the AI review valid, or at least plausible. --- tcp: Add TCP ROCCET congestion control module. This patch adds a new TCP congestion control algorithm derived from TCP CUBIC, called ROCCET, which adds bufferbloat detection based on srRTT and ACK arrival rate, and replaces HyStart with a custom slow start phase called LAUNCH. A few observations follow. [Low] The commit message says: The second metric is the acknowledgment arrival rate sampled over 5 RTT intervals. Looking at update_ack_rate(), the sampling window is a fixed 100 ms wall-clock interval (u32 interval = USEC_PER_MSEC * 100), not a multiple of the connection's RTT. The 5*curr_rtt window only governs ca->next_srrtt_check. Should the commit message match the implementation, or should the implementation be changed to actually sample over 5 RTT intervals? [Low] Could the commit message also discuss why ROCCET cannot be implemented as a BPF struct_ops program on top of CUBIC, given that this same patch already registers BTF kfuncs for the struct_ops interface and that most of the file is a verbatim copy of CUBIC?
quoted hunk ↗ jump to hunk
diff --git a/net/ipv4/Kconfig b/net/ipv4/Kconfig index 301b47660305..23197958324b 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 is a sender-side only modification of the TCP CUBIC + protocol stack/TCP CUBIC congestion control algorithm that + optimizes the performance of TCP congestion control. Especially + for networks with large buffers (wireless, cellular networks), + TCP ROCCET has improved performance by maintaining similar + throughput as CUBIC while reducing the latency. + For more information, see: https://arxiv.org/abs/2510.25281
[Low] The help text describes ROCCET as a "modification of the TCP CUBIC protocol stack/TCP CUBIC congestion control algorithm", but CUBIC itself is not modified by this patch; ROCCET is a new sibling module that copies large parts of CUBIC. The acronym ROCCET is also never expanded here. Should the help text be reworded?
quoted hunk ↗ jump to hunk
diff --git a/net/ipv4/tcp_roccet.h b/net/ipv4/tcp_roccet.h new file mode 100644 index 000000000000..5025d14667bb --- /dev/null +++ b/net/ipv4/tcp_roccet.h@@ -0,0 +1,46 @@
[Low] Is the separate header net/ipv4/tcp_roccet.h needed? It only declares struct ack_rate and struct roccettcp, both of which are used solely inside tcp_roccet.c. Other in-tree CCs (tcp_cubic.c, tcp_bbr.c, tcp_vegas.c, tcp_dctcp.c, tcp_bic.c, tcp_cdg.c, ...) keep their per-socket state in the .c file.
+struct roccettcp {
+ u32 cnt; /* increase cwnd by 1 after ACKs */[ ... ]
+ u32 curr_rtt; /* the minimum rtt of current round */
[Low] The comment says "the minimum rtt of current round", but in roccettcp_acked() curr_rtt is assigned the latest sample (ca->curr_rtt = sample->rtt_us;) without any min-of-round logic. Was the comment intended to be copied from tcp_cubic.c, where curr_rtt has different semantics?
quoted hunk ↗ jump to hunk
diff --git a/net/ipv4/tcp_roccet.c b/net/ipv4/tcp_roccet.c new file mode 100644 index 000000000000..900329bb070b --- /dev/null +++ b/net/ipv4/tcp_roccet.c
[ ... ]
+/* Parameters that are specific to the ROCCET-Algorithm */ +static int sr_rtt_upper_bound __read_mostly = 100; +static int ack_rate_diff_ss __read_mostly = 10; + +module_param(sr_rtt_upper_bound, int, 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 __read_mostly = 717; /* = 717/1024 (BICTCP_BETA_SCALE) */ +static int initial_ssthresh __read_mostly; +static int bic_scale __read_mostly = 41; +static int tcp_friendliness __read_mostly = 1;
[Low] sr_rtt_upper_bound and ack_rate_diff_ss are declared signed int and exposed as 0644 module params, but they are compared to and added to u32 quantities (curr_srrtt, get_ack_rate_diff(), roccet_xj). If a negative value is written, the signed-to-unsigned promotion turns it into a very large unsigned, effectively disabling the bufferbloat detection branch. Should these be unsigned int with a setter that rejects 0 / negative values? [Low] Is there a way to avoid adding seven new module_param tunables to a new in-tree CC? Five of them duplicate CUBIC's identically named knobs (fast_convergence, beta, initial_ssthresh, bic_scale, tcp_friendliness), which makes it hard for an admin to reason about which knob applies to which CC, and these are global (not per-netns) unlike the sysctls used by other modern CCs.
+static __always_inline 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);
+}[High] Can update_min_rtt() and update_srrtt() leave curr_min_rtt at 0 and crash the kernel via divide-by-zero in roccettcp_cong_avoid()? After roccettcp_reset() (called from roccettcp_init() and from roccettcp_state(TCP_CA_Loss)) curr_rtt is 0 and curr_min_rtt is ~0U. On the next ACK that reaches roccettcp_cong_avoid() before roccettcp_acked() has set curr_rtt to a non-zero value (the early return in roccettcp_acked() suppresses updates for HZ jiffies after fast recovery), update_min_rtt() does: if (ca->curr_rtt < ca->curr_min_rtt) ca->curr_min_rtt = max(ca->curr_rtt, 1); so curr_min_rtt is clamped to 1. Then update_srrtt() runs: if (ca->curr_rtt < ca->curr_min_rtt) ca->curr_min_rtt = ca->curr_rtt; which overwrites curr_min_rtt with 0 (curr_rtt). update_srrtt() returns early on its own divide-by-zero guard, but cong_avoid keeps running and reaches: roccet_xj = ((jitter * 100) / ca->curr_min_rtt) + sr_rtt_upper_bound; and divides by 0. Once curr_min_rtt becomes 0, the unsigned compare (ca->curr_rtt < ca->curr_min_rtt) is false for any later positive curr_rtt, so curr_min_rtt stays 0 for the lifetime of the socket. Is that intended?
+/* Compute srRTT.
+ */
+static __always_inline void update_srrtt(struct sock *sk)
+{[ ... ]
+ u32 rrtt = (100 * (ca->curr_rtt - ca->curr_min_rtt)) / + ca->curr_min_rtt;
[Medium] Can this overflow? curr_rtt and curr_min_rtt are microseconds and can reach values close to TCP_RTO_MAX (~120 s). 100 * (curr_rtt - curr_min_rtt) is computed in u32 and wraps once the difference exceeds about 42.9 ms. The wrapped rrtt is then folded into curr_srrtt and drives the LAUNCH/ORBITER decisions. Should this be promoted to u64 with div_u64()? [ ... ]
+__bpf_kfunc static void roccettcp_cwnd_event(struct sock *sk,
+ enum tcp_ca_event event)
+{
+ if (event == CA_EVENT_TX_START) {
+ 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;
+ }
+ return;
+ }
+}
[High]
Is this body reachable? In include/net/tcp.h tcp_ca_event() dispatches
CA_EVENT_TX_START exclusively to .cwnd_event_tx_start and returns
before calling .cwnd_event:
if (event == CA_EVENT_TX_START) {
if (icsk->icsk_ca_ops->cwnd_event_tx_start)
icsk->icsk_ca_ops->cwnd_event_tx_start(sk);
return;
}
tcp_cubic.c handles this by registering cubictcp_cwnd_event_tx_start
in .cwnd_event_tx_start. With the registration here only setting
.cwnd_event = roccettcp_cwnd_event, the entire idle-period
epoch_start adjustment is dead code, and on a transition out of
application-limited idle bictcp_update() will compute t with a stale
epoch_start, which then propagates through:
delta = (cube_rtt_scale * offs * offs * offs) >> (10 + 3 * BICTCP_HZ);
producing a large bic_target/cnt. Should this be wired to
.cwnd_event_tx_start instead?
[ ... ]+/* 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)
[ ... ]
+/* Compute congestion window to use. + */ +static __always_inline void bictcp_update(struct roccettcp *ca, u32 cwnd, + u32 acked)
[Low] cubic_root(), bictcp_update(), the module_param block, and the precomputation in roccettcp_register() appear to be byte-for-byte copies of tcp_cubic.c. Should these be factored as shared helpers in a common file (or ROCCET implemented as a BPF struct_ops on top of CUBIC) so that future numerical fixes to CUBIC propagate to ROCCET? [ ... ]
+__bpf_kfunc 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;
+ u32 roccet_xj;
+ u32 jitter;
+
+ if (after(ca->last_rtt, ca->curr_rtt))
+ jitter = ca->last_rtt - ca->curr_rtt;
+ else
+ jitter = ca->curr_rtt - ca->last_rtt;
[Low]
after() is the TCP sequence-number macro from include/net/tcp.h.
Using it here for an RTT-microsecond comparison works (it expands to
a signed 32-bit subtraction), but it is a sequence-space helper and
reads as a layering mistake. The same file uses raw subtraction with
<= a few lines below ("now - ca->roccet_last_event_time_us <= 100 *
USEC_PER_MSEC"), so the time arithmetic is internally inconsistent.
Could the after() uses be replaced with explicit (s32) subtractions?
[Low]
On the very first call after a reset, ca->last_rtt is 0, so this
computes jitter = |curr_rtt - 0| = curr_rtt, i.e. an entire RTT
worth of "jitter". That value then feeds roccet_xj and loosens the
bufferbloat-detection threshold during the LAUNCH phase the
algorithm is supposed to keep tightest. Should the first sample be
suppressed (e.g., skip jitter computation while last_rtt == 0)?
[ ... ]+ roccet_xj = ((jitter * 100) / ca->curr_min_rtt) + + sr_rtt_upper_bound; + if (roccet_xj < sr_rtt_upper_bound) + roccet_xj = sr_rtt_upper_bound;
[Medium] Same overflow class as update_srrtt(): jitter * 100 is computed in u32. With jitter above ~42.9 ms in microseconds the multiplication wraps, and the subsequent clamp "if (roccet_xj < sr_rtt_upper_bound)" only catches a subset of the wrap cases. The wrapped value drives whether the cwnd-reduction branch fires. Should jitter * 100 be performed in u64? [ ... ]
+static int __init roccettcp_register(void)
+{
+ int ret;
+
+ BUILD_BUG_ON(sizeof(struct roccettcp) > ICSK_CA_PRIV_SIZE);[ ... ]
+ beta_scale = + 8 * (BICTCP_BETA_SCALE + beta) / 3 / (BICTCP_BETA_SCALE - beta);
[ ... ]
+ /* divide by bic_scale and by constant Srtt (100ms) */ + do_div(cube_factor, bic_scale * 10);
[Low]
Can these divides panic at module load if bic_scale=0 or beta=1024 is
passed via modprobe? do_div(cube_factor, bic_scale * 10) divides by 0
when bic_scale is 0, and beta_scale divides by (BICTCP_BETA_SCALE -
beta) which is 0 when beta is 1024. The same shape exists in
tcp_cubic.c, but should the new file sanity-check these inputs
before computing the precomputed factors?
[Low]
MODULE_VERSION("1.0") is more typical of out-of-tree DKMS modules
than of in-tree code; should it be dropped here?
--
pw-bot: cr