* [PATCH net-next v9 1/4] net/sched: cake: return the classifier result to the caller
2026-11-10 14:40 [PATCH net-next v9 0/4] net/sched: cap the accounted backlog before it can wrap Jamal Hadi Salim
@ 2026-11-10 14:40 ` Jamal Hadi Salim
2026-11-10 14:40 ` [PATCH net-next v9 2/4] net/sched: cake: use a dedicated split-GSO list iterator Jamal Hadi Salim
` (2 subsequent siblings)
3 siblings, 0 replies; 9+ messages in thread
From: Jamal Hadi Salim @ 2026-11-10 14:40 UTC (permalink / raw)
To: netdev
Cc: Jamal Hadi Salim, Jiri Pirko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Victor Nogueira,
Toke Høiland-Jørgensen, Björn Töpel, cake,
Sashiko
Preparation:
cake runs an optional external classifier from its enqueue path and then
computes the flow hash inside the helper. Have the helper return the
filter's overrides instead, so the caller owns the flow selection.
This is preparation for the accounting changes that follow. Packet
classification and flow selection are unchanged.
Link: https://lore.kernel.org/netdev/179269577009.3135.5221412623031392098@kernel.org/
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Acked-by: Toke Høiland-Jørgensen <toke@toke.dk>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
v8 -> v9: reword the opening heading: this patch is preparation, not a
fix; and the classifier helper's comments now say the caller must drop
the skb, since a terminal action does not take ownership of it.
(Sashiko nipa reviews of v7 and v8)
---
net/sched/sch_cake.c | 36 +++++++++++++++++++++++-------------
1 file changed, 23 insertions(+), 13 deletions(-)
diff --git a/net/sched/sch_cake.c b/net/sched/sch_cake.c
index aa4043d662c1..67cb4cbda1c7 100644
--- a/net/sched/sch_cake.c
+++ b/net/sched/sch_cake.c
@@ -1715,18 +1715,25 @@ static struct cake_tin_data *cake_select_tin(struct Qdisc *sch,
return &qd->tins[tin];
}
-static u32 cake_classify(struct Qdisc *sch, struct cake_tin_data **t,
- struct sk_buff *skb, int flow_mode, int *qerr)
+/* Run the qdisc's optional external classifier. Returns true when a
+ * terminal TC action wants the packet dropped, with the reason in *qerr;
+ * the caller must drop the skb. Otherwise *flow and *host hold the
+ * filter's overrides (0 if none).
+ */
+static bool cake_tcf_classify(struct Qdisc *sch, struct sk_buff *skb,
+ u16 *flow, u16 *host, int *qerr)
{
struct cake_sched_data *q = qdisc_priv(sch);
struct tcf_proto *filter;
struct tcf_result res;
- u16 flow = 0, host = 0;
int result;
+ *flow = 0;
+ *host = 0;
+
filter = rcu_dereference_bh(q->filter_list);
if (!filter)
- goto hash;
+ return false;
*qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
result = tcf_classify_qdisc(skb, filter, &res, false);
@@ -1740,17 +1747,15 @@ static u32 cake_classify(struct Qdisc *sch, struct cake_tin_data **t,
*qerr = NET_XMIT_SUCCESS | __NET_XMIT_STOLEN;
fallthrough;
case TC_ACT_SHOT:
- return 0;
+ return true;
}
#endif
if (TC_H_MIN(res.classid) <= CAKE_QUEUES)
- flow = TC_H_MIN(res.classid);
+ *flow = TC_H_MIN(res.classid);
if (TC_H_MAJ(res.classid) <= (CAKE_QUEUES << 16))
- host = TC_H_MAJ(res.classid) >> 16;
+ *host = TC_H_MAJ(res.classid) >> 16;
}
-hash:
- *t = cake_select_tin(sch, skb);
- return cake_hash(*t, skb, flow_mode, flow, host) + 1;
+ return false;
}
static void cake_reconfigure(struct Qdisc *sch);
@@ -1761,20 +1766,25 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
u32 idx, tin, prev_qlen, prev_backlog, drop_id;
struct cake_sched_data *q = qdisc_priv(sch);
int len = qdisc_pkt_len(skb), ret;
+ u16 flow_override, host_override;
struct sk_buff *ack = NULL;
ktime_t now = ktime_get();
struct cake_tin_data *b;
struct cake_flow *flow;
bool same_flow = false;
- /* choose flow to insert into */
- idx = cake_classify(sch, &b, skb, q->config->flow_mode, &ret);
- if (idx == 0) {
+ /* Classify first: a terminal verdict means do not enqueue. */
+ if (cake_tcf_classify(sch, skb, &flow_override, &host_override, &ret)) {
if (ret & __NET_XMIT_BYPASS)
qdisc_qstats_drop(sch);
__qdisc_drop(skb, to_free);
return ret;
}
+
+ /* choose flow to insert into */
+ b = cake_select_tin(sch, skb);
+ idx = cake_hash(b, skb, q->config->flow_mode, flow_override,
+ host_override) + 1;
tin = (u32)(b - q->tins);
idx--;
flow = &b->flows[idx];
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH net-next v9 2/4] net/sched: cake: use a dedicated split-GSO list iterator
2026-11-10 14:40 [PATCH net-next v9 0/4] net/sched: cap the accounted backlog before it can wrap Jamal Hadi Salim
2026-11-10 14:40 ` [PATCH net-next v9 1/4] net/sched: cake: return the classifier result to the caller Jamal Hadi Salim
@ 2026-11-10 14:40 ` Jamal Hadi Salim
2026-11-12 15:13 ` Toke Høiland-Jørgensen
2026-11-10 14:40 ` [PATCH net-next v9 3/4] net/sched: cap the accounted backlog before it can wrap Jamal Hadi Salim
2026-11-10 14:40 ` [PATCH net-next v9 4/4] net/sched: cake: hash before segmenting and reject an overlong split Jamal Hadi Salim
3 siblings, 1 reply; 9+ messages in thread
From: Jamal Hadi Salim @ 2026-11-10 14:40 UTC (permalink / raw)
To: netdev
Cc: Jamal Hadi Salim, Jiri Pirko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Victor Nogueira,
Toke Høiland-Jørgensen, Björn Töpel, cake,
Sashiko
skb_list_walk_safe(segs, segs, nskb) reuses the list head as the walk
cursor, so segs no longer names the segment list once the walk finishes.
Use a separate seg cursor to keep the head and the walker distinct. No
behaviour change.
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
net/sched/sch_cake.c | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/net/sched/sch_cake.c b/net/sched/sch_cake.c
index 67cb4cbda1c7..dfc187f48a2f 100644
--- a/net/sched/sch_cake.c
+++ b/net/sched/sch_cake.c
@@ -1814,7 +1814,7 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
WRITE_ONCE(b->max_skblen, len);
if (qdisc_pkt_segs(skb) > 1 && q->config->rate_flags & CAKE_FLAG_SPLIT_GSO) {
- struct sk_buff *segs, *nskb;
+ struct sk_buff *segs, *nskb, *seg;
netdev_features_t features = netif_skb_features(skb);
unsigned int slen = 0, numsegs = 0;
@@ -1822,19 +1822,19 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
if (IS_ERR_OR_NULL(segs))
return qdisc_drop(skb, sch, to_free);
- skb_list_walk_safe(segs, segs, nskb) {
- skb_mark_not_on_list(segs);
- qdisc_skb_cb(segs)->pkt_len = segs->len;
- qdisc_skb_cb(segs)->pkt_segs = 1;
- cobalt_set_enqueue_time(segs, now);
- get_cobalt_cb(segs)->adjusted_len = cake_overhead(q,
- segs);
- flow_queue_add(flow, segs);
+ skb_list_walk_safe(segs, seg, nskb) {
+ skb_mark_not_on_list(seg);
+ qdisc_skb_cb(seg)->pkt_len = seg->len;
+ qdisc_skb_cb(seg)->pkt_segs = 1;
+ cobalt_set_enqueue_time(seg, now);
+ get_cobalt_cb(seg)->adjusted_len = cake_overhead(q,
+ seg);
+ flow_queue_add(flow, seg);
qdisc_qlen_inc(sch);
numsegs++;
- slen += segs->len;
- q->buffer_used += segs->truesize;
+ q->buffer_used += seg->truesize;
+ slen += seg->len;
WRITE_ONCE(b->packets, b->packets + 1);
}
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* Re: [PATCH net-next v9 2/4] net/sched: cake: use a dedicated split-GSO list iterator
2026-11-10 14:40 ` [PATCH net-next v9 2/4] net/sched: cake: use a dedicated split-GSO list iterator Jamal Hadi Salim
@ 2026-11-12 15:13 ` Toke Høiland-Jørgensen
0 siblings, 0 replies; 9+ messages in thread
From: Toke Høiland-Jørgensen @ 2026-11-12 15:13 UTC (permalink / raw)
To: Jamal Hadi Salim, netdev
Cc: Jamal Hadi Salim, Jiri Pirko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Victor Nogueira,
Björn Töpel, cake, Sashiko
Jamal Hadi Salim <jhs@mojatatu.com> writes:
> skb_list_walk_safe(segs, segs, nskb) reuses the list head as the walk
> cursor, so segs no longer names the segment list once the walk finishes.
> Use a separate seg cursor to keep the head and the walker distinct. No
> behaviour change.
>
> Reviewed-by: Victor Nogueira <victor@mojatatu.com>
> Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
Acked-by: Toke Høiland-Jørgensen <toke@toke.dk>
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net-next v9 3/4] net/sched: cap the accounted backlog before it can wrap
2026-11-10 14:40 [PATCH net-next v9 0/4] net/sched: cap the accounted backlog before it can wrap Jamal Hadi Salim
2026-11-10 14:40 ` [PATCH net-next v9 1/4] net/sched: cake: return the classifier result to the caller Jamal Hadi Salim
2026-11-10 14:40 ` [PATCH net-next v9 2/4] net/sched: cake: use a dedicated split-GSO list iterator Jamal Hadi Salim
@ 2026-11-10 14:40 ` Jamal Hadi Salim
2026-11-12 15:18 ` Toke Høiland-Jørgensen
2026-11-13 5:42 ` netdev-bot+sashiko
2026-11-10 14:40 ` [PATCH net-next v9 4/4] net/sched: cake: hash before segmenting and reject an overlong split Jamal Hadi Salim
3 siblings, 2 replies; 9+ messages in thread
From: Jamal Hadi Salim @ 2026-11-10 14:40 UTC (permalink / raw)
To: netdev
Cc: Jamal Hadi Salim, Jiri Pirko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Victor Nogueira,
Toke Høiland-Jørgensen, Björn Töpel, cake,
Sashiko
This is a followup of commit 709f34f7c28d ("net/sched: fq: add overflow
bounds to quantum and initial quantum") as reported by Sashiko.
The issue: fq_codel, cake, codel, pie, fq_pie, dualpi2, RED, pfifo,
plug and (non-WRED) gred account every enqueued packet's stab-adjusted
length into the 32-bit sch->qstats.backlog. A crafted tc size table
inflates qdisc_pkt_len() up to QDISC_PKT_LEN_MAX (1 MiB), and a dodgy
GSO skb can account even more through qdisc_pkt_len_segs_init(), so a
few thousand packets wrap the counter mod 2^32.
The AQM then reads a small backlog and makes the wrong drop decision,
and the dequeue-side subtractions leave it corrupt (for non-WRED gred
it is the reported stats and the dequeue-side subtractions that go
wrong instead). In fq_codel a wrapped per-flow backlog can also make
fq_codel_drop()'s scan pick an empty flow, dereferencing a NULL
flow->head.
The Fix: drop at enqueue once the stored backlog cannot represent the
incoming packet's accounted length.
Ordinary packets (qdisc_pkt_len() at most QDISC_PKT_LEN_MAX) are
covered by a standing ceiling: drop once the backlog is within
QDISC_MAX_BACKLOG (U32_MAX - QDISC_PKT_LEN_MAX) of the wrap point, the
largest backlog one more 1 MiB packet cannot wrap, so there is no
per-enqueue 64-bit add.
A larger wire estimate is checked against the space that actually
remains (len > U32_MAX - backlog): qdisc_pkt_len_segs_init() adds the
headers replicated on every output segment, so a supported small-MSS
BIG TCP aggregate can carry a wire estimate above 1 MiB while its raw
length stays within GSO_MAX_SIZE, and such traffic must pass on an
empty queue.
qdisc_pkt_len() itself stays truthful for every input: it is not
clamped and the initializer keeps succeeding, so BPF
__sk_buff.wire_len, act_police and the HTB/TBF shapers read the real
length, and sch_handle_ingress() -- which ignores the initializer's
return value and reads pkt_segs for packet statistics -- keeps
complete metadata.
Cover sch_plug, pfifo and non-WRED sch_gred as well. In RED, account the
packet before handing it to the child, bound the child backlog too, and
re-derive the aggregate from pre-reservation snapshots on failure.
The guard bounds each qdisc's own enqueue path: a child that segments a
GSO skb (tbf, netem) can still grow a parent's aggregate through
qdisc_tree_reduce_backlog()'s unclamped byte delta, which is left to a
separate change.
CAKE's split-GSO branch is the other exception; 4/4 sums and checks those
segments before linking any of them.
Conditions to recreate the bug:
ip tuntap add tun0 mode tun
ip link set tun0 txqueuelen 32 up
ip addr add 10.99.0.1/24 dev tun0
tc qdisc add dev tun0 root handle 1: stab overhead 2000000000 \
fq_codel flows 1 limit 20000 ecn
# hold the tun fd open without reading so the backlog persists, then
# send 6000 packets: the counter wraps and the unfixed kernel reports
# a wrapped backlog, while the fixed one drops at the ceiling.
Reported-by: Sashiko (nipa) <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/netdev/20260818101130.16203-1-jhs@mojatatu.com/
Link: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260818101130.16203-1-jhs@mojatatu.com
Link: https://lore.kernel.org/netdev/CANn89iLfMJV7ancKH1Gjzzm7ZUG-gKcrczkJjJEWN5sCuWd-ug@mail.gmail.com/
Link: https://lore.kernel.org/netdev/CANn89i+GOFH_8g+vSV0jUj0aBqLYVWuFOrHuDggZu9kPC_m6tQ@mail.gmail.com/
Link: https://lore.kernel.org/netdev/87a4oottcj.fsf@toke.dk/
Link: https://lore.kernel.org/netdev/87pkxks68f.fsf@toke.dk/
Link: https://lore.kernel.org/netdev/179231932946.3135.7497478990186385688@kernel.org/
Link: https://lore.kernel.org/netdev/179269577131.3135.3967915390403138015@kernel.org/
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
v8 -> v9: bound the child backlog as well as RED's aggregate, and
re-derive the aggregate from the pre-reservation snapshots so both
child conventions stay in step: leaves that reduce their ancestors on
NET_XMIT_CN (pfifo_head_drop, sfq, fq_codel) and leaves that do not
(hhf, skbprio). (Sashiko nipa review of v7)
---
include/net/pkt_sched.h | 1 -
include/net/sch_generic.h | 20 +++++++++++++++++
net/sched/sch_cake.c | 7 ++++++
net/sched/sch_codel.c | 16 ++++++++------
net/sched/sch_dualpi2.c | 1 +
net/sched/sch_fifo.c | 6 +++++
net/sched/sch_fq_codel.c | 9 ++++++++
net/sched/sch_fq_pie.c | 3 ++-
net/sched/sch_gred.c | 9 ++++++++
net/sched/sch_pie.c | 3 ++-
net/sched/sch_plug.c | 9 +++++++-
net/sched/sch_red.c | 46 ++++++++++++++++++++++++++++++++++-----
12 files changed, 114 insertions(+), 16 deletions(-)
diff --git a/include/net/pkt_sched.h b/include/net/pkt_sched.h
index 90d3e7943b19..18a419cd9d94 100644
--- a/include/net/pkt_sched.h
+++ b/include/net/pkt_sched.h
@@ -12,7 +12,6 @@
#define DEFAULT_TX_QUEUE_LEN 1000
#define STAB_SIZE_LOG_MAX 30
-#define QDISC_PKT_LEN_MAX (1 << 20) /* 1 MiB */
struct qdisc_walker {
int stop;
diff --git a/include/net/sch_generic.h b/include/net/sch_generic.h
index 1acaadb3e2cf..a5a6bdf55e0a 100644
--- a/include/net/sch_generic.h
+++ b/include/net/sch_generic.h
@@ -915,6 +915,26 @@ static inline unsigned int qdisc_pkt_len(const struct sk_buff *skb)
return qdisc_skb_cb(skb)->pkt_len;
}
+#define QDISC_PKT_LEN_MAX (1 << 20) /* 1 MiB */
+
+/* Largest backlog that still has room for one more 1 MiB packet. */
+#define QDISC_MAX_BACKLOG (U32_MAX - QDISC_PKT_LEN_MAX)
+
+/* Would this packet's accounted length wrap sch->qstats.backlog?
+ * Packets up to QDISC_PKT_LEN_MAX fit below QDISC_MAX_BACKLOG; GSO
+ * can make the length larger, so compare against the real headroom.
+ */
+static inline bool qdisc_backlog_overflow(const struct Qdisc *sch,
+ const struct sk_buff *skb)
+{
+ u32 backlog = READ_ONCE(sch->qstats.backlog);
+ u32 len = qdisc_pkt_len(skb);
+
+ if (likely(len <= QDISC_PKT_LEN_MAX))
+ return backlog > QDISC_MAX_BACKLOG;
+ return len > U32_MAX - backlog;
+}
+
static inline unsigned int qdisc_pkt_segs(const struct sk_buff *skb)
{
u32 pkt_segs = qdisc_skb_cb(skb)->pkt_segs;
diff --git a/net/sched/sch_cake.c b/net/sched/sch_cake.c
index dfc187f48a2f..3fd13bf6f3ce 100644
--- a/net/sched/sch_cake.c
+++ b/net/sched/sch_cake.c
@@ -1781,6 +1781,13 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
return ret;
}
+ /* Drop before touching the hash or shaper state. */
+ if (unlikely(qdisc_backlog_overflow(sch, skb))) {
+ qdisc_qstats_overlimit(sch);
+ return qdisc_drop_reason(skb, sch, to_free,
+ QDISC_DROP_OVERLIMIT);
+ }
+
/* choose flow to insert into */
b = cake_select_tin(sch, skb);
idx = cake_hash(b, skb, q->config->flow_mode, flow_override,
diff --git a/net/sched/sch_codel.c b/net/sched/sch_codel.c
index a1ba2e355f2d..78c5d72d6406 100644
--- a/net/sched/sch_codel.c
+++ b/net/sched/sch_codel.c
@@ -116,15 +116,17 @@ static struct sk_buff *codel_peek(struct Qdisc *sch)
static int codel_qdisc_enqueue(struct sk_buff *skb, struct Qdisc *sch,
struct sk_buff **to_free)
{
- struct codel_sched_data *q;
+ struct codel_sched_data *q = qdisc_priv(sch);
- if (likely(qdisc_qlen(sch) < sch->limit)) {
- codel_set_enqueue_time(skb);
- return qdisc_enqueue_tail(skb, sch);
+ if (unlikely(qdisc_qlen(sch) >= sch->limit) ||
+ unlikely(qdisc_backlog_overflow(sch, skb))) {
+ WRITE_ONCE(q->drop_overlimit, q->drop_overlimit + 1);
+ return qdisc_drop_reason(skb, sch, to_free,
+ QDISC_DROP_OVERLIMIT);
}
- q = qdisc_priv(sch);
- WRITE_ONCE(q->drop_overlimit, q->drop_overlimit + 1);
- return qdisc_drop_reason(skb, sch, to_free, QDISC_DROP_OVERLIMIT);
+
+ codel_set_enqueue_time(skb);
+ return qdisc_enqueue_tail(skb, sch);
}
static const struct nla_policy codel_policy[TCA_CODEL_MAX + 1] = {
diff --git a/net/sched/sch_dualpi2.c b/net/sched/sch_dualpi2.c
index 4947def7c49e..7331f0ec1c8f 100644
--- a/net/sched/sch_dualpi2.c
+++ b/net/sched/sch_dualpi2.c
@@ -392,6 +392,7 @@ static int dualpi2_enqueue_skb(struct sk_buff *skb, struct Qdisc *sch,
struct dualpi2_skb_cb *cb;
if (unlikely(qdisc_qlen(sch) >= sch->limit) ||
+ unlikely(qdisc_backlog_overflow(sch, skb)) ||
unlikely((u64)q->memory_used + skb->truesize > q->memory_limit)) {
qdisc_qstats_overlimit(sch);
if (skb_in_l_queue(skb))
diff --git a/net/sched/sch_fifo.c b/net/sched/sch_fifo.c
index 1b6388d50967..16c8d4f74e7e 100644
--- a/net/sched/sch_fifo.c
+++ b/net/sched/sch_fifo.c
@@ -29,6 +29,9 @@ static int bfifo_enqueue(struct sk_buff *skb, struct Qdisc *sch,
static int pfifo_enqueue(struct sk_buff *skb, struct Qdisc *sch,
struct sk_buff **to_free)
{
+ if (unlikely(qdisc_backlog_overflow(sch, skb)))
+ return qdisc_drop(skb, sch, to_free);
+
if (likely(sch->q.qlen < READ_ONCE(sch->limit)))
return qdisc_enqueue_tail(skb, sch);
@@ -43,6 +46,9 @@ static int pfifo_tail_enqueue(struct sk_buff *skb, struct Qdisc *sch,
if (unlikely(READ_ONCE(sch->limit) == 0))
return qdisc_drop(skb, sch, to_free);
+ if (unlikely(qdisc_backlog_overflow(sch, skb)))
+ return qdisc_drop(skb, sch, to_free);
+
if (likely(sch->q.qlen < READ_ONCE(sch->limit)))
return qdisc_enqueue_tail(skb, sch);
diff --git a/net/sched/sch_fq_codel.c b/net/sched/sch_fq_codel.c
index e6c87a32950f..a83f20534be1 100644
--- a/net/sched/sch_fq_codel.c
+++ b/net/sched/sch_fq_codel.c
@@ -201,6 +201,15 @@ static int fq_codel_enqueue(struct sk_buff *skb, struct Qdisc *sch,
}
idx--;
+ /* Drop before the flow is touched, so a dropped packet cannot
+ * create or update flow state.
+ */
+ if (unlikely(qdisc_backlog_overflow(sch, skb))) {
+ q->drop_overlimit++;
+ return qdisc_drop_reason(skb, sch, to_free,
+ QDISC_DROP_OVERLIMIT);
+ }
+
codel_set_enqueue_time(skb);
flow = &q->flows[idx];
flow_queue_add(flow, skb);
diff --git a/net/sched/sch_fq_pie.c b/net/sched/sch_fq_pie.c
index 5982847df8f8..9f927a08b7bc 100644
--- a/net/sched/sch_fq_pie.c
+++ b/net/sched/sch_fq_pie.c
@@ -155,7 +155,8 @@ static int fq_pie_qdisc_enqueue(struct sk_buff *skb, struct Qdisc *sch,
memory_limited = q->memory_usage > q->memory_limit + skb->truesize;
/* Checks if the qdisc is full */
- if (unlikely(qdisc_qlen(sch) >= sch->limit)) {
+ if (unlikely(qdisc_qlen(sch) >= sch->limit) ||
+ unlikely(qdisc_backlog_overflow(sch, skb))) {
q->stats.overlimit++;
goto out;
} else if (unlikely(memory_limited)) {
diff --git a/net/sched/sch_gred.c b/net/sched/sch_gred.c
index f3d9a7252949..de96badff654 100644
--- a/net/sched/sch_gred.c
+++ b/net/sched/sch_gred.c
@@ -170,6 +170,15 @@ static int gred_enqueue(struct sk_buff *skb, struct Qdisc *sch,
unsigned long qavg = 0;
u16 dp = tc_index_to_dp(skb);
+ /* Per-VQ limits don't bound the total: every VQ adds into the
+ * same u32 backlog, so check it before picking a VQ.
+ */
+ if (unlikely(qdisc_backlog_overflow(sch, skb))) {
+ qdisc_qstats_overlimit(sch);
+ return qdisc_drop_reason(skb, sch, to_free,
+ QDISC_DROP_OVERLIMIT);
+ }
+
if (dp >= t->DPs || (q = t->tab[dp]) == NULL) {
dp = t->def;
diff --git a/net/sched/sch_pie.c b/net/sched/sch_pie.c
index 3b7863ffd284..2a5fc493a7e5 100644
--- a/net/sched/sch_pie.c
+++ b/net/sched/sch_pie.c
@@ -89,7 +89,8 @@ static int pie_qdisc_enqueue(struct sk_buff *skb, struct Qdisc *sch,
struct pie_sched_data *q = qdisc_priv(sch);
bool enqueue = false;
- if (unlikely(qdisc_qlen(sch) >= sch->limit)) {
+ if (unlikely(qdisc_qlen(sch) >= sch->limit) ||
+ unlikely(qdisc_backlog_overflow(sch, skb))) {
WRITE_ONCE(q->stats.overlimit, q->stats.overlimit + 1);
goto out;
}
diff --git a/net/sched/sch_plug.c b/net/sched/sch_plug.c
index b60ddfee6a68..c90b5ca60bc0 100644
--- a/net/sched/sch_plug.c
+++ b/net/sched/sch_plug.c
@@ -89,7 +89,14 @@ static int plug_enqueue(struct sk_buff *skb, struct Qdisc *sch,
{
struct plug_sched_data *q = qdisc_priv(sch);
- if (likely((u64)sch->qstats.backlog + skb->len <= q->limit)) {
+ if (unlikely(qdisc_backlog_overflow(sch, skb))) {
+ qdisc_qstats_overlimit(sch);
+ return qdisc_drop_reason(skb, sch, to_free,
+ QDISC_DROP_OVERLIMIT);
+ }
+
+ if (likely((u64)sch->qstats.backlog + qdisc_pkt_len(skb) <=
+ q->limit)) {
if (!q->unplug_indefinite)
q->pkts_current_epoch++;
return qdisc_enqueue_tail(skb, sch);
diff --git a/net/sched/sch_red.c b/net/sched/sch_red.c
index d7598214270b..b5233d9fbf57 100644
--- a/net/sched/sch_red.c
+++ b/net/sched/sch_red.c
@@ -73,9 +73,22 @@ static int red_enqueue(struct sk_buff *skb, struct Qdisc *sch,
enum qdisc_drop_reason reason = QDISC_DROP_CONGESTED;
struct red_sched_data *q = qdisc_priv(sch);
struct Qdisc *child = q->qdisc;
+ u32 child_backlog;
+ u32 red_backlog;
unsigned int len;
int ret;
+ /* RED reads the child backlog into red_calc_qavg(), so bound both
+ * counters: a child that evicts on NET_XMIT_CN can otherwise grow
+ * its own backlog while RED's stays put.
+ */
+ if (unlikely(qdisc_backlog_overflow(sch, skb) ||
+ qdisc_backlog_overflow(child, skb))) {
+ qdisc_qstats_overlimit(sch);
+ return qdisc_drop_reason(skb, sch, to_free,
+ QDISC_DROP_OVERLIMIT);
+ }
+
q->vars.qavg = red_calc_qavg(&q->parms,
&q->vars,
child->qstats.backlog);
@@ -136,14 +149,37 @@ static int red_enqueue(struct sk_buff *skb, struct Qdisc *sch,
}
len = qdisc_pkt_len(skb);
+ /* Reserve the length before the child sees the packet: netem
+ * can re-enqueue a clone here before the backlog is updated.
+ */
+ red_backlog = READ_ONCE(sch->qstats.backlog);
+ child_backlog = READ_ONCE(child->qstats.backlog);
+ qstats_backlog_add(sch, len);
ret = qdisc_enqueue(skb, child, to_free);
if (likely(ret == NET_XMIT_SUCCESS)) {
- qstats_backlog_add(sch, len);
qdisc_qlen_inc(sch);
- } else if (net_xmit_drop_count(ret)) {
- WRITE_ONCE(q->stats.pdrop,
- q->stats.pdrop + 1);
- qdisc_qstats_drop(sch);
+ } else {
+ u32 propagated, grew;
+
+ /* Re-derive the aggregate from the snapshots: the child may
+ * have dropped the packet or evicted another one, and may or
+ * may not have reduced the ancestors' backlog itself
+ * (pfifo_head_drop, sfq and fq_codel do; hhf and skbprio
+ * return NET_XMIT_CN without qdisc_tree_reduce_backlog()).
+ * Keep what the child actually accounted for, and pass the
+ * difference on to the ancestors above RED.
+ */
+ propagated = READ_ONCE(sch->qstats.backlog) -
+ red_backlog - len;
+ grew = READ_ONCE(child->qstats.backlog) - child_backlog;
+ WRITE_ONCE(sch->qstats.backlog, red_backlog + grew);
+ if (propagated != grew)
+ qdisc_tree_reduce_backlog(sch, 0, propagated - grew);
+ if (net_xmit_drop_count(ret)) {
+ WRITE_ONCE(q->stats.pdrop,
+ q->stats.pdrop + 1);
+ qdisc_qstats_drop(sch);
+ }
}
return ret;
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* Re: [PATCH net-next v9 3/4] net/sched: cap the accounted backlog before it can wrap
2026-11-10 14:40 ` [PATCH net-next v9 3/4] net/sched: cap the accounted backlog before it can wrap Jamal Hadi Salim
@ 2026-11-12 15:18 ` Toke Høiland-Jørgensen
2026-11-13 5:42 ` netdev-bot+sashiko
1 sibling, 0 replies; 9+ messages in thread
From: Toke Høiland-Jørgensen @ 2026-11-12 15:18 UTC (permalink / raw)
To: Jamal Hadi Salim, netdev
Cc: Jamal Hadi Salim, Jiri Pirko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Victor Nogueira,
Björn Töpel, cake, Sashiko
Jamal Hadi Salim <jhs@mojatatu.com> writes:
> This is a followup of commit 709f34f7c28d ("net/sched: fq: add overflow
> bounds to quantum and initial quantum") as reported by Sashiko.
>
> The issue: fq_codel, cake, codel, pie, fq_pie, dualpi2, RED, pfifo,
> plug and (non-WRED) gred account every enqueued packet's stab-adjusted
> length into the 32-bit sch->qstats.backlog. A crafted tc size table
> inflates qdisc_pkt_len() up to QDISC_PKT_LEN_MAX (1 MiB), and a dodgy
> GSO skb can account even more through qdisc_pkt_len_segs_init(), so a
> few thousand packets wrap the counter mod 2^32.
>
> The AQM then reads a small backlog and makes the wrong drop decision,
> and the dequeue-side subtractions leave it corrupt (for non-WRED gred
> it is the reported stats and the dequeue-side subtractions that go
> wrong instead). In fq_codel a wrapped per-flow backlog can also make
> fq_codel_drop()'s scan pick an empty flow, dereferencing a NULL
> flow->head.
>
> The Fix: drop at enqueue once the stored backlog cannot represent the
> incoming packet's accounted length.
>
> Ordinary packets (qdisc_pkt_len() at most QDISC_PKT_LEN_MAX) are
> covered by a standing ceiling: drop once the backlog is within
> QDISC_MAX_BACKLOG (U32_MAX - QDISC_PKT_LEN_MAX) of the wrap point, the
> largest backlog one more 1 MiB packet cannot wrap, so there is no
> per-enqueue 64-bit add.
>
> A larger wire estimate is checked against the space that actually
> remains (len > U32_MAX - backlog): qdisc_pkt_len_segs_init() adds the
> headers replicated on every output segment, so a supported small-MSS
> BIG TCP aggregate can carry a wire estimate above 1 MiB while its raw
> length stays within GSO_MAX_SIZE, and such traffic must pass on an
> empty queue.
>
> qdisc_pkt_len() itself stays truthful for every input: it is not
> clamped and the initializer keeps succeeding, so BPF
> __sk_buff.wire_len, act_police and the HTB/TBF shapers read the real
> length, and sch_handle_ingress() -- which ignores the initializer's
> return value and reads pkt_segs for packet statistics -- keeps
> complete metadata.
>
> Cover sch_plug, pfifo and non-WRED sch_gred as well. In RED, account the
> packet before handing it to the child, bound the child backlog too, and
> re-derive the aggregate from pre-reservation snapshots on failure.
>
> The guard bounds each qdisc's own enqueue path: a child that segments a
> GSO skb (tbf, netem) can still grow a parent's aggregate through
> qdisc_tree_reduce_backlog()'s unclamped byte delta, which is left to a
> separate change.
>
> CAKE's split-GSO branch is the other exception; 4/4 sums and checks those
> segments before linking any of them.
>
> Conditions to recreate the bug:
> ip tuntap add tun0 mode tun
> ip link set tun0 txqueuelen 32 up
> ip addr add 10.99.0.1/24 dev tun0
> tc qdisc add dev tun0 root handle 1: stab overhead 2000000000 \
> fq_codel flows 1 limit 20000 ecn
> # hold the tun fd open without reading so the backlog persists, then
> # send 6000 packets: the counter wraps and the unfixed kernel reports
> # a wrapped backlog, while the fixed one drops at the ceiling.
>
> Reported-by: Sashiko (nipa) <sashiko-bot@kernel.org>
> Closes: https://lore.kernel.org/netdev/20260818101130.16203-1-jhs@mojatatu.com/
> Link: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260818101130.16203-1-jhs@mojatatu.com
> Link: https://lore.kernel.org/netdev/CANn89iLfMJV7ancKH1Gjzzm7ZUG-gKcrczkJjJEWN5sCuWd-ug@mail.gmail.com/
> Link: https://lore.kernel.org/netdev/CANn89i+GOFH_8g+vSV0jUj0aBqLYVWuFOrHuDggZu9kPC_m6tQ@mail.gmail.com/
> Link: https://lore.kernel.org/netdev/87a4oottcj.fsf@toke.dk/
> Link: https://lore.kernel.org/netdev/87pkxks68f.fsf@toke.dk/
> Link: https://lore.kernel.org/netdev/179231932946.3135.7497478990186385688@kernel.org/
> Link: https://lore.kernel.org/netdev/179269577131.3135.3967915390403138015@kernel.org/
> Reviewed-by: Victor Nogueira <victor@mojatatu.com>
> Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
Acked-by: Toke Høiland-Jørgensen <toke@toke.dk>
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH net-next v9 3/4] net/sched: cap the accounted backlog before it can wrap
2026-11-10 14:40 ` [PATCH net-next v9 3/4] net/sched: cap the accounted backlog before it can wrap Jamal Hadi Salim
2026-11-12 15:18 ` Toke Høiland-Jørgensen
@ 2026-11-13 5:42 ` netdev-bot+sashiko
1 sibling, 0 replies; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-11-13 5:42 UTC (permalink / raw)
To: jhs
Cc: netdev, jiri, davem, edumazet, kuba, pabeni, horms, victor, toke,
bjorn, sashiko-bot
Thank you for your contribution! Sashiko AI review found 5 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 4
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] The subject ('net/sched: cap the accounted backlog before it
can wrap') reads as a general fix, but only fq_codel, cake, codel,…
- [Low] The commit message has no Fixes: tag.
- [Low] The commit message describes the current length accounting
incorrectly in two places, and it points to an in-series patch by…
- [Low] plug_enqueue() (net/sched/sch_plug.c) changes its byte-limit
admission test from '(u64)sch->qstats.backlog + skb->len <= q->limit'…
- [Low] In red_enqueue() (net/sched/sch_red.c:81-84), the new comment
explains the second check, qdisc_backlog_overflow(child, skb), with:…
--
net/sched: cap the accounted backlog before it can wrap
This commit adds qdisc_backlog_overflow(), which drops a packet at enqueue
when adding its qdisc_pkt_len() would wrap the 32-bit sch->qstats.backlog.
The helper is called from fq_codel, cake, codel, pie, fq_pie, dualpi2, RED,
pfifo, plug and gred. The commit also switches plug's byte limit to
qdisc_pkt_len(), and RED now reserves the length before the child enqueue
and re-derives its backlog from snapshots when the enqueue fails.
Closes: https://lore.kernel.org/netdev/20260818101130.16203-1-jhs@mojatatu.com/
Link: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260818101130.16203-1-jhs@mojatatu.com
Link: https://lore.kernel.org/netdev/CANn89iLfMJV7ancKH1Gjzzm7ZUG-gKcrczkJjJEWN5sCuWd-ug@mail.gmail.com/
Link: https://lore.kernel.org/netdev/CANn89i+GOFH_8g+vSV0jUj0aBqLYVWuFOrHuDggZu9kPC_m6tQ@mail.gmail.com/
Link: https://lore.kernel.org/netdev/87a4oottcj.fsf@toke.dk/
Link: https://lore.kernel.org/netdev/87pkxks68f.fsf@toke.dk/
Link: https://lore.kernel.org/netdev/179231932946.3135.7497478990186385688@kernel.org/
Link: https://lore.kernel.org/netdev/179269577131.3135.3967915390403138015@kernel.org/
> This is a followup of commit 709f34f7c28d ("net/sched: fq: add overflow
> bounds to quantum and initial quantum") as reported by Sashiko.
[Severity: Low]
Should this have a Fixes: tag? The message describes a NULL flow->head
dereference in fq_codel_drop() after the per-flow backlog wraps. It also
describes wrong AQM drop decisions in several qdiscs.
The only commit it references is 709f34f7c28d. That commit changes sch_fq,
which this patch doesn't touch, and it doesn't look like the commit that
introduced the u32 backlog wrap.
The tags include Reported-by, Closes, Link and Reviewed-by, but none of them
names the commit(s) that introduced the problem. Examples would be the stab
size table support or the AQMs that read qstats.backlog. There is no
Cc: stable either, so stable tooling can't tell which trees need this.
> inflates qdisc_pkt_len() up to QDISC_PKT_LEN_MAX (1 MiB), and a dodgy
> GSO skb can account even more through qdisc_pkt_len_segs_init(), so a
> few thousand packets wrap the counter mod 2^32.
[Severity: Low]
Is this accurate? The SKB_GSO_DODGY branch of qdisc_pkt_len_segs_init()
already rejects wire estimates above QDISC_PKT_LEN_MAX:
net/core/dev.c:qdisc_pkt_len_segs_init() {
...
if (unlikely(gso_segs > GSO_MAX_SEGS ||
skb->len + (u64)(gso_segs - 1) * hdr_len >
QDISC_PKT_LEN_MAX))
return SKB_DROP_REASON_SKB_BAD_GSO;
...
}
If so, only trusted, locally built GSO can reach the
len > QDISC_PKT_LEN_MAX branch of qdisc_backlog_overflow(). That is the
BIG TCP case described later in the message.
> qdisc_pkt_len() itself stays truthful for every input: it is not
> clamped and the initializer keeps succeeding, so BPF
This doesn't match the code either. __qdisc_calculate_pkt_len() clamps
every stab-adjusted length, and the reproducer goes through that path:
net/sched/sch_api.c:__qdisc_calculate_pkt_len() {
...
pkt_len = clamp_t(int, pkt_len, 1, QDISC_PKT_LEN_MAX);
qdisc_skb_cb(skb)->pkt_len = pkt_len;
}
The initializer also fails for oversized dodgy GSO, as shown above. Could
this paragraph be reworded?
> CAKE's split-GSO branch is the other exception; 4/4 sums and checks those
> segments before linking any of them.
The "4/4" refers to a position in the series, which won't mean anything once
this is in git history. Could that commit be named by its subject instead?
> diff --git a/include/net/sch_generic.h b/include/net/sch_generic.h
> index 1acaadb3e2cf0..a5a6bdf55e0a1 100644
> --- a/include/net/sch_generic.h
> +++ b/include/net/sch_generic.h
> @@ -915,6 +915,26 @@ static inline unsigned int qdisc_pkt_len(const struct sk_buff *skb)
> return qdisc_skb_cb(skb)->pkt_len;
> }
>
> +#define QDISC_PKT_LEN_MAX (1 << 20) /* 1 MiB */
> +
> +/* Largest backlog that still has room for one more 1 MiB packet. */
> +#define QDISC_MAX_BACKLOG (U32_MAX - QDISC_PKT_LEN_MAX)
> +
> +/* Would this packet's accounted length wrap sch->qstats.backlog?
> + * Packets up to QDISC_PKT_LEN_MAX fit below QDISC_MAX_BACKLOG; GSO
> + * can make the length larger, so compare against the real headroom.
> + */
> +static inline bool qdisc_backlog_overflow(const struct Qdisc *sch,
> + const struct sk_buff *skb)
> +{
> + u32 backlog = READ_ONCE(sch->qstats.backlog);
> + u32 len = qdisc_pkt_len(skb);
> +
> + if (likely(len <= QDISC_PKT_LEN_MAX))
> + return backlog > QDISC_MAX_BACKLOG;
> + return len > U32_MAX - backlog;
> +}
[Severity: Medium]
The subject reads like a general fix, but this helper is only called from
fq_codel, cake, codel, pie, fq_pie, dualpi2, RED, pfifo, plug and gred. Can
sch->qstats.backlog still wrap the same way in other qdiscs?
Several leaves add the stab-inflated qdisc_pkt_len() without the guard:
fq_enqueue() -> qdisc_qstats_backlog_inc(sch, skb);
sfq_enqueue() -> qdisc_qstats_backlog_inc(sch, skb);
hhf_enqueue() -> qdisc_qstats_backlog_inc(sch, skb);
fq's default limit is 10000 packets per band, across 3 bands. That is far
more than the roughly 4096 1 MiB packets needed to wrap. sfq's limit can be
set as high as maxdepth * maxflows. skbprio, netem, choke, etf and taprio
look similar.
Classful parents such as prio and htb (and also drr, ets, qfq, hfsc, cbs and
sfb) add each child's admitted length into their own u32 backlog:
prio_enqueue() -> qstats_backlog_add(sch, len);
htb_enqueue() -> qstats_backlog_add(sch, len);
Each guarded child can hold a backlog just below U32_MAX, so several of them
together can still wrap the parent. The commit message names only child GSO
segmentation (tbf, netem) and CAKE split-GSO as remaining exceptions.
The patch counts stats-only corruption as in scope, since it guards pfifo and
non-WRED gred. Should the qdiscs above be covered too, or listed as known
gaps? None of the unguarded leaves seem to use sch->qstats.backlog for drop
decisions, so there the result would be wrong backlog stats in dumps.
[ ... ]
> diff --git a/net/sched/sch_plug.c b/net/sched/sch_plug.c
> index b60ddfee6a68a..c90b5ca60bc0b 100644
> --- a/net/sched/sch_plug.c
> +++ b/net/sched/sch_plug.c
> @@ -89,7 +89,14 @@ static int plug_enqueue(struct sk_buff *skb, struct Qdisc *sch,
> {
> struct plug_sched_data *q = qdisc_priv(sch);
>
> - if (likely((u64)sch->qstats.backlog + skb->len <= q->limit)) {
> + if (unlikely(qdisc_backlog_overflow(sch, skb))) {
> + qdisc_qstats_overlimit(sch);
> + return qdisc_drop_reason(skb, sch, to_free,
> + QDISC_DROP_OVERLIMIT);
> + }
> +
> + if (likely((u64)sch->qstats.backlog + qdisc_pkt_len(skb) <=
> + q->limit)) {
[Severity: Low]
The limit test in plug_enqueue() now uses qdisc_pkt_len(skb) instead of
skb->len, but the commit message only says "Cover sch_plug". Could this
change be mentioned?
qdisc_enqueue_tail() already counted the backlog in qdisc_pkt_len() units,
so only the measurement of the arriving packet changes (GSO replicated
headers, or stab overhead). Near the byte limit, some packets that were
admitted before will now be dropped.
This switch alone is enough to prevent the wrap. q->limit is a u32 and plug
is classless, so once this test passes,
backlog + qdisc_pkt_len(skb) <= q->limit <= U32_MAX holds.
Is the qdisc_backlog_overflow() check above it still needed? It only does
anything when q->limit > QDISC_MAX_BACKLOG. In that case it drops packets
that the limit would accept and that could not wrap the counter.
[ ... ]
> diff --git a/net/sched/sch_red.c b/net/sched/sch_red.c
> index d7598214270b8..b5233d9fbf572 100644
> --- a/net/sched/sch_red.c
> +++ b/net/sched/sch_red.c
> @@ -73,9 +73,22 @@ static int red_enqueue(struct sk_buff *skb, struct Qdisc *sch,
> enum qdisc_drop_reason reason = QDISC_DROP_CONGESTED;
> struct red_sched_data *q = qdisc_priv(sch);
> struct Qdisc *child = q->qdisc;
> + u32 child_backlog;
> + u32 red_backlog;
> unsigned int len;
> int ret;
>
> + /* RED reads the child backlog into red_calc_qavg(), so bound both
> + * counters: a child that evicts on NET_XMIT_CN can otherwise grow
> + * its own backlog while RED's stays put.
> + */
> + if (unlikely(qdisc_backlog_overflow(sch, skb) ||
> + qdisc_backlog_overflow(child, skb))) {
[Severity: Low]
Is this comment still accurate, given the re-derive logic added later in
red_enqueue()? On the non-success path the patch now does:
grew = READ_ONCE(child->qstats.backlog) - child_backlog;
WRITE_ONCE(sch->qstats.backlog, red_backlog + grew);
The later comment names the NET_XMIT_CN cases. hhf and skbprio return CN
without calling qdisc_tree_reduce_backlog(), while pfifo_head_drop, sfq and
fq_codel do call it. In all of these, RED's backlog now moves by exactly the
child's growth, so it no longer stays put.
The comment seems to describe an earlier revision. Does the child check need
a different justification, or is it now redundant?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-BA27.v9.r0.20261108195325%40mojatatu.com
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net-next v9 4/4] net/sched: cake: hash before segmenting and reject an overlong split
2026-11-10 14:40 [PATCH net-next v9 0/4] net/sched: cap the accounted backlog before it can wrap Jamal Hadi Salim
` (2 preceding siblings ...)
2026-11-10 14:40 ` [PATCH net-next v9 3/4] net/sched: cap the accounted backlog before it can wrap Jamal Hadi Salim
@ 2026-11-10 14:40 ` Jamal Hadi Salim
2026-11-13 5:42 ` netdev-bot+sashiko
3 siblings, 1 reply; 9+ messages in thread
From: Jamal Hadi Salim @ 2026-11-10 14:40 UTC (permalink / raw)
To: netdev
Cc: Jamal Hadi Salim, Jiri Pirko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Victor Nogueira,
Toke Høiland-Jørgensen, Björn Töpel, cake,
Sashiko
The Fix:
with CAKE_FLAG_SPLIT_GSO, cake computes the flow/host hash keys, then
splits a GSO skb into segments it accounts individually. Key extraction
must run on the unsegmented skb -- skb_gso_segment() can rewrite an
encapsulated packet's headers to the inner ones -- and the resolution
must wait until the packet is admitted, since it commits the
set-associative tags, host indices and way counters that a later
rejection would leave mutated (a GSO skb whose segmentation fails was
dropped with that state already committed).
Split cake_hash() into a side-effect-free cake_hash_keys(), run on the
unsegmented skb, and cake_hash_resolve(), run only once the packet is
admitted. Move the split-GSO path ahead of the resolution: sum the
segment list, test the total against the ceiling before linking any
segment, and drop the whole list on rejection so no persistent state is
committed. The list is deferred to to_free so the tail segment's
sock_wfree() never runs under the qdisc lock.
Tag every deferred segment with QDISC_DROP_OVERLIMIT: __skb_gso_segment()
keeps SKB_GSO_CB at cb offset 32, aliasing tc_skb_cb.drop_reason, and
__copy_skb_header() copies it into every segment, so tagging only the
head left the rest with GSO scratch (a headroom-sized value) as their
drop reason, which __tcf_kfree_skb_list() would report as an unrelated
core reason. The segmentation-failure return has the same aliasing and
uses qdisc_drop_reason(..., QDISC_DROP_GENERIC) so its drop is still
reported as a qdisc drop.
Read q->config->flow_mode once with READ_ONCE() in cake_enqueue() and
pass that value to both cake_hash_keys() and cake_hash_resolve(), and to
the later flowchain calls (cake_get_flow_quantum(),
cake_inc_srchost_bulk_flow_count(), cake_inc_dsthost_bulk_flow_count()).
cake_mq_change() updates the shared cake_config outside the qdisc lock, so
a reconfiguration could extract and resolve keys in different modes.
Conditions to recreate the bug:
ip tuntap add tun0 mode tun
ip link set tun0 txqueuelen 32 up
ip addr add 10.99.0.1/24 dev tun0
# hold the tun fd open without reading so the backlog persists; a stab
# overhead of 1048468 accounts each 92-byte packet at 1048560 bytes, so
# 4095 queued packets leave 65519 bytes below QDISC_MAX_BACKLOG. A
# 65000-byte UDP_SEGMENT payload with a 1400-byte segment size then has
# a segment sum past the remaining headroom, and cake rejects the whole
# split. Setup needs CAP_NET_ADMIN; the payload itself does not.
tc qdisc add dev tun0 root handle 1: stab overhead 1048468 \
cake split-gso memlimit 512Mb
Link: https://lore.kernel.org/netdev/179231933068.3135.80340792633077986@kernel.org/
Link: https://lore.kernel.org/netdev/179269577270.3135.16072065138845192450@kernel.org/
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Acked-by: Toke Høiland-Jørgensen <toke@toke.dk>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
v8 -> v9: use qdisc_drop_reason(QDISC_DROP_GENERIC) when segmentation
fails (__skb_gso_segment() has already clobbered the drop-reason slot),
and pass the flow_mode snapshot to the later flowchain calls. (Sashiko
nipa review of v7)
---
net/sched/sch_cake.c | 130 +++++++++++++++++++++++++++++++------------
1 file changed, 94 insertions(+), 36 deletions(-)
diff --git a/net/sched/sch_cake.c b/net/sched/sch_cake.c
index 3fd13bf6f3ce..74e2b09971ee 100644
--- a/net/sched/sch_cake.c
+++ b/net/sched/sch_cake.c
@@ -709,19 +709,28 @@ static u16 cake_get_flow_quantum(struct cake_tin_data *q,
get_random_u16()) >> 16;
}
-static u32 cake_hash(struct cake_tin_data *q, const struct sk_buff *skb,
- int flow_mode, u16 flow_override, u16 host_override)
+struct cake_hash_keys {
+ u32 srchost_hash;
+ u32 dsthost_hash;
+ u32 flow_hash;
+};
+
+static void cake_hash_keys(const struct sk_buff *skb, int flow_mode,
+ u16 flow_override, u16 host_override,
+ struct cake_hash_keys *out)
{
bool hash_flows = (!flow_override && !!(flow_mode & CAKE_FLOW_FLOWS));
bool hash_hosts = (!host_override && !!(flow_mode & CAKE_FLOW_HOSTS));
bool nat_enabled = !!(flow_mode & CAKE_FLOW_NAT_FLAG);
- u32 flow_hash = 0, srchost_hash = 0, dsthost_hash = 0;
- u16 reduced_hash, srchost_idx, dsthost_idx;
struct flow_keys keys, host_keys;
bool use_skbhash = skb->l4_hash;
+ out->flow_hash = 0;
+ out->srchost_hash = 0;
+ out->dsthost_hash = 0;
+
if (unlikely(flow_mode == CAKE_FLOW_NONE))
- return 0;
+ return;
/* If both overrides are set, or we can use the SKB hash and nat mode is
* disabled, we can skip packet dissection entirely. If nat mode is
@@ -756,50 +765,64 @@ static u32 cake_hash(struct cake_tin_data *q, const struct sk_buff *skb,
switch (host_keys.control.addr_type) {
case FLOW_DISSECTOR_KEY_IPV4_ADDRS:
host_keys.addrs.v4addrs.src = 0;
- dsthost_hash = flow_hash_from_keys(&host_keys);
+ out->dsthost_hash = flow_hash_from_keys(&host_keys);
host_keys.addrs.v4addrs.src = keys.addrs.v4addrs.src;
host_keys.addrs.v4addrs.dst = 0;
- srchost_hash = flow_hash_from_keys(&host_keys);
+ out->srchost_hash = flow_hash_from_keys(&host_keys);
break;
case FLOW_DISSECTOR_KEY_IPV6_ADDRS:
memset(&host_keys.addrs.v6addrs.src, 0,
sizeof(host_keys.addrs.v6addrs.src));
- dsthost_hash = flow_hash_from_keys(&host_keys);
+ out->dsthost_hash = flow_hash_from_keys(&host_keys);
host_keys.addrs.v6addrs.src = keys.addrs.v6addrs.src;
memset(&host_keys.addrs.v6addrs.dst, 0,
sizeof(host_keys.addrs.v6addrs.dst));
- srchost_hash = flow_hash_from_keys(&host_keys);
+ out->srchost_hash = flow_hash_from_keys(&host_keys);
break;
default:
- dsthost_hash = 0;
- srchost_hash = 0;
+ out->dsthost_hash = 0;
+ out->srchost_hash = 0;
}
/* This *must* be after the above switch, since as a
* side-effect it sorts the src and dst addresses.
*/
if (hash_flows && !use_skbhash)
- flow_hash = flow_hash_from_keys(&keys);
+ out->flow_hash = flow_hash_from_keys(&keys);
skip_hash:
if (flow_override)
- flow_hash = flow_override - 1;
+ out->flow_hash = flow_override - 1;
else if (use_skbhash && (flow_mode & CAKE_FLOW_FLOWS))
- flow_hash = skb->hash;
+ out->flow_hash = skb->hash;
if (host_override) {
- dsthost_hash = host_override - 1;
- srchost_hash = host_override - 1;
+ out->dsthost_hash = host_override - 1;
+ out->srchost_hash = host_override - 1;
}
if (!(flow_mode & CAKE_FLOW_FLOWS)) {
if (flow_mode & CAKE_FLOW_SRC_IP)
- flow_hash ^= srchost_hash;
+ out->flow_hash ^= out->srchost_hash;
if (flow_mode & CAKE_FLOW_DST_IP)
- flow_hash ^= dsthost_hash;
+ out->flow_hash ^= out->dsthost_hash;
}
+}
+
+/* Commit the flow into the set-associative table (q->tags[], host
+ * indices, bulk counters). Only after the packet is admitted.
+ */
+static u32 cake_hash_resolve(struct cake_tin_data *q,
+ const struct cake_hash_keys *keys, int flow_mode)
+{
+ u32 flow_hash = keys->flow_hash, srchost_hash = keys->srchost_hash,
+ dsthost_hash = keys->dsthost_hash;
+ u16 reduced_hash, srchost_idx, dsthost_idx;
+
+ if (unlikely(flow_mode == CAKE_FLOW_NONE))
+ return 0;
reduced_hash = flow_hash % CAKE_QUEUES;
@@ -1765,13 +1788,17 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
{
u32 idx, tin, prev_qlen, prev_backlog, drop_id;
struct cake_sched_data *q = qdisc_priv(sch);
+ int flow_mode = READ_ONCE(q->config->flow_mode);
int len = qdisc_pkt_len(skb), ret;
u16 flow_override, host_override;
+ struct sk_buff *segs = NULL;
+ struct cake_hash_keys keys;
struct sk_buff *ack = NULL;
ktime_t now = ktime_get();
struct cake_tin_data *b;
struct cake_flow *flow;
bool same_flow = false;
+ u64 slen = 0;
/* Classify first: a terminal verdict means do not enqueue. */
if (cake_tcf_classify(sch, skb, &flow_override, &host_override, &ret)) {
@@ -1788,12 +1815,50 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
QDISC_DROP_OVERLIMIT);
}
- /* choose flow to insert into */
+ /* Select the tin and extract the keys before segmenting:
+ * skb_gso_segment() rewrites encapsulated headers, which
+ * changes the keys.
+ */
b = cake_select_tin(sch, skb);
- idx = cake_hash(b, skb, q->config->flow_mode, flow_override,
- host_override) + 1;
+ cake_hash_keys(skb, flow_mode, flow_override, host_override, &keys);
+
+ if (qdisc_pkt_segs(skb) > 1 && q->config->rate_flags & CAKE_FLAG_SPLIT_GSO) {
+ netdev_features_t features = netif_skb_features(skb);
+ struct sk_buff *nskb, *seg;
+
+ segs = skb_gso_segment(skb, features & ~NETIF_F_GSO_MASK);
+ if (IS_ERR_OR_NULL(segs))
+ return qdisc_drop_reason(skb, sch, to_free,
+ QDISC_DROP_GENERIC);
+
+ /* we never use the original skb after splitting */
+ consume_skb(skb);
+ skb = NULL;
+
+ /* The split path accounts the segment sum, not the
+ * stab-adjusted qdisc_pkt_len(), so check it here.
+ */
+ skb_list_walk_safe(segs, seg, nskb)
+ slen += seg->len;
+
+ if (unlikely((u64)sch->qstats.backlog + slen > QDISC_MAX_BACKLOG)) {
+ qdisc_qstats_overlimit(sch);
+ /* __skb_gso_segment() puts SKB_GSO_CB at cb
+ * offset 32, over tc_skb_cb.drop_reason; every
+ * segment inherits it. Tag each one with the
+ * qdisc drop reason.
+ */
+ skb_list_walk_safe(segs, seg, nskb)
+ tcf_set_qdisc_drop_reason(seg,
+ QDISC_DROP_OVERLIMIT);
+
+ return qdisc_drop_all(segs, sch, to_free);
+ }
+ }
+
+ /* Admitted: resolve the flow. */
+ idx = cake_hash_resolve(b, &keys, flow_mode);
tin = (u32)(b - q->tins);
- idx--;
flow = &b->flows[idx];
/* ensure shaper state isn't stale */
@@ -1820,14 +1885,9 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
if (unlikely(len > b->max_skblen))
WRITE_ONCE(b->max_skblen, len);
- if (qdisc_pkt_segs(skb) > 1 && q->config->rate_flags & CAKE_FLAG_SPLIT_GSO) {
- struct sk_buff *segs, *nskb, *seg;
- netdev_features_t features = netif_skb_features(skb);
- unsigned int slen = 0, numsegs = 0;
-
- segs = skb_gso_segment(skb, features & ~NETIF_F_GSO_MASK);
- if (IS_ERR_OR_NULL(segs))
- return qdisc_drop(skb, sch, to_free);
+ if (segs) {
+ struct sk_buff *nskb, *seg;
+ unsigned int numsegs = 0;
skb_list_walk_safe(segs, seg, nskb) {
skb_mark_not_on_list(seg);
@@ -1841,7 +1901,6 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
qdisc_qlen_inc(sch);
numsegs++;
q->buffer_used += seg->truesize;
- slen += seg->len;
WRITE_ONCE(b->packets, b->packets + 1);
}
@@ -1853,9 +1912,8 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
WRITE_ONCE(b->backlogs[idx], b->backlogs[idx] + slen);
qdisc_tree_reduce_backlog(sch, 1-numsegs, len-slen);
- consume_skb(skb);
} else {
- /* not splitting */
+ /* not splitting - skb pointer still valid */
int ack_pkt_len = 0;
cobalt_set_enqueue_time(skb, now);
@@ -1949,7 +2007,7 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
flow->set = CAKE_SET_SPARSE;
WRITE_ONCE(b->sparse_flow_count, b->sparse_flow_count + 1);
- WRITE_ONCE(flow->deficit, cake_get_flow_quantum(b, flow, q->config->flow_mode));
+ WRITE_ONCE(flow->deficit, cake_get_flow_quantum(b, flow, flow_mode));
} else if (flow->set == CAKE_SET_SPARSE_WAIT) {
/* this flow was empty, accounted as a sparse flow, but actually
* in the bulk rotation.
@@ -1958,8 +2016,8 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
WRITE_ONCE(b->sparse_flow_count, b->sparse_flow_count - 1);
WRITE_ONCE(b->bulk_flow_count, b->bulk_flow_count + 1);
- cake_inc_srchost_bulk_flow_count(b, flow, q->config->flow_mode);
- cake_inc_dsthost_bulk_flow_count(b, flow, q->config->flow_mode);
+ cake_inc_srchost_bulk_flow_count(b, flow, flow_mode);
+ cake_inc_dsthost_bulk_flow_count(b, flow, flow_mode);
}
if (q->buffer_used > u64_stats_read(&q->buffer_max_used)) {
--
2.43.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* Re: [PATCH net-next v9 4/4] net/sched: cake: hash before segmenting and reject an overlong split
2026-11-10 14:40 ` [PATCH net-next v9 4/4] net/sched: cake: hash before segmenting and reject an overlong split Jamal Hadi Salim
@ 2026-11-13 5:42 ` netdev-bot+sashiko
0 siblings, 0 replies; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-11-13 5:42 UTC (permalink / raw)
To: jhs
Cc: netdev, jiri, davem, edumazet, kuba, pabeni, horms, victor, toke,
bjorn, sashiko-bot
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] Parts of the commit message do not match the code.
- [Low] The patch is presented as a fix ("The Fix:") but has no Fixes:
tag.
- [Low] The new split-GSO admission check in cake_enqueue() treats
QDISC_MAX_BACKLOG as a limit on the backlog after the add:…
Pre-existing issues:
- [Low] This problem was not introduced by this patch.
--
net/sched: cake: hash before segmenting and reject an overlong split
This patch splits cake_hash() into two parts. cake_hash_keys() has no side
effects. cake_hash_resolve() commits the set-associative state and now runs
only after the packet is admitted. The split-GSO segmentation moves ahead of
flow resolution, and the whole segment list is rejected when the segment sum
would pass QDISC_MAX_BACKLOG. cake_enqueue() also reads
q->config->flow_mode once with READ_ONCE().
Link: https://lore.kernel.org/netdev/179231933068.3135.80340792633077986@kernel.org/
Link: https://lore.kernel.org/netdev/179269577270.3135.16072065138845192450@kernel.org/
> Conditions to recreate the bug:
> ip tuntap add tun0 mode tun
> ip link set tun0 txqueuelen 32 up
> ip addr add 10.99.0.1/24 dev tun0
> # hold the tun fd open without reading so the backlog persists; a stab
> # overhead of 1048468 accounts each 92-byte packet at 1048560 bytes, so
> # 4095 queued packets leave 65519 bytes below QDISC_MAX_BACKLOG. A
> # 65000-byte UDP_SEGMENT payload with a 1400-byte segment size then has
> # a segment sum past the remaining headroom, and cake rejects the whole
> # split. Setup needs CAP_NET_ADMIN; the payload itself does not.
> tc qdisc add dev tun0 root handle 1: stab overhead 1048468 \
> cake split-gso memlimit 512Mb
[Severity: Low]
Does this recipe show a failure on the tree just before this patch?
With a stab overhead of 1048468, each 92-byte packet is accounted at
1048560 bytes. After 4095 packets the backlog is 4293853200.
For the 65000-byte UDP_SEGMENT skb, __qdisc_calculate_pkt_len() clamps
qdisc_pkt_len() to QDISC_PKT_LEN_MAX. qdisc_backlog_overflow() then sees
4293853200 <= QDISC_MAX_BACKLOG and admits the skb.
The 47 segments add up to 65000 + 47 * 28 = 66316 bytes. The old split
path would take the backlog to 4293919516, which is still about 1 MiB
below U32_MAX. After that, qdisc_backlog_overflow() refuses every later
packet.
So on an unfixed kernel nothing wraps, and the recipe only reaches the new
rejection. The message also doesn't say what goes wrong without the new
check. Presumably it is the u32 sch->qstats.backlog, b->tin_backlog and
b->backlogs[idx] wrapping. That needs a segment sum larger than the 1 MiB
headroom.
The subject and the opening paragraph also present "hash before
segmenting" as the fix. But the removed lines in cake_enqueue() already
call cake_hash() before skb_gso_segment(). Isn't the actual change that
the set-associative resolution and the shaper-state refresh now run only
after admission?
Finally, "tagging only the head left the rest with GSO scratch" seems to
describe an earlier revision of this patch. The code being replaced had
no rejection path and did not tag any segment.
> Link: https://lore.kernel.org/netdev/179269577270.3135.16072065138845192450@kernel.org/
> Reviewed-by: Victor Nogueira <victor@mojatatu.com>
> Acked-by: Toke Høiland-Jørgensen <toke@toke.dk>
> Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
[Severity: Low]
The message calls this "The Fix:", but there is no Fixes: tag. This patch
fixes three things:
- state committed before the drop when skb_gso_segment() fails
- the wrong drop reason for that drop
- no bound on the split segment sum
All of these appear to come from the original split-GSO code. Would
something like this be appropriate?
Fixes: 0c850344d388 ("sch_cake: Conditionally split GSO segments")
The hash-before-split ordering comes from 046f6fd5daef ("sched: Add Common
Applications Kept Enhanced (cake) qdisc").
Stable backporters would also need to know that this patch depends on
QDISC_MAX_BACKLOG and qdisc_backlog_overflow() from the previous patch in
the series.
> diff --git a/net/sched/sch_cake.c b/net/sched/sch_cake.c
> index 3fd13bf6f3cec..74e2b09971eea 100644
> --- a/net/sched/sch_cake.c
> +++ b/net/sched/sch_cake.c
[ ... ]
> @@ -1788,12 +1815,50 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
[ ... ]
> b = cake_select_tin(sch, skb);
> - idx = cake_hash(b, skb, q->config->flow_mode, flow_override,
> - host_override) + 1;
> + cake_hash_keys(skb, flow_mode, flow_override, host_override, &keys);
> +
> + if (qdisc_pkt_segs(skb) > 1 && q->config->rate_flags & CAKE_FLAG_SPLIT_GSO) {
> + netdev_features_t features = netif_skb_features(skb);
> + struct sk_buff *nskb, *seg;
> +
> + segs = skb_gso_segment(skb, features & ~NETIF_F_GSO_MASK);
> + if (IS_ERR_OR_NULL(segs))
> + return qdisc_drop_reason(skb, sch, to_free,
> + QDISC_DROP_GENERIC);
> +
> + /* we never use the original skb after splitting */
> + consume_skb(skb);
> + skb = NULL;
> +
> + /* The split path accounts the segment sum, not the
> + * stab-adjusted qdisc_pkt_len(), so check it here.
> + */
> + skb_list_walk_safe(segs, seg, nskb)
> + slen += seg->len;
> +
> + if (unlikely((u64)sch->qstats.backlog + slen > QDISC_MAX_BACKLOG)) {
[Severity: Low]
Is QDISC_MAX_BACKLOG the right bound for this check? The header defines it
as a limit on the backlog before the add:
include/net/sch_generic.h:
/* Largest backlog that still has room for one more 1 MiB packet. */
#define QDISC_MAX_BACKLOG (U32_MAX - QDISC_PKT_LEN_MAX)
qdisc_backlog_overflow() rejects only when the add would actually wrap:
if (likely(len <= QDISC_PKT_LEN_MAX))
return backlog > QDISC_MAX_BACKLOG;
return len > U32_MAX - backlog;
Here the backlog after the add is compared against that constant. Take a
split GSO skb that has just passed qdisc_backlog_overflow() with a stab
clamping qdisc_pkt_len() to 1 MiB. If the backlog is in
(QDISC_MAX_BACKLOG - slen, QDISC_MAX_BACKLOG], the skb is dropped as
QDISC_DROP_OVERLIMIT even though backlog + slen would not wrap. Small
packets are still admitted in that window.
The recipe in the commit message is such a case: 4293919516 is about 1 MiB
below U32_MAX.
Would something like slen > U32_MAX - sch->qstats.backlog match the
helper's semantics?
> + qdisc_qstats_overlimit(sch);
[ ... ]
> @@ -1958,8 +2016,8 @@ static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
> WRITE_ONCE(b->sparse_flow_count, b->sparse_flow_count - 1);
> WRITE_ONCE(b->bulk_flow_count, b->bulk_flow_count + 1);
>
> - cake_inc_srchost_bulk_flow_count(b, flow, q->config->flow_mode);
> - cake_inc_dsthost_bulk_flow_count(b, flow, q->config->flow_mode);
> + cake_inc_srchost_bulk_flow_count(b, flow, flow_mode);
> + cake_inc_dsthost_bulk_flow_count(b, flow, flow_mode);
[Severity: Low]
This isn't a bug introduced by this patch, but cake_dequeue() still reads
q->config->flow_mode with a separate plain load at each call:
net/sched/sch_cake.c:cake_dequeue() {
...
cake_inc_srchost_bulk_flow_count(b, flow, q->config->flow_mode);
cake_inc_dsthost_bulk_flow_count(b, flow, q->config->flow_mode);
...
flow->deficit + cake_get_flow_quantum(b, flow, q->config->flow_mode));
...
cake_dec_srchost_bulk_flow_count(b, flow, q->config->flow_mode);
cake_dec_dsthost_bulk_flow_count(b, flow, q->config->flow_mode);
...
}
With cake_mq, cake_mq_change()->cake_config_change() does
WRITE_ONCE(q->flow_mode, flow_mode) on the shared config before any child
sch_tree_lock() is taken.
Can a reconfiguration during dequeue make the src and dst bulk-count
updates for the same flow use different modes? The plain reads racing
with that WRITE_ONCE() also look like something KCSAN would report.
Indexing stays in bounds, because the inc helpers cap at CAKE_QUEUES and
the dec helpers check for zero.
Should cake_dequeue() use the same READ_ONCE() snapshot that this patch
adds to cake_enqueue()?
> }
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-BA27.v9.r0.20261108195325%40mojatatu.com
^ permalink raw reply [flat|nested] 9+ messages in thread