返回

文章详情

我发现了苹果的 Fsck_hfs 中的一个漏洞

Hacker News2026年6月3日 00:59

9分钟阅读 2026年4月15日 -- TL;DR: macOS Sequoia (版本 hfs-683.x) 中的 fsck_hfs 存在一个缓存耗尽漏洞,会在大型 HFS+ 卷上报告错误的损坏信息。在配置有8GB内存的机器上,24TB或更大的卷在扩展属性检查期间会触发“无法读取节点”的错误。你的数据是安全的——漏洞出在工具,而不是文件系统。由 16 GB 以上内存的机器不受影响,旧版本的 macOS 也不会。如果你一直在关注我与 HFS+ 24TB 卷漏洞的冒险,这就是续集。在我的上一篇文章中,我记录了我24TB外部硬盘上的一个持续损坏错误。这次,我将展示我如何将错误追溯到其根本原因——而且这并不是我所预期的。剧透:文件系统一直是好的。漏洞出现在 fsck_hfs 本身。设置 我有一个格式化为 Journaled HFS+ 的 24TB 外部硬盘。每次我在我的 Mac mini M1 上运行 fsck_hfs 时,它都会以相同的错误失败:** 检查扩展属性文件。无法读取节点 #61432 ** 卷 24TB TOSHIBA 未能完全验证。卷检查失败,错误12 错误12是 ENOMEM —“内存不足。”在一台具有8GB内存的机器上?用于文件系统检查?有些事情不太对劲。这个错误是完全可以重现的。每次都是相同的节点编号。即使在一个几乎没有数据的新格式化卷上也是如此。这排除了逐渐的数据损坏,指向了某种确定性。排除硬件 我首先怀疑是硬件——也许外壳中的 USB 桥接芯片在损坏数据,或者驱动器本身有缺陷。但是在一台完全不同的 24TB 驱动器上,出现了相同的错误,使用的是不同的外壳和桥接芯片。两个不同的驱动器,在相同的节点编号上出现相同的错误。这已经排除了硬件问题。为了彻底,我使用 dmesg 检查内核日志,并在 fsck 运行期间监控 I/O: sudo fs_usage -w -f diskio fsck_hfs 没有 I/O 错误。整个检查过程中,读取干净且线性。来自磁盘的数据是正常的。无论失败的是什么,都不是硬件。读取磁盘结构 如果硬件没有问题,也许文件系统元数据本身有损坏。我决定转储并解析原始卷头,以查看属性 B-tree 的外观。HFS+ 卷头位于分区的字节偏移量 1024(扇区 2): sudo xxd -l 1024 -s 1024 /dev/rdisk7s2 解析结果显示属性文件分叉数据:512 MiB 逻辑大小,单个连续的范围,从分配块 36588 开始,共有 16,384 个分配块,每个块 32KB。接下来,我读取 B-tree 头节点——属性文件本身的前 8192 字节: sudo xxd -l 512 -s 1198915584 /dev/rdisk7s2 B-tree 头告诉我一切:节点大小:8192 字节 总节点:65,536(填满整个 512 MiB 文件) 空闲节点:65,341 已用节点:195 最后叶节点:155 树深度:3 所以属性 B-tree 具有 65,536 个节点槽,但只使用了 195 个。活动树完全位于前大约 155 个节点中。节点 #61432 深藏在未使用区域中。节点 #61432 适合这个文件吗?每个节点 8192 字节,节点 #61432 位于字节偏移量 503,250,944——完全在 536,870,912 字节的文件范围内。它在界限之内。节点 #61432 在位图中标记为使用中吗?B-tree 节点使用位图存储为头节点的记录 2。我计算得出节点 #61432 对应于位图的字节 7679,位 7(MSB优先)。读取该区域: sudo xxd -l 512 -s $((1198915584 + 8192 - 512)) /dev/rdisk7s2 所有区域均为零。节点 #61432 正确标记为自由。节点 #61432 上有什么? sudo xxd -l 256 -s $((1198915584 + 61432 * 8192)) /dev/rdisk7s2 所有区域均为零。作为一个自由节点,应该完全为空。所有的磁盘结构都是有效的且内部一致的。卷头是正确的,B-tree 头是正确的,位图正确标记了节点 61432 为自由,节点本身也被正确地清零。文件系统没有任何问题。查找代码 由于数据正常,漏洞一定出在 fsck_hfs 中。苹果将 HFS+ 作为其 Darwin 版本的一部分开源,因此我克隆了该仓库: git clone https://github.com/apple-oss-distributions/hfs.git 快速 grep 找到了错误消息: grep -rn "Couldn.t read node" --include="*.c" 两个命中: SRepair.c 和 SVerify2.c 。 SVerify2.c 中的相关代码是函数 BTCheckUnusedNodes: int BTCheckUnusedNodes(SGlobPtr GPtr, short fileRefNum, UInt16 *btStat) { BTreeControlBlock *btcb = GetBTreeControlBlock(fileRefNum); unsigned char *bitmap = ...; unsigned char mask = 0x80; UInt32 nodeNum; for (nodeNum = 0; nodeNum < btcb->totalNodes; ++nodeNum) { if ((*bitmap & mask) == 0) // 节点是自由的 { // 读取节点以验证它是否全为零 err = btcb->getBlockProc(btcb->fcbPtr, nodeNum, kGetBlock, &node); if (err) { fsck_print(ctx, LOG_TYPE_INFO, "无法读取节点 #%u\n", nodeNum); return err; } // ... 验证节点内容是否为零 ... // 释放节点 btcb

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