一个未正式卸载却从内存中消失的DLL案例,第二部分
摘要
Ray Chen的一篇技术博文,探究一个内存损坏bug:单个字节0x01破坏HMODULE句柄,导致DLL被错误释放,进而在进程终止时崩溃。
<p>上次,<a title="一个未正式卸载却从内存中消失的DLL案例" href="https://devblogs.microsoft.com/oldnewthing/20260625-00/?p=112467">我们看了由于DLL在背后被从内存中移除而导致的崩溃</a>,当有人试图调用那个大家都以为还在但其实已不存在的DLL时就会崩溃。</p>
<p>我的一个同事正在调查来自同一进程的其他崩溃,发现大部分其他崩溃也是同样的形式:“数据结构因有人向其中写入了单个字节<tt>01</tt>而损坏。”这个消息让我的调查豁然开朗。</p>
<p>我们之前看到,<a title="我的HMODULE的最低位被设置是什么意思?" href="https://devblogs.microsoft.com/oldnewthing/20260619-00/?p=112447">数据文件模块句柄的<code>HMODULE</code>的最低位被设置</a>。因此,如果这些散乱的<tt>01</tt>字节恰好覆盖了现有<code>HMODULE</code>句柄的最低位字节,就会将其变成一个(假的)数据文件模块句柄。然后,在进程销毁期间,某个组件尽职尽责地清理它们加载的DLL(例如,因为它们存储在RAII类型如<code>wil::<wbr />unique_<wbr />hmodule</code>中),代码会将这个假的数据文件模块句柄传递给<code>FreeLibrary</code>。<code>FreeLibrary</code>函数看到最低位被设置,就会说:“哦,这一定是一个通过<code>LOAD_<wbr />LIBRARY_<wbr />AS_<wbr />DATAFILE</code>加载的模块的句柄”,因此它将作为数据文件释放。</p>
<p>释放数据文件模块意味着撤销加载该模块作为数据文件时所采取的步骤:从内存中取消映射DLL。特别地,以数据文件方式加载模块并不会将DLL添加到作为代码加载的DLL列表中;因此,卸载数据文件模块并不会将其从该列表中移除。就DLL列表而言,该DLL仍在内存中。</p>
<p>一位比特错误导致代码说谎,并试图释放一个不对应于<code>LoadLibrary</code>调用的模块句柄,从而引发了大规模的混乱。</p>
<p>“DLL从内存中取消映射”的崩溃只是“有人在不应写入的地方写入<tt>01</tt>字节”bug的另一种表现。这个原始bug的<a title="Microspeak: 桶bug、桶喷溅、bug喷溅和故障转移" href="https://devblogs.microsoft.com/oldnewthing/20200121-00/?p=103351">桶喷溅</a>范围比我们最初想象的要大。</p>
<p>好消息是,所有崩溃都归结为一个单一的bug。坏消息是,你现在必须调试这一个内存损坏bug。</p>
<p>不幸的是,在撰写本文时,第三方程序中的根本内存损坏bug尚未确定。我们不知道它是来自操作系统组件还是程序本身。不过,它似乎只发生在一个进程中,并且在该进程中跨多个模块喷溅,这表明是该程序本身的问题,或者该特定进程使用系统的方式有些特殊。</p>
<p>如果你看原始堆栈跟踪,会发现问题发生在进程终止时。这可能是问题潜伏了这么久的原因:退出时的崩溃通常会被忽略,因为不会导致最终用户功能损失。用户本来就已经用完程序了。无论是干净退出还是崩溃退出,对用户来说影响不大。</p>
<p>抱歉。并非所有故事都有美满结局。</p>
<p>本文<a href="https://devblogs.microsoft.com/oldnewthing/20260626-00/?p=112472">一个未正式卸载却从内存中消失的DLL案例,第二部分</a>首发于<a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>。</p>
查看缓存全文
缓存时间: 2026/06/27 05:12
# 内存中不存在的DLL,尽管它没有被正式卸载——第二部分 - 旧事新说
来源:https://devblogs.microsoft.com/oldnewthing/20260626-00?p=112472
上次,我们探讨了因 DLL 在众人背后被悄然从内存中移除(https://devblogs.microsoft.com/oldnewthing/20260625-00/?p=112467)而导致的崩溃问题——当有人试图调用这个每个人以为还在、实则已消失的 DLL 时,就会发生崩溃。
我的一位同事正在查看此进程产生的其他崩溃,他发现其中大部分崩溃也属于“数据结构被损坏,因为有人向其中写入了一个单字节 `01`”的类型。这一信息让我的调查豁然开朗。
我们之前谈到过(https://devblogs.microsoft.com/oldnewthing/20260619-00/?p=112447),`HMODULE` 的最低位被设置为数据文件模块句柄的标志。因此,如果这些四处乱窜的 `01` 字节恰好覆盖了某个现有 `HMODULE` 句柄的最低位字节,就会将其变成一个(伪造的)数据文件模块句柄。接着,在进程销毁过程中,某组件会尽职尽责地清理它加载的 DLL 并释放它们(例如,因为它们被存储在像 `wil::unique_hmodule` 这样的 RAII 类型中),代码会将这个(伪造的)数据文件模块句柄传递给 `FreeLibrary`。`FreeLibrary` 函数检测到最低位被设置,就会认为:“哦,这一定是通过 `LOAD_LIBRARY_AS_DATAFILE` 加载的模块的句柄”,于是将其作为数据文件释放。
释放一个数据文件模块意味着撤销加载数据文件时采取的操作:将 DLL 从内存中取消映射。特别地,以数据文件方式加载模块并不会将该 DLL 添加到作为代码加载的 DLL 列表中;因此,卸载一个数据文件模块并不会将其从该列表中移除。就 DLL 列表而言,该 DLL 仍然在内存中。
一个比特的错误导致代码做出错误判断,试图释放一个并非对应 `LoadLibrary` 调用的模块句柄,从而引发了大混乱。
“DLL 从内存中取消映射”的崩溃,只是“有人把 `01` 字节写到了不该写的地方”这个 bug 的另一种表现形式。原本的这个 bug 的“桶式喷洒”(https://devblogs.microsoft.com/oldnewthing/20200121-00/?p=103351)范围比我们最初想象的要大得多。
好消息是,所有这些崩溃都归结为了一个单一的 bug。坏消息是,你现在需要调试这个内存损坏 bug。
遗憾的是,截至本文撰写时,该第三方程序中根本的内存损坏 bug 尚未被确认。我们不知道它是来自操作系统组件还是程序本身。尽管它似乎只发生在一个进程中,并波及多个模块,这暗示问题可能出在那个程序,或者这个特定进程使用系统的方式有些特殊。
如果你看看原始的调用栈,你会发现问题发生在进程终止时。这可能就是问题潜伏这么久的原因:退出时的崩溃往往不被注意,因为终端用户的功能没有损失。用户反正已经用完了程序。无论它是正常退出还是崩溃退出,对用户影响都不大。
抱歉,并非所有故事都有美好的结局。
### 分类
### 主题
## 作者
Raymond Chen
Raymond 参与 Windows 的发展已有 30 多年。2003 年,他创办了一个名为“The Old New Thing”的网站,其受欢迎程度远远超出了他最疯狂的想象,这一发展至今仍让他感到毛骨悚然。该网站催生了一本书,巧合的是书名也叫做《The Old New Thing》(Addison Wesley 2007)。他偶尔会出现在 Windows 开发文档的 Twitter 账号上,讲述一些毫无实际信息的故事。
相似文章
一个DLL未正式卸载却从内存消失的案例,第一部分
本文调查了一个崩溃转储文件:进程关闭期间,shell32.dll中发生栈溢出,导致重复的异常处理循环。分析追踪了递归异常处理,并确定了涉及DLL卸载的根本原因。
我的 HMODULE 的最低位置位意味着什么?
本文解释了 Windows 加载器通过设置 HMODULE 句柄的最低位来指示该 DLL 是作为数据文件加载的(LOAD_LIBRARY_AS_DATAFILE),这会影响资源查找和释放行为,且不会将其添加到模块列表中。
Windows DLL加载器锁:Rust线程如何使JVM挂起
QuestDB工程师调试一个偶发的Windows挂起问题,该问题由涉及Windows DLL加载器锁、Rust线程本地存储销毁、JNI分离和JVM垃圾收集安全点机制的死锁引起。
追捕 EtherSlip(DOS 网络)中潜伏 34 年的指针 Bug
一位开发者讲述如何利用 Open Watcom 的堆损坏哨兵,追踪并修复 EtherSlip DOS 包驱动里一个存在了 34 年的 NULL 指针错误。
Let's Decode the Mystery Bytes [video]
本视频通过反汇编和WinDbg调试工具,揭示了在x86环境下calloc分配内存时出现的8个神秘字节实际上是Windows堆管理器的条目头部,包含当前块大小、前一块大小等信息。