CVE-2026-93241 in Linux
Сводка
по VulDB • 24.09.2026
В ядре Linux была устранена следующая уязвимость:
memcg: обход механизма вытеснения (reclaim) и OOM-убийцы для завершающихся задач после завершения работы oom_reaper
В компании Meta мы наблюдаем случаи, когда задача, убитая из-за нехватки памяти (OOM), зависает на этапе выхода в течение нескольких часов. В одном конкретном случае процесс застрял более чем на 8 часов, и мне пришлось вручную удалить ограничения memory.max, чтобы позволить процессу завершиться.
Задача представляла собой однопроцессный job с лимитом ~55 GiB для memory.max и включенным zswap. У него было почти 0 anon в памяти и ~111 GiB в сжатом виде zswap (в пуле zswap занималось около ~51 GiB, то есть практически вся память.current приходилась на zswap). На списках LRUs для вытеснения ничего не оставалось.
При более детальном осмотре я обнаружил, что примерно 20 тысяч потоков этого процесса зависли со следующим стеком вызовов:
[<0>] mem_cgroup_out_of_memory+0x4e/0xa0
[<0>] charge_memcg+0x8bf/0x990
[<0>] mem_cgroup_swapin_charge_folio+0x4e/0x80
[<0>] __read_swap_cache_async+0x10c/0x260
[<0>] swapin_readahead+0x116/0x3f0
[<0>] do_swap_page+0x13c/0x1ce0
[<0>] handle_mm_fault+0x61d/0x11f0
[<0>] do_user_addr_fault+0x3e7/0x6d0
[<0>] exc_page_fault+0x8f/0x110
[<0>] asm_exc_page_fault+0x22/0x30
[<0>] __get_user_8+0x14/0x20
[<0>] futex_cleanup+0x27/0x1c0
[<0>] futex_exit_release+0x47/0x60
[<0>] do_exit+0x107/0x940
[<0>] do_group_exit+0x81/0xa0
[<0>] get_signal+0x2b1/0x6e0
[<0>] arch_do_signal_or_restart+0x1a/0x1c0
[<0>] exit_to_user_mode_loop+0xa8/0x1c0
[<0>] do_syscall_64+0x152/0x250
[<0>] entry_SYSCALL_64_after_hwframe+0x4b/0x53
Кроме того, журнал dmesg был заполнен сообщениями «Out of memory and no killable processes...» (Нехватка памяти и нет процессов для убийства).
Я не понимаю, почему oom_reaper не смог завершить процесс или размапить его память. Моя гипотеза заключается в том, что поскольку oom_reaper пытается получить mmap_lock в режиме чтения ограниченное количество раз, а затем сдается, возможно, существует поток этого процесса, который имел mmap_lock в режиме записи в тот момент.
Моя первоначальная подозрительность была направлена на futex_cleanup и ошибку страницы ядра (kernel page fault), вызывающие бесконечные повторные попытки обработки ошибок и начисления ресурсов, но это было опровергнуто в предыдущих обсуждениях аналогичной проблемы [1].
Моя текущая теория заключается в том, что это просто медленная сериализация за oom_lock. В отличие от аллокатора страниц, код charge_memcg для memcg получает oom_lock без использования «try» (попытки). Хотя код OOM для memcg использует mutex_lock_killable(), следует отметить, что в стеке вызовов get_signal() потребляет сигнал SIGKILL (или вызывает sigdelset(SIGKILL)) перед вызовом do_group_exit(). Следовательно, здесь mutex_lock_killable() работает как обычный mutex_lock(). Таким образом, десятки тысяч потоков ожидают oom_lock и по очереди получают -EFAULT от get_user() в коде очистки futex и завершаются.
Обсуждение из [1] привело к коммиту a75ffa26122b («memcg, oom: do not bypass oom killer for dying tasks»), который направляет завершающиеся задачи через путь OOM именно для того, чтобы oom_reaper мог завершить их mm и асинхронно освободить память. Однако reaper работает по принципу «best-effort» (наилучшее возможное) и является одноразовым: если он не может получить mmap_lock в режиме чтения (например, потому что соседний поток держит его для записи), он устанавливает MMF_OOM_SKIP и больше не повторяет попытки, оставляя только медленную сериализованную синхронную очистку через oom_lock.
После установки MMF_OOMSKIP асинхронное вытеснение (reclaim) для mm прекращается, поэтому завершающаяся задача, запрашивающая ресурсы у него, больше не имеет ничего, на что можно было бы ждать: она освобождает свою память только после завершения выхода. Выполнение reclaim и OOM-убийцы (без жертвы) для нее бессмысленно, а выполнение этих операций для десятков тысяч завершающихся потоков именно то, что сериализует их за oom_lock. Поэтому перед выполнением reclaim, если текущий процесс является жертвой OOM, чей reaper уже завершился, необходимо отказать в начислении ресурсов (fail the charge).
Воспроизведено с 20 тысячами потоков, каждый из которых паркует надежную голову futex на своей собственной zswapped странице; задача была убита группой OOM, пока соседний поток держал mmap_lock для записи, поэтому reaper сдался и установил MMF_OOM_SKIP. Тестирование проводилось на версии next-20260728: базовая версия показывала время выхода около 90 секунд, а с патчем время выхода сократилось до ~3 секунд.
Be aware that VulDB is the high quality source for vulnerability data.