Discovery & Validation in the Linux Kernel (Part 1): CAN Use-After-Free Race

Samuel Page

In the last month at Bynario, our LLM-driven pipeline has autonomously discovered, validated and patched two vulnerabilities in the Linux kernel:

  • CVE-2026-31532 is a use-after-free in Controller Area Network (CAN) raw sockets.

  • CVE-2026-31694 is a page cache overflow in the Filesystem in Userspace (FUSE).

Both provide opportunities to share some insights into the kind of vulnerabilities LLMs are able to surface, validate and patch as well as where they struggle. Plus, it's also a good excuse for me to do some write-ups on cool kernel bugs!

This first part will focus on the CAN use-after-free, which I think demonstrates a very kernel-y, non-trivial race condition. First we'll give a bit of background on CAN, which is a networking technology used in automotive and embedded fields, before diving into a technical analysis of the vulnerability, then discuss impact, validation, remediation and some final thoughts.

Part two will follow a similar format, focusing on the page cache overflow in the FUSE subsystem and how the validator successfully exploited the vulnerability to get root.

In the final part, we evaluate how local models perform in vulnerability discovery and validation. After establishing a baseline using the CVEs above as a benchmark, we compare their performance across the different stages of our pipeline.

Our pipeline uses a mix of models, but for this case, Opus 4.6 was the primary model involved in discovery and validation. The vulnerability was discovered while analysing the 7.0 Linux kernel, all snippets, unless otherwise specified, refer to this version.

CVE-2026-31532: Use-After-Free in net/can

The vulnerability occurs due to a race-condition present in the CAN raw socket teardown (raw_release()), whereby missing RCU synchronisation allows a concurrent frame reception (raw_rcv()) to access freed per-CPU state. Let's dig into the details.

Background

CAN (Controller Area Network) is a networking technology widely used in automation, embedded devices, and automotive fields.

In automotive systems, CAN is commonly used as an in-vehicle network connecting electronic control units (ECUs). These ECUs handle functions such as engine control, braking, steering assistance, airbags, body electronics, diagnostics, and infotainment.

These components communicate over CAN on a shared bus, where messages are broadcast rather than sent via direct links. This allows loosely coupled coordination between subsystems while maintaining deterministic timing and robustness, making it well suited to the distributed nature of in-vehicle control systems.

This model also extends beyond vehicles into industrial automation and embedded systems, as well as development and testing environments where virtual CAN (vcan) interfaces enable simulation without physical hardware.

SocketCAN, net/can/, is an implementation of CAN protocols that uses the Linux kernel's Berkeley socket API, the Linux network stack and implements the CAN device drivers as network interfaces [1].

The module (CONFIG_CAN) is enabled by default in most distributions [2], along with virtual CAN driver support (CONFIG_VCAN), which allows users to create virtual local CAN interfaces. The implication being SocketCAN code is reachable on devices without CAN hardware, so long as a virtual CAN device is accessible or can be created.

The CAN module implements the AF_CAN socket family, with several CAN protocol implementations. The CAN_RAW protocol uses the SOCK_RAW socket type, and is used for raw frame send/receive with configurable filters. Here's how the CAN_RAW socket is defined:

/* A raw socket has a list of can_filters attached to it, each receiving
 * the CAN frames matching that filter.  If the filter list is empty,
 * no CAN frames will be received by the socket.  The default after
 * opening the socket, is to have one filter which receives all frames.
 * The filter list is allocated dynamically with the exception of the
 * list containing only one item.  This common case is optimized by
 * storing the single filter in dfilter, to avoid using dynamic memory.
 */

struct uniqframe {
	const struct sk_buff *skb;
	u32 hash;
	unsigned int join_rx_count;
};

struct raw_sock {
	struct sock sk;
	struct net_device *dev;
	// ...
	int count;                 /* number of active filters */
	struct can_filter dfilter; /* default/single filter */
	struct can_filter *filter; /* pointer to filter(s) */
	struct uniqframe __percpu *uniq;
};
/* A raw socket has a list of can_filters attached to it, each receiving
 * the CAN frames matching that filter.  If the filter list is empty,
 * no CAN frames will be received by the socket.  The default after
 * opening the socket, is to have one filter which receives all frames.
 * The filter list is allocated dynamically with the exception of the
 * list containing only one item.  This common case is optimized by
 * storing the single filter in dfilter, to avoid using dynamic memory.
 */

struct uniqframe {
	const struct sk_buff *skb;
	u32 hash;
	unsigned int join_rx_count;
};

struct raw_sock {
	struct sock sk;
	struct net_device *dev;
	// ...
	int count;                 /* number of active filters */
	struct can_filter dfilter; /* default/single filter */
	struct can_filter *filter; /* pointer to filter(s) */
	struct uniqframe __percpu *uniq;
};
/* A raw socket has a list of can_filters attached to it, each receiving
 * the CAN frames matching that filter.  If the filter list is empty,
 * no CAN frames will be received by the socket.  The default after
 * opening the socket, is to have one filter which receives all frames.
 * The filter list is allocated dynamically with the exception of the
 * list containing only one item.  This common case is optimized by
 * storing the single filter in dfilter, to avoid using dynamic memory.
 */

struct uniqframe {
	const struct sk_buff *skb;
	u32 hash;
	unsigned int join_rx_count;
};

struct raw_sock {
	struct sock sk;
	struct net_device *dev;
	// ...
	int count;                 /* number of active filters */
	struct can_filter dfilter; /* default/single filter */
	struct can_filter *filter; /* pointer to filter(s) */
	struct uniqframe __percpu *uniq;
};

When a CAN raw socket is created, it registers a per-filter receive callback, raw_rcv(). During the CAN frame RX path, a socket's callback is invoked once per matching filter. The callback's job is to decide whether this match (frame) should be received by the socket.

If there are multiple matches for a single frame, raw_rcv() may run in parallel across multiple CPUs, causing the same frame to be processed concurrently.

The uniq field is a __percpu allocation, meaning each CPU has its own uniq object accessible using this_cpu_ptr(ro->uniq), used to deduplicate or AND-combine matches when a single frame is matched by more than one of the socket's filters.

Analysis

Alright, let's dive into the technical analysis. You are probably wondering why I am telling you about raw sockets and uniq fields. The answer? uniq is the source of the use-after-free!

As we're dealing with a use-after-free, let's take a look at the lifetime of the uniq ptr. We know uniq is a __percpu object allocated during raw socket creation, in raw_init():

