Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2023-2598 — Technical analysis and proof-of-concept exploit for CVE-2023-2598, a Linux kernel privilege escalation vulnerability in io_uring's buffer registration, with detailed explanation of Compound Page and folio internals. | Kitploit
Tools/GitHubGitHub/cainiao159357/cve-2023-2598
Privilege EscalationVulnerability AnalysisExploitationPapers & ResearchLearning & EducationBinary Exploitation
GitHubcainiao159357/cve-2023-2598

CVE-2023-2598

Technical analysis and proof-of-concept exploit for CVE-2023-2598, a Linux kernel privilege escalation vulnerability in io_uring's buffer registration, with detailed explanation of Compound Page and folio internals.

View Repository
52 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2023-2598 Privilege Escalation

Understand the Compound Page and folio mechanisms in Linux through CVE-2023-2598, and subsequently see if it's possible to complete the exploitation of the 1day CVE-2023-6560.

Compound Page (huge page)

Memory is growing larger and larger, but the basic page allocation unit of Linux is still 4K, which becomes insufficient. Therefore, compound pages are introduced to solve this problem. A compound page is essentially a collection of multiple pages, combining two or more physically contiguous pages into a unit, which in many aspects can be treated as a single larger page. They are most commonly used to create huge pages, used in hugetlbfs or transparent huge pages subsystems, but they also appear in other scenarios. Compound pages can be used as anonymous memory or as buffers in the kernel; however, they cannot appear in the page cache, which can only handle single pages.

Allocating a compound page involves calling alloc_pages() with the __GFP_COMP allocation flag set and a page frame number greater than 1, i.e., an order of at least 1. This is determined by the compound page implementation mechanism.

Note: Compound pages are always physically contiguous.

The flag in the first page will mark PG_head, indicating it is the head page of the compound page;

All subsequent pages will configure two properties: mapping and compound_head, and through compound_head it is determined whether it is a tail page or head page, see compound_head() function for details;

The second page stores more information about the compound page, which is also why the order of a compound page is at least 1;

static inline unsigned long _compound_head(const struct page *page)
{
        unsigned long head = READ_ONCE(page->compound_head);
 
        if (unlikely(head & 1))
                return head - 1;
        return (unsigned long)page;
}

It can be seen that this field contains not only the flag but also a pointer to the head page.

So when a page is obtained, it is easy to determine whether it is a compound page, and if so, whether it is a head page or a tail page. However, there is still a crucial piece of information missing: the size of the compound page. If the size is unknown, it must be known when freeing this compound page. This information is all stored in the lru field of the first tail page: the size (order) of the compound page is first forcibly cast to a pointer type and stored in lru.prev, and the destructor is stored in lru.next.

As long as the head page and the size of the compound page are known, the huge page can be correctly freed, because compound pages are always physically contiguous.

The structure is shown in the following figure:

img

folio

A folio can be seen as a wrapper around a page, without overhead. A folio can be a single page or a compound page.

img

The above figure is a schematic of the page structure, 64 bytes managing information such as flags, lru, mapping, index, private, {ref_, map_}count, memcg_data, etc. When a page is a compound page, the above flags and other information reside in the head page, while the tail page reuses fields such as compound_{head, mapcount, order, nr, dtor} for management.

struct folio {
        /* private: don't document the anon union */
        union {
                struct {
        /* public: */
                        unsigned long flags;
                        struct list_head lru;
                        struct address_space *mapping;
                        pgoff_t index;
                        void *private;
                        atomic_t _mapcount;
                        atomic_t _refcount;
#ifdef CONFIG_MEMCG
                        unsigned long memcg_data;
#endif
        /* private: the union with struct page is transitional */
                };
                struct page page;
        };
};

In the definition of the folio structure, the fields flags, lru, etc. are exactly the same as in the page structure, so they can be unioned with the page. This allows direct use of folio->flags instead of folio->page->flags.

#define page_folio(p)           (_Generic((p),                          \
        const struct page *:    (const struct folio *)_compound_head(p), \
        struct page *:          (struct folio *)_compound_head(p)))

#define nth_page(page,n) ((page) + (n))
#define folio_page(folio, n)    nth_page(&(folio)->page, n)

At first glance, page_folio might be confusing, but it is equivalent to:

switch (typeof(p)) {
  case const struct page *:
    return (const struct folio *)_compound_head(p);
  case struct page *:
    return (struct folio *)_compound_head(p)));
}

From the page_folio macro definition, it can be seen that a folio is actually a head page of a compound page. When converting a folio to a page, folio->page is used to obtain the head page, and folio_page(folio, n) can be used to obtain a tail page.

So what is the purpose of folio? More importantly, it is for development and efficiency considerations. Without folio, the function cannot determine whether the current page is a head page, so it would call _compound_head. If there are many execution paths, calling _compound_head in every function on the path would affect efficiency. However, if the function only accepts a struct folio * parameter, this folio points to the head page, so the function no longer needs to call _compound_head internally.

Therefore, there are three main functions:

  1. Reduce excessive redundant calls to _compound_head.
  2. Provide a hint to developers: seeing a folio confirms it is a head page.
  3. Fix potential bugs caused by tail pages.

Vulnerability Principle

In io_uring's io_uring_register_buffer, there is a logic section:

image-20240830214419290

When the pages passed from user space exceed 1, io_uring checks whether the passed buffer is a folio. The method is to use page_folio() to obtain the head page of page[i]. If the head page of page[i] equals page[0], it is considered to belong to the same compound page table.

Generally, this handling is fine, but there is a special case: if the user space uses mmap to map the same physical page table into contiguous virtual addresses, it also meets this judgment condition, and will eventually enter this branch:

image-20240830225137539

At this point, the user space only allocates one physical page, but the final size is the size of the contiguous virtual addresses, resulting in the size being potentially larger than the actual allocated physical address area. Ultimately, this leads to out-of-bounds read/write.

Exploitation

Spray cred, then use this out-of-bounds read/write interface to modify the uid.

Compared to the exp online, this exp modifies the uid, so there is no address dependency. Any system with this vulnerability can use this exp.

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <liburing.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <mqueue.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <assert.h>
Download Tool