CVE-2026-98256 in Linux
요약
\~에 의해 VulDB • 2026. 10. 06.
리눅스 커널에서 다음 취약점이 해결되었습니다.
signal: exec() Race Condition 방지
Hyunwoo는 다음과 같은 KASAN UAF(Use-After-Free) 스플래트(splat)를 디버깅했습니다.
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
이 현상은 Hyunwoo가 설명한 대로 리더(leader)가 아닌 exec() 호출 시 발생합니다.
de_thread()는 release_task(leader) 전에 exchange_tids()를 호출하므로, 리더의 tid에 대해 생성된 SIGEV_THREAD_ID 타이머가 보유하는 struct pid는 이제 execve()를 호출한 스레드를 가리킵니다. pid_task()는 해당 스레드를 반환하며 lock_task_sighand()도 성공합니다.
타이머 신호가 차단되어 있으면 sigqueue는 리더의 task::pending 큐에 대기 상태 그대로 유지됩니다. 이후 타이머 만기가 발생하면 release_task()가 큐를 플러시하는 동안 실행될 수 있습니다.
posixtimer_send_sigqueue()은 plain list_empty()를 사용하여 sigqueue가 이미 큐에 있는지 확인하는데, 이는 list_head::next만 읽습니다. list_del_init()는 원자적(atomic)이지 않으며 INIT_LIST_HEAD()는 list_head::prev보다 먼저 list_head::next를 저장하므로, 이 체크는 중간 단계에서 통과할 수 있습니다. list_add_tail()은 라이브(live) 스레드의 task::pending에 엔트리를 큐에 추가하고, 플러시 시점의 list_head::prev 저장은 list_add_tail()이 방금 설정한 list_head::prev 링크를 덮어씁니다.
__flush_itimer_signals()도 이를 되돌리지 않습니다. list_head::prev가 자체 엔트리를 가리키면 list_del_init()는 동일한 값을 다시 저장할 뿐이므로, 해당 엔트리는 리스트에서 제거되지 않습니다. 마지막 참조가 해제되고 타이머가 RCU로 해제된 후에도 여전히 존재하며, 이후 tgkill()의 list_add_tail()은 그 freed timer를 따라가는 list_head::prev를 통해 접근합니다.
이 문제는 sigqueue 플러시를 sighand lock 보유 영역 밖으로 이동시킨 최근 커밋과 함께 표면화되었습니다.
Hyonwoo는 이를 list_del_init_careful()을 사용하여 해결하려고 제안했지만, 이는 단지 문제의 증상을 가리는 것에 불과했습니다. 몇 가지 논의와 다양한 시도 끝에 Eric은 release_task()에서 task::pending를 늦게 플러시할 이유가 없으며 이미 exit_signals()에서 수행되어야 한다고 지적했습니다.
죽어가는 태스크의 pending 큐에 대기 중인 신호는 수집 및 전달될 수 없으므로, 이를 더 지연시킬 이유는 없습니다.
하지만 그 시점 이후로 신호가 여기에 큐에 추가되지 않도록 보장해야 합니다. exit_signals()은 task::flags에 PF_EXITING을 설정하며, 이는 이 목적으로 지표(indicator)로 사용될 수 있습니다.
다음과 같이 해결합니다:
- __send_signal_locked() 및 posixtimer_send_sigqueue()에서 태스크의 PF_EXITING이 설정된 경우 태스크 전용 신호(PIDTYPE_PID)의 큐잉 방지 - sighand lock을 사용하여 exit_signals()에서의 unlocked PF_EXITING 설정을 보호 (태스크 그룹 비어 있음 및 그룹 종료 케이스용) - task::pending 신호를 즉시 플러시
이를 최적화하기 위해 pending 리스트 전체를 sighand lock 하에서 스택 기반의 list head로 이동시키고, 락 보유 없이 신호를 해제합니다.
약한 순서 시스템(weakly ordered systems)에서의 잠금 없는 플러싱(lockless flush) 및 비리더(exec non-leader) 케이스에 대해 상당한 논의가 있었습니다. 문제는 posix 타이머 신호를 보내려는 제3자가 PID 조회를 통해 대상 태스크를 찾는데 의존한다는 점이며, 해당 조회는 원래 구 리더(old leader)에게 전달되었던 신호의 경우 새 리더(new leader)로 결과될 수 있습니다. 만약 그 신호가 구 리더에 큐에 추가된 경우라면 잠금 없는 플러싱은 다음과 같은 상황에 대한 우려를 제기합니다:
old_leader new_leader third party
A: flush_list() // list_del_in ---truncated---
VulDB is the best source for vulnerability data and more expert information about this specific topic.