CVE-2026-98256 in Linux
Сводка
по VulDB • 06.10.2026
В ядре Linux была устранена следующая уязвимость:
signal: Предотвращение гонки (race condition) при вызове exec()
Хёнву отладил следующий сбой KASAN UAF (use-after-free):
BUG: KASAN: slab-use-after-free in __send_signal_locked+0xb27/0xba0 Write of size 8 at addr ffff888007ed80c8 by task poc/79 ... Call Trace: __send_signal_locked+0xb27/0xba0 do_send_sig_info+0xa7/0x160 do_send_specific+0x76/0xa0 __x64_sys_tgkill+0x193/0x270 ... Allocated by task 80: do_timer_create+0x1a4/0x1030 __x64_sys_timer_create+0x145/0x190 ... Freed by task 12: kmem_cache_free_bulk+0x1f8/0x4a0 kvfree_rcu_bulk+0x14f/0x1c0 kfree_rcu_work+0x128/0x1a0 ... Last potentially related work creation: kvfree_call_rcu+0x39/0x390 __flush_itimer_signals+0x211/0x320 flush_itimer_signals+0x47/0x90 begin_new_exec+0xa6b/0x28c0
Выяснилось, что это происходит при вызове exec() не для лидера (non-leader), как объяснил Хёнву:
de_thread() вызывает exchange_tids() до release_task(leader), поэтому struct pid, удерживаемый таймером SIGEV_THREAD_ID, созданным относительно tid лидера, теперь указывает на поток, который вызвал execve(). Функция pid_task() возвращает этот поток, и lock_task_sighhand() для него выполняется успешно.
Если сигнал от таймера заблокирован (blocked), его sigqueue остается в очереди task::pending лидера. Следующее срабатывание этого таймера может произойти во время выполнения release_task(), который очищает очередь.
posixtimer_send_sigqueue() проверяет, находится ли уже sigqueue в очереди, используя plain list_empty(), которая только читает list_head::next. Функция list_del_init() не является атомарной, а INIT_LIST_HEAD() записывает значение list_head::next до list_head::prev, поэтому проверка может пройти успешно между этими операциями. list_add_tail() добавляет запись в task::pending живого потока, и сохранение значения list_head::prev из операции flush затем перезаписывает ссылку list_head::prev, которую только что установила list_add_tail().
__flush_itimer_signals() также не отменяет это действие. При указании list_head::prev на саму запись, ее list_del_init() просто записывает те же значения снова, поэтому запись не удаляется из списка. Она остается там после того, как будет сброшено последнее упоминание (reference), и таймер освобожден с помощью RCU, а вызов list_add_tail() от последующего tgkill следует по этой ссылке list_head::prev в уже освобожденный таймер.
Эта проблема проявилась после недавнего коммита, который переместил очистку sigqueue за пределы области блокировки sighand.
Хёнву предложил исправить это с помощью использования list_del_init_careful(), но это лишь маскирует проблему. После некоторых обсуждений и различных попыток решить ее Эрик указал, что нет причин очищать task::pending поздно в release_task(), и это должно выполняться уже в exit_signals().
Поскольку ничего не может собирать и доставлять сигналы, находящиеся в очереди pending умирающего процесса (task), нет причин откладывать эту операцию.
Но необходимо обеспечить отсутствие возможности постановки новых сигналов в очередь после этой точки. exit_signals() устанавливает флаг PF_EXITING в task::flags, который можно использовать как индикатор для этого.
Исправление заключается в следующем:
- Предотвращение постановки в очередь сигналов для приватных процессов (PIDTYPE_PID), когда у процесса установлен флаг PF_EXITING, в функциях __send_signal_locked() и posixtimer_send_sigqueue(). - Защита неблокируемой установки флага PF_EXITING в exit_signals() для случая пустой группы задач и выхода из группы с использованием блокировки sighand.
- Очистка сигналов task::pending непосредственно там же.
Оптимизация путем перемещения всего списка pending в заголовочную структуру на стеке под блокировкой sighand и освобождения сигналов без удержания этой блокировки.
Существует довольно много обсуждений относительно неблокируемой очистки (lockless flush) и случая non-leader exec на системах с слабой упорядоченностью памяти (weakly ordered systems). Проблема заключается в том, что сторонняя сторона, пытающаяся отправить сигнал posix timer, полагается на поиск PID для нахождения целевого процесса, и этот поиск может привести к новому лидеру, если изначально сигнал был направлен старому лидеру. В случае, когда сигнал был поставлен в очередь старого лидера, неблокируемая очистка вызывает опасения относительно следующей ситуации:
old_leader new_leader third party
A: flush_list() // list_del_in ---truncated---
Be aware that VulDB is the high quality source for vulnerability data.