fscrypt:避免 fscrypt_get_devices() 动态分配 — 热路径减分配
💡 一句话总结
在启用内联加密(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 分析))。
🎯 解决什么问题
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、根本不需要设备列表。
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 升级成内存安全漏洞」的典型,不是单纯性能问题。
- 执行路径:① 加密文件首次打开/访问 →
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 被回收(VFSevict)→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 风险最大。
🧩 核心机制
核心就一件事:把「获取设备列表」从「函数内动态分配 + 返回指针」改成「调用方提供栈上数组 + 返回数量」。include/linux/fscrypt.h 新增 #define FSCRYPT_MAX_DEVICES 8;fscrypt_operations.get_devices 回调签名改为填调用方数组;三个调用方各在栈上声明 struct block_device *devs[FSCRYPT_MAX_DEVICES](8×8 = 64 字节)。
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 决策表判定可豁免图表。
/* 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,使栈数组大小在编译期确定。
- /* 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 在结构上不可能发生。
📈 性能影响
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 次 kfree | 0 次(栈数组) |
| 多设备路径(f2fs 多盘,≤8 设备) | 1 次 kmalloc(8B×N) + 1 次 kfree | 0 次(栈数组) |
说明:补丁未提供基准(commit message 无 benchmark 数字)。上表是 diff 的结构性对比(每次调用少一次堆分配/释放),非实测性能数字;实际吞吐收益需在加密文件 I/O 负载上测。以下为逻辑分析(解读(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 链重构,如实标注。
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 分析))。
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()均无变化,块层无感知。
- 块层权威认可:
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 则不需要设备列表。
🔗 交叉引用
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 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。