fscrypt:避免 fscrypt_get_devices() 动态分配 — 热路径减分配

fscrypt 内联加密 · blk-crypto 设备列表获取 · 栈上数组替换堆分配 · Linux 7.2

💡 一句话总结

在启用内联加密(blk-crypto)的文件系统上,加密 key 开始使用或撤销(文件打开、解锁、inode 回收)时,内核都要先取一份「该文件系统的块设备列表」,再逐个对设备做 blk_crypto_config_supported() / blk_crypto_start_using_key() / blk_crypto_evict_key()。改前这份列表用 kmalloc 动态分配一个指针数组存放——分配可能失败,尤其在 inode 回收(内存直接回收路径)时;而 fscrypt_destroy_inline_crypt_key() 对失败不处理,直接释放 key,导致块层仍持有已释放 key 的悬垂指针(use-after-free)。改后换成 8 项(64 字节)的栈上数组 devs[FSCRYPT_MAX_DEVICES],文件系统把设备指针直接填入调用方数组——既消灭了 UAF,又让三条内联加密路径每次调用少一次 kmalloc/kfree。补丁未提供基准;逻辑分析(解读(AI 分析))指向「on-CPU 减分配」+「回收上下文免分配」的双重收益,但单次省下的只是一个小分配,吞吐差异预计微小。

📋 补丁基本信息

项目内容
补丁类型bugfix(修复 use-after-free)+ 热路径减分配优化 —— 主导是 bugfix(Fixes: 链 + UAF),性能角度是热路径免分配
状态Merged(绿)· 合入版本:Linux 7.2(v7.2-rc5 已含,git describe --contains 确认)
当前版本主线合入版(本 commit 即最终版)· git.kernel.org 链接
版本演进 本 commit 为合入主线的最终版。commit 的 Closes: 链指向 sashiko 静态分析工具对 2026-07-13 早期补丁集(msgid 20260713023708.9245-1-ebiggers@kernel.org)的 UAF 报告,说明合入前存在更早版本(推测);本地 lore 索引不可用,无法回溯 v1 与最终版的逐版本差异。
讨论来源为本地 git commit message 的 Link/Closes/Reviewed-by 链(lore 不可用,如实标注);版本超链接均经 checking-references 验证
作者机构Eric Biggers(fscrypt 子系统维护者,ebiggers@kernel.org)
提交日期2026-07-18
改动范围3 文件(fs/crypto/inline_crypt.c、fs/f2fs/super.c、include/linux/fscrypt.h),+44/-56 行
核心函数fscrypt_get_devices() / fscrypt_destroy_inline_crypt_key() / f2fs_get_devices()
原始链接commit 6fe4e4b8259e(Link 标签:patch.msgid.link)

📊 速览卡片

核心机制
栈上数组
优化目标
免热路径分配
适用场景
内联加密
特性等级
★★★
实测提升
未提供

特性等级依据:修复的是 CRITICAL 级 use-after-free(内存安全、价值高、必改),同时顺带消掉三条热路径上的 kmalloc/kfree;但无基准数据、多设备场景仅 f2fs 使用、单设备场景省下的只是一次 8 字节小分配(收益小),纯性能角度有限——综合 4 原则评 ★★★(解读(AI 分析))。

🎯 解决什么问题

