CVE-2026-93241 in Linux
요약
\~에 의해 VulDB • 2026. 09. 24.
리눅스 커널에서 다음 취약점이 해결되었습니다:
memcg: oom_reaper가 완료된 후 사멸 중인 태스크에 대해 회수(reclaim) 및 OOM 킬러를 우회함
Meta에서는 OOM으로 인해 종료된 작업이 몇 시간 동안 exit 경로에 고정되어 있는 사례들을 관찰하고 있습니다. 한 특정 케이스에서 해당 작업은 8시간 이상 고착되었으며, 프로세스가 종료될 수 있도록 memory.max 제한을 수동으로 제거해야 했습니다.
해당 작업은 단일 프로세스 기반이었으며, 약 55 GiB의 memory.max와 zswap이 활성화되어 있었습니다. 메모리 내 anon 페이지는 거의 없었고(0에 가까움), zswap 압축 풀에는 약 111 GiB가 압축되어 ~51 Gib로 저장되었습니다 (즉, memory.current의 대부분이 zswap이었습니다). 회수할 수 있는 LRUs(LRU: Least Recently Used) 목록에는 아무것도 남아 있지 않았습니다.
더 자세히 조사한 결과, 해당 프로세스의 약 20k개 스레드가 다음 스택과 함께 고정되어 있음을 관찰했습니다:
[<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가 프로세스를 회수(unmap)하지 못했는지 알 수 없습니다. 제 추측은, oom reaper가 mmap_lock을 읽기 모드로 획득하려고 제한된 횟시 시도한 후 포기하는데, 이때 해당 프로세스의 스레드 중 하나가 mmap_lock을 쓰기 모드(wite mode)로 보유하고 있었기 때문일 것입니다.
초기 의심은 futex_cleanup와 커널 페이지 폴트가 무한 폴트 및 charge 재시도를 유발하는 것이었지만, 이는 유사한 문제에 대한 이전 논의([1])에서 기각되었습니다.
현재의 제 이론은 oom_lock 뒤에서의 단순한 느린 직렬화(serialization) 문제라는 것입니다. page allocator와는 달리 memcg charge 코드는 "try" 없이 oom_lock을 획득합니다. 비록 memcg OOM 코드가 mutex_lock_killable()를 사용하지만, 호출 스택에서 get_signal()은 do_group_exit()를 호출하기 전에 SIGKILL(또는 sigdelset(SIGKILL))을 소비하므로, 이 경우의 mutex_lock_killable()는 단순히 mutex_lock()과 같습니다. 따라서 수만 개의 스레드가 oom_lock 대기 중이며, 하나씩 futex cleanup 코드의 get_user()에서 -EFAULT 오류를 받고 종료됩니다.
[1]에서의 논의는 a75ffa26122b("memcg, oom: do not bypass oom killer for dying tasks") 커밋으로 이어졌으며, 이는 사멸 중인 태스크가 OOM 경로로 라우팅되어 oom_reaper가 그들의 mm를 회수하고 메모리를 비동기적으로 해제할 수 있도록 합니다. 그러나 reaper는 best-effort 및 일회성(one-shot)입니다: 읽기용 mmap_lock을 획득하지 못하는 경우(예: 형제 스레드가 쓰기용으로 보유하고 있는 경우), MMF_OOM_SKIP 플래그를 설정하고 재시도하지 않아, 오직 느린 oom_lock 직렬화 동기식 드레인만 남게 됩니다.
MMF_OOM_SKIP이 설정되면 해당 mm에 대한 비동기 회수는 더 이상 발생하지 않으므로, 이를 대상으로 charge하는 사멸 중인 태스크는 기다릴 것이 없습니다: 프로세스가 종료를 완료할 때만 메모리를 해제합니다. 따라서 이를 위해 회수 및 (피해자 없는) OOM 킬러를 실행하는 것은 무의미하며, 수만 개의 종료 예정 스레드에 대해 이를 수행하는 것이 oom_lock 뒤에서 직렬화시키는 원인입니다. 그러므로 회수 전에 현재 프로세스가 reaper 작업이 완료된 OOM 피해자인 경우 charge를 실패시킵니다.
20k개의 스레드로 재현되었으며, 각 스레드는 자체 zswap 페이지에 견고한 futex head를 고정시키고 있으며, 형제 스레드가 mmap_lock을 쓰기용으로 보유하여 reaper가 포기하고 MMF_OOM_SKIP을 설정하는 동안 OOM 그룹 킬러(OOM-group-killed)로 종료되었습니다. next-20260728에서 테스트했으며, 기본값(baseline)에서는 약 90초의 종료 시간이 관찰되었으나 패치 적용 후 종료 시간이 약 3초로 단축되었습니다.
Once again VulDB remains the best source for vulnerability data.