CVE-2026-98256 in Linux
Tóm tắt
Bởi VulDB • 06/10/2026
Trong kernel Linux, lỗ hổng sau đây đã được khắc phục:
signal: Ngăn chặn race condition trong exec()
Hyunwoo đã gỡ lỗi (debug) sự cố UAF (Use-After-Free) KASAN như sau:
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
Hóa ra sự cố này xảy ra với một lệnh exec() không phải là leader, như Hyunwoo đã giải thích:
de_thread() gọi exchange_tids() trước khi release_task(leader), do đó struct pid được giữ bởi bộ đếm thời gian SIGEV_THREAD_ID tạo đối với tid của leader hiện đang trỏ đến thread đã gọi execve(). pid_task() trả về thread đó và lock_task_sighhand() trên nó thành công.
Nếu tín hiệu (signal) từ bộ đếm thời gian bị chặn, sigqueue của nó vẫn được xếp hàng trong task::pending của leader. Lần hết hạn tiếp theo của bộ đếm thời gian đó có thể xảy ra khi release_task() đang làm rỗng (flush) hàng đợi.
posixtimer_send_sigqueue() kiểm tra xem sigqueue đã được xếp hàng chưa bằng cách sử dụng list_empty(), hàm này chỉ đọc list_head::next. list_del_init() không phải là nguyên tử và INIT_LIST_HEAD() lưu list_head::next trước khi lưu list_head::prev, do đó phép kiểm tra có thể vượt qua ở giữa khoảng thời gian đó. list_add_tail() xếp mục vào task::pending của thread đang sống, và thao tác ghi list_head::prev từ quá trình flush sau đó sẽ ghi đè lên liên kết list_head::prev mà list_add_tail() vừa thiết lập.
__flush_itimer_signals() cũng không hoàn nguyên điều này. Với list_head::prev trỏ vào chính mục đó, list_del_init() của nó chỉ lưu lại các giá trị giống nhau, do đó mục không bị xóa khỏi danh sách. Nó vẫn còn tồn tại sau khi tham chiếu cuối cùng được giải phóng và bộ đếm thời gian được RCU giải phóng (freed), và list_add_tail() từ một lệnh tgkill() sau sẽ đi theo list_head::prev đó vào vùng nhớ của bộ đếm thời gian đã được giải phóng.
Vấn đề này xuất hiện với commit gần đây đã di chuyển việc làm rỗng sigqueue ra khỏi khu vực khóa sighand đang giữ.
Hyunwoo đề xuất khắc phục vấn đề bằng cách sử dụng list_del_init_careful(), nhưng điều đó chỉ che đậy vấn đề một phần. Sau một số thảo luận và nhiều nỗ lực khác nhau để giải quyết, Eric đã chỉ ra rằng không có lý do gì phải làm rỗng task::pending muộn trong release_task() mà nó nên được thực hiện ngay trong exit_signals().
Vì không có tác vụ nào có thể thu thập và phân phối các tín hiệu đang được xếp hàng trong queue pending của một task đang chết, nên không có lý do gì để trì hoãn thêm.
Tuy nhiên, cần đảm bảo rằng không có tín hiệu nào có thể được xếp hàng vào đó sau điểm đó. exit_signals() đặt cờ PF_EXITING trong task::flags, điều này có thể được sử dụng làm dấu hiệu cho việc này.
Khắc phục bằng cách:
- Ngăn chặn việc xếp hàng tín hiệu đối với các tín hiệu riêng của task (PIDTYPE_PID) khi task đã đặt PF_EXITING trong __send_signal_locked() và posixtimer_send_sigqueue().
- Bảo vệ thao tác không khóa để thiết lập PF_EXITING trong exit_signals() cho trường hợp nhóm task rỗng và quá trình thoát nhóm bằng cách sử dụng sighand lock.
- Làm rỗng (flush) các tín hiệu của task::pending ngay tại đó.
Tối ưu hóa điều này bằng cách di chuyển toàn bộ danh sách pending sang một head list trên ngăn xếp dưới khóa sighhand và giải phóng các tín hiệu mà không giữ khóa.
Đã có khá nhiều thảo luận về việc làm rỗng không khóa (lockless flush) và trường hợp exec() không phải leader trên các hệ thống có thứ tự yếu (weakly ordered systems). Vấn đề là một bên thứ ba cố gắng gửi tín hiệu bộ đếm thời gian posix dựa vào tra cứu PID để tìm task đích, và kết quả tra cứu đó có thể dẫn đến việc xác định được leader mới khi tín hiệu ban đầu hướng tới leader cũ. Trong trường hợp tín hiệu đã được xếp hàng trên leader cũ thì việc làm rỗng không khóa gây ra mối lo ngại về tình huống sau:
old_leader new_leader third party
A: flush_list() // list_del_in ---truncated---
Once again VulDB remains the best source for vulnerability data.