[PATCH net] net/sched: gred: reject table shrink with queued packets

From: shaos

Date: Mon Nov 09 2026 - 11:45:13 EST


GRED frees VQs above the configured DP count when its table is
shrunk. Queued skbs keep their DP number. If the table is later
expanded and that VQ is recreated, dequeue subtracts the old skb
length from the new VQ's zeroed u32 backlog.

Reject a shrink while a removed VQ has accounted backlog or an skb
in gso_skb still carries a removed DP. This keeps the per-VQ
accounting state alive until all packets using it are drained.

The issue was identified by LLM-assisted source analysis and
reproduced with a purpose-built AF_PACKET/tc test in QEMU. The test
requires CAP_NET_ADMIN and CAP_NET_RAW in its network namespace; it
does not modify the kernel or use error injection.

On Linux 7.4-rc2, the unpatched reproducer wrapped VQ5 backlog to
4294964296 bytes and dropped a probe. With this patch, shrinking
returns -EBUSY while VQ5 has queued packets. After the queue drains,
shrinking and re-expanding succeeds, and the probe is accepted.

The modified sch_gred.o builds with W=1 under allmodconfig and
allyesconfig. Full-tree W=1 builds stop on unrelated warnings in
lib/tests/stackinit_kunit.c, arch/x86/kernel/callthunks.c, and
net/8021q/vlan.c. The QEMU regression passes; tc-testing was not run.

Fixes: 6639607ed9de ("[PKT_SCHED]: GRED: Use a central table definition change procedure")
Link: https://lists.openwall.net/linux-kernel/2026/10/22/1545
Signed-off-by: shaos <shawnq1@xxxxxxx>
---
net/sched/sch_gred.c | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/net/sched/sch_gred.c b/net/sched/sch_gred.c
index f3d9a7252949d..8ddbd7f058a6b 100644
--- a/net/sched/sch_gred.c
+++ b/net/sched/sch_gred.c
@@ -408,8 +408,9 @@ static inline void gred_destroy_vq(struct gred_sched_data *q)
static int gred_change_table_def(struct Qdisc *sch, struct nlattr *dps,
struct netlink_ext_ack *extack)
{
- struct gred_sched *table = qdisc_priv(sch);
struct tc_gred_sopt *sopt;
+ struct gred_sched *table = qdisc_priv(sch);
+ struct sk_buff *skb;
bool red_flags_changed;
int i;

@@ -437,6 +438,25 @@ static int gred_change_table_def(struct Qdisc *sch, struct nlattr *dps,
}

sch_tree_lock(sch);
+ skb_queue_walk(&sch->gso_skb, skb) {
+ if (tc_index_to_dp(skb) >= sopt->DPs) {
+ sch_tree_unlock(sch);
+ NL_SET_ERR_MSG_MOD(extack,
+ "cannot remove a GRED VQ with queued packets");
+ return -EBUSY;
+ }
+ }
+
+ for (i = sopt->DPs; i < MAX_DPs; i++) {
+ struct gred_sched_data *q = table->tab[i];
+
+ if (q && q->backlog) {
+ sch_tree_unlock(sch);
+ NL_SET_ERR_MSG_MOD(extack,
+ "cannot remove a GRED VQ with queued packets");
+ return -EBUSY;
+ }
+ }
table->DPs = sopt->DPs;
table->def = sopt->def_DP;
red_flags_changed = table->red_flags != sopt->flags;