]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
net/sched: Handle TC_ACT_REDIRECT from qdisc filter chains
authorJamal Hadi Salim <jhs@mojatatu.com>
Mon, 6 Jul 2026 18:56:08 +0000 (20:56 +0200)
committerJakub Kicinski <kuba@kernel.org>
Tue, 21 Jul 2026 01:16:43 +0000 (18:16 -0700)
When a TC filter attached to a qdisc filter chain returns
TC_ACT_REDIRECT (ex: via an eBPF program calling bpf_redirect() or an
act_bpf action), the redirect was silently lost i.e no qdisc classify
function handled TC_ACT_REDIRECT, so the packet fell through the
switch and was enqueued normally instead of being redirected.

This has been broken since bpf_redirect() was introduced for TC in
commit 27b29f63058d ("bpf: add bpf_redirect() helper"). We got lucky
for a long time because bpf_net_context was a per-CPU variable that
was always available.

commit 401cb7dae813 ("net: Reference bpf_redirect_info via task_struct
on PREEMPT_RT.") turned bpf_net_context into a task_struct member that
is only set up by explicit callers. Without a caller setting it up,
bpf_redirect() itself crashes with a NULL pointer dereference in
bpf_net_ctx_get_ri(). However, even with bpf_net_context available,
TC_ACT_REDIRECT from qdisc filter chains cannot be honored without
adding skb_do_redirect() calls to every qdisc classify function, which
would require changes across net/sched/. Isolate it to ebpf core where
it belongs.

Instead, add a tcf_classify_qdisc() inline helper in pkt_cls.h, as a
wrapper around tcf_classify() for use by qdisc classify functions and
tcf_qevent_handle(). When the classify verdict is TC_ACT_REDIRECT,
the wrapper converts it to TC_ACT_SHOT, dropping the packet rather
than letting it continue silently. Dropping is preferred over
letting the packet through because the user immediately sees packet
loss. Silently passing the packet through would hide the problem and
leave the user wondering why their redirect is not working.

The clsact fast path, tc_run() continues to call tcf_classify() directly
and is unaffected: TC_ACT_REDIRECT is returned as-is and handled by
sch_handle_egress/ingress() calling skb_do_redirect() as before.

Fixes: 27b29f63058d ("bpf: add bpf_redirect() helper")
Fixes: 401cb7dae813 ("net: Reference bpf_redirect_info via task_struct on PREEMPT_RT.")
Tested-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Reviewed-by: Sebastian Andrzej Siewior <bigeasy@linutronix.de>
Link: https://patch.msgid.link/20260706185609.330006-3-daniel@iogearbox.net
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
15 files changed:
include/net/pkt_cls.h
net/sched/cls_api.c
net/sched/sch_cake.c
net/sched/sch_drr.c
net/sched/sch_dualpi2.c
net/sched/sch_ets.c
net/sched/sch_fq_codel.c
net/sched/sch_fq_pie.c
net/sched/sch_hfsc.c
net/sched/sch_htb.c
net/sched/sch_multiq.c
net/sched/sch_prio.c
net/sched/sch_qfq.c
net/sched/sch_sfb.c
net/sched/sch_sfq.c

index 3bd08d7f39c18c0b45b389d75963ecd14b5f9730..5f5cb36439fe26d940632e845d11e1d64b0b8df9 100644 (file)
@@ -156,8 +156,20 @@ static inline int tcf_classify(struct sk_buff *skb,
 {
        return TC_ACT_UNSPEC;
 }
-
 #endif
+static inline int tcf_classify_qdisc(struct sk_buff *skb,
+                                    const struct tcf_proto *tp,
+                                    struct tcf_result *res, bool compat_mode)
+{
+       int ret = tcf_classify(skb, NULL, tp, res, compat_mode);
+
+       /* TC_ACT_REDIRECT from qdisc filter chains is not supported.
+        * Use BPF via tcx or mirred redirect instead.
+        */
+       if (unlikely(ret == TC_ACT_REDIRECT))
+               ret = TC_ACT_SHOT;
+       return ret;
+}
 
 static inline unsigned long
 __cls_set_class(unsigned long *clp, unsigned long cl)
index 523cf2a8bd1d6d1257bf7110709be89f0b5daec3..fee4524adc98efabcbccf5f9d0ad4e2c313ac217 100644 (file)
@@ -4045,9 +4045,7 @@ struct sk_buff *tcf_qevent_handle(struct tcf_qevent *qe, struct Qdisc *sch, stru
 
        fl = rcu_dereference_bh(qe->filter_chain);
 
-       switch (tcf_classify(skb, NULL, fl, &cl_res, false)) {
-       case TC_ACT_REDIRECT:
-               fallthrough;
+       switch (tcf_classify_qdisc(skb, fl, &cl_res, false)) {
        case TC_ACT_SHOT:
                qdisc_qstats_drop(sch);
                __qdisc_drop(skb, to_free);
index f78f8e9507766703306da1e025f7fa21a44346d3..505f63fecf64050efbd9de4c0f2f5ed5b8443fc4 100644 (file)
@@ -1727,7 +1727,7 @@ static u32 cake_classify(struct Qdisc *sch, struct cake_tin_data **t,
                goto hash;
 
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
-       result = tcf_classify(skb, NULL, filter, &res, false);
+       result = tcf_classify_qdisc(skb, filter, &res, false);
 
        if (result >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
index 020657f959b575667f7a4b4d6aacd05c03c654a0..91b1ef824afaf29e3f9821ce99b9217f6923990a 100644 (file)
@@ -312,7 +312,7 @@ static struct drr_class *drr_classify(struct sk_buff *skb, struct Qdisc *sch,
 
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
        fl = rcu_dereference_bh(q->filter_list);
-       result = tcf_classify(skb, NULL, fl, &res, false);
+       result = tcf_classify_qdisc(skb, fl, &res, false);
        if (result >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
                switch (result) {
index 27088760eff4431ada466226269ac5e0dbf6c0a6..4f678d4ff10ec73789d2d2c5f848015b65a7699e 100644 (file)
@@ -364,7 +364,7 @@ static int dualpi2_skb_classify(struct dualpi2_sched_data *q,
        if (!fl)
                return NET_XMIT_SUCCESS;
 
-       result = tcf_classify(skb, NULL, fl, &res, false);
+       result = tcf_classify_qdisc(skb, fl, &res, false);
        if (result >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
                switch (result) {
index cb8cf437ce87086c9eedf777dc7610137d5e09a5..25fcf4079fece1639ed88db58683dc6c68f041c0 100644 (file)
@@ -391,7 +391,7 @@ static struct ets_class *ets_classify(struct sk_buff *skb, struct Qdisc *sch,
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
        if (TC_H_MAJ(skb->priority) != sch->handle) {
                fl = rcu_dereference_bh(q->filter_list);
-               err = tcf_classify(skb, NULL, fl, &res, false);
+               err = tcf_classify_qdisc(skb, fl, &res, false);
 #ifdef CONFIG_NET_CLS_ACT
                switch (err) {
                case TC_ACT_STOLEN:
index cafd1f943d99cd368fdbdb84d069027969decdd6..6cce86ba383cf68137010fd21f375c54726e15e4 100644 (file)
@@ -91,7 +91,7 @@ static unsigned int fq_codel_classify(struct sk_buff *skb, struct Qdisc *sch,
                return fq_codel_hash(q, skb) + 1;
 
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
-       result = tcf_classify(skb, NULL, filter, &res, false);
+       result = tcf_classify_qdisc(skb, filter, &res, false);
        if (result >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
                switch (result) {
index 72f48fa4010bebbe6be212938b457db21ff3c5a0..069e1facd4130dcd3586f0cc2b114550ddb44c12 100644 (file)
@@ -96,7 +96,7 @@ static unsigned int fq_pie_classify(struct sk_buff *skb, struct Qdisc *sch,
                return fq_pie_hash(q, skb) + 1;
 
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
-       result = tcf_classify(skb, NULL, filter, &res, false);
+       result = tcf_classify_qdisc(skb, filter, &res, false);
        if (result >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
                switch (result) {
index 7e537295b8b678812fbbc87a7655751f02e86df8..e87f5021a199584645c74a57864533f122cd5026 100644 (file)
@@ -1143,7 +1143,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
        head = &q->root;
        tcf = rcu_dereference_bh(q->root.filter_list);
-       while (tcf && (result = tcf_classify(skb, NULL, tcf, &res, false)) >= 0) {
+       while (tcf && (result = tcf_classify_qdisc(skb, tcf, &res, false)) >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
                switch (result) {
                case TC_ACT_QUEUED:
index 908b9ba9ba2efa8df396b51d437a714afc802a73..fdac0dc8f35a3783803b28b6e3b9371e4daaced1 100644 (file)
@@ -243,7 +243,7 @@ static struct htb_class *htb_classify(struct sk_buff *skb, struct Qdisc *sch,
        }
 
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
-       while (tcf && (result = tcf_classify(skb, NULL, tcf, &res, false)) >= 0) {
+       while (tcf && (result = tcf_classify_qdisc(skb, tcf, &res, false)) >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
                switch (result) {
                case TC_ACT_QUEUED:
index a467dd1223697091b8447097c497306fbdb8ca8b..66df30939aa5c1aaf3596681f284273d7219f35f 100644 (file)
@@ -36,7 +36,7 @@ multiq_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
        int err;
 
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
-       err = tcf_classify(skb, NULL, fl, &res, false);
+       err = tcf_classify_qdisc(skb, fl, &res, false);
 #ifdef CONFIG_NET_CLS_ACT
        switch (err) {
        case TC_ACT_STOLEN:
index e4dd56a890725b4c14d6715c96f5b3fa44a8f4f2..79437c587e7ed2e02b73bd6d20a446fb2f4b7516 100644 (file)
@@ -39,7 +39,7 @@ prio_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
        if (TC_H_MAJ(skb->priority) != sch->handle) {
                fl = rcu_dereference_bh(q->filter_list);
-               err = tcf_classify(skb, NULL, fl, &res, false);
+               err = tcf_classify_qdisc(skb, fl, &res, false);
 #ifdef CONFIG_NET_CLS_ACT
                switch (err) {
                case TC_ACT_STOLEN:
index cb56787e1d258c06f2e86959c3b2cfaeb12df1ac..6f3b7273cb160e1149b0644a7ec92723e29b1728 100644 (file)
@@ -709,7 +709,7 @@ static struct qfq_class *qfq_classify(struct sk_buff *skb, struct Qdisc *sch,
 
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
        fl = rcu_dereference_bh(q->filter_list);
-       result = tcf_classify(skb, NULL, fl, &res, false);
+       result = tcf_classify_qdisc(skb, fl, &res, false);
        if (result >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
                switch (result) {
index b1d46509427692eeeabcfa19957c83fae3fa306e..ed39869199c084ede8a365c1225984c048b98d6b 100644 (file)
@@ -260,7 +260,7 @@ static bool sfb_classify(struct sk_buff *skb, struct tcf_proto *fl,
        struct tcf_result res;
        int result;
 
-       result = tcf_classify(skb, NULL, fl, &res, false);
+       result = tcf_classify_qdisc(skb, fl, &res, false);
        if (result >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
                switch (result) {
index 758b88f218652704454647f25da270a0254cafcf..77675f9a4c46e73765b709a6ec5fa60571a5fe24 100644 (file)
@@ -171,7 +171,7 @@ static unsigned int sfq_classify(struct sk_buff *skb, struct Qdisc *sch,
                return sfq_hash(q, skb) + 1;
 
        *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
-       result = tcf_classify(skb, NULL, fl, &res, false);
+       result = tcf_classify_qdisc(skb, fl, &res, false);
        if (result >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
                switch (result) {