背景 / 原始动机
commit message 把动机说得很直接:当 blk_crypto_key 开始被使用或撤销时,fs/crypto 调用 fscrypt_get_devices() 获取文件系统的块设备列表,然后逐个调用 blk_crypto_config_supported() / blk_crypto_start_using_key() / blk_crypto_evict_key()。当前这份块设备指针放在动态分配的数组里,问题有二:
① 分配可能失败——尤其在 fscrypt_destroy_inline_crypt_key() 被 inode 回收调用(发生在直接回收 direct reclaim 上下文)时;
② 失败处理缺失——fscrypt_destroy_inline_crypt_key() 不处理失败,直接 zeroize + free 掉 blk_crypto_key,没有调用 blk_crypto_evict_key(),造成 use-after-free。
修复方式选择「直接而易于 backport」的栈上数组:多设备功能目前只有 f2fs 使用,f2fs 硬编码上限 8 个块设备(include/linux/f2fs_fs.h 的 MAX_DEVICES),栈上数组足够。commit message 也预告了大规模设备数的替代方案:把设备迭代下沉到文件系统,或像 btrfs(只用 blk-crypto-fallback)那样直接调 fallback、根本不需要设备列表。
系统层面:内联加密热路径上的 GFP_KERNEL 分配 + 失败处理缺失 → 悬垂指针
模块是 fs/crypto/inline_crypt.c(fscrypt 内联加密子系统)。fscrypt_get_devices() 通过 kmalloc_obj/kmalloc_objs 动态分配 struct block_device * 数组(默认 GFP_KERNEL)。这在三条关键路径上每次被调用:fscrypt_select_encryption_impl()(决定某文件用内联加密还是 crypto API)、fscrypt_prepare_inline_crypt_key()(准备 per-file key)、fscrypt_destroy_inline_crypt_key()(撤销 key)。
真正的缺陷在第三条:撤销路径上如果 fscrypt_get_devices() 返回 ERR_PTR(-ENOMEM),旧代码直接跳过 evict 循环、执行 kfree_sensitive(blk_key) 释放 key。但在此之前 blk_crypto_start_using_key() 已经把该 key 注册进块设备的 blk-crypto profile(keyslot 持有 key 指针),释放后 keyslot 就变成悬垂指针——后续该设备处理加密 bio 或再次 evict 时就会访问已释放内存。这是一个「错误处理缺失把可恢复的 -ENOMEM 升级成内存安全漏洞」的典型,不是单纯性能问题。
场景层面:加密文件打开/key 生命周期 + 内存压力下的 inode 回收
  • 执行路径:① 加密文件首次打开/访问 → fscrypt_get_encryption_info() → fscrypt_select_encryption_impl()(keysetup.c 决定 inline crypt 与 crypto API 二选一);② 拿到 master key 后准备 per-file key → fscrypt_prepare_key() → fscrypt_prepare_inline_crypt_key();③ inode 被回收(VFS evict)→ fscrypt_put_encryption_info() → fscrypt_destroy_prepared_key() → fscrypt_destroy_inline_crypt_key()。三条路径每次调用 fscrypt_get_devices()。
  • 高频触发场景:内联加密的文件系统(当前为 f2fs,含多设备配置)上的加密文件 I/O——每次打开加密文件、每次 key 建立/撤销都走一遍。
  • 缺陷被放大的硬件/系统状态:内存压力场景。inode 回收往往发生在 VFS dcache/icache 收缩、直接内存回收(direct reclaim)时——此时内核本身缺页,kmalloc(GFP_KERNEL) 最容易失败,也最不该再递归进入回收做分配。于是「回收的受害者 inode」在「最不该分配的时候」做分配,失败后还不处理 → UAF 风险最大。
受影响负载:内联加密文件系统上的加密文件 I/O(f2fs 单盘/多盘)· 因果:key 生命周期操作在热路径上做堆分配,回收路径上分配失败未处理 → 把「可恢复的 -ENOMEM」升级成悬垂指针 + use-after-free

🧩 核心机制

核心就一件事:把「获取设备列表」从「函数内动态分配 + 返回指针」改成「调用方提供栈上数组 + 返回数量」。include/linux/fscrypt.h 新增 #define FSCRYPT_MAX_DEVICES 8;fscrypt_operations.get_devices 回调签名改为填调用方数组;三个调用方各在栈上声明 struct block_device *devs[FSCRYPT_MAX_DEVICES](8×8 = 64 字节)。

从系统层面看
① 单设备快路径(绝大多数部署,零分配):改前即使只有 1 个块设备(ext4 / f2fs 单盘),旧代码也要 kmalloc_obj(*devs) 分配 1 个指针(8 字节,kmalloc-8 slab),拷贝 sb->s_bdev,用完再 kfree。改后直接 devs[0] = sb->s_bdev; return 1;——不经过分配器,kmalloc/kfree 各少一次。
② 多设备路径(f2fs 多盘):f2fs_get_devices() 把设备指针逐个写入调用方数组;static_assert(MAX_DEVICES <= FSCRYPT_MAX_DEVICES) 在编译期保证 f2fs 的 8 上限不超 FSCRYPT 的 8 上限;运行时若 s_ndevs > 8(理论不发生,防御性),WARN_ON_ONCE 后截断到 8。
③ UAF 修复(本补丁的真正动机):撤销路径上 fscrypt_get_devices() 不再有失败分支,evict 循环无条件执行——「先 evict、后 free」的不变量从「靠错误处理碰运气」变成「代码结构上必然成立」。
④ 为什么支撑场景收益:三条路径每次少一次 kmalloc+kfree(on-CPU 减指令);直接回收上下文里不再有 GFP_KERNEL 分配(消除分配失败/递归回收风险)。注意这是「分配位置迁移」而非「分配变少但路径不变」——对单设备场景分配次数真正归零。
步骤操作目的
定义上限include/linux/fscrypt.h 加 FSCRYPT_MAX_DEVICES 8与 f2fs 硬编码 MAX_DEVICES=8 对齐,栈数组大小编译期确定
回调改签名get_devices 改为填调用方数组 + 返回数量(unsigned int)设备列表所有权归调用方,函数内无需分配也无需 kfree
单设备快路径无回调时直接 devs[0]=sb->s_bdev; return 1最常用场景零分配
多设备适配f2fs_get_devices() 逐个填数组 + static_assert + WARN_ON_ONCE 截断多设备仍支持,超限防御性处理
调用方去 kfree三条路径删除 IS_ERR/kfree(devs)消灭失败分支与释放配对,UAF 无触发条件

图表决策:本补丁为简单改动(API 替换 + 分配方式迁移),核心机制用「从系统层面看」文字 + 机制表 + 两段关键 diff 已讲透,删图读者理解不变差,按 visualizing-report-charts 决策表判定可豁免图表。

关键代码片段 ①:fscrypt_get_devices() 从「分配返回」改为「填栈数组」
 /* Maximum value for the third parameter of fscrypt_operations.set_context(). */
 #define FSCRYPT_SET_CONTEXT_MAX_SIZE	40
 
+/* Maximum supported number of block devices per filesystem */
+#define FSCRYPT_MAX_DEVICES	8
+
 #ifdef CONFIG_FS_ENCRYPTION
 
-static struct block_device **fscrypt_get_devices(struct super_block *sb,
-						 unsigned int *num_devs)
+static unsigned int
+fscrypt_get_devices(struct super_block *sb,
+		    struct block_device *devs[FSCRYPT_MAX_DEVICES])
 {
-	struct block_device **devs;
-
-	if (sb->s_cop->get_devices) {
-		devs = sb->s_cop->get_devices(sb, num_devs);
-		if (devs)
-			return devs;
-	}
-	devs = kmalloc_obj(*devs);
-	if (!devs)
-		return ERR_PTR(-ENOMEM);
+	if (sb->s_cop->get_devices)
+		return sb->s_cop->get_devices(sb, devs);
 	devs[0] = sb->s_bdev;
-	*num_devs = 1;
-	return devs;
+	return 1;
 }

▲ 机制本体:函数不再拥有分配,只填调用方的栈上数组并返回数量。kmalloc_obj(*devs)(8 字节单指针)与 ERR_PTR(-ENOMEM) 失败分支整体消失——返回值语义从「指针或错误」变为「永不失败的设备数」。FSCRYPT_MAX_DEVICES=8 对齐 f2fs 的 MAX_DEVICES,使栈数组大小在编译期确定。

关键代码片段 ②:撤销路径的 UAF 修复 —— evict 不再可能被跳过
-	/* Evict the key from all the filesystem's block devices. */
-	devs = fscrypt_get_devices(sb, &num_devs);
-	if (!IS_ERR(devs)) {
-		for (i = 0; i < num_devs; i++)
-			blk_crypto_evict_key(devs[i], blk_key);
-		kfree(devs);
-	}
+	/*
+	 * Evict the key from all the filesystem's block devices.
+	 * This *must* be done before the key is freed.
+	 */
+	num_devs = fscrypt_get_devices(sb, devs);
+	for (i = 0; i < num_devs; i++)
+		blk_crypto_evict_key(devs[i], blk_key);
+
 	kfree_sensitive(blk_key);

▲ 这是本补丁的真正动机。改前当 fscrypt_get_devices() 返回错误时,if (!IS_ERR(devs)) 保护块被整体跳过——evict 静默不做,然后 kfree_sensitive(blk_key) 照样释放 key,块层 keyslot 残留悬垂指针。改后新加的注释把不变量明示为代码结构:「This *must* be done before the key is freed」——循环无条件执行,evict 必然先于 free,UAF 在结构上不可能发生。

📈 性能影响