static int raw_init(struct sock *sk)
{
	struct raw_sock *ro = raw_sk(sk);

	// ...

	/* alloc_percpu provides zero'ed memory */
	ro->uniq = alloc_percpu(struct uniqframe);
	if (unlikely(!ro->uniq))
		return -ENOMEM;
static int raw_init(struct sock *sk)
{
	struct raw_sock *ro = raw_sk(sk);

	// ...

	/* alloc_percpu provides zero'ed memory */
	ro->uniq = alloc_percpu(struct uniqframe);
	if (unlikely(!ro->uniq))
		return -ENOMEM;
static int raw_init(struct sock *sk)
{
	struct raw_sock *ro = raw_sk(sk);

	// ...

	/* alloc_percpu provides zero'ed memory */
	ro->uniq = alloc_percpu(struct uniqframe);
	if (unlikely(!ro->uniq))
		return -ENOMEM;

Unsurprisingly, it is also freed during the teardown of the raw socket, in raw_release():

static int raw_release(struct socket *sock)
{
	struct sock *sk = sock->sk;
	struct raw_sock *ro;
	struct net *net;

	if (!sk)
		return 0;

	ro = raw_sk(sk);
	net = sock_net(sk);

	spin_lock(&raw_notifier_lock);
	while (raw_busy_notifier == ro) {
		spin_unlock(&raw_notifier_lock);
		schedule_timeout_uninterruptible(1);
		spin_lock(&raw_notifier_lock);
	}
	list_del(&ro->notifier);
	spin_unlock(&raw_notifier_lock);

	rtnl_lock();
	lock_sock(sk);

	/* remove current filters & unregister */
	if (ro->bound) {
		if (ro->dev) {
			raw_disable_allfilters(dev_net(ro->dev), ro->dev, sk);  // [1]
			netdev_put(ro->dev, &ro->dev_tracker);
		} else {
			raw_disable_allfilters(net, NULL, sk);
		}
	}

	if (ro->count > 1)
		kfree(ro->filter);

	ro->ifindex = 0;
	ro->bound = 0;
	ro->dev = NULL;
	ro->count = 0;
	free_percpu(ro->uniq);  // [3]
static int raw_release(struct socket *sock)
{
	struct sock *sk = sock->sk;
	struct raw_sock *ro;
	struct net *net;

	if (!sk)
		return 0;

	ro = raw_sk(sk);
	net = sock_net(sk);

	spin_lock(&raw_notifier_lock);
	while (raw_busy_notifier == ro) {
		spin_unlock(&raw_notifier_lock);
		schedule_timeout_uninterruptible(1);
		spin_lock(&raw_notifier_lock);
	}
	list_del(&ro->notifier);
	spin_unlock(&raw_notifier_lock);

	rtnl_lock();
	lock_sock(sk);

	/* remove current filters & unregister */
	if (ro->bound) {
		if (ro->dev) {
			raw_disable_allfilters(dev_net(ro->dev), ro->dev, sk);  // [1]
			netdev_put(ro->dev, &ro->dev_tracker);
		} else {
			raw_disable_allfilters(net, NULL, sk);
		}
	}

	if (ro->count > 1)
		kfree(ro->filter);

	ro->ifindex = 0;
	ro->bound = 0;
	ro->dev = NULL;
	ro->count = 0;
	free_percpu(ro->uniq);  // [3]
static int raw_release(struct socket *sock)
{
	struct sock *sk = sock->sk;
	struct raw_sock *ro;
	struct net *net;

	if (!sk)
		return 0;

	ro = raw_sk(sk);
	net = sock_net(sk);

	spin_lock(&raw_notifier_lock);
	while (raw_busy_notifier == ro) {
		spin_unlock(&raw_notifier_lock);
		schedule_timeout_uninterruptible(1);
		spin_lock(&raw_notifier_lock);
	}
	list_del(&ro->notifier);
	spin_unlock(&raw_notifier_lock);

	rtnl_lock();
	lock_sock(sk);

	/* remove current filters & unregister */
	if (ro->bound) {
		if (ro->dev) {
			raw_disable_allfilters(dev_net(ro->dev), ro->dev, sk);  // [1]
			netdev_put(ro->dev, &ro->dev_tracker);
		} else {
			raw_disable_allfilters(net, NULL, sk);
		}
	}

	if (ro->count > 1)
		kfree(ro->filter);

	ro->ifindex = 0;
	ro->bound = 0;
	ro->dev = NULL;
	ro->count = 0;
	free_percpu(ro->uniq);  // [3]

At first glance this looks straightforward, but let's dig deeper. At [1], raw_disable_allfilters() calls raw_disable_filters(), which iterates each registered filter and calls can_rx_unregister():

static void raw_disable_filters(struct net *net, struct net_device *dev,
				struct sock *sk, struct can_filter *filter,
				int count)
{
	int i;

	for (i = 0; i < count; i++)
		can_rx_unregister(net, dev, filter[i].can_id,
				  filter[i].can_mask, raw_rcv, sk);  // [2]
}
static void raw_disable_filters(struct net *net, struct net_device *dev,
				struct sock *sk, struct can_filter *filter,
				int count)
{
	int i;

	for (i = 0; i < count; i++)
		can_rx_unregister(net, dev, filter[i].can_id,
				  filter[i].can_mask, raw_rcv, sk);  // [2]
}
static void raw_disable_filters(struct net *net, struct net_device *dev,
				struct sock *sk, struct can_filter *filter,
				int count)
{
	int i;

	for (i = 0; i < count; i++)
		can_rx_unregister(net, dev, filter[i].can_id,
				  filter[i].can_mask, raw_rcv, sk);  // [2]
}

At [2], can_rx_unregister() removes the receiver from the RCU-protected hash list and schedules deletion via call_rcu():

/**
 * can_rx_unregister - unsubscribe CAN frames from a specific interface
 * @net: the applicable net namespace
 * @dev: pointer to netdevice (NULL => unsubscribe from 'all' CAN devices list)
 * @can_id: CAN identifier
 * @mask: CAN mask
 * @func: callback function on filter match
 * @data: returned parameter for callback function
 *
 * Description:
 *  Removes subscription entry depending on given (subscription) values.
 */
void can_rx_unregister(struct net *net, struct net_device *dev, canid_t can_id,
		       canid_t mask, void (*func)(struct sk_buff *, void *),
		       void *data)
{

	// ...

 out:
	spin_unlock_bh(&net->can.rcvlists_lock);

	/* schedule the receiver item for deletion */
	if (rcv) {
		if (rcv->sk)
			sock_hold(rcv->sk);
		call_rcu(&rcv->rcu, can_rx_delete_receiver);
	}
}
/**
 * can_rx_unregister - unsubscribe CAN frames from a specific interface
 * @net: the applicable net namespace
 * @dev: pointer to netdevice (NULL => unsubscribe from 'all' CAN devices list)
 * @can_id: CAN identifier
 * @mask: CAN mask
 * @func: callback function on filter match
 * @data: returned parameter for callback function
 *
 * Description:
 *  Removes subscription entry depending on given (subscription) values.
 */
void can_rx_unregister(struct net *net, struct net_device *dev, canid_t can_id,
		       canid_t mask, void (*func)(struct sk_buff *, void *),
		       void *data)
{

	// ...

 out:
	spin_unlock_bh(&net->can.rcvlists_lock);

	/* schedule the receiver item for deletion */
	if (rcv) {
		if (rcv->sk)
			sock_hold(rcv->sk);
		call_rcu(&rcv->rcu, can_rx_delete_receiver);
	}
}
/**
 * can_rx_unregister - unsubscribe CAN frames from a specific interface
 * @net: the applicable net namespace
 * @dev: pointer to netdevice (NULL => unsubscribe from 'all' CAN devices list)
 * @can_id: CAN identifier
 * @mask: CAN mask
 * @func: callback function on filter match
 * @data: returned parameter for callback function
 *
 * Description:
 *  Removes subscription entry depending on given (subscription) values.
 */
void can_rx_unregister(struct net *net, struct net_device *dev, canid_t can_id,
		       canid_t mask, void (*func)(struct sk_buff *, void *),
		       void *data)
{

	// ...

 out:
	spin_unlock_bh(&net->can.rcvlists_lock);

	/* schedule the receiver item for deletion */
	if (rcv) {
		if (rcv->sk)
			sock_hold(rcv->sk);
		call_rcu(&rcv->rcu, can_rx_delete_receiver);
	}
}

Note call_rcu() is asynchronous. It schedules can_rx_delete_receiver() to run after the current RCU (Read, Copy, Update) grace period ends [3], which will free the receiver and drop the reference it holds on the sock object.

However raw_release() does not wait for this grace period, it proceeds directly to [3] and calls free_percpu(ro->uniq). This means ro->uniq can be freed before the grace period ends and can_rx_delete_receive() runs, dropping its (usually last) socket reference.

Meanwhile, if frames are still being received, raw_rcv() may still be executing on another CPU while raw_release() is running. Critically, raw_rcv() is invoked inside an rcu_read_lock(), which means the socket cannot be released (via the RCU callback earlier, can_rx_delete_receiver()) until raw_rcv() is complete.

So that rules out any receiver or socket use-after-free shenanigans, but as we recall, the rest of raw_release() does not wait, meaning ro->uniq can be freed while raw_rcv() is running, and if we look at the code, this is an issue:

static void raw_rcv(struct sk_buff *oskb, void *data)
{
	struct sock *sk = (struct sock *)data;
	struct raw_sock *ro = raw_sk(sk);
	// ...

	/* eliminate multiple filter matches for the same skb */
	if (this_cpu_ptr(ro->uniq)->skb == oskb && // READ from freed memory
	    this_cpu_ptr(ro->uniq)->hash == oskb->hash) { // READ from freed memory
		if (!ro->join_filters)
			return;

		this_cpu_inc(ro->uniq->join_rx_count); // WRITE to freed memory
		/* drop frame until all enabled filters matched */
		if (this_cpu_ptr(ro->uniq)->join_rx_count < ro->count)
			return;
	} else {
		this_cpu_ptr(ro->uniq)->skb = oskb; // WRITE to freed memory
		this_cpu_ptr(ro->uniq)->hash = oskb->hash; // WRITE to freed memory
		this_cpu_ptr(ro->uniq)->join_rx_count = 1; // WRITE to freed memory
		/* drop first frame to check all enabled filters? */
		if (ro->join_filters && ro->count > 1)
			return;
	}

	// ...
}
static void raw_rcv(struct sk_buff *oskb, void *data)
{
	struct sock *sk = (struct sock *)data;
	struct raw_sock *ro = raw_sk(sk);
	// ...

	/* eliminate multiple filter matches for the same skb */
	if (this_cpu_ptr(ro->uniq)->skb == oskb && // READ from freed memory
	    this_cpu_ptr(ro->uniq)->hash == oskb->hash) { // READ from freed memory
		if (!ro->join_filters)
			return;

		this_cpu_inc(ro->uniq->join_rx_count); // WRITE to freed memory
		/* drop frame until all enabled filters matched */
		if (this_cpu_ptr(ro->uniq)->join_rx_count < ro->count)
			return;
	} else {
		this_cpu_ptr(ro->uniq)->skb = oskb; // WRITE to freed memory
		this_cpu_ptr(ro->uniq)->hash = oskb->hash; // WRITE to freed memory
		this_cpu_ptr(ro->uniq)->join_rx_count = 1; // WRITE to freed memory
		/* drop first frame to check all enabled filters? */
		if (ro->join_filters && ro->count > 1)
			return;
	}

	// ...
}
static void raw_rcv(struct sk_buff *oskb, void *data)
{
	struct sock *sk = (struct sock *)data;
	struct raw_sock *ro = raw_sk(sk);
	// ...

	/* eliminate multiple filter matches for the same skb */
	if (this_cpu_ptr(ro->uniq)->skb == oskb && // READ from freed memory
	    this_cpu_ptr(ro->uniq)->hash == oskb->hash) { // READ from freed memory
		if (!ro->join_filters)
			return;

		this_cpu_inc(ro->uniq->join_rx_count); // WRITE to freed memory
		/* drop frame until all enabled filters matched */
		if (this_cpu_ptr(ro->uniq)->join_rx_count < ro->count)
			return;
	} else {
		this_cpu_ptr(ro->uniq)->skb = oskb; // WRITE to freed memory
		this_cpu_ptr(ro->uniq)->hash = oskb->hash; // WRITE to freed memory
		this_cpu_ptr(ro->uniq)->join_rx_count = 1; // WRITE to freed memory
		/* drop first frame to check all enabled filters? */
		if (ro->join_filters && ro->count > 1)
			return;
	}

	// ...
}

As we can see, several reads/writes occur to the now freed uniq allocation. Essentially, this is a race condition in the cleanup of the CAN raw socket, due to uniq being freed outside of the RCU grace period used to protect the lifetime of the receiver/socket:

CPU 0 (close path)                    CPU 1 (receive softirq)
========================              ========================
raw_release()
  rtnl_lock()
  lock_sock(sk)
  raw_disable_allfilters()
    can_rx_unregister()
      hlist_del_rcu(rcv)              rcu_read_lock()  [implicit in softirq]
      call_rcu(can_rx_delete_rcv)     raw_rcv(oskb, data)
         |                              ro = raw_sk(sk)
         | [async - grace period        |
         |  NOT yet elapsed]            |
         v                              |
  free_percpu(ro->uniq)  <-- BUG        |
                                        this_cpu_ptr(ro->uniq)->skb  <-- UAF READ
                                        this_cpu_ptr(ro->uniq)->skb = oskb  <-- UAF WRITE
                                        rcu_read_unlock()
CPU 0 (close path)                    CPU 1 (receive softirq)
========================              ========================
raw_release()
  rtnl_lock()
  lock_sock(sk)
  raw_disable_allfilters()
    can_rx_unregister()
      hlist_del_rcu(rcv)              rcu_read_lock()  [implicit in softirq]
      call_rcu(can_rx_delete_rcv)     raw_rcv(oskb, data)
         |                              ro = raw_sk(sk)
         | [async - grace period        |
         |  NOT yet elapsed]            |
         v                              |
  free_percpu(ro->uniq)  <-- BUG        |
                                        this_cpu_ptr(ro->uniq)->skb  <-- UAF READ
                                        this_cpu_ptr(ro->uniq)->skb = oskb  <-- UAF WRITE
                                        rcu_read_unlock()
CPU 0 (close path)                    CPU 1 (receive softirq)
========================              ========================
raw_release()
  rtnl_lock()
  lock_sock(sk)
  raw_disable_allfilters()
    can_rx_unregister()
      hlist_del_rcu(rcv)              rcu_read_lock()  [implicit in softirq]
      call_rcu(can_rx_delete_rcv)     raw_rcv(oskb, data)
         |                              ro = raw_sk(sk)
         | [async - grace period        |
         |  NOT yet elapsed]            |
         v                              |
  free_percpu(ro->uniq)  <-- BUG        |
                                        this_cpu_ptr(ro->uniq)->skb  <-- UAF READ
                                        this_cpu_ptr(ro->uniq)->skb = oskb  <-- UAF WRITE
                                        rcu_read_unlock()

Validation

As I mentioned at the top of the post, our pipeline doesn't just stop at discovery, but will attempt to validate any findings to reduce noise. For kernel bugs, this can range from instrumenting the kernel and producing a suitable crash all the way to generating a proof-of-concept demonstrating local privilege escalation (LPE).

This bug was particularly interesting from a validation perspective. Besides being a subtle race condition, the use-after-free'd object was a per-CPU allocation, which generally isn't instrumented by the kernel address sanitizer (KASAN), making detecting the bug trickier.

To tackle this, the validator compiled the kernel with custom instrumentation, a logical "marker" in struct raw_sock to confirm raw_rcv() accesses ro->uniq AFTER free_percpu(ro->uniq) has been called:

diff --git a/net/can/raw.c b/net/can/raw.c
index eee244ffc31e..ae47f113d5ea 100644
--- a/net/can/raw.c
+++ b/net/can/raw.c
@@ -102,6 +102,7 @@ struct raw_sock {
 	struct can_filter dfilter; /* default/single filter */
 	struct can_filter *filter; /* pointer to filter(s) */
 	struct uniqframe __percpu *uniq;
+	int uniq_freed;		       /* UAF logical free marker */
 };

 static LIST_HEAD(raw_notifier_list);
@@ -163,6 +164,16 @@ static void raw_rcv(struct sk_buff *oskb, void *data)
 		}
 	}

+	/* UAF: detect raw_rcv running after free_percpu(ro->uniq). */
+	if (unlikely(READ_ONCE(ro->uniq_freed))) {
+		WARN_ONCE(1,
+			"raw_rcv racing with raw_release!\n"
+			"  sk=%px ro=%px ro->uniq=%px cpu=%d bound=%d count=%d\n",
+			sk, ro, ro->uniq, smp_processor_id(),
+			ro->bound, ro->count);
+		return;
+	}
+
 	/* eliminate multiple filter matches for the same skb */
 	if (this_cpu_ptr(ro->uniq)->skb == oskb &&
 	    this_cpu_ptr(ro->uniq)->hash == oskb->hash) {
@@ -437,6 +448,7 @@ static int raw_release(struct socket *sock)
 	ro->dev = NULL;
 	ro->count = 0;
 	free_percpu(ro->uniq);
+	WRITE_ONCE(ro->uniq_freed, 1);

 	sock_orphan(sk);
 	sock->sk = NULL;
diff --git a/net/can/raw.c b/net/can/raw.c
index eee244ffc31e..ae47f113d5ea 100644
--- a/net/can/raw.c
+++ b/net/can/raw.c
@@ -102,6 +102,7 @@ struct raw_sock {
 	struct can_filter dfilter; /* default/single filter */
 	struct can_filter *filter; /* pointer to filter(s) */
 	struct uniqframe __percpu *uniq;
+	int uniq_freed;		       /* UAF logical free marker */
 };

 static LIST_HEAD(raw_notifier_list);
@@ -163,6 +164,16 @@ static void raw_rcv(struct sk_buff *oskb, void *data)
 		}
 	}

+	/* UAF: detect raw_rcv running after free_percpu(ro->uniq). */
+	if (unlikely(READ_ONCE(ro->uniq_freed))) {
+		WARN_ONCE(1,
+			"raw_rcv racing with raw_release!\n"
+			"  sk=%px ro=%px ro->uniq=%px cpu=%d bound=%d count=%d\n",
+			sk, ro, ro->uniq, smp_processor_id(),
+			ro->bound, ro->count);
+		return;
+	}
+
 	/* eliminate multiple filter matches for the same skb */
 	if (this_cpu_ptr(ro->uniq)->skb == oskb &&
 	    this_cpu_ptr(ro->uniq)->hash == oskb->hash) {
@@ -437,6 +448,7 @@ static int raw_release(struct socket *sock)
 	ro->dev = NULL;
 	ro->count = 0;
 	free_percpu(ro->uniq);
+	WRITE_ONCE(ro->uniq_freed, 1);

 	sock_orphan(sk);
 	sock->sk = NULL;
diff --git a/net/can/raw.c b/net/can/raw.c
index eee244ffc31e..ae47f113d5ea 100644
--- a/net/can/raw.c
+++ b/net/can/raw.c
@@ -102,6 +102,7 @@ struct raw_sock {
 	struct can_filter dfilter; /* default/single filter */
 	struct can_filter *filter; /* pointer to filter(s) */
 	struct uniqframe __percpu *uniq;
+	int uniq_freed;		       /* UAF logical free marker */
 };

 static LIST_HEAD(raw_notifier_list);
@@ -163,6 +164,16 @@ static void raw_rcv(struct sk_buff *oskb, void *data)
 		}
 	}

+	/* UAF: detect raw_rcv running after free_percpu(ro->uniq). */
+	if (unlikely(READ_ONCE(ro->uniq_freed))) {
+		WARN_ONCE(1,
+			"raw_rcv racing with raw_release!\n"
+			"  sk=%px ro=%px ro->uniq=%px cpu=%d bound=%d count=%d\n",
+			sk, ro, ro->uniq, smp_processor_id(),
+			ro->bound, ro->count);
+		return;
+	}
+
 	/* eliminate multiple filter matches for the same skb */
 	if (this_cpu_ptr(ro->uniq)->skb == oskb &&
 	    this_cpu_ptr(ro->uniq)->hash == oskb->hash) {
@@ -437,6 +448,7 @@ static int raw_release(struct socket *sock)
 	ro->dev = NULL;
 	ro->count = 0;
 	free_percpu(ro->uniq);
+	WRITE_ONCE(ro->uniq_freed, 1);

 	sock_orphan(sk);
 	sock->sk = NULL;

Using this instrumentation, the validator was able to trigger the race condition:

[*] Running PoC (10 rounds)...
[*] Round 1/10...
[*] PoC: Finding #17 - CAN raw free_percpu UAF
[*] vcan0 ifindex=4, 8 senders, 8 racers, 20000 iterations
[   12.578151] ------------[ cut here ]------------
[   12.578353] raw_rcv racing with raw_release!
[   12.578353]   sk=ffff0000cbad3400 ro=ffff0000cbad3400 ro->uniq=0000bd0f5723e078 cpu=6 bound=0 count=0
[   12.578962] WARNING: net/can/raw.c:169 at raw_rcv+0x828/0xb38, CPU#6: poc/116
[   12.580506] Modules linked in: vcan can_dev
[   12.581511] CPU: 6 UID: 0 PID: 116 Comm: poc Not tainted 7.0.0-rc6-dirty #5 PREEMPT
[   12.581739] Hardware name: linux,dummy-virt (DT)
[   12.582461] pstate: 60000005 (nZCv daif -PAN -UAO -TCO -DIT -SSBS BTYPE=--)
[   12.582636] pc : raw_rcv+0x828/0xb38
[   12.582729] lr : raw_rcv+0x828/0xb38
[   12.582807] sp : ffff800080067980
[   12.582888] x29: ffff800080067a10 x28: ffff0000de956590 x27: ffff0000de9566f8
[   12.583110] x26: 0000000000000003 x25: 0000000000000123 x24: dfff800000000000
[   12.583240] x23: ffff0000c8838a00 x22: 0000000000000010 x21: 1ffff0001000cf36
[   12.583388] x20: ffff0000c5d931c0 x19: ffff0000cbad3400 x18: 000000000000000f
[   12.583519] x17: 0000000000000003 x16: 0000000000000000 x15: 000000000000000a
[   12.583661] x14: 0000000000000000 x13: dfff800000000000 x12: ffff70001000cecb
[   12.583785] x11: 1ffff0001000ceca x10: ffff70001000ceca x9 : dfff800000000000
[   12.583959] x8 : ffff800080067657 x7 : 0000000000000001 x6 : 0000000000000000
[   12.584084] x5 : 0000000000000000 x4 : 1fffe00018bb2639 x3 : 0000000000000000
[   12.584207] x2 : 0000000000000000 x1 : 0000000000000000 x0 : ffff0000c5d931c0
[   12.584416] Call trace:
[   12.584574]  raw_rcv+0x828/0xb38 (P)
[   12.584708]  can_rcv_filter+0x1f0/0x758
[   12.584778]  can_receive+0x12c/0x29c
[   12.584843]  can_rcv+0x1b8/0x308
[   12.584904]  __netif_receive_skb_one_core+0xf0/0x16c
[   12.585004]  __netif_receive_skb+0x20/0x16c
[   12.585079]  process_backlog+0x158/0x444
[   12.585153]  __napi_poll+0x90/0x530
[   12.585252]  net_rx_action+0x348/0xb00
[   12.585335]  handle_softirqs+0x2b4/0x758
[   12.585414]  __do_softirq+0x14/0x20
[   12.585484]  ____do_softirq+0x10/0x20
[   12.585655]  call_on_irq_stack+0x30/0x48
[   12.585853]  do_softirq_own_stack+0x1c/0x40
[   12.586503]  do_softirq+0xa0/0xd0
[   12.586633]  __local_bh_enable_ip+0x1fc/0x2c0
[   12.586714]  netif_rx+0x120/0x184
[   12.586790]  can_send+0x5b0/0x9d4
[   12.586857]  raw_sendmsg+0x808/0xe58
[   12.586926]  sock_write_iter+0x230/0x384
[   12.587000]  vfs_write+0x774/0xa68
[   12.587068]  ksys_write+0x180/0x1d4
[   12.587137]  __arm64_sys_write+0x68/0xac
[   12.587312]  invoke_syscall.constprop.0+0x5c/0x2a0
[   12.587441]  el0_svc_common.constprop.0+0xa0/0x240
[   12.587529]  do_el0_svc+0x3c/0x60
[   12.587600]  el0_svc+0x38/0xb8
[   12.587670]  el0t_64_sync_handler+0xa0/0xe4
[   12.587745]  el0t_64_sync+0x198/0x19c
[   12.587948] ---[ end trace 0000000000000000 ]

[*] Running PoC (10 rounds)...
[*] Round 1/10...
[*] PoC: Finding #17 - CAN raw free_percpu UAF
[*] vcan0 ifindex=4, 8 senders, 8 racers, 20000 iterations
[   12.578151] ------------[ cut here ]------------
[   12.578353] raw_rcv racing with raw_release!
[   12.578353]   sk=ffff0000cbad3400 ro=ffff0000cbad3400 ro->uniq=0000bd0f5723e078 cpu=6 bound=0 count=0
[   12.578962] WARNING: net/can/raw.c:169 at raw_rcv+0x828/0xb38, CPU#6: poc/116
[   12.580506] Modules linked in: vcan can_dev
[   12.581511] CPU: 6 UID: 0 PID: 116 Comm: poc Not tainted 7.0.0-rc6-dirty #5 PREEMPT
[   12.581739] Hardware name: linux,dummy-virt (DT)
[   12.582461] pstate: 60000005 (nZCv daif -PAN -UAO -TCO -DIT -SSBS BTYPE=--)
[   12.582636] pc : raw_rcv+0x828/0xb38
[   12.582729] lr : raw_rcv+0x828/0xb38
[   12.582807] sp : ffff800080067980
[   12.582888] x29: ffff800080067a10 x28: ffff0000de956590 x27: ffff0000de9566f8
[   12.583110] x26: 0000000000000003 x25: 0000000000000123 x24: dfff800000000000
[   12.583240] x23: ffff0000c8838a00 x22: 0000000000000010 x21: 1ffff0001000cf36
[   12.583388] x20: ffff0000c5d931c0 x19: ffff0000cbad3400 x18: 000000000000000f
[   12.583519] x17: 0000000000000003 x16: 0000000000000000 x15: 000000000000000a
[   12.583661] x14: 0000000000000000 x13: dfff800000000000 x12: ffff70001000cecb
[   12.583785] x11: 1ffff0001000ceca x10: ffff70001000ceca x9 : dfff800000000000
[   12.583959] x8 : ffff800080067657 x7 : 0000000000000001 x6 : 0000000000000000
[   12.584084] x5 : 0000000000000000 x4 : 1fffe00018bb2639 x3 : 0000000000000000
[   12.584207] x2 : 0000000000000000 x1 : 0000000000000000 x0 : ffff0000c5d931c0
[   12.584416] Call trace:
[   12.584574]  raw_rcv+0x828/0xb38 (P)
[   12.584708]  can_rcv_filter+0x1f0/0x758
[   12.584778]  can_receive+0x12c/0x29c
[   12.584843]  can_rcv+0x1b8/0x308
[   12.584904]  __netif_receive_skb_one_core+0xf0/0x16c
[   12.585004]  __netif_receive_skb+0x20/0x16c
[   12.585079]  process_backlog+0x158/0x444
[   12.585153]  __napi_poll+0x90/0x530
[   12.585252]  net_rx_action+0x348/0xb00
[   12.585335]  handle_softirqs+0x2b4/0x758
[   12.585414]  __do_softirq+0x14/0x20
[   12.585484]  ____do_softirq+0x10/0x20
[   12.585655]  call_on_irq_stack+0x30/0x48
[   12.585853]  do_softirq_own_stack+0x1c/0x40
[   12.586503]  do_softirq+0xa0/0xd0
[   12.586633]  __local_bh_enable_ip+0x1fc/0x2c0
[   12.586714]  netif_rx+0x120/0x184
[   12.586790]  can_send+0x5b0/0x9d4
[   12.586857]  raw_sendmsg+0x808/0xe58
[   12.586926]  sock_write_iter+0x230/0x384
[   12.587000]  vfs_write+0x774/0xa68
[   12.587068]  ksys_write+0x180/0x1d4
[   12.587137]  __arm64_sys_write+0x68/0xac
[   12.587312]  invoke_syscall.constprop.0+0x5c/0x2a0
[   12.587441]  el0_svc_common.constprop.0+0xa0/0x240
[   12.587529]  do_el0_svc+0x3c/0x60
[   12.587600]  el0_svc+0x38/0xb8
[   12.587670]  el0t_64_sync_handler+0xa0/0xe4
[   12.587745]  el0t_64_sync+0x198/0x19c
[   12.587948] ---[ end trace 0000000000000000 ]

[*] Running PoC (10 rounds)...
[*] Round 1/10...
[*] PoC: Finding #17 - CAN raw free_percpu UAF
[*] vcan0 ifindex=4, 8 senders, 8 racers, 20000 iterations
[   12.578151] ------------[ cut here ]------------
[   12.578353] raw_rcv racing with raw_release!
[   12.578353]   sk=ffff0000cbad3400 ro=ffff0000cbad3400 ro->uniq=0000bd0f5723e078 cpu=6 bound=0 count=0
[   12.578962] WARNING: net/can/raw.c:169 at raw_rcv+0x828/0xb38, CPU#6: poc/116
[   12.580506] Modules linked in: vcan can_dev
[   12.581511] CPU: 6 UID: 0 PID: 116 Comm: poc Not tainted 7.0.0-rc6-dirty #5 PREEMPT
[   12.581739] Hardware name: linux,dummy-virt (DT)
[   12.582461] pstate: 60000005 (nZCv daif -PAN -UAO -TCO -DIT -SSBS BTYPE=--)
[   12.582636] pc : raw_rcv+0x828/0xb38
[   12.582729] lr : raw_rcv+0x828/0xb38
[   12.582807] sp : ffff800080067980
[   12.582888] x29: ffff800080067a10 x28: ffff0000de956590 x27: ffff0000de9566f8
[   12.583110] x26: 0000000000000003 x25: 0000000000000123 x24: dfff800000000000
[   12.583240] x23: ffff0000c8838a00 x22: 0000000000000010 x21: 1ffff0001000cf36
[   12.583388] x20: ffff0000c5d931c0 x19: ffff0000cbad3400 x18: 000000000000000f
[   12.583519] x17: 0000000000000003 x16: 0000000000000000 x15: 000000000000000a
[   12.583661] x14: 0000000000000000 x13: dfff800000000000 x12: ffff70001000cecb
[   12.583785] x11: 1ffff0001000ceca x10: ffff70001000ceca x9 : dfff800000000000
[   12.583959] x8 : ffff800080067657 x7 : 0000000000000001 x6 : 0000000000000000
[   12.584084] x5 : 0000000000000000 x4 : 1fffe00018bb2639 x3 : 0000000000000000
[   12.584207] x2 : 0000000000000000 x1 : 0000000000000000 x0 : ffff0000c5d931c0
[   12.584416] Call trace:
[   12.584574]  raw_rcv+0x828/0xb38 (P)
[   12.584708]  can_rcv_filter+0x1f0/0x758
[   12.584778]  can_receive+0x12c/0x29c
[   12.584843]  can_rcv+0x1b8/0x308
[   12.584904]  __netif_receive_skb_one_core+0xf0/0x16c
[   12.585004]  __netif_receive_skb+0x20/0x16c
[   12.585079]  process_backlog+0x158/0x444
[   12.585153]  __napi_poll+0x90/0x530
[   12.585252]  net_rx_action+0x348/0xb00
[   12.585335]  handle_softirqs+0x2b4/0x758
[   12.585414]  __do_softirq+0x14/0x20
[   12.585484]  ____do_softirq+0x10/0x20
[   12.585655]  call_on_irq_stack+0x30/0x48
[   12.585853]  do_softirq_own_stack+0x1c/0x40
[   12.586503]  do_softirq+0xa0/0xd0
[   12.586633]  __local_bh_enable_ip+0x1fc/0x2c0
[   12.586714]  netif_rx+0x120/0x184
[   12.586790]  can_send+0x5b0/0x9d4
[   12.586857]  raw_sendmsg+0x808/0xe58
[   12.586926]  sock_write_iter+0x230/0x384
[   12.587000]  vfs_write+0x774/0xa68
[   12.587068]  ksys_write+0x180/0x1d4
[   12.587137]  __arm64_sys_write+0x68/0xac
[   12.587312]  invoke_syscall.constprop.0+0x5c/0x2a0
[   12.587441]  el0_svc_common.constprop.0+0xa0/0x240
[   12.587529]  do_el0_svc+0x3c/0x60
[   12.587600]  el0_svc+0x38/0xb8
[   12.587670]  el0t_64_sync_handler+0xa0/0xe4
[   12.587745]  el0t_64_sync+0x198/0x19c
[   12.587948] ---[ end trace 0000000000000000 ]

The proof-of-concept (PoC) was fairly straightforward and engineered like I'd expect, with multiple iterations and pthreads being attempted to try and win the race.

It runs multiple sender threads to keep sending matching CAN frames while multiple racer threads repeatedly create, bind, filter, and close short-lived CAN raw sockets. This triggers the race between the short-lived sockets' receive callback (raw_rcv()) handling the matching frames and the sockets teardown path, freeing the ro->uniq field.

Impact

The vulnerable code was introduced in commit 514ac99c64b2, "can: fix multiple delivery of a single CAN frame for overlapping CAN filters", on 2015-04-01. This commit added the struct uniqframe __percpu *uniq field. Versions v4.1 through present are affected.

A local attacker able to trigger the vulnerability can cause kernel memory corruption, potentially leading to elevation of privileges.

Reachability, however, requires a system with CAN support and an accessible physical CAN adapter/virtual CAN (vcan) interface. Relevant kernel configurations for reachability: CONFIG_CAN, CONFIG_CAN_RAW and CONFIG_CAN_VCAN. Note: By default CAP_NET_RAW is not required to use CAN_RAW sockets, unlike some other socket families.

Without an existing interface, an attacker would need to create a vcan interface which requires CAP_NET_ADMIN (i.e. unprivileged user namespaces are required for an attacker to set one up). Unprivileged user namespaces are typically restricted or disabled on the latest distributions, however some LTS distributions such as Ubuntu 22.04 still ship with them enabled by default, making this vulnerability reachable.

Unprivileged user namespaces expose a large attack surface, including this one, so it's worth checking whether they are restricted or not on your system (there's a few ways):

  • CONFIG_USER_NS determines if the feature is supported by the kernel

  • There is a kernel toggle to enable/disable them, cat /proc/sys/kernel/unprivileged_userns_clone, where 1=enabled.

  • There's a namespace limit, cat /proc/sys/user/max_user_namespaces, where 0=disabled.

  • Some distros (such as Ubuntu), may use AppArmor-based restrictions, cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns, where 1=restricted by policy. Note this policy may or may not be effective.

  • There is also cat /proc/sys/kernel/unprivileged_userns_apparmor_policy, where 1=restricted by policy.

  • If the command unshare -Ur true works, they are enabled for that context.

Remediation

After validating the bug, the following patch was proposed and accepted upstream:

From a535a9217ca3f2fccedaafb2fddb4c48f27d36dc Mon Sep 17 00:00:00 2001
From: Samuel Page <sam@bynar.io>
Date: Wed, 8 Apr 2026 15:30:13 +0100
Subject: [PATCH] can: raw: fix ro->uniq use-after-free in raw_rcv()

raw_release() unregisters raw CAN receive filters via can_rx_unregister(),
but receiver deletion is deferred with call_rcu(). This leaves a window
where raw_rcv() may still be running in an RCU read-side critical section
after raw_release() frees ro->uniq, leading to a use-after-free of the
percpu uniq storage.

Move free_percpu(ro->uniq) out of raw_release() and into a raw-specific
socket destructor. can_rx_unregister() takes an extra reference to the
socket and only drops it from the RCU callback, so freeing uniq from
sk_destruct ensures the percpu area is not released until the relevant
callbacks have drained.

Fixes: 514ac99c64b2 ("can: fix multiple delivery of a single CAN frame for overlapping CAN filters")
Cc: stable@vger.kernel.org # v4.1+
Assisted-by: Bynario AI
Signed-off-by: Samuel Page <sam@bynar.io>
Link: https://patch.msgid.link/26ec626d-cae7-4418-9782-7198864d070c@bynar.io
Acked-by: Oliver Hartkopp <socketcan@hartkopp.net>
[mkl: applied manually]
Signed-off-by: Marc Kleine-Budde <mkl@pengutronix.de>
---
 net/can/raw.c | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/net/can/raw.c b/net/can/raw.c
index eee244ffc31ecc..58a96e933debb5 100644
--- a/net/can/raw.c
+++ b/net/can/raw.c
@@ -361,6 +361,14 @@ static int raw_notifier(struct notifier_block *nb, unsigned long msg,
 	return NOTIFY_DONE;
 }

+static void raw_sock_destruct(struct sock *sk)
+{
+	struct raw_sock *ro = raw_sk(sk);
+
+	free_percpu(ro->uniq);
+	can_sock_destruct(sk);
+}
+
 static int raw_init(struct sock *sk)
 {
 	struct raw_sock *ro = raw_sk(sk);
@@ -387,6 +395,8 @@ static int raw_init(struct sock *sk)
 	if (unlikely(!ro->uniq))
 		return -ENOMEM;

+	sk->sk_destruct = raw_sock_destruct;
+
 	/* set notifier */
 	spin_lock(&raw_notifier_lock);
 	list_add_tail(&ro->notifier, &raw_notifier_list);
@@ -436,7 +446,6 @@ static int raw_release(struct socket *sock)
 	ro->bound = 0;
 	ro->dev = NULL;
 	ro->count = 0;
-	free_percpu(ro->uniq);

 	sock_orphan(sk);
 	sock->sk = NULL;
From a535a9217ca3f2fccedaafb2fddb4c48f27d36dc Mon Sep 17 00:00:00 2001
From: Samuel Page <sam@bynar.io>
Date: Wed, 8 Apr 2026 15:30:13 +0100
Subject: [PATCH] can: raw: fix ro->uniq use-after-free in raw_rcv()

raw_release() unregisters raw CAN receive filters via can_rx_unregister(),
but receiver deletion is deferred with call_rcu(). This leaves a window
where raw_rcv() may still be running in an RCU read-side critical section
after raw_release() frees ro->uniq, leading to a use-after-free of the
percpu uniq storage.

Move free_percpu(ro->uniq) out of raw_release() and into a raw-specific
socket destructor. can_rx_unregister() takes an extra reference to the
socket and only drops it from the RCU callback, so freeing uniq from
sk_destruct ensures the percpu area is not released until the relevant
callbacks have drained.

Fixes: 514ac99c64b2 ("can: fix multiple delivery of a single CAN frame for overlapping CAN filters")
Cc: stable@vger.kernel.org # v4.1+
Assisted-by: Bynario AI
Signed-off-by: Samuel Page <sam@bynar.io>
Link: https://patch.msgid.link/26ec626d-cae7-4418-9782-7198864d070c@bynar.io
Acked-by: Oliver Hartkopp <socketcan@hartkopp.net>
[mkl: applied manually]
Signed-off-by: Marc Kleine-Budde <mkl@pengutronix.de>
---
 net/can/raw.c | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/net/can/raw.c b/net/can/raw.c
index eee244ffc31ecc..58a96e933debb5 100644
--- a/net/can/raw.c
+++ b/net/can/raw.c
@@ -361,6 +361,14 @@ static int raw_notifier(struct notifier_block *nb, unsigned long msg,
 	return NOTIFY_DONE;
 }

+static void raw_sock_destruct(struct sock *sk)
+{
+	struct raw_sock *ro = raw_sk(sk);
+
+	free_percpu(ro->uniq);
+	can_sock_destruct(sk);
+}
+
 static int raw_init(struct sock *sk)
 {
 	struct raw_sock *ro = raw_sk(sk);
@@ -387,6 +395,8 @@ static int raw_init(struct sock *sk)
 	if (unlikely(!ro->uniq))
 		return -ENOMEM;

+	sk->sk_destruct = raw_sock_destruct;
+
 	/* set notifier */
 	spin_lock(&raw_notifier_lock);
 	list_add_tail(&ro->notifier, &raw_notifier_list);
@@ -436,7 +446,6 @@ static int raw_release(struct socket *sock)
 	ro->bound = 0;
 	ro->dev = NULL;
 	ro->count = 0;
-	free_percpu(ro->uniq);

 	sock_orphan(sk);
 	sock->sk = NULL;
From a535a9217ca3f2fccedaafb2fddb4c48f27d36dc Mon Sep 17 00:00:00 2001
From: Samuel Page <sam@bynar.io>
Date: Wed, 8 Apr 2026 15:30:13 +0100
Subject: [PATCH] can: raw: fix ro->uniq use-after-free in raw_rcv()

raw_release() unregisters raw CAN receive filters via can_rx_unregister(),
but receiver deletion is deferred with call_rcu(). This leaves a window
where raw_rcv() may still be running in an RCU read-side critical section
after raw_release() frees ro->uniq, leading to a use-after-free of the
percpu uniq storage.

Move free_percpu(ro->uniq) out of raw_release() and into a raw-specific
socket destructor. can_rx_unregister() takes an extra reference to the
socket and only drops it from the RCU callback, so freeing uniq from
sk_destruct ensures the percpu area is not released until the relevant
callbacks have drained.

Fixes: 514ac99c64b2 ("can: fix multiple delivery of a single CAN frame for overlapping CAN filters")
Cc: stable@vger.kernel.org # v4.1+
Assisted-by: Bynario AI
Signed-off-by: Samuel Page <sam@bynar.io>
Link: https://patch.msgid.link/26ec626d-cae7-4418-9782-7198864d070c@bynar.io
Acked-by: Oliver Hartkopp <socketcan@hartkopp.net>
[mkl: applied manually]
Signed-off-by: Marc Kleine-Budde <mkl@pengutronix.de>
---
 net/can/raw.c | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/net/can/raw.c b/net/can/raw.c
index eee244ffc31ecc..58a96e933debb5 100644
--- a/net/can/raw.c
+++ b/net/can/raw.c
@@ -361,6 +361,14 @@ static int raw_notifier(struct notifier_block *nb, unsigned long msg,
 	return NOTIFY_DONE;
 }

+static void raw_sock_destruct(struct sock *sk)
+{
+	struct raw_sock *ro = raw_sk(sk);
+
+	free_percpu(ro->uniq);
+	can_sock_destruct(sk);
+}
+
 static int raw_init(struct sock *sk)
 {
 	struct raw_sock *ro = raw_sk(sk);
@@ -387,6 +395,8 @@ static int raw_init(struct sock *sk)
 	if (unlikely(!ro->uniq))
 		return -ENOMEM;

+	sk->sk_destruct = raw_sock_destruct;
+
 	/* set notifier */
 	spin_lock(&raw_notifier_lock);
 	list_add_tail(&ro->notifier, &raw_notifier_list);
@@ -436,7 +446,6 @@ static int raw_release(struct socket *sock)
 	ro->bound = 0;
 	ro->dev = NULL;
 	ro->count = 0;
-	free_percpu(ro->uniq);

 	sock_orphan(sk);
 	sock->sk = NULL;

Notably, as mentioned in the impact section, if no CAN interface is present, restricting unprivileged user namespaces or disabling the CAN module are both effective mitigations and general attack surface reductions.

Wrap-up

I think this vulnerability is a good demonstration of what LLMs are capable of for vulnerability discovery and validation, and how those capabilities become more reliable and actionable when paired with the right orchestration and domain expertise.

From a discovery perspective, this is a non-trivial kernel race condition involving subtle lifetime issues around a kernel-specific API (RCU). Identifying it required reasoning about asynchronous teardown paths and per-CPU state, patterns that are difficult to detect with traditional tooling but which models can surface when guided with the right context.

However, as I discussed in my introductory post, LLMs (like fuzzers) can produce noise. Validation is an important part of the pipeline to reduce this noise. In this case, the validator was able to reliably trigger the race and demonstrate the use-after-free by understanding the gaps in existing instrumentation and developing a custom approach.

In the next part we'll dive into another kernel bug, CVE-2026-31694, in the FUSE subsystem, which was fully validated with a proof-of-concept demonstrating privilege escalation.

SPtAa0rUt4  lDo$oCkWiGn8g%  aKtA  yFoNuBr#  sEo&f&tQwHa2r&eO  c0rPi%tXiDc4a&l1lGy$.U

request briefing

request briefing

SCt9a8rMtJ  l9oIoCk#iKnIg2  aYt$  yHoKuSrM  sXo1fKt3wSa#r2eR  cFr8iWtQiGc9a8lUl0yB.8

request briefing

request briefing

S&tFa4rAtM  lQoXoIk1i1nNgH  aAtT  yRoWu1rE  sMoFfCtFwAaXr$eL  cOr&iCt1i@c0aLl&lHy8.@

request briefing

request briefing

BYNARIO s.r.l. | PIAZZA BORROMEO 12, 20129 MILAN, ITALY | VAT- IT14434720968

all rights reserved

2026

BYNARIO s.r.l. | PIAZZA BORROMEO 12, 20129 MILAN, ITALY | VAT- IT14434720968

all rights reserved

2026

BYNARIO s.r.l. | PIAZZA BORROMEO 12, 20129 MILAN, ITALY | VAT- IT14434720968

all rights reserved

2026