net/sched: Fixes for sch_ingress and sch_clsact
Subsystems (3)
v4 cover net/sched: Fixes for sch_ingress and sch_clsact
Link to v3 (incomplete): https://lore.kernel.org/r/[email protected]/
Link to v2: https://lore.kernel.org/r/[email protected]/
Link to v1: https://lore.kernel.org/r/[email protected]/
Hi all,
These are v4 fixes for ingress and clsact Qdiscs.
Change since v2:
- add in-body From: tags
Changes in v2:
- for [1-5/6], include tags from Jamal and Pedro
- for [6/6], as suggested by Vlad, replay the request if the current Qdisc has any ongoing (RTNL-unlocked) filter requests, instead of returning -EBUSY to the user
- use Closes: tag as warned by checkpatch
[1,2/6]: ingress and clsact Qdiscs should only be created under ffff:fff1
[3/6]: Under ffff:fff1, only create ingress and clsact Qdiscs (for now,
at least)
[4/6]: After creating ingress and clsact Qdiscs under ffff:fff1, do not
graft them again to anywhere else (e.g. as the inner Qdisc of a TBF Qdisc)
[5/6]: Prepare for [6/6], do not reuse that for-loop in qdisc_graft()
for ingress and clsact Qdiscs
[6/6]: Fix use-after-free [a] in mini_qdisc_pair_swap()
[a] https://syzkaller.appspot.com/bug?extid=b53a9c0d1ea4ad62da8b
Thanks,
Peilin Ye (6):
net/sched: sch_ingress: Only create under TC_H_INGRESS net/sched: sch_clsact: Only create under TC_H_CLSACT net/sched: Reserve TC_H_INGRESS (TC_H_CLSACT) for ingress (clsact)
Qdiscs
net/sched: Prohibit regrafting ingress or clsact Qdiscs net/sched: Refactor qdisc_graft() for ingress and clsact Qdiscs net/sched: qdisc_destroy() old ingress and clsact Qdiscs before
grafting
include/net/sch_generic.h | 8 ++++++ net/sched/sch_api.c | 60 +++++++++++++++++++++++++++++---------- net/sched/sch_generic.c | 14 +++++++-- net/sched/sch_ingress.c | 10 +++++-- 4 files changed, 72 insertions(+), 20 deletions(-)
Signature · 1 line
2.20.1
PATCH v4 1/6 net/sched: sch_ingress: Only create under TC_H_INGRESS
From: Peilin Ye <[email protected]>
ingress Qdiscs are only supposed to be created under TC_H_INGRESS. Similar to mq_init(), return -EOPNOTSUPP if 'parent' is not TC_H_INGRESS.
---
change since v2:
- add in-body From: tag
net/sched/sch_ingress.c | 3 +++ 1 file changed, 3 insertions(+)
net/sched/sch_ingress.c +3 -0
@@ -80,6 +80,9 @@ static int ingress_init(struct Qdisc *sch, struct nlattr *opt,struct net_device *dev =qdisc_dev (sch);int err; +if (sch->parent !=TC_H_INGRESS ) +return -EOPNOTSUPP ; +net_inc_ingress_queue ();mini_qdisc_pair_init (&q->miniqp, sch, &dev->miniq_ingress);
Signature · 1 line
2.20.1
PATCH v4 2/6 net/sched: sch_clsact: Only create under TC_H_CLSACT
In reply to Peilin YeFrom: Peilin Ye <[email protected]>
clsact Qdiscs are only supposed to be created under TC_H_CLSACT (which equals TC_H_INGRESS). Return -EOPNOTSUPP if 'parent' is not TC_H_CLSACT.
---
change since v2:
- add in-body From: tag
net/sched/sch_ingress.c | 3 +++ 1 file changed, 3 insertions(+)
net/sched/sch_ingress.c +3 -0
@@ -222,6 +222,9 @@ static int clsact_init(struct Qdisc *sch, struct nlattr *opt,struct net_device *dev =qdisc_dev (sch);int err; +if (sch->parent !=TC_H_CLSACT ) +return -EOPNOTSUPP ; +net_inc_ingress_queue ();net_inc_egress_queue ();
Signature · 1 line
2.20.1
PATCH v4 3/6 net/sched: Reserve TC_H_INGRESS (TC_H_CLSACT) for ingress (clsact) Qdiscs
In reply to Peilin YeFrom: Peilin Ye <[email protected]>
Currently it is possible to add e.g. an HTB Qdisc under ffff:fff1 (TC_H_INGRESS, TC_H_CLSACT):
$ ip link add name ifb0 type ifb $ tc qdisc add dev ifb0 parent ffff:fff1 htb $ tc qdisc add dev ifb0 clsact Error: Exclusivity flag on, cannot modify. $ drgn ...
Quoted 3 lines
Quoted 3 lines
Quoted 3 lines
ifb0 =
netdev_get_by_name(prog, "ifb0")
qdisc = ifb0.ingress_queue.qdisc_sleeping
print(qdisc.ops.id.string_().decode())
htb
Quoted 1 line
Quoted 1 line
Quoted 1 line
qdisc.flags.value_()#TCQ_F_INGRESS
2
Only allow ingress and clsact Qdiscs under ffff:fff1. Return -EINVAL for everything else. Make TCQ_F_INGRESS a static flag of ingress and clsact Qdiscs.
---
change since v2:
- add in-body From: tag
net/sched/sch_api.c | 7 ++++++- net/sched/sch_ingress.c | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-)
net/sched/sch_api.c +6 -1
@@ -1252,7 +1252,12 @@ static struct Qdisc *qdisc_create(struct net_device *dev, sch->parent = parent;if (handle ==TC_H_INGRESS ) { - sch->flags |=TCQ_F_INGRESS ; +if (!(sch->flags &TCQ_F_INGRESS )) { +NL_SET_ERR_MSG (extack, +"Specified parent ID is reserved for ingress and clsact Qdiscs" ); + err = -EINVAL ; + goto err_out3; + } handle =TC_H_MAKE (TC_H_INGRESS ,0 ); }else {if (handle ==0 ) {
net/sched/sch_ingress.c +2 -2
@@ -137,7 +137,7 @@ static struct Qdisc_ops ingress_qdisc_ops __read_mostly = { .cl_ops = &ingress_class_ops, .id = "ingress", .priv_size = sizeof(struct ingress_sched_data), - .static_flags = TCQ_F_CPUSTATS, + .static_flags = TCQ_F_INGRESS | TCQ_F_CPUSTATS, .init = ingress_init, .destroy = ingress_destroy, .dump = ingress_dump,
@@ -275,7 +275,7 @@ static struct Qdisc_ops clsact_qdisc_ops __read_mostly = { .cl_ops = &clsact_class_ops, .id = "clsact", .priv_size = sizeof(struct clsact_sched_data), - .static_flags = TCQ_F_CPUSTATS, + .static_flags = TCQ_F_INGRESS | TCQ_F_CPUSTATS, .init = clsact_init, .destroy = clsact_destroy, .dump = ingress_dump,
Signature · 1 line
2.20.1
PATCH v4 4/6 net/sched: Prohibit regrafting ingress or clsact Qdiscs
In reply to Peilin YeFrom: Peilin Ye <[email protected]>
Currently, after creating an ingress (or clsact) Qdisc and grafting it under TC_H_INGRESS (TC_H_CLSACT), it is possible to graft it again under e.g. a TBF Qdisc:
$ ip link add ifb0 type ifb
$ tc qdisc add dev ifb0 handle 1: root tbf rate 20kbit buffer 1600 limit 3000
$ tc qdisc add dev ifb0 clsact
$ tc qdisc link dev ifb0 handle ffff: parent 1:1
$ tc qdisc show dev ifb0
qdisc tbf 1: root refcnt 2 rate 20Kbit burst 1600b lat 560.0ms
qdisc clsact ffff: parent ffff:fff1 refcnt 2
^^^^^^^^clsact's refcount has increased: it is now grafted under both TC_H_CLSACT and 1:1.
ingress and clsact Qdiscs should only be used under TC_H_INGRESS (TC_H_CLSACT). Prohibit regrafting them.
---
change since v2:
- add in-body From: tag
net/sched/sch_api.c | 5 +++++ 1 file changed, 5 insertions(+)
net/sched/sch_api.c +5 -0
@@ -1596,6 +1596,11 @@ static int tc_modify_qdisc(struct sk_buff *skb, struct nlmsghdr *n,NL_SET_ERR_MSG (extack,"Invalid qdisc name" );return -EINVAL ; } +if (q->flags &TCQ_F_INGRESS ) { +NL_SET_ERR_MSG (extack, +"Cannot regraft ingress or clsact Qdiscs" ); +return -EINVAL ; + }if (q == p || (p &&check_loop (q, p,0 ))) {NL_SET_ERR_MSG (extack,"Qdisc parent/child loop detected" );
Signature · 1 line
2.20.1
PATCH v4 5/6 net/sched: Refactor qdisc_graft() for ingress and clsact Qdiscs
In reply to Peilin YeFrom: Peilin Ye <[email protected]>
Grafting ingress and clsact Qdiscs does not need a for-loop in qdisc_graft(). Refactor it. No functional changes intended.
---
change since v2:
- add in-body From: tag
net/sched/sch_api.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-)
net/sched/sch_api.c +10 -10
@@ -1073,12 +1073,12 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (parent ==NULL ) {unsigned int i, num_q, ingress; +struct netdev_queue *dev_queue; ingress =0 ; num_q = dev->num_tx_queues;if ((q && q->flags &TCQ_F_INGRESS ) || (new && new->flags &TCQ_F_INGRESS )) { - num_q =1 ; ingress =1 ;if (!dev_ingress_queue (dev)) {NL_SET_ERR_MSG (extack,"Device does not have an ingress queue" );
@@ -1094,18 +1094,18 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (new && new->ops->attach && !ingress) goto skip; -for (i =0 ; i < num_q; i++) { -struct netdev_queue *dev_queue =dev_ingress_queue (dev); - -if (!ingress) +if (!ingress) { +for (i =0 ; i < num_q; i++) { dev_queue =netdev_get_tx_queue (dev, i); + old =dev_graft_qdisc (dev_queue, new); - old =dev_graft_qdisc (dev_queue, new); -if (new && i >0 ) -qdisc_refcount_inc (new); - -if (!ingress) +if (new && i >0 ) +qdisc_refcount_inc (new);qdisc_put (old); + } + }else { + dev_queue =dev_ingress_queue (dev); + old =dev_graft_qdisc (dev_queue, new); } skip:
Signature · 1 line
2.20.1
PATCH v4 6/6 net/sched: qdisc_destroy() old ingress and clsact Qdiscs before grafting
In reply to Peilin YeFrom: Peilin Ye <[email protected]>
mini_Qdisc_pair::p_miniq is a double pointer to mini_Qdisc, initialized in ingress_init() to point to net_device::miniq_ingress. ingress Qdiscs access this per-net_device pointer in mini_qdisc_pair_swap(). Similar for clsact Qdiscs and miniq_egress.
Unfortunately, after introducing RTNL-unlocked RTM_{NEW,DEL,GET}TFILTER requests (thanks Hillf Danton for the hint), when replacing ingress or clsact Qdiscs, for example, the old Qdisc ("@old") could access the same miniq_{in,e}gress pointer(s) concurrently with the new Qdisc ("@new"), causing race conditions [1] including a use-after-free bug in mini_qdisc_pair_swap() reported by syzbot:
BUG: KASAN: slab-use-after-free in mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 Write of size 8 at addr ffff888045b31308 by task syz-executor690/14901 ... Call Trace: <TASK> __dump_stack lib/dump_stack.c:88 [inline] dump_stack_lvl+0xd9/0x150 lib/dump_stack.c:106 print_address_description.constprop.0+0x2c/0x3c0 mm/kasan/report.c:319 print_report mm/kasan/report.c:430 [inline] kasan_report+0x11c/0x130 mm/kasan/report.c:536 mini_qdisc_pair_swap+0x1c2/0x1f0 net/sched/sch_generic.c:1573 tcf_chain_head_change_item net/sched/cls_api.c:495 [inline] tcf_chain0_head_change.isra.0+0xb9/0x120 net/sched/cls_api.c:509 tcf_chain_tp_insert net/sched/cls_api.c:1826 [inline] tcf_chain_tp_insert_unique net/sched/cls_api.c:1875 [inline] tc_new_tfilter+0x1de6/0x2290 net/sched/cls_api.c:2266
...
@old and @new should not affect each other. In other words, @old should never modify miniq_{in,e}gress after @new, and @new should not update @old's RCU state. Fixing without changing sch_api.c turned out to be difficult (please refer to Closes: for discussions). Instead, make sure @new's first call always happen after @old's last call, in qdisc_destroy(), has finished:
In qdisc_graft(), return -EAGAIN and tell the caller to replay (suggested by Vlad Buslov) if @old has any ongoing RTNL-unlocked filter requests, and call qdisc_destroy() for @old before grafting @new.
Introduce qdisc_refcount_dec_if_one() as the counterpart of qdisc_refcount_inc_nz() used for RTNL-unlocked filter requests. Introduce a non-static version of qdisc_destroy() that does a TCQ_F_BUILTIN check, just like qdisc_put() etc.
Depends on patch "net/sched: Refactor qdisc_graft() for ingress and clsact Qdiscs".
[1] To illustrate, the syzkaller reproducer adds ingress Qdiscs under TC_H_ROOT (no longer possible after patch "net/sched: sch_ingress: Only create under TC_H_INGRESS") on eth0 that has 8 transmission queues:
Thread 1 creates ingress Qdisc A (containing mini Qdisc a1 and a2), then adds a flower filter X to A.
Thread 2 creates another ingress Qdisc B (containing mini Qdisc b1 and b2) to replace A, then adds a flower filter Y to B.
Thread 1 A's refcnt Thread 2
RTM_NEWQDISC (A, RTNL-locked)
qdisc_create(A) 1
qdisc_graft(A) 9
RTM_NEWTFILTER (X, RTNL-unlocked)
__tcf_qdisc_find(A) 10
tcf_chain0_head_change(A)
mini_qdisc_pair_swap(A) (1st)
|
| RTM_NEWQDISC (B, RTNL-locked)
RCU sync 2 qdisc_graft(B)
| 1 notify_and_destroy(A)
|
tcf_block_release(A) 0 RTM_NEWTFILTER (Y, RTNL-unlocked)
qdisc_destroy(A) tcf_chain0_head_change(B)
tcf_chain0_head_change_cb_del(A) mini_qdisc_pair_swap(B) (2nd)
mini_qdisc_pair_swap(A) (3rd) |
... ...
Here, B calls mini_qdisc_pair_swap(), pointing eth0->miniq_ingress to its mini Qdisc, b1. Then, A calls mini_qdisc_pair_swap() again during ingress_destroy(), setting eth0->miniq_ingress to NULL, so ingress packets on eth0 will not find filter Y in sch_handle_ingress().
This is only one of the possible consequences of concurrently accessing miniq_{in,e}gress pointers. The point is clear though: again, A should never modify those per-net_device pointers after B, and B should not update A's RCU state.
---
change since v2:
- add in-body From: tag
changes in v2:
- replay the request if the current Qdisc has any ongoing RTNL-unlocked filter requests (Vlad)
- minor changes in code comments and commit log
include/net/sch_generic.h | 8 ++++++++ net/sched/sch_api.c | 32 ++++++++++++++++++++++++++------ net/sched/sch_generic.c | 14 +++++++++++--- 3 files changed, 45 insertions(+), 9 deletions(-)
include/net/sch_generic.h +8 -0
@@ -137,6 +137,13 @@ static inline void qdisc_refcount_inc(struct Qdisc *qdisc)refcount_inc (&qdisc->refcnt); } +static inline bool qdisc_refcount_dec_if_one (struct Qdisc *qdisc) +{ +if (qdisc->flags &TCQ_F_BUILTIN ) +return true; +return refcount_dec_if_one (&qdisc->refcnt); +} +/* Intended to be used by unlocked users, when concurrent qdisc release is * possible. */
@@ -652,6 +659,7 @@ void dev_deactivate_many(struct list_head *head);struct Qdisc *dev_graft_qdisc (struct netdev_queue *dev_queue,struct Qdisc *qdisc);void qdisc_reset (struct Qdisc *qdisc); +void qdisc_destroy (struct Qdisc *qdisc);void qdisc_put (struct Qdisc *qdisc);void qdisc_put_unlocked (struct Qdisc *qdisc);void qdisc_tree_reduce_backlog (struct Qdisc *qdisc,int n,int len);
net/sched/sch_api.c +26 -6
@@ -1080,10 +1080,18 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if ((q && q->flags &TCQ_F_INGRESS ) || (new && new->flags &TCQ_F_INGRESS )) { ingress =1 ; -if (!dev_ingress_queue (dev)) { + dev_queue =dev_ingress_queue (dev); +if (!dev_queue) {NL_SET_ERR_MSG (extack,"Device does not have an ingress queue" );return -ENOENT ; } + +/* Replay if the current ingress (or clsact) Qdisc has ongoing +* RTNL-unlocked filter request(s). This is the counterpart of that +* qdisc_refcount_inc_nz() call in __tcf_qdisc_find(). +*/ +if (!qdisc_refcount_dec_if_one (dev_queue->qdisc_sleeping)) +return -EAGAIN ; }if (dev->flags &IFF_UP )
@@ -1104,8 +1112,16 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,qdisc_put (old); } }else { - dev_queue =dev_ingress_queue (dev); - old =dev_graft_qdisc (dev_queue, new); + old =dev_graft_qdisc (dev_queue,NULL ); + +/* {ingress,clsact}_destroy() @old before grafting @new to avoid +* unprotected concurrent accesses to net_device::miniq_{in,e}gress +* pointer(s) in mini_qdisc_pair_swap(). +*/ +qdisc_notify (net, skb, n, classid, old, new, extack); +qdisc_destroy (old); + +dev_graft_qdisc (dev_queue, new); } skip:
@@ -1119,8 +1135,6 @@ static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,if (new && new->ops->attach) new->ops->attach (new); - }else { -notify_and_destroy (net, skb, n, classid, old, new, extack); }if (dev->flags &IFF_UP )
@@ -1458,6 +1472,7 @@ static int tc_get_qdisc(struct sk_buff *skb, struct nlmsghdr *n,struct Qdisc *p =NULL ;int err; +replay: err =nlmsg_parse_deprecated (n,sizeof (*tcm), tca,TCA_MAX , rtm_tca_policy, extack);if (err <0 )
@@ -1515,8 +1530,11 @@ static int tc_get_qdisc(struct sk_buff *skb, struct nlmsghdr *n,return -ENOENT ; } err =qdisc_graft (dev, p, skb, n, clid,NULL , q, extack); -if (err !=0 ) +if (err !=0 ) { +if (err == -EAGAIN ) + goto replay;return err; + } }else {qdisc_notify (net, skb, n, clid,NULL , q,NULL ); }
@@ -1704,6 +1722,8 @@ static int tc_modify_qdisc(struct sk_buff *skb, struct nlmsghdr *n,if (err) {if (q)qdisc_put (q); +if (err == -EAGAIN ) + goto replay;return err; }
net/sched/sch_generic.c +11 -3
@@ -1046,7 +1046,7 @@ static void qdisc_free_cb(struct rcu_head *head)qdisc_free (q); } -static void qdisc_destroy (struct Qdisc *qdisc) +static void __qdisc_destroy (struct Qdisc *qdisc) {const struct Qdisc_ops *ops = qdisc->ops;
@@ -1070,6 +1070,14 @@ static void qdisc_destroy(struct Qdisc *qdisc)call_rcu (&qdisc->rcu, qdisc_free_cb); } +void qdisc_destroy (struct Qdisc *qdisc) +{ +if (qdisc->flags &TCQ_F_BUILTIN ) +return ; + +__qdisc_destroy (qdisc); +} +void qdisc_put (struct Qdisc *qdisc) {if (!qdisc)
@@ -1079,7 +1087,7 @@ void qdisc_put(struct Qdisc *qdisc) !refcount_dec_and_test(&qdisc->refcnt))return ; -qdisc_destroy (qdisc); +__qdisc_destroy (qdisc); }EXPORT_SYMBOL (qdisc_put);
@@ -1094,7 +1102,7 @@ void qdisc_put_unlocked(struct Qdisc *qdisc) !refcount_dec_and_rtnl_lock(&qdisc->refcnt)) return; -qdisc_destroy (qdisc); +__qdisc_destroy (qdisc);rtnl_unlock (); }EXPORT_SYMBOL (qdisc_put_unlocked);
Signature · 1 line
2.20.1
Quoted 9 lines · Peilin Ye
[1,2/6]: ingress and clsact Qdiscs should only be created under ffff:fff1
[3/6]: Under ffff:fff1, only create ingress and clsact Qdiscs (for now,
at least)
[4/6]: After creating ingress and clsact Qdiscs under ffff:fff1, do not
graft them again to anywhere else (e.g. as the inner Qdisc of a TBF Qdisc)
[5/6]: Prepare for [6/6], do not reuse that for-loop inqdisc_graft()
for ingress and clsact Qdiscs
[6/6]: Fix use-after-free [a] inmini_qdisc_pair_swap()
In v5, I'll improve [6/6] according to Vlad's suggestion [a], and fix [1,2/6] according to Pedro's report [b].
[a] https://lore.kernel.org/r/[email protected]/
[b] https://lore.kernel.org/r/[email protected]/
Thanks,
Peilin Ye