Исследователи из VEGA нашли в ядре Linux уязвимость GhostLock (CVE-2026-43499). Она живёт во всех крупных дистрибутивах с 2011 года — её завезли в Linux 2.6.39 вместе с переработкой rtmutex, а починили только в Linux 7.1. Для срабатывания не нужны ни особая конфигурация ядра, ни привилегии. Google оценил стабильный эксплойт в $92,337.
Ошибка в функции remove_waiter() в kernel/locking/rtmutex.c. Изначально её писали для одного сценария: поток сам блокируется и сам же чистит за собой. Она всегда чистила current->pi_blocked_on, полагая, что current и есть владелец waiter. Но Requeue-PI ломает это допущение: rt_mutex_start_proxy_lock() ставит waiter за другой поток, а на откате remove_waiter() чистит pi_blocked_on не у того потока. В результате waiter просыпается с висячим указателем на собственный стековый фрейм.
Чтобы запустить цепочку, нужно три futex-слова и три потока. waiter блокируется на FUTEX_WAIT_REQUEUE_PI, owner держит целевой PI-futex, а главный поток делает FUTEX_CMP_REQUEUE_PI. Когда обход цепочки упирается в deadlock, срабатывает баг. После этого waiter сидит в userspace с pi_blocked_on, указывающим в освобождённый стек. Окно UAF не ограничено по времени — sched_setattr() запускает цепочку когда угодно.
Чтобы подсунуть поддельный rt_mutex_waiter на то же место в стеке, используют prctl(PR_SET_MM, PR_SET_MM_MAP). Он копирует auxv в стековый буфер, который накладывается поверх старого фрейма. Через дребезг страниц с fallocate(PUNCH_HOLE) растягивают окно copy_from_user.
Сфабрикованный waiter превращается в ограниченную запись: rb-дерево при удалении пишет один дочерний указатель (W0_BASE) в корень. Цель — inet6_protos[IPPROTO_UDP], потому что соседние поля подходят под жестокие ограничения (разблокированный спин-лок, нулевые rb_leftmost и owner). После записи туда летит указатель на CPU entry area (CEA). CEA — предсказуемое место: до Linux 6.2 адрес фиксирован, после — адрес в direct-map вычисляется из physmap base, который утекается через prefetch.
В CEA размещают фейковый inet6_protocol с handler, ведущим на JOP-гаджет. Отправка IPv6 UDP-пакета на loopback дёргает этот handler и даёт управление. Из-за нехватки места для полноценного ROP используют DirtyMode: короткая цепочка переписывает биты режима core_pattern через coredump_sysctls[1].mode. После этого world-writable core_pattern позволяет подсунуть |/proc/%P/fd/666 %P — и при краше ядро запускает бинарник от root.
Исправление простое: в remove_waiter() берут waiter->task->pi_lock и чистят pi_blocked_on у правильного потока. Защита RANDOMIZE_KSTACK_OFFSET ломает привязку к стеку (шанс угадывания 1/32), но в целевых образах kernelCTF она выключена. STATIC_USERMODE_HELPER перекрыл бы DirtyMode, но сама идея универсальна — подойдёт любой sysctl-переключатель в предсказуемом сегменте данных.