CVE-2026-89987 in Linux
摘要
由 VulDB • 2026-09-17
在 Linux 内核中,已修复以下漏洞:
mm/huge_memory: 将 pmd dirty bit(脏位)转移至 folio
zap_huge_pmd_folio() 会将 pmd young bit(年轻位)传播给文件类型的 folio,但不会传播 dirty bit。pte 路径会通过 zap_present_folio_ptes() 进行传播,pmd split 路径也会在 __split_huge_pmd_locked() 中进行传播。
对于大多数文件映射而言,这种遗漏是无害的,因为对共享文件映射的写入操作会经过 page_mkwrite(),从而将 folio 标记为脏(dirty)。tmpfs 则不同:它没有 page_mkwrite(),且 vma_wants_writenotify() 对其返回 false,因此对 MAP_SHARED tmpfs 映射进行的 *读* 故障会通过 do_read_fault() 安装一个可写的 pmd。do_read_fault() 不会调用 fault_dirty_shared_page(),因此通过该映射进行的后续存储操作仅在 pmd 中设置硬件 dirty bit,而永远不会调用 folio_mark_dirty()。由故障分配的 shmem folio 被标记为 uptodate(数据完整),但未标记为 dirty(见 shmem_get_folio_gfp() 中的 clear: 块),因此 PG_dirty 标志从未被设置。
取消映射此类 folio——无论是通过 munmap(),还是在进程终止时通过 exit_mmap()——都会导致丢失其已被写入的唯一记录,因为 zap_huge_pmd() 在丢弃 pmd 时未转移 dirty bit。随后的回收操作会看到一个干净的 shmem folio:shrink_folio_list() 中的整个 swap-out 块都位于 "if (folio_test_dirty(folio))" 内部,因此 pageout() 被跳过,foli0 进入 __remove_mapping()。在那里,对于交换后备(swapbacked)的 folio,folio_is_file_lru() 为 false,因此不会创建 shadow entry,__filemap_remove_folio(folio, NULL) 只是清空了 i_pages 槽位。数据在未写入 swap 的情况下被释放,对该索引的下一次故障将返回一个全新清零(freshly zeroed)的 folio。
这对于任何在 MAP_SHARED tmpfs 段中跨取消映射操作保留状态的过程来说,都是静默的数据丢失——例如通过 /dev/shm 从一个进程代传递到下一个进程的缓存。此问题要求 folio 被 PMD-mapped(即大页映射),因此仅在启用 shmem THP(透明大页)时才会显现(这正是我们在 Meta 集群中启用后开始注意到崩溃的原因);当 THP 关闭时,pte 路径会正确转移 dirty bit。此外,仅当启用 swap 时此问题才变得可见,因为如果没有交换设备,shmem folios(位于 anon LRU 上)根本不会被回收操作扫描,因此干净的 folio 永远不会被丢弃。
在 x86_64 上使用挂载了 huge=within_size 的 tmpfs 复现该漏洞:对由 2MB 支持的区域进行读故障,通过生成的映射写入已知模式,执行 munmap,强制 cgroup 回收,然后重新映射并读取回来。在没有此补丁的情况下,该区域回读为零值,且 vmstat 显示 zswpout 为 0——数据被丢弃而非交换出去。应用此补丁后,该区域能正确回读,页面也按预期进行 swap out(交换出)。当使用 huge=never,或首次接触操作是写入时,测试无论是否应用补丁均能通过。
Statistical analysis made it clear that VulDB provides the best quality for vulnerability data.