Netdev List
 help / color / mirror / Atom feed
* [PATCH net] tcp: cubic: do not trust snd_cwnd set by BPF congestion controls
@ 2026-11-06 13:37 Eric Dumazet
  2026-11-06 13:41 ` netdev-bot+sinfo
  2026-11-09 16:40 ` netdev-bot+sashiko
  0 siblings, 2 replies; 4+ messages in thread
From: Eric Dumazet @ 2026-11-06 13:37 UTC (permalink / raw)
  To: David S . Miller, Jakub Kicinski, Paolo Abeni, Neal Cardwell
  Cc: Kuniyuki Iwashima, Simon Horman, netdev, Eric Dumazet, Sashiko

Follow up of 475ff14ba558 ("tcp: bound snd_cwnd_clamp to TCP_MAX_SND_CWND")

The TCP friendliness part of bictcp_update() does:

	delta = (cwnd * scale) >> 3;
	while (ca->ack_cnt > delta) {
		ca->ack_cnt -= delta;
		ca->tcp_cwnd++;
	}

beta_scale is at least 8, so delta is 0 only if cwnd is 0,
or if the u32 product cwnd * scale wraps. Native paths keep
cwnd in [1, TCP_MAX_SND_CWND], where this can not happen,
even with the largest valid beta (beta_scale = 5458).

A BPF congestion control (struct_ops) can store any u32 value
in tp->snd_cwnd and tp->snd_ssthresh, then:

- call the cubictcp_cong_avoid() kfunc.

- switch the socket to CUBIC with TCP_CONGESTION,
  as tcp_reinit_congestion_control() keeps snd_cwnd.

With snd_cwnd = 0 (or 0x88888889 with the default beta_scale of 15)
and snd_ssthresh low enough to leave slow start, delta is 0 and
the loop never ends, hanging the CPU in ACK processing.

Adding a check in bictcp_update() would cost native CUBIC a
conditional branch in its fast path.

Instead, fix the two entry points:

- The cubictcp_cong_avoid() kfunc becomes a wrapper ignoring
  calls with a cwnd outside [1, TCP_MAX_SND_CWND].
  Native CUBIC uses __cubictcp_cong_avoid(), marked noinline
  so that its generated code does not change.

- tcp_reinit_congestion_control() bounds snd_cwnd to
  [1, snd_cwnd_clamp]. This also protects other native
  congestion controls, whose ->ssthresh() handlers do u32
  multiplications on cwnd.

Fixes: 0baf26b0fcd7 ("bpf: tcp: Support tcp_congestion_ops in bpf")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://lore.kernel.org/netdev/179363987528.3131.2987776489427079887@kernel.org/
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
 net/ipv4/tcp_cong.c  |  7 +++++++
 net/ipv4/tcp_cubic.c | 18 ++++++++++++++++--
 2 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/net/ipv4/tcp_cong.c b/net/ipv4/tcp_cong.c
index e3f462f161dd6c3f4e9eeae99b45a93391c87079..2c20524489168da0062bf7ff1b8bfc69e2c8b6de 100644
--- a/net/ipv4/tcp_cong.c
+++ b/net/ipv4/tcp_cong.c
@@ -251,8 +251,15 @@ static void tcp_reinit_congestion_control(struct sock *sk,
 					  const struct tcp_congestion_ops *ca)
 {
 	struct inet_connection_sock *icsk = inet_csk(sk);
+	struct tcp_sock *tp = tcp_sk(sk);
 
 	tcp_cleanup_congestion_control(sk);
+
+	/* A BPF congestion control could have stored any value
+	 * in tp->snd_cwnd. Native ones expect a sane value.
+	 */
+	tcp_snd_cwnd_set(tp, clamp(tcp_snd_cwnd(tp), 1U, tp->snd_cwnd_clamp));
+
 	WRITE_ONCE(icsk->icsk_ca_ops, ca);
 	icsk->icsk_ca_setsockopt = 1;
 	memset(icsk->icsk_ca_priv, 0, sizeof(icsk->icsk_ca_priv));
diff --git a/net/ipv4/tcp_cubic.c b/net/ipv4/tcp_cubic.c
index a88e4bf86150f06eb3c0d3da99761e5d45c8f6a3..9595bdc9589c255b673744ce501f50c418d82e2b 100644
--- a/net/ipv4/tcp_cubic.c
+++ b/net/ipv4/tcp_cubic.c
@@ -318,7 +318,7 @@ static inline void bictcp_update(struct bictcp *ca, u32 cwnd, u32 acked)
 	ca->cnt = max(ca->cnt, 2U);
 }
 
