应用程序热补丁(三):完整的设计与实现 | U刻
  • 应用程序热补丁(三):完整的设计与实现

    栏目:技术分享

    前言

    在前两篇文章介绍了应用程序热补丁的关键技术:

    • 修复运行时进程的函数
    • 加载热补丁到进程中
    • 自动生成热补丁等等

    这些是组成应用程序热补丁技术框架的关键部分,但是在生产环境中使用热补丁技术还需要考虑适应现代软件的属性、热补丁的安全性、以及在运营中对热补丁的管理等等。

    通过介绍UCloud应用程序热补丁框架的设计理念和框架中各个组件,我们会解决以下实践中遇到的问题:

    • 热补丁的管理(加载、卸载、激活、回滚热补丁等)
    • 打入热补丁时的安全检查(简单说是什么时候打入热补丁是安全的)
    • 热补丁对多线程的支持等等。

    应用程序热补丁的意义

    介绍UCloud应用程序热补丁框架之前,首先介绍一下我们为什么研发和使用热补丁技术。

    目前主流的热补丁技术,例如Ksplice、kpatch、kGraft、以及后来的livepatch等都是特别针对Linux内核的热补丁技术,可以在不重启系统的情况下,修复内核缺陷。我们一般称为内核热补丁。

    UCloud使用了内核热补丁修复了若干内核问题,避免了重启系统导致的服务中断,保证了操作系统本身的可用性。

    在此基础上,我们的下一个目标提高是核心组件中的单点的可用性。例如虚拟化的核心组件QEMU,虽然作为单点程序运行,但是可用性的要求和内核是一致的。虽然QEMU本身支持在线迁移,可以迁移客户的虚拟机到新版本的QEMU上,但是迁移本身比较笨重。在迁移过程中会牵扯多个模块,例如网络、存储等,同时迁移时间和虚拟机的downtime、break time在运营上也会带来挑战。

    对比QEMU通过在线迁移升级,使用热补丁修复极快,并且对虚拟机周边环境没有依赖,可以对用户的虚拟机做到静默升级。由于热补丁本身的天然属性,热补丁更适用于代码改动较小的修复(例如安全漏洞),而在线迁移升级比较适用于大版本的升级。

    在UCloud我们通过热补丁修复了若干次QEMU的缺陷和安全漏洞,极大提高了可用性和安全性。因此我们认为对于代码改动较小的问题时,热补丁是一个完美的解决方案。

    为什么要自研应用程序热补丁技术?答案也很简单,我们无法找到一个实用并且易用的应用程序热补丁技术,同时也由于我们已经在内核热补丁领域的具有一定的积累,所以决定敢为人先、自研应用程序热补丁技术。

    设计理念

    提出需求

    介绍设计理念之前,首先应该提出应用程序热补丁在UCloud云平台的需求:

    • 应用程序热补丁的适用场景和内核热补丁是一致的,目的是修复缺陷,而不是增加功能和升级版本。所以应用程序热补丁必须允许函数级别上的修改(不论是本地函数还是全局函数)。
    • 应用程序热补丁必须是安全的,也就是打入热补丁的前后进程的状态必须一致,热补丁只会操作修改的函数,不会影响进程的正常运行。
    • 应用程序热补丁必须支持云平台环境中现代软件具有的特性,比如说Linux x86_64、多线程等等。
    • 热补丁必须由工具来构建,也必须要由工具来管理(加载卸载等)。
    • 热补丁必须同时支持回滚,同时支持一个进程多次热补丁修复。
    • 降低热补丁运营的难度。

    我们针对这些需求,设计出如下的应用程序热补丁框架。

    设计思路

    • 支持修复函数级别的、并且可以自动化生成热补丁的工具。
    • 支持多线程、热补丁安全检查、多热补丁状态管理的热补丁加载工具。
    • 运行中的应用程序应记录热补丁的信息和状态,可供外部工具查询。

    或者简单来说,我们要做到,拿到源码和patch就能通过工具自动生成热补丁,热补丁可以安全的打入运行的多线程应用程序中(不会引起程序的错乱和崩溃),并且支持打入多个热补丁。打入后的热补丁可以被回滚取消,可以查询当前应用程序中热补丁的状态和信息。

    框架组件

    这个框架的设计我们通过以下组件实现:

    • Creator

    负责通过patch和源码自动化生成热补丁。

    支持函数级别修复,不论本地函数还是全局函数。

    • Loader

    负责加载热补丁到目标进程中,也负责管理热补丁(类似于客户端程序)。

    目标进程支持Linux x86_64、多线程等。

    支持热补丁安全检查。

    支持对热补丁状态的操作(例如加载、卸载、激活、回滚查询等等)。

    • Core Runtime

    负责记录多个热补丁的状态和信息,同时提供热补丁通用操作。

    作为热补丁模块的通用运行时框架被Loader加载到目标进程中。

    Loader对热补丁状态的操作最终由Core Runtime在目标进程空间中执行。

    • 热补丁(补丁本身)

    负责提供修复后的替换代码和额外信息。

    被Loader加载到目标进程中,注册自己的信息到Core Runtime

    在激活后使用自身包含的替换代码代替有问题的函数。

    组件之间协作如下图所示,Creator工具根据程序源码和patch生成热补丁模块,然后Loader将热补丁模块加载到目标进程Process的地址空间里,最后热补丁和通用运行时Core Runtime一起完成热修复。

    实现方法

    接下来分别讲各个组件的实现:

    Creator

    基于对多种内核热补丁技术的理解,我们认为应用程序的热补丁也是可以通过工具自动生成的。虽然相比内核,应用程序的格式更加复杂、编译链接的过程也更不固定,但是自动生成热补丁应该是可行的。

    我们知道,编译源代码之后会生成目标文件,将单个或多个目标文件链接可以生成可执行文件。目标文件和可执行文件都是ELF格式(Executable and Linkable Format)。ELF是一种标准且通用的文件格式,Linux上的可执行文件、目标文件、库、core dump都是ELF。

    Creator工具根据ELF标准的格式,解析修复前后的目标文件,找到前后不同的函数,提取出差异(包括改变和新增的函数),连同差异本身的属性信息,生成一个动态链接库格式的热补丁。如下图所示:

    之前的文章介绍过二进制比较生成热补丁替换代码,这里不再赘述。

    Loader

    Loader工具作为一个客户端程序,操作目标进程Process,包括热补丁的加载、激活、回滚、卸载、查看等。如下所示:

    Loader利用了ptrace调用。Loader通过ptrace可以对Process的内存、寄存器进行读写,改变Process的运行状态,也可以捕获Process的信号。这样Loader可以停止Process的运行,并根据AMD64 ABI对内存和寄存器进行修改,使Process执行dlopen等函数,加载热补丁到Process的地址空间中,或者执行其他热补丁的操作。热补丁被加载之后,在/proc/pid/maps文件中可以看到。

    Loader如何利用ptrace加载热补丁在之前的文章中有详细描述,不再赘述。

    Loader随后会停止Process所有的线程,准备激活热补丁,此时Loader需要进行一致性检查,也就是查看热补丁的激活对当前的所有线程来讲是否安全。需要检查的是热补丁的需要替换的函数是否在线程当前函数调用栈上,如果在调用栈上,说明现在是不安全的,激活热补丁不能保证一致性。

    在停止所有线程的时候,首先需要得到全部的线程信息,可以通过libthread_db库与进程中libc进行交互得到线程的信息,也可以通过/proc/pid/tasks/目录从内核中直接得到线程的信息。在停止所有线程之后,需要再次获取所有线程信息,查看是否有新增线程,如果有需要停止新增线程。重复以上动作直到没有新线程出现。

    Core Runtime

    在一个进程的生命周期中,可能需要多次热补丁修复,同时多个热补丁也会使用一些通用的功能,因此需要一个通用的核心模块来提供通用功能,并且记录进程中每个热补丁的信息。这个通用模块作为一个动态链接库,我们叫做Core Runtime。

    Loader在加载热补丁时,首先需要加载Core Runtime到进程的地址空间,对进程而言,Core Runtime只需要加载一次。

    在热补丁被加载到进程的地址空间后,通过构造函数,首先向Core Runtime提供自己的信息,注册到Core Runtime里,然后将热补丁中差异函数的需要重定向的部分手动计算重定向。在激活热补丁的时候,Core Runtime会根据热补丁注册时得到的信息,保存旧函数,并把旧函数入口位置替换成跳转到新的函数的机器码,完成热修复。如下所示:

    在回滚热补丁的时候,Core Runtime把旧函数入口位置恢复,完成回滚。

    Core Runtime同时可以管理多个热补丁,以热补丁的名字作为ID,区分不同的热补丁,记录必要的信息。

    如下所示:

    热补丁(补丁本身)

    这里的热补丁指的是狭义上的作为动态链接库被Loader加载到目标进程Process中的热补丁。

    热补丁由Creator产生,包含了替换代码和一些动态信息(比如新旧函数的地址、大小、重定向信息等)。热补丁被加载后,包含的函数和变量就存在于目标进程的地址空间中。热补丁激活以后,所有对老函数的访问,都会重定向到热补丁地址范围内的新函数。

    如下所示:

    总结

    Creator、Loader、Core Runtime、热补丁这四者构成了UCloud热补丁技术框架,这四个组件相辅相成,互相协作完成热补丁。

    Creator负责生成热补丁,Loader负责热补丁的进程外管理(包括加载、卸载、激活、回滚热补丁等),Core Runtime负责热补丁的进程内管理(记录热补丁、备份旧函数、恢复旧函数等)。虽然是四个组件,但是都必须遵守同一个热补丁规格标准,这样才能共同完成热补丁的工作。

    通过这个框架,极大降低了我们制作热补丁、打入热补丁和运营热补丁的难度。

    例如,一个QEMU安全漏洞修复的流程可以简化为:

    1. 1. Creator通过QEMU源码和漏洞修复patch生成热补丁。
    2. 2. 热补丁被Loader打入正在运行的应用程序中(加载并且激活)。
    3. 3. (可选)对运行中的应用程序查询热补丁的状态和信息。
    4. 4. (可选)对已经打入的热补丁进行回滚和卸载。

    值得指出的是,目前不是全部patch都可以自动生成热补丁,原因是极少部分由于程序修改复杂,但是可以通过手动修改patch简化代码或者简化逻辑做到可以自动生成热补丁。大约90%的patch在无需修改的情况下都能自动生成热补丁。

    在一些特定场景下,我们通过第一篇文章(《应用程序热补丁(一):几行代码构造免重启修复补丁》)中介绍的热补丁技术手动编写热补丁即可,无需使用复杂的自动生成热补丁技术。

    另外,目前UCloud应用程序热补丁技术支持Linux C语言程序,但对于其他编译型语言解决思路基本一致(例如C++等)。

    在UCloud,我们利用应用程序热补丁修复了若干紧急安全漏洞和缺陷,在关键时刻迅速解决问题,相比于传统的软件升级方式,解决问题更加及时。

    希望通过一系列的文章填补目前应用程序热补丁的空白部分,使更多人了解热补丁的技术原理,让热补丁技术给更多人带来更多的价值。

    6