
Introduction
In november 2025 I started a fuzzing campaign against cryptodev-linux as part of a school project. I found +10 bugs (UAF, NULL pointer dereferences and integer overflows) and among all of these bugs one was surprisingly suitable for a privilege escalation.
For a little bit of background, according to their github page:
This is a /dev/crypto device driver, equivalent to those in OpenBSD or FreeBSD. The main idea is to access existing ciphers in kernel space from userspace, thus enabling the re-use of a hardware implementation of a cipher.
Cryptodev-linux is not widely used today, but it was popular when the native kernel crypto (socket) API was slower. Nowadays it is supported and included in various frameworks and projects such as: dpdk, OpenEmbedded and kobol NAS.
Basic design
The cryptodev API is quite straightforward. You first create a session using the file descriptor of the /dev/crypto device, specifying the encryption type and the key size:
1 | session.cipher = CRYPTO_AES_CBC; |
Then, you can start encrypting data like this:
1 | cryp.ses = session.ses; // session id |
The actual encryption logic for zero copy encryptions is located in the __crypto_run_zc function:
1 | /* This is the main crypto function - zero-copy edition */ |
hash_n_crypt is basically just using the internal crypto drivers of the linux kernel.
release_user_pages is a key part of the exploitation process, it is iterating through the userland pages provided by the user (the src and dst buffers) and is calling put_page on it. Notably, ses->pages is not cleared out.
1 | void release_user_pages(struct csession *ses) |
These pages (ses->pages) are returned by get_user_pages_remote in __get_userbuf. __get_userbuf is returning an array of struct page*, what happens is that, to be able to handle the userland pages, the linux crypto drivers need to have a proper scatterlist initialized such as each node contains a reference to the target userland page.
By returning this array, get_user_pages_remote increments the refcount of each of these pages. So what happens is that release_user_pages is releasing these references towards the struct page* used during the encryption request. And releasing references means basically to decrement the reference counter.
The bug
The exploitable bug we will be focusing on in this blogpost lies in the get_userbuf function:
1 |
|
There are actually a lot of issues with this function, including tons of integer overflows, but what is interesting is that if the destination exists, is and invalid and that the src is NULL for example, then the last call to __get_userbuf will fail and call release_user_pages at line 77.
At this point, ses->used_pages contains the number of pages of the destination buffer given src != dst.
The bug is basically that release_user_pages is called while ses->pages hasn’t been modified and that ses->used_pages exists. It leads to a double free. Not exactly a double free, it allows an attacker to decrement the reference counter of a userland page he controls as many times as he want to.
When the reference counter hits zero the page gets freed, it gets freed while we can still access it through the PTEs of our process which is a very powerful UAF primitive.
Exploitation
Now I described the bug, we can have fun exploiting this powerful primitive!
The exploitation strategy is actually quite simple: triggering a slab request for our set of freed pages so we can hijack interesting structures.
When a page is freed, it is initially sent back to the Per-CPU Page allocator (PCP), which is not ideal if we want to reallocate it as a slab. When a slab requires more memory, it allocates pages through the buddy allocator. Therefore, the first step is to return our pages to the buddy allocator. To achieve this, we must flush the PCP, which typically occurs when a large volume of pages is freed simultaneously.
To trigger this, we can allocate a large number of pages prior to triggering the bug, and immediately afterward, free the entire set of allocated pages:
1 |
|
This will successfully flush the vulnerable pages back to the buddy allocator.
Page migration
This is the only tricky part of the exploit, because slabs are typically allocated using GFP_KERNEL. For example, a struct file* is allocated this way, which causes the buddy allocator to look for pages that are unmovable (MIGRATE_UNMOVABLE). This logic is handled within __rmqueue:
1 |
|
It will first look for pages of the same order and migratetype, then it will look for pages of higher order (increasing the risk of fragmentation) with the same migratetype, and finally, it will look for pages of a different migratetype. This is exactly what we want for our exploit.
To address this issue, we just need to adjust one thing: the spray of userland pages in our process. I mentioned page migration from MIGRATE_UNMOVABLE requests to MIGRATE_MOVABLE, but the opposite is also true. If we allocate a large number of pages in our process, it will exhaust the MIGRATE_UNMOVABLE buddy allocator freelists as well. Consequently, when we start spraying the target objects, the RMQUEUE_STEAL switch case will be reached quickly.
struct file spraying
I am not sure about the official name of this technique. Kuzey calls it DirtyCred, but to me, DirtyCred implies struct cred * swapping, which is not what is happening here. However, a file-based DirtyCred method was previously demonstrated by StarLabs. Regardless, I am using the technique described in the Exodus Intelligence blog post.
The technique is straightforward: once we have successfully sprayed enough struct file objects in memory, we search for the ext4_file_operations pointer pattern, which corresponds to the second field of the struct file. Once found, we simply modify the file mode. This allows us to write to the file even if it was originally opened with read-only permissions.
A good target file would be /etc/passwd, as a non-root user we are allowed to open it in read only and thanks to this technique we can write arbitrary content to this file:
1 | for (int i = 0; i < 10485; i++) { |
If we failed to spray correctly the struct file, nothing stops us to try it again, and again. Which makes the exploit pretty stable!
Which gives:
1 | nasm@syzkaller:~$ ./poc |
You can find the final exploit code here.
Misc
I used the following kernel options to compile my kernel:make defconfig && make kvm_guest.config && ./scripts/config -e CONFIG_DEBUG_INFO_DWARF4 -e CONFIG_CONFIGFS_FS && make olddefconfig
You can download the kernel source from https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.15.4.tar.gz:
1 | 6bfb8a8d4b33ddbec44d78789e0988a78f5f5db1df0b3c98e4543ef7a5b15b97 linux-6.15.4.tar.gz |
I used the following qemu options:
1 | qemu-system-x86_64 \ |
You might have to adjust the amount of pages you spray according to the amount of memory / cores of the target system.