-__bpf_kfunc static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
+static noinline void __cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
 {
 	struct tcp_sock *tp = tcp_sk(sk);
 	struct bictcp *ca = inet_csk_ca(sk);
@@ -335,6 +335,20 @@ __bpf_kfunc static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
 	tcp_cong_avoid_ai(tp, ca->cnt, acked);
 }
 
+/* BPF congestion controls can store any value in tp->snd_cwnd.
+ * bictcp_update() would loop forever with cwnd == 0, or with a cwnd
+ * large enough for cwnd * beta_scale to wrap.
+ * Native callers do not need this check.
+ */
+__bpf_kfunc static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
+{
+	u32 cwnd = tcp_snd_cwnd(tcp_sk(sk));
+
+	if (unlikely(!cwnd || cwnd > TCP_MAX_SND_CWND))
+		return;
+	__cubictcp_cong_avoid(sk, ack, acked);
+}
+
 __bpf_kfunc static u32 cubictcp_recalc_ssthresh(struct sock *sk)
 {
 	const struct tcp_sock *tp = tcp_sk(sk);
@@ -475,7 +489,7 @@ __bpf_kfunc static void cubictcp_acked(struct sock *sk, const struct ack_sample
 static struct tcp_congestion_ops cubictcp __read_mostly = {
 	.init		= cubictcp_init,
 	.ssthresh	= cubictcp_recalc_ssthresh,
-	.cong_avoid	= cubictcp_cong_avoid,
+	.cong_avoid	= __cubictcp_cong_avoid,
 	.set_state	= cubictcp_state,
 	.undo_cwnd	= tcp_reno_undo_cwnd,
 	.cwnd_event_tx_start = cubictcp_cwnd_event_tx_start,
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH net] tcp: cubic: do not trust snd_cwnd set by BPF congestion controls
  2026-11-06 13:37 [PATCH net] tcp: cubic: do not trust snd_cwnd set by BPF congestion controls Eric Dumazet
@ 2026-11-06 13:41 ` netdev-bot+sinfo
  2026-11-09 16:40 ` netdev-bot+sashiko
  1 sibling, 0 replies; 4+ messages in thread
From: netdev-bot+sinfo @ 2026-11-06 13:41 UTC (permalink / raw)
  To: Eric Dumazet
  Cc: David S . Miller, Jakub Kicinski, Paolo Abeni, Neal Cardwell,
	Kuniyuki Iwashima, Simon Horman, netdev, Sashiko

Hi!

This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:

 - Whether the issue was actually triggered, or is only theoretical
   (e.g. found by code inspection). If it was triggered please include
   the symptoms, like the stack trace or error messages.

Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.

The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH net] tcp: cubic: do not trust snd_cwnd set by BPF congestion controls
  2026-11-06 13:37 [PATCH net] tcp: cubic: do not trust snd_cwnd set by BPF congestion controls Eric Dumazet
  2026-11-06 13:41 ` netdev-bot+sinfo
@ 2026-11-09 16:40 ` netdev-bot+sashiko
  2026-11-09 17:11   ` Eric Dumazet
  1 sibling, 1 reply; 4+ messages in thread
From: netdev-bot+sashiko @ 2026-11-09 16:40 UTC (permalink / raw)
  To: edumazet; +Cc: davem, kuba, pabeni, ncardwell, kuniyu, horms, netdev,
	sashiko-bot

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

Critical: 0 · High: 2 (1) · Medium: 1 · Low: 1

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

- [High] The fix is incomplete: a BPF congestion control can still stall
  the CPU in the TCP-friendliness loop of bictcp_update() through the…
- [High] tcp: unsanitized snd_ssthresh in tcp_reinit_congestion_control()
  hangs CUBIC
- [Medium] tcp: BPF CC can overwrite snd_cwnd after
  bpf_setsockopt(TCP_CONGESTION)
- [Low] The new comment above the cubictcp_cong_avoid() wrapper
  (net/ipv4/tcp_cubic.c) says bictcp_update() 'would loop forever ...…

Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but it is in the
  same 'BPF can store any value' threat model and is High severity.

--

tcp: cubic: do not trust snd_cwnd set by BPF congestion controls

This patch turns the cubictcp_cong_avoid() kfunc into a wrapper. The
wrapper ignores calls when snd_cwnd is outside [1, TCP_MAX_SND_CWND],
while native CUBIC calls __cubictcp_cong_avoid() directly. It also
clamps snd_cwnd to [1, snd_cwnd_clamp] in
tcp_reinit_congestion_control(), to stop the TCP friendliness loop in
bictcp_update() from hanging the CPU.

Link: https://lore.kernel.org/netdev/179363987528.3131.2987776489427079887@kernel.org/

> diff --git a/net/ipv4/tcp_cubic.c b/net/ipv4/tcp_cubic.c
> index a88e4bf86150f..9595bdc9589c2 100644
> --- a/net/ipv4/tcp_cubic.c
> +++ b/net/ipv4/tcp_cubic.c
> @@ -318,7 +318,7 @@ static inline void bictcp_update(struct bictcp *ca, u32 cwnd, u32 acked)
>  	ca->cnt = max(ca->cnt, 2U);
>  }
>  
> -__bpf_kfunc static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
> +static noinline void __cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
>  {
>  	struct tcp_sock *tp = tcp_sk(sk);
>  	struct bictcp *ca = inet_csk_ca(sk);
> @@ -335,6 +335,20 @@ __bpf_kfunc static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
>  	tcp_cong_avoid_ai(tp, ca->cnt, acked);

[Severity: High]
Does clamping only snd_cwnd here still leave the same hang reachable
through tp->snd_ssthresh?

A BPF congestion control can write any u32 to tp->snd_ssthresh via
bpf_tcp_ca_btf_struct_access(). That includes its .release()
callback, which tcp_cleanup_congestion_control() runs just before
this clamp. It can also return any value from .ssthresh(), which
tcp_init_cwnd_reduction() and tcp_enter_loss() store into
tp->snd_ssthresh. A BPF .ssthresh() can simply return 0.

tcp_reinit_congestion_control() does not touch snd_ssthresh,
icsk_ca_state or tp->undo_marker. With the default hystart and
initial_ssthresh parameters, cubictcp_init() leaves snd_ssthresh
alone as well.

Suppose the socket is in TCP_CA_CWR or TCP_CA_Recovery when it is
moved to CUBIC. The next ACK that advances snd_una past high_seq then
goes through tcp_fastretrans_alert()->tcp_end_cwnd_reduction():

net/ipv4/tcp_input.c:tcp_end_cwnd_reduction() {
    ...
	if (tp->snd_ssthresh < TCP_INFINITE_SSTHRESH &&
	    (inet_csk(sk)->icsk_ca_state == TCP_CA_CWR || tp->undo_marker)) {
		tcp_snd_cwnd_set(tp, tp->snd_ssthresh);
		tp->snd_cwnd_stamp = tcp_jiffies32;
	}
    ...
}

This copies snd_ssthresh into snd_cwnd without any bound.
tcp_snd_cwnd_set() warns once on 0 but still stores it.

Later in the same tcp_ack(), tcp_cong_control() calls
__cubictcp_cong_avoid() with snd_cwnd == 0 and snd_ssthresh == 0.
tcp_in_slow_start() is false, so bictcp_update(ca, 0, acked)
computes delta = 0 and the TCP friendliness loop never terminates.

A snd_ssthresh between TCP_MAX_SND_CWND and TCP_INFINITE_SSTHRESH
would instead leave snd_cwnd above TCP_MAX_SND_CWND. There
cwnd * beta_scale wraps and the u32 multiplications in native
->ssthresh() handlers overflow, which is the state the commit message
says native congestion controls must not see.

Should tcp_reinit_congestion_control() also bound snd_ssthresh, for
example to snd_cwnd_clamp or by resetting it to
TCP_INFINITE_SSTHRESH? Or should it reset the CWR/Recovery state
when switching away from a BPF congestion control? Or is there
something that keeps tcp_end_cwnd_reduction() from running after
such a switch that I am missing?

[Severity: High]

This isn't a bug introduced by this patch, but it falls under the same
"BPF can store any value" threat model. tcp_cong_avoid_ai() is exposed as
a kfunc through bpf_tcp_ca_check_kfunc_ids. It divides by w without
checking for zero:

net/ipv4/tcp_cong.c:tcp_cong_avoid_ai() {
    ...
	tp->snd_cwnd_cnt += acked;
	if (tp->snd_cwnd_cnt >= w) {
		u32 delta = tp->snd_cwnd_cnt / w;
    ...
}

When w == 0 the guard is always true.

Can a BPF congestion control reach this division by zero in any of these
ways?

- Call tcp_cong_avoid_ai(tp, 0, acked) directly. The verifier does not
  range check scalar kfunc arguments.

- Store snd_cwnd = 0 and snd_ssthresh = 0, then call the
  tcp_reno_cong_avoid() kfunc. It passes tcp_snd_cwnd(tp) as w and does
  not check cwnd.

- Use the new cubictcp_cong_avoid() wrapper with a valid snd_cwnd, after
  writing ca->cnt = 0, ca->last_cwnd = snd_cwnd and ca->last_time = a
  recent tcp_jiffies32 value (readable from fields such as
  tp->lsndtime). bictcp_update() then returns early, before
  ca->cnt = max(ca->cnt, 2U), and __cubictcp_cong_avoid() calls
  tcp_cong_avoid_ai(tp, 0, acked).

On x86 this would raise a divide error during ACK processing, usually in
softirq context. Native callers never pass w == 0.

>  }
>  
> +/* BPF congestion controls can store any value in tp->snd_cwnd.
> + * bictcp_update() would loop forever with cwnd == 0, or with a cwnd
> + * large enough for cwnd * beta_scale to wrap.

[Severity: Low]

This isn't a bug, but is this comment accurate? It suggests that the
TCP_MAX_SND_CWND check rules out every cwnd for which cwnd * beta_scale
wraps.

cubictcp_register() accepts beta up to 1023, which gives
beta_scale = 8 * 2047 / 3 / 1 = 5458. With that value the u32 product
wraps for any cwnd above 786912. So every cwnd in [786913, 1048576]
passes the wrapper and still wraps.

In that range the product wraps only once and delta stays at least 482,
so the infinite loop is still prevented. The comment, though, treats
"wraps" as if it meant "loops forever", and the wrap still produces a
much smaller delta than intended.

The TCP_MAX_SND_CWND comment in include/net/tcp.h (from 475ff14ba558)
says multipliers go "up to 2047", which does not account for beta_scale
in bictcp_update().

> + * Native callers do not need this check.
> + */
> +__bpf_kfunc static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
> +{
> +	u32 cwnd = tcp_snd_cwnd(tcp_sk(sk));
> +
> +	if (unlikely(!cwnd || cwnd > TCP_MAX_SND_CWND))
> +		return;
> +	__cubictcp_cong_avoid(sk, ack, acked);

[Severity: High]

Does this wrapper fully close the hang in the bictcp_update() TCP
friendliness loop? It checks only snd_cwnd. The loop runs about
ack_cnt / delta times, and ack_cnt comes either from the acked argument,
which the BPF program chooses, or from struct bictcp state in
icsk_ca_priv. BPF is allowed to write that state:

net/ipv4/bpf_tcp_ca.c:bpf_tcp_ca_btf_struct_access() {
    ...
	case bpf_ctx_range(struct inet_connection_sock, icsk_ca_priv):
		end = offsetofend(struct inet_connection_sock, icsk_ca_priv);
    ...
}

snd_cwnd = 1 passes the check and gives delta = (1 * 15) >> 3 = 1 with
the default beta_scale. Suppose a struct_ops program stores
snd_cwnd = 1, snd_ssthresh <= 1 (so tcp_in_slow_start() is false and
acked is not consumed), ca->epoch_start = 0 and ca->last_cwnd != 1, and
then calls:

    cubictcp_cong_avoid(sk, 0, 0xffffffff);

The call chain is
cubictcp_cong_avoid()->__cubictcp_cong_avoid()->bictcp_update(ca, 1, 0xffffffff).
The epoch_start == 0 branch sets ca->ack_cnt = 0xffffffff, and then
this loop runs about 2^32 times during ACK processing with the socket
lock held:

		delta = (cwnd * scale) >> 3;
		while (ca->ack_cnt > delta) {
			ca->ack_cnt -= delta;
			ca->tcp_cwnd++;
		}

beta_scale is a runtime global, so the compiler is not expected to turn
this loop into a division.

The program can set up that state again and call the kfunc repeatedly,
either unrolled or through bpf_loop(). Would that keep the CPU stuck
past the soft lockup and RCU stall thresholds, which is the same hang
in ACK processing the commit message describes?

Could the kfunc wrapper also bound acked (and/or ca->ack_cnt), or could
the BPF path compute the friendliness update with a division instead of
the loop?

> +}
> +

[ ... ]

> @@ -475,7 +489,7 @@ __bpf_kfunc static void cubictcp_acked(struct sock *sk, const struct ack_sample
>  static struct tcp_congestion_ops cubictcp __read_mostly = {
>  	.init		= cubictcp_init,
>  	.ssthresh	= cubictcp_recalc_ssthresh,
> -	.cong_avoid	= cubictcp_cong_avoid,
> +	.cong_avoid	= __cubictcp_cong_avoid,
>  	.set_state	= cubictcp_state,
>  	.undo_cwnd	= tcp_reno_undo_cwnd,
>  	.cwnd_event_tx_start = cubictcp_cwnd_event_tx_start,

[Severity: Medium]
Can a BPF congestion control still hand native CUBIC an invalid
snd_cwnd after the clamp in tcp_reinit_congestion_control() has run?

bpf_tcp_ca_get_func_proto() allows bpf_setsockopt() from every
tcp_congestion_ops callback except .release. So a BPF .cong_avoid,
.in_ack_event, .pkts_acked, .set_state or .undo_cwnd can call
bpf_setsockopt(sk, SOL_TCP, TCP_CONGESTION, "cubic", ...).

That call goes through sol_tcp_sockopt_congestion(),
do_tcp_setsockopt() and tcp_set_congestion_control() into
tcp_reinit_congestion_control(). That function clamps snd_cwnd and
switches icsk_ca_ops to cubictcp synchronously. Control then returns
to the BPF callback, which is still running.

The callback still has write access to tp->snd_cwnd and
tp->snd_ssthresh through bpf_tcp_ca_btf_struct_access(). It can
store snd_cwnd = 0 (or 0x88888889) and snd_ssthresh = 0 right after
bpf_setsockopt() returns. sol_tcp_sockopt_congestion() already
carries a comment about a similar after-the-switch hazard for cdg.

The .undo_cwnd case does not even need a direct write:

net/ipv4/tcp_input.c:tcp_undo_cwnd_reduction() {
    ...
		tcp_snd_cwnd_set(tp, icsk->icsk_ca_ops->undo_cwnd(sk));
    ...
}

The value returned by the BPF .undo_cwnd() is stored after that same
call has already switched icsk_ca_ops to cubictcp.

The hang then follows on the next ACK, or later in the same tcp_ack()
when the switch is done from .in_ack_event or .pkts_acked.
__cubictcp_cong_avoid() runs bictcp_update() with cwnd == 0 or
0x88888889, so delta is 0. The TCP friendliness loop then spins
forever in ACK processing, usually from softirq with the socket
spinlock held.

__cubictcp_cong_avoid() now relies on snd_cwnd being sane after
tcp_reinit_congestion_control(). Given this ordering, is that the
right place for the check? Would it be better to reject
TCP_CONGESTION from bpf_setsockopt() inside tcp_congestion_ops
callbacks, the way cdg is already rejected? Or to validate snd_cwnd
at a point the BPF program cannot run after?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106133748.2181133-1-edumazet%40kernel.org

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH net] tcp: cubic: do not trust snd_cwnd set by BPF congestion controls
  2026-11-09 16:40 ` netdev-bot+sashiko
@ 2026-11-09 17:11   ` Eric Dumazet
  0 siblings, 0 replies; 4+ messages in thread
From: Eric Dumazet @ 2026-11-09 17:11 UTC (permalink / raw)
  To: netdev-bot+sashiko
  Cc: davem, kuba, pabeni, ncardwell, kuniyu, horms, netdev,
	sashiko-bot

Le lun. 9 nov. 2026 à 17:40, <netdev-bot+sashiko@kernel.org> a écrit :
>
> Thank you for your contribution! Sashiko AI review found 4 potential
> issue(s) to consider.
>
> Critical: 0 · High: 2 (1) · Medium: 1 · Low: 1
>
> 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).
>
> - [High] The fix is incomplete: a BPF congestion control can still stall
>   the CPU in the TCP-friendliness loop of bictcp_update() through the…
> - [High] tcp: unsanitized snd_ssthresh in tcp_reinit_congestion_control()
>   hangs CUBIC
> - [Medium] tcp: BPF CC can overwrite snd_cwnd after
>   bpf_setsockopt(TCP_CONGESTION)
> - [Low] The new comment above the cubictcp_cong_avoid() wrapper
>   (net/ipv4/tcp_cubic.c) says bictcp_update() 'would loop forever ...…
>
> Pre-existing issues:
> - [High] This problem wasn't introduced by this patch, but it is in the
>   same 'BPF can store any value' threat model and is High severity.
>
> --
>
> tcp: cubic: do not trust snd_cwnd set by BPF congestion controls
>
> This patch turns the cubictcp_cong_avoid() kfunc into a wrapper. The
> wrapper ignores calls when snd_cwnd is outside [1, TCP_MAX_SND_CWND],
> while native CUBIC calls __cubictcp_cong_avoid() directly. It also
> clamps snd_cwnd to [1, snd_cwnd_clamp] in
> tcp_reinit_congestion_control(), to stop the TCP friendliness loop in
> bictcp_update() from hanging the CPU.
>
> Link: https://lore.kernel.org/netdev/179363987528.3131.2987776489427079887@kernel.org/
>
> > diff --git a/net/ipv4/tcp_cubic.c b/net/ipv4/tcp_cubic.c
> > index a88e4bf86150f..9595bdc9589c2 100644
> > --- a/net/ipv4/tcp_cubic.c
> > +++ b/net/ipv4/tcp_cubic.c
> > @@ -318,7 +318,7 @@ static inline void bictcp_update(struct bictcp *ca, u32 cwnd, u32 acked)
> >       ca->cnt = max(ca->cnt, 2U);
> >  }
> >
> > -__bpf_kfunc static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
> > +static noinline void __cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
> >  {
> >       struct tcp_sock *tp = tcp_sk(sk);
> >       struct bictcp *ca = inet_csk_ca(sk);
> > @@ -335,6 +335,20 @@ __bpf_kfunc static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
> >       tcp_cong_avoid_ai(tp, ca->cnt, acked);
>
> [Severity: High]
> Does clamping only snd_cwnd here still leave the same hang reachable
> through tp->snd_ssthresh?

Yes, and the Medium issue below (writes done after
bpf_setsockopt(TCP_CONGESTION) from a BPF callback) shows that
sanitizing snd_cwnd at entry points can not be complete.

v2 will instead make the friendliness code in bictcp_update() safe
for any cwnd and ack_cnt values: delta is at least 1, and the loop
is replaced by a division, only done when ack_cnt > delta.

>
> A BPF congestion control can write any u32 to tp->snd_ssthresh via
> bpf_tcp_ca_btf_struct_access(). That includes its .release()
> callback, which tcp_cleanup_congestion_control() runs just before
> this clamp. It can also return any value from .ssthresh(), which
> tcp_init_cwnd_reduction() and tcp_enter_loss() store into
> tp->snd_ssthresh. A BPF .ssthresh() can simply return 0.
>
> tcp_reinit_congestion_control() does not touch snd_ssthresh,
> icsk_ca_state or tp->undo_marker. With the default hystart and
> initial_ssthresh parameters, cubictcp_init() leaves snd_ssthresh
> alone as well.
>
> Suppose the socket is in TCP_CA_CWR or TCP_CA_Recovery when it is
> moved to CUBIC. The next ACK that advances snd_una past high_seq then
> goes through tcp_fastretrans_alert()->tcp_end_cwnd_reduction():
>
> net/ipv4/tcp_input.c:tcp_end_cwnd_reduction() {
>     ...
>         if (tp->snd_ssthresh < TCP_INFINITE_SSTHRESH &&
>             (inet_csk(sk)->icsk_ca_state == TCP_CA_CWR || tp->undo_marker)) {
>                 tcp_snd_cwnd_set(tp, tp->snd_ssthresh);
>                 tp->snd_cwnd_stamp = tcp_jiffies32;
>         }
>     ...
> }
>
> This copies snd_ssthresh into snd_cwnd without any bound.
> tcp_snd_cwnd_set() warns once on 0 but still stores it.
>
> Later in the same tcp_ack(), tcp_cong_control() calls
> __cubictcp_cong_avoid() with snd_cwnd == 0 and snd_ssthresh == 0.
> tcp_in_slow_start() is false, so bictcp_update(ca, 0, acked)
> computes delta = 0 and the TCP friendliness loop never terminates.
>
> A snd_ssthresh between TCP_MAX_SND_CWND and TCP_INFINITE_SSTHRESH
> would instead leave snd_cwnd above TCP_MAX_SND_CWND. There
> cwnd * beta_scale wraps and the u32 multiplications in native
> ->ssthresh() handlers overflow, which is the state the commit message
> says native congestion controls must not see.
>
> Should tcp_reinit_congestion_control() also bound snd_ssthresh, for
> example to snd_cwnd_clamp or by resetting it to
> TCP_INFINITE_SSTHRESH? Or should it reset the CWR/Recovery state
> when switching away from a BPF congestion control? Or is there
> something that keeps tcp_end_cwnd_reduction() from running after
> such a switch that I am missing?
>
> [Severity: High]
>
> This isn't a bug introduced by this patch, but it falls under the same
> "BPF can store any value" threat model. tcp_cong_avoid_ai() is exposed as
> a kfunc through bpf_tcp_ca_check_kfunc_ids. It divides by w without
> checking for zero:

Right, this is a separate issue, I will send a separate patch.

>
> net/ipv4/tcp_cong.c:tcp_cong_avoid_ai() {
>     ...
>         tp->snd_cwnd_cnt += acked;
>         if (tp->snd_cwnd_cnt >= w) {
>                 u32 delta = tp->snd_cwnd_cnt / w;
>     ...
> }
>
> When w == 0 the guard is always true.
>
> Can a BPF congestion control reach this division by zero in any of these
> ways?
>
> - Call tcp_cong_avoid_ai(tp, 0, acked) directly. The verifier does not
>   range check scalar kfunc arguments.
>
> - Store snd_cwnd = 0 and snd_ssthresh = 0, then call the
>   tcp_reno_cong_avoid() kfunc. It passes tcp_snd_cwnd(tp) as w and does
>   not check cwnd.
>
> - Use the new cubictcp_cong_avoid() wrapper with a valid snd_cwnd, after
>   writing ca->cnt = 0, ca->last_cwnd = snd_cwnd and ca->last_time = a
>   recent tcp_jiffies32 value (readable from fields such as
>   tp->lsndtime). bictcp_update() then returns early, before
>   ca->cnt = max(ca->cnt, 2U), and __cubictcp_cong_avoid() calls
>   tcp_cong_avoid_ai(tp, 0, acked).
>
> On x86 this would raise a divide error during ACK processing, usually in
> softirq context. Native callers never pass w == 0.
>
> >  }
> >
> > +/* BPF congestion controls can store any value in tp->snd_cwnd.
> > + * bictcp_update() would loop forever with cwnd == 0, or with a cwnd
> > + * large enough for cwnd * beta_scale to wrap.
>
> [Severity: Low]
>

Comment gone in v2.

> This isn't a bug, but is this comment accurate? It suggests that the
> TCP_MAX_SND_CWND check rules out every cwnd for which cwnd * beta_scale
> wraps.
>
> cubictcp_register() accepts beta up to 1023, which gives
> beta_scale = 8 * 2047 / 3 / 1 = 5458. With that value the u32 product
> wraps for any cwnd above 786912. So every cwnd in [786913, 1048576]
> passes the wrapper and still wraps.
>
> In that range the product wraps only once and delta stays at least 482,
> so the infinite loop is still prevented. The comment, though, treats
> "wraps" as if it meant "loops forever", and the wrap still produces a
> much smaller delta than intended.
>
> The TCP_MAX_SND_CWND comment in include/net/tcp.h (from 475ff14ba558)
> says multipliers go "up to 2047", which does not account for beta_scale
> in bictcp_update().
>
> > + * Native callers do not need this check.
> > + */
> > +__bpf_kfunc static void cubictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
> > +{
> > +     u32 cwnd = tcp_snd_cwnd(tcp_sk(sk));
> > +
> > +     if (unlikely(!cwnd || cwnd > TCP_MAX_SND_CWND))
> > +             return;
> > +     __cubictcp_cong_avoid(sk, ack, acked);
>
> [Severity: High]
>
> Does this wrapper fully close the hang in the bictcp_update() TCP
> friendliness loop? It checks only snd_cwnd.

Same answer: there is no loop anymore in v2.

The loop runs about
> ack_cnt / delta times, and ack_cnt comes either from the acked argument,
> which the BPF program chooses, or from struct bictcp state in
> icsk_ca_priv. BPF is allowed to write that state:
>
> net/ipv4/bpf_tcp_ca.c:bpf_tcp_ca_btf_struct_access() {
>     ...
>         case bpf_ctx_range(struct inet_connection_sock, icsk_ca_priv):
>                 end = offsetofend(struct inet_connection_sock, icsk_ca_priv);
>     ...
> }
>
> snd_cwnd = 1 passes the check and gives delta = (1 * 15) >> 3 = 1 with
> the default beta_scale. Suppose a struct_ops program stores
> snd_cwnd = 1, snd_ssthresh <= 1 (so tcp_in_slow_start() is false and
> acked is not consumed), ca->epoch_start = 0 and ca->last_cwnd != 1, and
> then calls:
>
>     cubictcp_cong_avoid(sk, 0, 0xffffffff);
>
> The call chain is
> cubictcp_cong_avoid()->__cubictcp_cong_avoid()->bictcp_update(ca, 1, 0xffffffff).
> The epoch_start == 0 branch sets ca->ack_cnt = 0xffffffff, and then
> this loop runs about 2^32 times during ACK processing with the socket
> lock held:
>
>                 delta = (cwnd * scale) >> 3;
>                 while (ca->ack_cnt > delta) {
>                         ca->ack_cnt -= delta;
>                         ca->tcp_cwnd++;
>                 }
>
> beta_scale is a runtime global, so the compiler is not expected to turn
> this loop into a division.
>
> The program can set up that state again and call the kfunc repeatedly,
> either unrolled or through bpf_loop(). Would that keep the CPU stuck
> past the soft lockup and RCU stall thresholds, which is the same hang
> in ACK processing the commit message describes?
>
> Could the kfunc wrapper also bound acked (and/or ca->ack_cnt), or could
> the BPF path compute the friendliness update with a division instead of
> the loop?
>
> > +}
> > +
>
> [ ... ]
>
> > @@ -475,7 +489,7 @@ __bpf_kfunc static void cubictcp_acked(struct sock *sk, const struct ack_sample
> >  static struct tcp_congestion_ops cubictcp __read_mostly = {
> >       .init           = cubictcp_init,
> >       .ssthresh       = cubictcp_recalc_ssthresh,
> > -     .cong_avoid     = cubictcp_cong_avoid,
> > +     .cong_avoid     = __cubictcp_cong_avoid,
> >       .set_state      = cubictcp_state,
> >       .undo_cwnd      = tcp_reno_undo_cwnd,
> >       .cwnd_event_tx_start = cubictcp_cwnd_event_tx_start,
>
> [Severity: Medium]
> Can a BPF congestion control still hand native CUBIC an invalid
> snd_cwnd after the clamp in tcp_reinit_congestion_control() has run?
>
> bpf_tcp_ca_get_func_proto() allows bpf_setsockopt() from every
> tcp_congestion_ops callback except .release. So a BPF .cong_avoid,
> .in_ack_event, .pkts_acked, .set_state or .undo_cwnd can call
> bpf_setsockopt(sk, SOL_TCP, TCP_CONGESTION, "cubic", ...).
>
> That call goes through sol_tcp_sockopt_congestion(),
> do_tcp_setsockopt() and tcp_set_congestion_control() into
> tcp_reinit_congestion_control(). That function clamps snd_cwnd and
> switches icsk_ca_ops to cubictcp synchronously. Control then returns
> to the BPF callback, which is still running.
>
> The callback still has write access to tp->snd_cwnd and
> tp->snd_ssthresh through bpf_tcp_ca_btf_struct_access(). It can
> store snd_cwnd = 0 (or 0x88888889) and snd_ssthresh = 0 right after
> bpf_setsockopt() returns. sol_tcp_sockopt_congestion() already
> carries a comment about a similar after-the-switch hazard for cdg.
>
> The .undo_cwnd case does not even need a direct write:
>
> net/ipv4/tcp_input.c:tcp_undo_cwnd_reduction() {
>     ...
>                 tcp_snd_cwnd_set(tp, icsk->icsk_ca_ops->undo_cwnd(sk));
>     ...
> }
>
> The value returned by the BPF .undo_cwnd() is stored after that same
> call has already switched icsk_ca_ops to cubictcp.
>
> The hang then follows on the next ACK, or later in the same tcp_ack()
> when the switch is done from .in_ack_event or .pkts_acked.
> __cubictcp_cong_avoid() runs bictcp_update() with cwnd == 0 or
> 0x88888889, so delta is 0. The TCP friendliness loop then spins
> forever in ACK processing, usually from softirq with the socket
> spinlock held.
>
> __cubictcp_cong_avoid() now relies on snd_cwnd being sane after
> tcp_reinit_congestion_control(). Given this ordering, is that the
> right place for the check? Would it be better to reject
> TCP_CONGESTION from bpf_setsockopt() inside tcp_congestion_ops
> callbacks, the way cdg is already rejected? Or to validate snd_cwnd
> at a point the BPF program cannot run after?
>
> --
> Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106133748.2181133-1-edumazet%40kernel.org


pw-bot: cr

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-11-09 17:11 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-06 13:37 [PATCH net] tcp: cubic: do not trust snd_cwnd set by BPF congestion controls Eric Dumazet
2026-11-06 13:41 ` netdev-bot+sinfo
2026-11-09 16:40 ` netdev-bot+sashiko
2026-11-09 17:11   ` Eric Dumazet

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox