
为何热补丁需要原子性?即使能暂停所有线程也不行
来源标题:Why worry about hot-patch atomicity if we can suspend all threads?
Windows 热补丁系统要求补丁原子激活,因为仅靠暂停进程内所有线程无法避免指令截断风险。x86 等变长指令架构下,若线程在执行原指令的中间字节时被冻结,替换后的指令可能从中间字节开始执行。因此必须保证原指令长度不小于替换指令,但这仍不足以完全避免问题。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
我提到过,Windows 热补丁系统的一个关键考量是补丁必须能够原子激活。但为什么一定要原子性呢?难道不能直接挂起进程中的所有线程(执行补丁的那个除外)吗?这样一来,就没有其他线程会执行到只安装了一半的补丁了。
对于使用变长指令的处理器(说的就是你,x86),被补丁的指令不能比替换它的指令短。即使你能挂起进程中的所有线程,这条规则依然适用,因为你可能在某个线程即将执行替换指令后半部分字节的时候把它挂起。
如果你运气不好,在线程执行完 XX 指令、即将执行 YY 指令的瞬间把它冻住了,然后你用一条两字节指令 AA BB 替换了这两个字节,那么当线程恢复运行时,它会把 BB 当作一条新指令的开头来执行。
好吧,那我们确保原始指令至少和替换指令一样大,这样被冻住的线程就永远不会卡在替换指令的中间。但假设我们做到了这一点,难道不能直接挂起所有线程,然后随意打补丁,而不必操心原子性吗?
确实,你挂起了进程中的所有线程,但这并不能阻止通过 CreateRemoteThread 之类的函数在进程中创建新线程。CreateRemoteThread 的调用者可以指定新线程是否挂起,如果它选择了不挂起(或者选择了挂起之后又决定恢复它),那么在你忙着打补丁的时候,就有一个活着的线程在进程里运行了。
原子性之所以重要,就是为了避免在注入的线程恰好正在执行某段代码时,你不小心给这段代码打了补丁。
补充说明:当然,原子性也不是万无一失的,因为即使你成功挂起了所有线程,并阻止了新线程的注入,仍然存在有人在进程内所有线程都被挂起时调用 WriteProcessMemory 来修改进程字节的情况。
而我们前面已经说过,WriteProcessMemory 不是原子的,所以有可能一个两字节的 WriteProcessMemory 先写入了第一个字节,然后你做了两字节的原子更新,接着 WriteProcessMemory 完成了第二个字节的写入,结果你的补丁和那个乱入的写入混在了一起。
另一方面,任何用 WriteProcessMemory 来做代码补丁的人,本来就因为没有原子性而已经处于不安全状态了,所以我们并没有引入新问题。
此外,由于热补丁空间是保留给 Windows Update 使用的,外部程序用 WriteProcessMemory 从另一个进程来打热补丁,本身就已经违反了规则。到了这一步,Windows Update 的态度是:“你看,我没法防住这种情况,而且事情发展到这一步你已经处于双重不受支持的境地了,所以出什么问题都是你自己的事。”
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。