提升角度(方法论分类)
主角度:on-CPU 计算效率 / 开销降低(免热路径分配)。补丁在三条 key 生命周期路径上各去掉一次 kmalloc + 一次 kfree + IS_ERR 分支。kmalloc/kfree 即便命中 slab fastpath 也有几十 ns 量级的指令与访存开销;未命中(slab 空、回收上下文)会更贵。从 Brendan Gregg 的 on-CPU vs off-CPU 视角:主要是 on-CPU(减少分配/释放指令与分支);但直接回收上下文里 GFP_KERNEL 分配可能触发回收(off-CPU 阻塞),去掉后这一 off-CPU 风险也随之消除——稳定性收益大于吞吐收益(解读(AI 分析))。
受益场景
  • 内联加密 f2fs 上的加密文件打开与 key 生命周期:每次 fscrypt_select_encryption_impl() / fscrypt_prepare_inline_crypt_key() / fscrypt_destroy_inline_crypt_key() 少一次堆分配。单设备是绝大多数部署,此时旧代码也要 kmalloc 1 个指针(kmalloc-8),所以即便单设备也有收益。
  • 「大量打开/关闭加密文件」或「频繁 key 设置/撤销」的负载(如高并发加密文件访问):调用次数越多,累计算掉的分配次数越多。
  • 内存压力 + inode 批量回收:这是最大的稳定性/延迟收益点——回收路径上不再做可能失败/递归的 GFP_KERNEL 分配,消除 UAF 触发条件,也避免在回收上下文里给内存子系统加负担。
路径 / 调用点改前分配(每次调用)改后分配(每次调用)
单设备快路径(ext4 / f2fs 单盘)1 次 kmalloc(8B) + 1 次 kfree0 次(栈数组)
多设备路径(f2fs 多盘,≤8 设备)1 次 kmalloc(8B×N) + 1 次 kfree0 次(栈数组)

说明:补丁未提供基准(commit message 无 benchmark 数字)。上表是 diff 的结构性对比(每次调用少一次堆分配/释放),非实测性能数字;实际吞吐收益需在加密文件 I/O 负载上测。以下为逻辑分析(解读(AI 分析))。

逻辑收益分析(解读(AI 分析))
收益分三层:
① on-CPU 减指令:每次调用少一次 kmalloc/kfree(分配器 fastpath 的整数运算 + 可能的 slab 锁)、少 IS_ERR 分支与 kfree 配对——对「大量打开/关闭加密文件」的负载可累加成可测差异,但单次省下的是小分配,吞吐提升预计微小。
② 回收上下文免分配:inode 回收路径不再做 GFP_KERNEL 分配,避免在 direct reclaim 下「分配失败」或「递归回收」造成的延迟尖峰——这部分收益在内存压力场景可能比纯吞吐更显著。
③ 稳定性 → 无谓开销归零:UAF 被消灭,不再有「块层访问悬垂 key」引发的潜在崩溃/数据损坏。这类 bug 一旦触发是灾难性的,修复的间接「性能价值」是把一条随时可能崩掉的路径变成安全路径。

🔄 方案演进

本 commit 是合入主线的最终版。由于本地 lore 索引不可用(lore MCP 查询超时),演进信息基于本地 git commit message 的 Link/Closes/Reviewed-by 链重构,如实标注。

从 2026-07-13 早期补丁集到最终版
2026-07-13 早期版本:commit 的 Closes: 标签指向 sashiko 对补丁集 20260713023708.9245-1 的分析报告(msgid 前缀 20260713 = 7 月 13 日)——sashiko 静态分析工具(sashiko-bot@kernel.org)在该版本上检出 use-after-free,触发本修复(推测:早期版本已包含相关动态分配路径,sashiko 报告即本 commit 的 Closes 所关问题)。
2026-07-18/19 最终版(本 commit):作者提交修复——栈上数组方案,Reported-by: Sashiko <sashiko-bot@kernel.org>,Fixes: 22e9947a4b2b,Cc: stable@vger.kernel.org(请求 backport),Reviewed-by: Christoph Hellwig。
lore 不可用说明:无法拉取两版本邮件正文对比逐版本差异;以上时间线与归属来自 commit message 的 trailer 链,非 lore 讨论(解读(AI 分析))。
设计权衡 / 讨论推进
commit message 自述了方案的边界与替代路径(这也大概率是 review 中最受关注的点,解读(AI 分析)):栈上数组「won't scale up to large number of block devices」——若未来某文件系统设备数大幅增长,需把块设备迭代下沉到文件系统内部,或像 btrfs(只用 blk-crypto-fallback)那样直接调 fallback、根本不需要设备列表。当前用「最简单、最易 backport」的栈数组,是因为多设备场景目前仅 f2fs 且硬编码上限 8。Reviewed-by: Christoph Hellwig(块层资深维护者)表明方案获得块层权威认可。无已知 NACK。

