4

Topics In Live Kernel Patching

 3 years ago
source link: https://kernel.taobao.org/2017/01/topics-in-live-kernel-patching/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Jan 1, 2017

Topics In Live Kernel Patching

在内核主线提供热补丁的能力是个长期的过程。4.0版本开始就已经集成了基本的热补丁功能,但是更多的支持阻塞在一致性模型(代码保证一个热补丁应用到一个正在运行的内核上是安全的)该如ä½工作上面。此外,kernel stack validation一文提出了最大的反对意见,因此,现在是时候向前走了。在2016年Linux Plumbers Conference上面,热补丁方向的内核开发者们坐在一起讨论内核热补丁当前的挑战以及未来的方向。

这篇文章并不是那个为时半天的讨论的一个总结,而是想通过展示热补丁开发者所面临的一些挑战以及他们准备如何应对这些挑战来聊一聊一些更有趣的话题。 [编辑] 帮倒忙的编译器优化

一个聪明的编译器对于每个想要他们的代码获得可观的性能的人来说都很重要,但是,当编译器变得过分聪明的时,就会有另一些问题。譬如,有时候跟并发执行的内核打交道的开å者不得不担忧编译器的过分优化。Miroslav Benes说,内核热补丁的开发者也得担心这些问题。编译器优化可能会在一些微妙的方式上改变代码编译的方式,以至于当热补丁应用上的时候会导致错乱ã

我们从最简单的问题开始,之后在讨论那些比较tricky的。Benes发现当需要对一个内联函数打patch时,自动inlining功能会是个问题。不过这种情况的解决方法相对比较简答,对于所有的要patch的内è函数的调用者统统都需要变。gcc的-fpartial-inlining选项会导致这个问题变得复杂,但是并不会改变这个问题的本质。

-fipa-sra选项相对来说更加微妙一些,它可能导致删除一些没有用到的函数参数,或者改变函数参数传递的方式。也就是说,对于函数的调用者来说,它改变了函数的ABI。对于这种函数的热补丁会改变这种-fipa-sra优化的工作方式(譬如之前优化是会删除某个没有用到的参数,打了热补丁之后,这个参数就需要用到了,优化就会保留这个参数),因此可能会导致ABI出现奇怪的改变。好消息是,当这种情况发生的时候,GCC同时会改变被编译的函数的名字,这样,被破坏的ABI会立马显现出来。但是,这样导致不能直接给一个有bug的函数打补丁,这个函数的调用者也就得跟着打patch了。

带-fipa-pure-const编译的代码可能会改变一个函数工作的方式;如果一个函数看起æ¥并不会访问主存,编译器会假设主存的状态在这个函数调用前后不会发生变化。当一个热补丁改变了这个函数的行为时,这些假设可能不在成立;再一次,这个函数的所有调用者又得全部跟着都打一次补丁。

一个更加”疯狂“的选项是-fipa-icf,这个选项会执行相同代码折叠(Identical Code Folding)。简单来说,它会把拥有相同功能的函数合并成一个,这个功能可以减小代码量,但是很难检æµ某个函数是否被”折叠“了。代码折叠对于内核stack unwinder来说也是个问题。另外,还有一些其他问题。譬如,当一个GCC认为一个函数不会改变某个全局变量时。当一个函数打了patch后会改变这个全局变量,调用这个函数的代码可能就不对了。这种类型的问题,同样很难检测。或者如果GCC有一个选项,可以要求它对于它做的优化创建一个日志会更好一些。

或许最吓人的选项是-fipa-ra,è¿个选项追踪调用的函数所使用的寄存器,并且避免去保存这些不会被改变的寄存器的值。对于这种函数打补丁很容易会导致这个函数使用新的寄存器,从而导致调用者数据错乱,并且很可能大幅减少热补丁开发者期望热补丁工作的时间。这个选项很难被检测到,同时,它也可以看做是一种的ABI的改变,但是函数名却没有变。当前,这个优化选项在GCC开启了-pg时候会被禁用,并且Ftrace子ç³»统需要-pg选项来支持热补丁。但是-pg和–fipa-ra选项并没有内在的原因导致两者不兼容,因此,这个行为可能在任何某个时候会被修改。

Miroslav说,上述几点,只是编译器优化对于热补丁技术所带来的问题的一个小的子集。随着编译器开发者追求更加激进的优化,这些问题会变得越来越严重。 [编辑] 补丁构建

内核有一个标准的方式应用一个热补丁,但没有任何类型的主线机制来创建ç­补丁。Josh Poimboeuf给出了一些补丁制作工具的简要总结,着眼于选择一个用于upstream。

第一个工具是kpatch-build。它的工作原理是构建有补丁应用和没补丁应用的内核,然后比较二进制diff看哪些å能改变。所有改变功能的提取和包装成一个“Josh Poimboeuf 内核模块”,而这个模块是随附着热补丁的。他说,这是一个给力的系统,具有许多优点,包括它自动处理前面提到的大多数优化问题。另一方面,kpatch-build相当复杂。它必须知道内核所使用的所有特殊部分(special sections),以及它对某些类型的更改是有问题的.。目前它只能运行中x86_64体系结构,对于不同的体系结构,这些特殊部分(special sections)也不同,所以把kpatch-build变成一个多架构的工具并不容易。而且,他说,kpatch建立是易碎的,甚至是一场噩梦。

另一种方法是使用常规的内核构建系统和它的模块构建基础设施ã将改变的函数复制并粘贴到一个新的模块,增加一些样板并用热补丁API注册函数,最终完成任务。这种方式很容易,但有它自己的问题,特别是这个模块是无法访问非导出的符号,而这些符号可能会被打补丁的函数使用到。这个问题可以通过使用kallsyms_lookup_name()身边工作,但是这个解决方案容易出错,速度慢,还有点“恶心”。

第三种方法有点新,事实上,他在会议召开前一周就发å¸了这个建议。这种方法使用复制和粘贴的方法,但增加了一个API和后处理工具,可以使生成的模块能够访问非导出的符号。目前这种方法有效,尽管现在还有许多可以改进的地方,包括自动连æ¥到非导出符号的过程和检测编译器优化的干扰。

在演讲结束时的简短讨论中,很明显可以发现,人们对新的工具没有太多的关注,因此这可能就是要发展的方向。 [编辑] 模块依赖

内核热补丁æ术可以修改内核模块以便修复其中的代码错误。而这带来了一个有趣的问题:如果需要修复的内核模块在补丁模块加载时没有到加载系统中,而后又被人为加载了该如何处理。当前内核热补丁æ术实现了一套复杂的基础架构来监测这一问题并在内核模块加载时一并加载补丁模块。Jessica Yu是当前内核模块加载子系统的维护者,她在会议上介绍了当前这一机制对内核模块加载的影响。

补丁模块自己其实也是一个内核模块。允许这样一个补丁模块加载时先加载问题模块本身需要模块携带很多额外的信息,并需要一套复杂的架构来实现这一功能。此外,这样一套基础架构还跳过了å½前内核模块加载的依赖机制,包括在热补丁相关代码中几乎重写了一遍内核模块加载器。

当然还有其他的方法来解决这一问题。一种方式是简单的要求问题模块必须在补丁模块加载前先行加载。这样做当然是可以解决问题的,但缺点是这样会加载很多无用的内核模块。另外一种可能的解决方法是将补丁模块根据修复的问题模块做拆分,拆分成多个不同的补丁模块,根据当前系统中加è½½的问题模块加载对应的补丁模块。

第一种方式可以极大地简化内核热补丁模块的相关代码,同时减小代码冗余。但有个问题缺不容易解决。即如何强制加载需要打补丁的所有问题模块。depmod工具无法识别这种依赖。FreeBSD系统上有一个MODULE_DEPEND()宏可以完美的处理此问题,但是该宏定义在Linux上并没有实现。

第二种拆分补丁模块的方式对于影响面很大的补丁并不适用。以CVE-2016-7097为ä¾,该补丁修复了一处位于VFS层API上的安全漏洞。但这一补丁会涉及所有文件系统模块。如果将这一补丁模块进行拆分,结果则是生成一长串模块列表。

在一系列的讨论中,Steve Rostedt提出了一个观点,既然问题模块并没有加载,为什么不直接将磁盘上的问题模块替换成修复后的新模块。而Jiri Kosina的回答是这样会造成发行版软件包管理上的混乱。即内核模块和其对应的软件包版本并不ä¸致。而这样的不一致会造成很多麻烦。同时新模块并不像补丁模块一样可以随时回退,把磁盘上已经替换掉的问题模块恢复回来也是个麻烦事。尽管如此,Rostdt依然坚持认为直接替换磁盘上的å核模块才是问题的解决之道。

讨论的最后大家达成的一致意见是当前内核热补丁对于模块依赖问题的解决方法已经足够了。 [编辑] 其他话题

Petr Mladek讨论了关于数据结构修改的问题。对于全å±变量这类数据结构的修改还是比较容易的。困难的是串联在多个链表上的数据结构的修改,这几乎是不太可能直接通过类型的转换来做到的。对于这类数据结构的修改要十分小心。现在有一些技术来尝试解决这一问题,比如对于需要修改的数据结构添加影子结构。但是这样会造成额外的性能开销。同时当补丁模块卸载时的处理也十分复杂。

Miroslav Benes讨论了关于一些调度器相关代码的修改。比如schedule()就是一个很棘手的函数。因为这个函数在返回前后可能调用者的调用栈已经改变了。

他给出了一个解决的方法是在上下文切换时将指令指针保存到上线文中。这样补丁模块在å 载后可以通过这一信息判断schedule()前后的上线文信息是否一致。这一方式被证明是可以工作的,但是是否值得就另当别论了。毕竟schedule()极少出现问题。

Jiri Kosina讨论了内核热补丁未来的工ä½。其中之一是一致性模型。现在的栈验证工作已经完成,内核栈回溯机制是可信的,因此一致性模型的相关工作可以继续开展了。

特别是混合一致性模型可以继续推进。但是还有另外一个问题ï¼为了构建确保栈信息可靠,需要内核编译时打开栈针。而这会带来约10%的性能开销。目前还不知道打开这一选项为什么会带来如此巨大的性能损失。Mel Gorman正在跟进这一问题。

Kosina说当前正å¨将内核热补丁移植到arm64平台上。而其他相关工作也在有序开展中。

此外会中还讨论了PowerPC的相关问题,以及用户态热补丁的问题。

Balbir Singh提出是否真的需要在集群中使用热补丁技术?如æ一个集群可以分批下线进行升级,那么热补丁技术其实并不是必须的。当然对于用户来说热补丁技术可能确实是需要。

最后一个问题是关于rootkits的。一个可以将指定代码注入到运行的内核中ç工具。Kosina说他并不担心这个工具。因为内核热补丁本身就是内核模块,如果攻击者可以加载一个内核模块,那他也可以做任何他想做的事情。但Singh说,如果热补丁本身存在缺陷怎么办。如果ä¿®了一个安全问题却引入了新的安全问题呢。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK