Description
The commit fixes a race in the net_shaper data path where a reader could observe a valid entry and then the entry could be replaced/freed by a writer, leading to a use-after-free (UAF). The previous implementation relied on XA marks (VALID) to gate access, but marks are not stored atomically with the entry, allowing a reader to continue using a pointer that has been replaced and potentially freed. The patch introduces an explicit valid field with acquire/release semantics, checks validity before dereferencing, and frees entries via kfree_rcu, thereby eliminating the race window and preventing UAF on readers.
Proof of Concept
Pseudo-kernel PoC illustrating the race and how the fix prevents it:
// Thread A (Reader)
while (true) {
struct net_shaper *sh = net_shaper_lookup(binding, &handle);
if (!sh)
continue;
// Potentially dangerous if used with a torn/freed object
int val = sh->some_field; // read after obtaining pointer
// do something with val
schedule();
}
// Thread B (Writer/Commit/Rollback)
while (true) {
// Perform an update which can replace the entry and free the old one via kfree_rcu
net_shaper_commit(binding, ...);
// or trigger a rollback that erases old entries and frees them
net_shaper_rollback(binding, ...);
schedule();
}
Expected behavior before fix:
- Thread A could read sh, and then Thread B could replace/free the underlying object, causing Thread A to dereference freed memory when accessing sh->some_field or other fields.
Expected behavior after fix:
- net_shaper_lookup checks sh->valid (acquire semantics) before dereferencing; even if the underlying pointer is replaced, a reader will see the invalid state and exit early, preventing UAF.
- Writers free entries via kfree_rcu after ensuring readers observing the old pointer can complete safely.
Notes:
- The PoC is conceptual and targets kernel-space race conditions between readers and writers in the net_shaper data structure. It demonstrates how an old approach using marks could lead to UAF and how the explicit valid flag with proper memory ordering prevents the vulnerability.
Commit Details
Author: Jakub Kicinski
Date: 2026-05-15 22:13 UTC
Message:
net: shaper: rework the VALID marking (again)
Recent commit changed the semantics from NOT_VALID to VALID.
I didn't realize that the flags are not stored atomically
with the entry in XArray. There's still a race of reader
observing a VALID mark for a slot, getting interrupted,
writer replacing the entry with a different one, reader
continuing, fetching the entry which is now a different
pointer than the pointer for which VALID was meant.
The biggest consequence of this is that we may see a UAF
since net_shaper_rollback() assumed that entries without
VALID can be freed without observing RCU.
Looks like the XArray marks are buying us nothing at this
point. Let's convert the code to an explicit valid field.
The smp_load_acquire() / smp_store_release() barriers are
marginally cleaner.
Reported-by: Sashiko <sashiko-bot@kernel.org>
Fixes: 93954b40f6a4 ("net-shapers: implement NL set and delete operations")
Reviewed-by: Simon Horman <horms@kernel.org>
Link: https://patch.msgid.link/20260515221325.1685455-3-kuba@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Code Diff
diff --git a/include/net/net_shaper.h b/include/net/net_shaper.h
index 5c3f49b52fe969..3939b816b0011f 100644
--- a/include/net/net_shaper.h
+++ b/include/net/net_shaper.h
@@ -53,6 +53,7 @@ struct net_shaper {
/* private: */
u32 leaves; /* accounted only for NODE scope */
+ bool valid;
struct rcu_head rcu;
};
diff --git a/net/shaper/shaper.c b/net/shaper/shaper.c
index 520cefdc3d9083..dea9270f3e57df 100644
--- a/net/shaper/shaper.c
+++ b/net/shaper/shaper.c
@@ -306,31 +306,24 @@ static void net_shaper_default_parent(const struct net_shaper_handle *handle,
parent->id = 0;
}
-/* MARK_0 is already in use due to XA_FLAGS_ALLOC. The VALID mark is set on
- * an entry only after the device-side configuration has completed
- * successfully (see net_shaper_commit()). Lookups and dumps must filter on
- * this mark to avoid exposing tentative entries inserted by
- * net_shaper_pre_insert() while the driver call is still in flight.
- */
-#define NET_SHAPER_VALID XA_MARK_1
-
static struct net_shaper *
net_shaper_lookup(struct net_shaper_binding *binding,
const struct net_shaper_handle *handle)
{
u32 index = net_shaper_handle_to_index(handle);
struct net_shaper_hierarchy *hierarchy;
+ struct net_shaper *cur;
hierarchy = net_shaper_hierarchy_rcu(binding);
- if (!hierarchy || !xa_get_mark(&hierarchy->shapers, index,
- NET_SHAPER_VALID))
+ if (!hierarchy)
return NULL;
- /* Pairs with smp_wmb() in net_shaper_commit(): if the entry is
- * valid, its contents must be visible too.
- */
- smp_rmb();
- return xa_load(&hierarchy->shapers, index);
+ cur = xa_load(&hierarchy->shapers, index);
+ /* Check valid before reading fields */
+ if (!cur || !smp_load_acquire(&cur->valid))
+ return NULL;
+
+ return cur;
}
/* Allocate on demand the per device shaper's hierarchy container.
@@ -444,12 +437,10 @@ static void net_shaper_commit(struct net_shaper_binding *binding,
if (WARN_ON_ONCE(!cur))
continue;
- /* Successful update: drop the tentative mark
- * and update the hierarchy container.
- */
+ /* Successful update: update the hierarchy container... */
net_shaper_copy(cur, &shapers[i]);
- smp_wmb();
- __xa_set_mark(&hierarchy->shapers, index, NET_SHAPER_VALID);
+ /* ... publish to lockless readers. */
+ smp_store_release(&cur->valid, true);
}
xa_unlock(&hierarchy->shapers);
}
@@ -466,10 +457,10 @@ static void net_shaper_rollback(struct net_shaper_binding *binding)
xa_lock(&hierarchy->shapers);
xa_for_each(&hierarchy->shapers, index, cur) {
- if (xa_get_mark(&hierarchy->shapers, index, NET_SHAPER_VALID))
+ if (cur->valid)
continue;
__xa_erase(&hierarchy->shapers, index);
- kfree(cur);
+ kfree_rcu(cur, rcu);
}
xa_unlock(&hierarchy->shapers);
}
@@ -882,12 +873,12 @@ int net_shaper_nl_get_dumpit(struct sk_buff *skb,
goto out_unlock;
for (; (shaper = xa_find(&hierarchy->shapers, &ctx->start_index,
- U32_MAX, NET_SHAPER_VALID));
+ U32_MAX, XA_PRESENT));
ctx->start_index++) {
- /* Pairs with smp_wmb() in net_shaper_commit(): the entry
- * is marked VALID, so its contents must be visible too.
- */
- smp_rmb();
+ /* Check valid before reading fields */
+ if (!smp_load_acquire(&shaper->valid))
+ continue;
+
ret = net_shaper_fill_one(skb, binding, shaper, info);
if (ret)
break;