Commit 630e0afa authored by Paolo Abeni's avatar Paolo Abeni

Merge branch 'net-sched-improve-class-lifetime-handling'

Pedro Tammela says:

====================
net/sched: improve class lifetime handling

Valis says[0]:
============
Three classifiers (cls_fw, cls_u32 and cls_route) always copy
tcf_result struct into the new instance of the filter on update.

This causes a problem when updating a filter bound to a class,
as tcf_unbind_filter() is always called on the old instance in the
success path, decreasing filter_cnt of the still referenced class
and allowing it to be deleted, leading to a use-after-free.
============

Turns out these could have been spotted easily with proper warnings.
Improve the current class lifetime with wrappers that check for
overflow/underflow.

While at it add an extack for when a class in use is deleted.

[0] https://lore.kernel.org/all/20230721174856.3045-1-sec@valis.email/
====================

Link: https://lore.kernel.org/r/20230728153537.1865379-1-pctammela@mojatatu.comSigned-off-by: default avatarPaolo Abeni <pabeni@redhat.com>
parents 05e1d8bd e20e7501
...@@ -599,6 +599,7 @@ get_default_qdisc_ops(const struct net_device *dev, int ntx) ...@@ -599,6 +599,7 @@ get_default_qdisc_ops(const struct net_device *dev, int ntx)
struct Qdisc_class_common { struct Qdisc_class_common {
u32 classid; u32 classid;
unsigned int filter_cnt;
struct hlist_node hnode; struct hlist_node hnode;
}; };
...@@ -633,6 +634,31 @@ qdisc_class_find(const struct Qdisc_class_hash *hash, u32 id) ...@@ -633,6 +634,31 @@ qdisc_class_find(const struct Qdisc_class_hash *hash, u32 id)
return NULL; return NULL;
} }
static inline bool qdisc_class_in_use(const struct Qdisc_class_common *cl)
{
return cl->filter_cnt > 0;
}
static inline void qdisc_class_get(struct Qdisc_class_common *cl)
{
unsigned int res;
if (check_add_overflow(cl->filter_cnt, 1, &res))
WARN(1, "Qdisc class overflow");
cl->filter_cnt = res;
}
static inline void qdisc_class_put(struct Qdisc_class_common *cl)
{
unsigned int res;
if (check_sub_overflow(cl->filter_cnt, 1, &res))
WARN(1, "Qdisc class underflow");
cl->filter_cnt = res;
}
static inline int tc_classid_to_hwtc(struct net_device *dev, u32 classid) static inline int tc_classid_to_hwtc(struct net_device *dev, u32 classid)
{ {
u32 hwtc = TC_H_MIN(classid) - TC_H_MIN_PRIORITY; u32 hwtc = TC_H_MIN(classid) - TC_H_MIN_PRIORITY;
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
struct drr_class { struct drr_class {
struct Qdisc_class_common common; struct Qdisc_class_common common;
unsigned int filter_cnt;
struct gnet_stats_basic_sync bstats; struct gnet_stats_basic_sync bstats;
struct gnet_stats_queue qstats; struct gnet_stats_queue qstats;
...@@ -150,8 +149,10 @@ static int drr_delete_class(struct Qdisc *sch, unsigned long arg, ...@@ -150,8 +149,10 @@ static int drr_delete_class(struct Qdisc *sch, unsigned long arg,
struct drr_sched *q = qdisc_priv(sch); struct drr_sched *q = qdisc_priv(sch);
struct drr_class *cl = (struct drr_class *)arg; struct drr_class *cl = (struct drr_class *)arg;
if (cl->filter_cnt > 0) if (qdisc_class_in_use(&cl->common)) {
NL_SET_ERR_MSG(extack, "DRR class is in use");
return -EBUSY; return -EBUSY;
}
sch_tree_lock(sch); sch_tree_lock(sch);
...@@ -187,8 +188,8 @@ static unsigned long drr_bind_tcf(struct Qdisc *sch, unsigned long parent, ...@@ -187,8 +188,8 @@ static unsigned long drr_bind_tcf(struct Qdisc *sch, unsigned long parent,
{ {
struct drr_class *cl = drr_find_class(sch, classid); struct drr_class *cl = drr_find_class(sch, classid);
if (cl != NULL) if (cl)
cl->filter_cnt++; qdisc_class_get(&cl->common);
return (unsigned long)cl; return (unsigned long)cl;
} }
...@@ -197,7 +198,7 @@ static void drr_unbind_tcf(struct Qdisc *sch, unsigned long arg) ...@@ -197,7 +198,7 @@ static void drr_unbind_tcf(struct Qdisc *sch, unsigned long arg)
{ {
struct drr_class *cl = (struct drr_class *)arg; struct drr_class *cl = (struct drr_class *)arg;
cl->filter_cnt--; qdisc_class_put(&cl->common);
} }
static int drr_graft_class(struct Qdisc *sch, unsigned long arg, static int drr_graft_class(struct Qdisc *sch, unsigned long arg,
......
...@@ -116,7 +116,6 @@ struct hfsc_class { ...@@ -116,7 +116,6 @@ struct hfsc_class {
struct net_rate_estimator __rcu *rate_est; struct net_rate_estimator __rcu *rate_est;
struct tcf_proto __rcu *filter_list; /* filter list */ struct tcf_proto __rcu *filter_list; /* filter list */
struct tcf_block *block; struct tcf_block *block;
unsigned int filter_cnt; /* filter count */
unsigned int level; /* class level in hierarchy */ unsigned int level; /* class level in hierarchy */
struct hfsc_sched *sched; /* scheduler data */ struct hfsc_sched *sched; /* scheduler data */
...@@ -1094,8 +1093,11 @@ hfsc_delete_class(struct Qdisc *sch, unsigned long arg, ...@@ -1094,8 +1093,11 @@ hfsc_delete_class(struct Qdisc *sch, unsigned long arg,
struct hfsc_sched *q = qdisc_priv(sch); struct hfsc_sched *q = qdisc_priv(sch);
struct hfsc_class *cl = (struct hfsc_class *)arg; struct hfsc_class *cl = (struct hfsc_class *)arg;
if (cl->level > 0 || cl->filter_cnt > 0 || cl == &q->root) if (cl->level > 0 || qdisc_class_in_use(&cl->cl_common) ||
cl == &q->root) {
NL_SET_ERR_MSG(extack, "HFSC class in use");
return -EBUSY; return -EBUSY;
}
sch_tree_lock(sch); sch_tree_lock(sch);
...@@ -1223,7 +1225,7 @@ hfsc_bind_tcf(struct Qdisc *sch, unsigned long parent, u32 classid) ...@@ -1223,7 +1225,7 @@ hfsc_bind_tcf(struct Qdisc *sch, unsigned long parent, u32 classid)
if (cl != NULL) { if (cl != NULL) {
if (p != NULL && p->level <= cl->level) if (p != NULL && p->level <= cl->level)
return 0; return 0;
cl->filter_cnt++; qdisc_class_get(&cl->cl_common);
} }
return (unsigned long)cl; return (unsigned long)cl;
...@@ -1234,7 +1236,7 @@ hfsc_unbind_tcf(struct Qdisc *sch, unsigned long arg) ...@@ -1234,7 +1236,7 @@ hfsc_unbind_tcf(struct Qdisc *sch, unsigned long arg)
{ {
struct hfsc_class *cl = (struct hfsc_class *)arg; struct hfsc_class *cl = (struct hfsc_class *)arg;
cl->filter_cnt--; qdisc_class_put(&cl->cl_common);
} }
static struct tcf_block *hfsc_tcf_block(struct Qdisc *sch, unsigned long arg, static struct tcf_block *hfsc_tcf_block(struct Qdisc *sch, unsigned long arg,
......
...@@ -102,7 +102,6 @@ struct htb_class { ...@@ -102,7 +102,6 @@ struct htb_class {
struct tcf_proto __rcu *filter_list; /* class attached filters */ struct tcf_proto __rcu *filter_list; /* class attached filters */
struct tcf_block *block; struct tcf_block *block;
int filter_cnt;
int level; /* our level (see above) */ int level; /* our level (see above) */
unsigned int children; unsigned int children;
...@@ -1710,8 +1709,10 @@ static int htb_delete(struct Qdisc *sch, unsigned long arg, ...@@ -1710,8 +1709,10 @@ static int htb_delete(struct Qdisc *sch, unsigned long arg,
* tc subsys guarantee us that in htb_destroy it holds no class * tc subsys guarantee us that in htb_destroy it holds no class
* refs so that we can remove children safely there ? * refs so that we can remove children safely there ?
*/ */
if (cl->children || cl->filter_cnt) if (cl->children || qdisc_class_in_use(&cl->common)) {
NL_SET_ERR_MSG(extack, "HTB class in use");
return -EBUSY; return -EBUSY;
}
if (!cl->level && htb_parent_last_child(cl)) if (!cl->level && htb_parent_last_child(cl))
last_child = 1; last_child = 1;
...@@ -2107,7 +2108,7 @@ static unsigned long htb_bind_filter(struct Qdisc *sch, unsigned long parent, ...@@ -2107,7 +2108,7 @@ static unsigned long htb_bind_filter(struct Qdisc *sch, unsigned long parent,
* be broken by class during destroy IIUC. * be broken by class during destroy IIUC.
*/ */
if (cl) if (cl)
cl->filter_cnt++; qdisc_class_get(&cl->common);
return (unsigned long)cl; return (unsigned long)cl;
} }
...@@ -2115,8 +2116,7 @@ static void htb_unbind_filter(struct Qdisc *sch, unsigned long arg) ...@@ -2115,8 +2116,7 @@ static void htb_unbind_filter(struct Qdisc *sch, unsigned long arg)
{ {
struct htb_class *cl = (struct htb_class *)arg; struct htb_class *cl = (struct htb_class *)arg;
if (cl) qdisc_class_put(&cl->common);
cl->filter_cnt--;
} }
static void htb_walk(struct Qdisc *sch, struct qdisc_walker *arg) static void htb_walk(struct Qdisc *sch, struct qdisc_walker *arg)
......
...@@ -130,8 +130,6 @@ struct qfq_aggregate; ...@@ -130,8 +130,6 @@ struct qfq_aggregate;
struct qfq_class { struct qfq_class {
struct Qdisc_class_common common; struct Qdisc_class_common common;
unsigned int filter_cnt;
struct gnet_stats_basic_sync bstats; struct gnet_stats_basic_sync bstats;
struct gnet_stats_queue qstats; struct gnet_stats_queue qstats;
struct net_rate_estimator __rcu *rate_est; struct net_rate_estimator __rcu *rate_est;
...@@ -545,8 +543,10 @@ static int qfq_delete_class(struct Qdisc *sch, unsigned long arg, ...@@ -545,8 +543,10 @@ static int qfq_delete_class(struct Qdisc *sch, unsigned long arg,
struct qfq_sched *q = qdisc_priv(sch); struct qfq_sched *q = qdisc_priv(sch);
struct qfq_class *cl = (struct qfq_class *)arg; struct qfq_class *cl = (struct qfq_class *)arg;
if (cl->filter_cnt > 0) if (qdisc_class_in_use(&cl->common)) {
NL_SET_ERR_MSG_MOD(extack, "QFQ class in use");
return -EBUSY; return -EBUSY;
}
sch_tree_lock(sch); sch_tree_lock(sch);
...@@ -580,8 +580,8 @@ static unsigned long qfq_bind_tcf(struct Qdisc *sch, unsigned long parent, ...@@ -580,8 +580,8 @@ static unsigned long qfq_bind_tcf(struct Qdisc *sch, unsigned long parent,
{ {
struct qfq_class *cl = qfq_find_class(sch, classid); struct qfq_class *cl = qfq_find_class(sch, classid);
if (cl != NULL) if (cl)
cl->filter_cnt++; qdisc_class_get(&cl->common);
return (unsigned long)cl; return (unsigned long)cl;
} }
...@@ -590,7 +590,7 @@ static void qfq_unbind_tcf(struct Qdisc *sch, unsigned long arg) ...@@ -590,7 +590,7 @@ static void qfq_unbind_tcf(struct Qdisc *sch, unsigned long arg)
{ {
struct qfq_class *cl = (struct qfq_class *)arg; struct qfq_class *cl = (struct qfq_class *)arg;
cl->filter_cnt--; qdisc_class_put(&cl->common);
} }
static int qfq_graft_class(struct Qdisc *sch, unsigned long arg, static int qfq_graft_class(struct Qdisc *sch, unsigned long arg,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment