- 改进 pgdat_balanced() 避免高阶分配过度回收
- 限制 filemap_fault 的 readahead 不越过 VMA 边界
- Rust MapleTree 实现 Send 和 Sync trait
- mm/page_alloc: 用批量页清零替换 kernel_init_pages()
- DAMON sysfs path / memcg_path use-after-free 修复
1. vmemmap 优化的记账与初始化修复
系列:[PATCH v4 0/5] mm: Fix vmemmap optimization accounting and initialization作者: Muchun Song版本: v4(5 个 patch)
背景
vmemmap 优化(vmemmap optimization)是内核为了节省 struct page 内存占用而引入的机制,对 DAX/ZONE_DEVICE 的 compound page 场景尤其重要——它通过让多个尾部 struct page 共享同一块物理内存,将原本每个 4 KB 页一个 64 字节 struct page 的开销大幅压缩。随着该机制在 DAX 和 memory hotplug 路径中被广泛应用,一些边界情况的 bug 逐渐暴露出来:vmemmap 页数的记账在 section activation 失败路径上会出现下溢;ZONE_DEVICE compound page(如 1 GB hugepage)在大于 pageblock 的情形下,只有首个 pageblock 的 migratetype 被正确初始化;DAX 热插拔复用早期 section 的未优化 memmap 时,compound_nr_pages() 仍按优化情形只初始化少量 struct page,残留的尾部 struct page 处于未初始化状态,可能导致后续崩溃。
解决的问题
section_activate() 失败时,vmemmap 页数记账出现下溢(accounting underflow)- DAX vmemmap 在优化场景下释放的页数被错误计算
- 1 GB hugepage 等超过
pageblock_nr_pages 的 ZONE_DEVICE compound page 只有首个 pageblock 的 migratetype 被初始化,其余 pageblock 留存未定义状态 - DAX 热插拔到 early section 的未占用 subsection 时,复用未优化 boot memmap,但
compound_nr_pages() 仍按优化路径返回 VMEMMAP_RESERVE_NR,导致尾部 struct page 未初始化 - 修复对应
Fixes: c4386bd8ee3a 和 Fixes: 6fd3620b3428
如何做
整个方案分两条主线。第一条是记账修复:通过向 arch_remove_memory()、__remove_pages() 等 memory deactivation 路径新增 struct dev_pagemap *pgmap 参数,使得 teardown 侧能够判断 vmemmap 优化是否实际生效,进而在 sparse-vmemmap 核心中正确减去已优化后的 vmemmap 尺寸;这一改动横跨 arm64、loongarch、powerpc、riscv、s390、x86 六个架构的 arch_remove_memory() 实现,因此 arch 层都需要同步更新签名。第二条是初始化修复:将原本嵌在 __init_zone_device_page() 内的 pageblock migratetype 初始化抽出到独立函数 pageblock_migratetype_init_range(),通过对整个 PFN 区间以 pageblock_nr_pages 为步长迭代,确保 compound page 跨越多个 pageblock 时每一个 pageblock 都被标记为 MIGRATE_MOVABLE,并保留每 PAGES_PER_SECTION 调用 cond_resched() 的调度点;同时 compound_nr_pages() 新增 pfn 参数并判断 early_section(__pfn_to_section(pfn)),遇到 early section 就按未优化路径返回 pgmap_vmemmap_nr(pgmap),保证所有尾部 struct page 都被初始化。
收益
作者未提供性能数据,修复价值主要体现在正确性层面:消除 DAX 与 memory hotplug 路径上的 vmemmap 记账下溢、避免 1 GB hugepage 等大 compound page 遗留未初始化 pageblock 导致的潜在崩溃与页分配异常,以及避免 DAX 热插拔到 early section 时未初始化尾部 struct page 引发的后续未定义行为。作为涉及六个架构的基础性修复,它保障了 DAX/ZONE_DEVICE 在实际生产场景中的稳定性,避免了长时间运行后记账偏差积累带来的内存统计异常。
2. 改进 pgdat_balanced() 避免高阶分配过度回收
系列:[RFC PATCH v2] mm: Improve pgdat_balanced() to avoid over-reclamation for higher-order allocation作者: Barry Song (Xiaomi), Wang Lian, Kunwu Chan版本: v2(1 个 patch)
背景
kswapd 的核心职责是把节点拉回"balanced"状态(所有 zone 的 free pages 高于 high watermark)。在低阶(order-0)分配场景下这套机制运作良好,但在高阶分配路径上暴露出严重的过度回收问题。作者在手机场景下观察到:突发的网络传输(需要高阶 skb 缓冲)会导致设备发热;Baolin 和 Kairui 也在服务器上报告了类似行为。根本原因在于 pgdat_balanced() 通过 __zone_watermark_ok() 判断节点是否平衡,而 __zone_watermark_ok() 在高阶场景下只要 freelist 中没有合适阶的 buddy 就返回 false —— 即使 free pages 远高于 high watermark 也会被判定为"不平衡"。RFC v1 曾尝试通过禁用网络高阶 buffer 的 kswapd 回收来解决,但方案太特定;v2 转向在 mm 侧从根源上修正判定逻辑。
解决的问题
pgdat_balanced() 在高阶分配触发 kswapd 时误判节点不平衡,即便系统空闲内存充裕。kswapd_shrink_node() 据此把 sc->nr_to_reclaim 累加为每个 zone max(high_wmark_pages(zone), SWAP_CLUSTER_MAX) 的总和,导致回收目标被放大到远超实际需求。- 过度回收后
compact_gap(sc->order) 的保护逻辑(回收达到 2 倍分配尺寸后重置 sc->order=0)才介入,此时已经 over-reclaim 大量页。 - 如果高阶分配请求持续到来,kswapd CPU 利用率会长期飙高,同时系统仍保持大量空闲内存,形成"忙碌但无益"的病态。
如何做
补丁仅在 mm/vmscan.c 的 pgdat_balanced() 中插入 7 行:当常规的 __zone_watermark_ok() 失败(即某个 zone 没有合适高阶空闲页)时,再额外判定一次 —— 如果 order > 0 且 compaction_suitable(zone, order, mark, highest_zoneidx) 返回 true,就认为该 zone 已经平衡。核心改动如下:
if (order && compaction_suitable(zone, order, mark, highest_zoneidx))
returntrue;
compaction_suitable() 的语义是"该 zone 拥有足够的空闲空间使 compaction 可能成功"。换言之,当空闲页富余、只是碎片化导致高阶 buddy 缺失时,正确的后续动作应该是让 compaction 去整理碎片,而不是让 kswapd 继续回收干净的 page cache。这样就把高阶分配失败的压力从"reclaim 轴"转向"compaction 轴",避免了 kswapd_shrink_node() 累加出不合理的 sc->nr_to_reclaim 目标。patch 把判定前置到 kswapd 主循环入口,从源头消除过度回收,而不是依赖事后的 compact_gap 截断。
收益
作者未提供直接 benchmark 数据,但给出了生产环境的观测现象:手机上网络突发分配导致的发热问题,以及服务器上 Baolin、Kairui 观察到的 kswapd CPU 与空闲内存并存的异常。根据代码逻辑预期收益为:
| | |
|---|
| 累加各 zone high_wmark_pages | |
| | |
| | |
| | |
3. 限制 filemap_fault 的 readahead 不越过 VMA 边界
系列:[PATCH] mm: limit filemap_fault readahead to VMA boundaries作者: Frederick Mayle版本: v1(1 个 patch)
背景
当一个 file mapping 只覆盖文件的严格子集时(例如只 mmap 了文件中间一段),对该映射的访问会触发对 VMA 范围之外文件页的 readahead。readahead 的前提假设是"即将被访问的页提前预取到 page cache",但 VMA 外的页通过这条映射根本无法访问。作者以 ELF 文件为例说明:程序只读段末尾被访问,并不意味着文件里紧邻的内容马上会被读 —— 因为它们很可能被映射到不连续的虚拟地址段,或压根不会被映射。这种"好心办坏事"的预读会把真正有用的页从 page cache 中挤出去。在 Android 的 ELF 场景下,作者观察到 page cache 里约 20% 的页是 VMA 之外的"冤枉页"。此前社区讨论曾在邮件链接中触及该话题,本补丁是一次完整实现。
解决的问题
filemap_fault 路径触发的 readahead 会越出 VMA 范围,把永远不会被这条映射访问的文件页拉进 page cache。- 现有
file_ra_state->size 只是"提示",会被 readahead 启发式(ondemand_readahead 等)放大,无法当作硬上限使用。 - mmap read-around(fault-around)向前展开时,起点可能落在 VMA 起始之前,同样造成浪费。
- 结果是 useful pages 被提前驱逐,尤其是在多个小 ELF 段共享同一文件的 Android 场景下内存压力放大。
如何做
方案分三步。第一,在 struct readahead_control 中新增 unsigned long max_index 字段作为硬上限,DEFINE_READAHEAD 宏默认把它初始化为 ULONG_MAX(保持原行为):
structreadahead_control {
...
unsignedlong max_index; /* limit readahead to i<=max_index */
};
第二,在 mm/filemap.c 的两个 fault 入口 do_sync_mmap_readahead() 与 do_async_mmap_readahead() 中,把 ractl.max_index 设为 vmf->vma->vm_pgoff + vma_pages(vmf->vma) - 1,即当前 VMA 覆盖的最后一个 page index;同时把 mmap read-around 的起点 ra->start 用 max(ra->start, vmf->vma->vm_pgoff) 钳制到 VMA 起点,防止向前越界。第三,在 mm/readahead.c 的 do_page_cache_ra() 和 page_cache_ra_order() 里用 max_index 收紧末端上限:
pgoff_t limit = min_t(pgoff_t, (i_size_read(mapping->host) - 1) >> PAGE_SHIFT,
ractl->max_index);
值得注意的边界情况:如果一个文件被多个 VMA 分段映射(例如整文件 RW 映射后对中间一段 mprotect 成 RO,形成两个 VMA),顺序大读会在 VMA 边界处被打断,每个边界触发 minor fault;但作者指出这类场景本来就因为 fault-around 被限制而性能不佳,属于可接受的权衡。此外该改动仅影响 fault 路径触发的 readahead,不影响 read(2) 系统调用路径;两类访问混合时行为取决于 PG_readahead marker 的放置者。
收益
作者用 cachestat 验证读取小范围后 page cache 未超出映射大小,并在 Android ELF 上观察到"约 20% 更少的页被拉入 cache"。fio 3.38 benchmark(4 k 块,1 G 偏移 1 G 读,文件 3 G)结果:
| | | |
|---|
| | | |
| | | |
| | | |
| | | |
| Android ELF page cache 占用 | | | |
吞吐结果呈中性(psync 路径不受影响符合设计预期),而内存收益在 Android ELF 这类小片段 mmap 密集的场景上非常可观。
4. Rust MapleTree 实现 Send 和 Sync trait
系列:[PATCH v2] rust: maple_tree: implement Send and Sync for MapleTree作者: Joel Fernandes版本: v2(1 个 patch)
背景
Rust 内核抽象层正逐步包装 C 侧的核心数据结构,MapleTree 是其中之一,作为针对非重叠 range 存储优化的树形结构被 nova-core 等 GPU 驱动使用。然而底层 C 的 struct maple_tree 内嵌了 *mut c_void 指针(用于根节点/entry 指针),Rust 编译器对裸指针默认不会自动 derive Send/Sync。这导致包含 MapleTree 的类型无法跨线程传递,而 Rust PCI 驱动模型要求 pci::Driver 实现必须满足 Send 约束。
解决的问题
*mut c_void 阻止编译器自动 derive Send/Sync,错误从 MapleTree<()> → MapleTreeAlloc<()> → Box<...> → Vmm → BarUser → Gpu → NovaCore 层层向上传播,最终使 impl pci::Driver for NovaCore 编译失败。- 现有的
MapleGuard<'tree, T> 是一个元组结构体 MapleGuard(&'tree MapleTree<T>),未显式标注"不可跨 CPU 释放"的语义;而内核 spinlock 必须在获取它的同一 CPU 上释放,Guard 本身天然不能 Send。
如何做
Patch 在 rust/kernel/maple_tree.rs 中显式为 MapleTree<T> 添加了手工 unsafe impl:
unsafeimpl<T: ForeignOwnable + Send> Sendfor MapleTree<T> {}
unsafeimpl<T: ForeignOwnable + Send> Syncfor MapleTree<T> {}
关键论证有两点:其一,MapleTree 拥有所存储的 entry,树内无线程局部状态,所有共享访问都通过内部的 ma_lock spinlock 序列化,因此只要 T: Send,跨线程移动整棵树是安全的;其二,&MapleTree<T> 并不会向外暴露 &T(所有 entry 访问要么持锁要么经由 &mut MapleGuard),因此 Sync 同样只需 T: Send 而无需 T: Sync。与此同时,MapleGuard 被从元组结构体重构为命名字段结构体,新增一个零大小的 _not_send: NotThreadSafe 标记字段,使 Guard 类型显式变为非 Send,从而在类型系统层面保证"谁获取谁释放"的 spinlock 约束。相应地,lock() 构造 Guard 时、ma_state()/load() 等方法内部对 self.0 的引用全部改写为 self.tree。v1→v2 仅调整注释措辞和 use 导入分组。
收益
作者未提供性能数据(这是纯类型系统层面的安全标注,无运行时开销)。预期收益是:解除 nova-core GPU 驱动及其他使用 MapleTree 的 Rust 驱动在编译 pci::Driver trait 时的 Send 约束阻塞,使 MapleTree<T> 及其外层容器(如 Box<MapleTreeAlloc<T>>)能够自由嵌入到需要跨线程传递所有权的 Rust 内核抽象中;同时通过 NotThreadSafe 标记让 Guard 的 per-CPU 释放语义在编译期被静态检查,避免运行时锁释放 CPU 不匹配的潜在 BUG。
5. mm/page_alloc: 用批量页清零替换 kernel_init_pages()
系列:[PATCH v3] mm/page_alloc: replace kernel_init_pages() with batch page clearing作者: Hrushikesh Salunke版本: v3(1 个 patch)
背景
当用户通过 init_on_alloc=1 启用分配时清零(用于防御未初始化内存泄漏)时,page allocator 的快路径 post_alloc_hook() 和 __free_pages_prepare() 都会调用 kernel_init_pages() 对分配的页面清零。现有实现采用逐页循环:每页都独立调用 clear_highpage_kasan_tagged(),其中包含 kmap_local_page() 和 kunmap_local()。对于 2 MB HugeTLB 或 high-order 分配这类场景,循环会对 512 个连续物理页反复做 kmap/kunmap,尽管在非 HIGHMEM 架构上 kmap_local_page() 等价于 page_address() 近乎空操作,但仍阻止了底层架构清零原语(例如 x86 的 REP STOS、ARM64 的 DC ZVA)对整段连续虚拟地址一次性处理,丧失了批量优化机会。
解决的问题
kernel_init_pages() 的逐页循环产生大量冗余 per-page 操作,形成明显热点;- 架构清零原语(
clear_pages())原本可以对任意 numpages 连续范围单次调用并利用 cacheline 预取/非临时存储优化,但被拆成 N 次单页调用后失效; - 即使非 HIGHMEM 系统(主流 64 位架构)也承担了 HIGHMEM 语义下的 kmap 抽象开销。
如何做
在 include/linux/highmem.h 引入新的 inline 批量辅助函数 clear_highpages_kasan_tagged(),按 CONFIG_HIGHMEM 分流:
staticinlinevoidclear_highpages_kasan_tagged(struct page *page, int numpages)
{
kasan_disable_current();
if (!IS_ENABLED(CONFIG_HIGHMEM)) {
clear_pages(kasan_reset_tag(page_address(page)), numpages);
} else {
int i;
for (i = 0; i < numpages; i++)
clear_highpage_kasan_tagged(page + i);
}
kasan_enable_current();
}
非 HIGHMEM 路径通过 page_address(page) 取出连续首地址,再经 kasan_reset_tag() 剥离 MTE tag 后一次性交给 clear_pages(addr, numpages),完全绕过 kmap;HIGHMEM 保留逐页回退。KASAN 关/开现场由新 helper 自管,因为 s390 的 memset 可能覆盖 KASAN redzone。然后彻底移除旧的 kernel_init_pages(),把 mm/page_alloc.c 中 __free_pages_prepare() 和 post_alloc_hook() 两处调用点直接改写为 clear_highpages_kasan_tagged(page, 1 << order)。v1 曾尝试加入 cond_resched(),v2 撤回——因为该路径可能处于原子上下文;v2→v3 把 kasan_disable_current()/kasan_enable_current() 下沉到 helper 内部,并顺势删掉已变为空壳的 kernel_init_pages()。
收益
作者在 init_on_alloc=1 下给出了完整基准数据。2 MB HugeTLB 批量分配 8192 页(共 16 GB)耗时从 0.445 s 降至 0.166 s,**−62.7%**,提速 2.68 倍。真实负载 sys 时间对比:
6. DAMON sysfs path / memcg_path use-after-free 修复
系列:[RFC PATCH 0/2] mm/damon/sysfs-schemes: fix use-after-free for [memcg_]path作者: SeongJae Park版本: v1(2 个 patch,RFC)
背景
DAMON 通过 sysfs 暴露一系列可调参数,其中 mm/damon/sysfs-schemes.c 中的 damon_sysfs_scheme_filter->memcg_path(用于按 memcg 过滤)和 damos_sysfs_quota_goal->path(位于 quota goal 目录下)都是用户可读写的字符串指针。这两个字段在写入时会 kfree 旧 buffer 并替换为新分配的 buffer。DAMON 在参数 online/offline commit 流程中已经通过 damon_sysfs_lock 保护了对这些字段的间接读取,但用户态通过 sysfs 直接 read/write 这两个文件时,并没有任何锁保护。尽管 kernfs 对同一个打开文件句柄内部有互斥,但现实中不同进程用独立的 fd 同时读写同一文件非常常见,于是一侧正在 kfree 旧 buffer,另一侧恰好读取 filter->memcg_path 或 goal->path,即触发 use-after-free,读到已释放的堆内存内容。
解决的问题
memcg_path_show()/memcg_path_store() 与 path_show()/path_store() 均未加锁,用户态并发读写会造成 use-after-free- 该 UAF 在 reader/writer 使用不同 open file 时可稳定触发(kernfs 的 open-file 锁无法跨 fd 防护)
- 分别修复
Fixes: 4f489fe6afb3(memcg_path,影响 6.16.x)和 Fixes: c41e253a411e(quota goal path,影响 6.19.x),均标记 Cc: stable
如何做
两个 patch 都采用相同的加锁方案:在 show 和 store 路径上使用 mutex_trylock(&damon_sysfs_lock) 包裹对 memcg_path 和 goal->path 的访问与释放。选用 trylock 而非阻塞 mutex_lock() 是为了避免 sysfs 操作在持锁者长时间占用 damon_sysfs_lock(例如 DAMON 参数 commit 过程)时无限阻塞用户态 read/write 系统调用——拿不到锁时直接返回 -EBUSY,让用户态感知到临时冲突而非挂起。对于 store 路径,实现上先 kmalloc 并 strscpy 将用户 buffer 拷贝到本地 path,再尝试 trylock;若加锁失败则立即 kfree(path) 避免泄漏;加锁成功后才 kfree(filter->memcg_path) / kfree(goal->path) 并赋新值,保证释放和读取之间完整互斥。这样就把原本由 commit 路径单向持有的 damon_sysfs_lock 扩展为覆盖所有读写访问路径的统一同步点,和已有的 commit 时机保护形成闭环。
收益
作者未提供量化数据,但该修复关闭的是一个用户态可触发、可读到已释放堆内存的 use-after-free 窗口。结合 Cc: stable 和 Fixes: 标签,影响范围明确至 6.16.x(memcg_path)和 6.19.x(quota goal path),对启用 DAMON sysfs 接口的生产环境具有直接安全意义——尤其在允许非特权进程读取 DAMON sysfs 属性的部署中,可以防止攻击者通过 race 读取释放后的内核堆内容、降低信息泄露与进一步利用的风险。
7. 总结
新机制 / 新接口
| | |
|---|
| limit filemap_fault readahead to VMA boundaries | 在 readahead_control 新增硬上限字段 max_index,让 fault 路径的 readahead 不越出 VMA 边界 |
| rust: maple_tree: implement Send and Sync for MapleTree | 为 Rust MapleTree<T> 手工实现 Send/Sync,并把 MapleGuard 显式标注为非 Send,解锁 nova-core 等 Rust 驱动 |
性能优化
| | |
|---|
| mm: Improve pgdat_balanced() for higher-order allocation | 无 benchmark;预期消除高阶分配下 kswapd 过度回收、缓解手机发热与服务器 kswapd CPU 持续高位 |
| limit filemap_fault readahead to VMA boundaries | Android ELF page cache 占用约 −20%;fio mmap 吞吐 +1.7%/+0.3%,psync 路径不受影响 |
| mm/page_alloc: batch page clearing | init_on_alloc=1 下 2 MB HugeTLB 清零 −62.7%(2.68×);Graph500/Pagerank sys 时间 −35.7%~−50.3% |
Bug Fix
| | |
|---|
| mm: Fix vmemmap optimization accounting and initialization | 修复 DAX/ZONE_DEVICE 热插拔路径的记账下溢与 1 GB compound page 跨 pageblock migratetype/尾部 struct page 未初始化;Fixes: c4386bd8ee3a, 6fd3620b3428,横跨 6 个架构 |
| mm/damon/sysfs-schemes: fix UAF for [memcg_]path | 修复 DAMON sysfs memcg_path/quota goal path 并发读写造成的 use-after-free;Fixes: 4f489fe6afb3, c41e253a411e,Cc: stable |
| rhashtable: Check for vmalloc in emergency rehash error path | |
内部优化 / 清理
| | |
|---|
| selftests/mm: clean up build output and verbosity | |