Breaking News
Menu
Advertisement

CVE-2026-31532: How an LLM Discovered a Critical CAN Use-After-Free Flaw in the Linux Kernel

CVE-2026-31532: How an LLM Discovered a Critical CAN Use-After-Free Flaw in the Linux Kernel
AI Image Generated
Advertisement

Table of Contents

A critical use-after-free vulnerability tracked as CVE-2026-31532 in the Linux kernel's Controller Area Network (CAN) subsystem exposes systems to local privilege escalation. Discovered autonomously by Bynario's LLM-driven security pipeline using the Opus 4.6 model, the flaw resides in the net/can raw socket implementation. This vulnerability allows a concurrent frame reception to access freed per-CPU state due to missing Read, Copy, Update (RCU) synchronization during socket teardown.

CAN is a networking technology heavily utilized in automotive systems, industrial automation, and embedded devices to connect electronic control units (ECUs). Because the CONFIG_CAN module and virtual CAN (CONFIG_VCAN) are enabled by default in most major Linux distributions, the attack surface extends to standard desktop and server environments where virtual interfaces can be created.

The Anatomy of the CAN Use-After-Free Race

The vulnerability stems from a race condition in the CAN raw socket teardown process (raw_release()). When a CAN raw socket is created, it registers a per-filter receive callback, raw_rcv(). The socket structure includes a per-CPU allocation called uniq, which is used to deduplicate matches when a single frame is matched by multiple filters.

/* 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; };

During socket creation in raw_init(), this per-CPU object is allocated:

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;

The critical error occurs during the teardown phase in raw_release(). The function calls raw_disable_allfilters(), which iterates through registered filters and triggers can_rx_unregister(). This unregistration removes the receiver from the RCU-protected hash list and schedules deletion asynchronously via call_rcu().

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]

Because call_rcu() is asynchronous, it waits for the current RCU grace period to end before freeing the receiver. However, raw_release() does not wait. It proceeds directly to free_percpu(ro->uniq). If frames are still being received, raw_rcv() may execute concurrently on another CPU inside an rcu_read_lock(), leading directly to a use-after-free scenario when it attempts to read or write to the freed uniq allocation.

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; }

// ... }

Validation and Custom Instrumentation

Validating this vulnerability presented a unique challenge. The use-after-free object was a per-CPU allocation, which is generally not instrumented by the Kernel Address Sanitizer (KASAN). To prove the vulnerability, Bynario's validator compiled the kernel with custom instrumentation, adding a logical marker (uniq_freed) to confirm that raw_rcv() accesses the memory after it has been freed.

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 custom patch, the proof-of-concept (PoC) successfully triggered the race condition by running multiple sender threads alongside racer threads that repeatedly created and closed short-lived CAN raw sockets.

[*] 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

How to Protect Your System

The vulnerable code was introduced in commit 514ac99c64b2 back in April 2015, meaning Linux kernel versions 4.1 and newer are affected. To exploit this, an attacker needs an accessible physical CAN adapter or a virtual CAN (vcan) interface. If no interface exists, they must create one, which requires CAP_NET_ADMIN privileges. To secure your infrastructure, follow these steps:

  • Apply the Upstream Patch: The official fix moves free_percpu(ro->uniq) out of raw_release() and into a raw-specific socket destructor (raw_sock_destruct). This ensures the per-CPU area is not released until all relevant RCU callbacks have drained. The patch is available via the official kernel mailing list.
  • Restrict Unprivileged User Namespaces: If patching immediately is not feasible, restrict unprivileged user namespaces. Attackers rely on these namespaces to create virtual CAN interfaces. You can check your system's status by running cat /proc/sys/kernel/unprivileged_userns_clone (a value of 1 means it is enabled).
  • Disable the CAN Module: If your server or desktop environment does not require CAN networking, disable the CONFIG_CAN and CONFIG_VCAN modules entirely to eliminate the attack surface.

The Shift Toward AI-Driven Vulnerability Discovery

The discovery of CVE-2026-31532 highlights a significant leap in how we approach kernel security. Traditional fuzzers and static analysis tools often struggle with complex, asynchronous teardown paths and per-CPU state tracking, primarily because these mechanisms rely heavily on kernel-specific APIs like RCU. By leveraging an LLM-driven pipeline, Bynario was able to reason through the logical lifetime of the uniq allocation across multiple CPU cores.

However, the real breakthrough here isn't just the discovery - it's the automated validation. Because standard tools like KASAN fail to instrument per-CPU allocations effectively, a raw LLM output would typically result in an unverified, noisy alert. By autonomously writing custom kernel instrumentation to prove the race condition, the pipeline bridged the gap between theoretical code analysis and actionable security engineering. As local models become more sophisticated, we can expect them to uncover decades-old logic flaws that traditional tooling has consistently overlooked.

Sources: bynar.io ↗
Did you like this article?
Advertisement

Popular Searches