⚠️ 风险与局限

收益成立的前提
  • 设备数上限前提:多设备(>1 设备)场景当前只有 f2fs 使用,且 f2fs 硬编码上限 8(include/linux/f2fs_fs.h 的 MAX_DEVICES),恰好 ≤ FSCRYPT_MAX_DEVICES=8,栈数组方案才成立。设备数 >8 时本方案失效(commit message 明确自述,属设计边界)。
  • 单设备 no-op 语义:单设备文件系统(ext4 等)不实现 get_devices 回调,走 devs[0]=sb->s_bdev 快路径——对它们只是少一次 kmalloc,行为无变化。
  • 不依赖硬件:纯内核逻辑改动,无硬件/配置门槛,任何支持 blk-crypto 的部署都适用。
生产落地影响
  • backport 意图明确:Cc: stable@vger.kernel.org,改动小且自包含,backport 到稳定分支风险低;Fixes: 链到 2022 年的 22e9947a4b2b,即 6.1 起的内核都受影响、都可能需要此修复。
  • 栈使用:三条调用路径各多 64 字节栈占用(8 指针数组)。fscrypt 调用栈本身不深,64B 增量可接受;严格栈审计场景(如 4K 栈的嵌入式)需留意(解读(AI 分析))。
  • 防御性截断:f2fs 若未来 s_ndevs > 8(static_assert 已保证编译期不可能,仅防御),会 WARN_ON_ONCE 并截断——被截掉的多余设备不再参与 blk-crypto,属防御性行为,正常配置不触发。
生态/兼容性
  • 回调签名变更:struct fscrypt_operations.get_devices 从「返回分配的数组」改为「填调用方数组 + 返回数量」,是 API break。in-tree 实现该回调的只有 f2fs(本补丁已一并更新);out-of-tree 文件系统若用了该回调会编译报错,需同步适配。
  • blk-crypto 层接口不变:blk_crypto_config_supported() / blk_crypto_start_using_key() / blk_crypto_evict_key() 均无变化,块层无感知。
review 质疑(若有)
lore 不可用:本次 lore MCP 查询超时,无法回溯邮件列表完整 review 讨论(每条观点带 message-id 的要求无法满足)。以下为 commit message trailer 链呈现的社区信号(解读(AI 分析)):
  • 块层权威认可:Reviewed-by: Christoph Hellwig <hch@lst.de>——方案获块层资深维护者认可。
  • 静态分析检出:Reported-by: Sashiko <sashiko-bot@kernel.org> + Closes: 链——UAF 由自动化静态分析工具报告(sashiko.dev)。
  • 可能的讨论点(推测):为何不直接把设备迭代下沉到文件系统?commit message 已预答——那是大量设备时的方案,当前 f2fs 上限 8 用栈数组最直接、最易 backport;btrfs 走 blk-crypto-fallback 则不需要设备列表。
严重度:修复对象是 CRITICAL(use-after-free 内存安全漏洞,Fixes 可追溯到 2022 年 22e9947a4b2b,需 backport 到稳定分支);但修复本身风险 MINOR(栈数组替换堆数组,逻辑简单、无新分配器依赖、易 backport、对单设备无行为变化)· 落地场景:最需关注 out-of-tree 文件系统对 get_devices 回调签名变更的适配

🔗 交叉引用

📌 关联工作
22e9947a4b2b fscrypt: stop holding extra request_queue references(2022-09-01,本补丁 Fixes: 的源头) — 引入「用 super_block 取块设备列表」机制与动态分配;本补丁修复其分配失败处理缺失导致的 UAF
本补丁的 Link 标签(patch.msgid.link → lore.kernel.org 20260719055602.78828-1) — 合入主线的邮件入口(lore 索引不可用,未拉取正文)
Sashiko 静态分析报告(Closes 指向) — 检出 use-after-free 并触发本修复的自动化报告
内核文档 Inline Encryption(Documentation/block/inline-encryption.rst) — fscrypt 内联加密机制背景(blk-crypto 由块层在 bio 中做加解密)
⚠️ 免责声明

本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。