2. 修改内核的困难旅程

eBPF 允许 在Linux 内核中运行用户编写代码,让我们确保你对内核的作用有所了解。然后我们可以讨论为什么 eBPF 改变修改内核行为的游戏规则。

Linux 内核

Linux 内核是应用程序和其所运行的硬件之间的软件层。应用程序运行在被称为"用户空间"的非特权层,不能直接访问硬件。相反,应用程序通过系统调用(syscall)请求内核代表其执行。硬件访问可能涉及到文件的读写,收发网络流量,甚至只是访问内存。内核还负责协调并发执行多个应用程序进程。

作为应用程序的开发者,我们通常不直接使用系统调用接口,这是因为编程语言进行了更加易用的高层次抽象和 标准库。因此,很多人并不知道在我们的程序在运行时,内核中发生的流程。如果想了解内核调用的频率,你可以使用 strace 工具来跟踪程序所有的系统调用。这里有个样例,用 cat 命令从文件中读取 hello 单词并将显示,这个简单的命令就涉及到 100 多个系统调用:

2-0.png

由于应用程序很大程度上依赖于内核,这意味着如果能够观察到应用程序与内核的交互流程,我们就可以了解到很多关于程序的行为。例如,如果能够捕获到打开文件的系统调用,你就可以准确地分析到任何应用程序访问到哪些文件。但是,你怎么能做到这种捕获呢?考虑一下,如果我们需要修改内核,添加新的代码并打印某种输出,我们会需要做哪些工作?

为内核添加新的函数

Linux 内核很复杂,在撰写本文时,大约有 3000 万代码行 1。对代码进行修改都需要对现有的代码有一定的熟悉度,如你不是一个内核开发者,这将会是一个巨大的挑战。

但是这还不够,你将面临的挑战并不只是纯粹的技术问题。Linux 是一个通用的操作系统,需要在不同的环境和条件下运行。这意味着,如果你想对内核进行修改,这不仅仅是编写有效的代码这么简单。社区(更确切地说,是 Linux 的作者和主要开发者 Linus Torvalds)必须保证你修改的代码是为了所有人的都可获益。这并不是提交都可通过的事情,大概只有三分之一的提交的内核补丁被接受2

假设你想出了一个非常好的技术方法来拦截打开文件的系统调用。让我们想象一下,经过几个月的讨论和艰苦的开发工作,提交被接受可合并到内核代中。很好!但是,要到什么时候该功能才会出现在每个人的机器上呢?

每隔两三个月就会有一个新 的Linux 内核版本,但是即使修改已经进入了其中一个版本,仍然需要一段时间才能在大多数人的生产环境中使用。这是因为我们大多数人并不直接使用 Linux 内核--我们使用像Debian、Red Hat、Alpine、Ubuntu 等 Linux 发行版,它们将 Linux 内核的一个版本与其他各种组件打包在一起。你可能会发现,你最喜欢的发行版使用的是几年前的内核版本。

例如,很多企业用户使用 Red Hat® Enterprise Linux®(RHEL)。在写这篇文章的时候,目前的版本是RHEL 8.5,日期是 2021 年 11 月。这使用的是基于 4.18 版本的内核。这个内核是在 2018 年 8 月发布的。

正如图 2-1 中的漫画所示,新功能从想法阶段进入生产环境的 Linux 内核,简直需要数年时间。3

fig2-1.png

图 2-1. 向内核添加特性功能 (cartoon by Vadim Shchekoldin, Isovalent)

内核模块

如果你不想等待数年的时间让更改进入内核,那么还有另外一种选择。Linux 内核被设计为可接受内核模块,可以按需加载和卸载。如果你想改变或扩展内核行为,当然,编写模块是一种方法。在我们为打开文件检测系统调用的示例中,你可以编写一个内核模块来执行此操作。

这里最大的挑战仍然是完整的内核编程。用户历来对使用内核模块非常谨慎,原因很简单:如果内核代码崩溃,会导致机器及运行程序瘫痪。用户如何确信内核模块可以安全运行?

“安全运行” 并不仅仅意味着不会崩溃——用户还需要知道内核模块是安全的。例如模块是否包含攻击者可以利用的漏洞?我们是否相信模块的作者不会将恶意代码放入其中?因为内核是特权代码,它可以访问机器上的所有内容,这也包括所有数据,因此内核中的恶意代码将是一个严重的问题。当然,这也包括内核模块。

内核的安全性是 Linux 发行版需要如此长时间才会整合新的内核版本的重要原因之一。如果其他人已经在各种情况下运行内核版本数月或数年,那么大多数问题已经都会被解决。发行版维护者可以确信他们提供给用户/客户的内核是经过加固,并且可以安全运行。

eBPF 提供了一种非常不同的安全方法:eBPF 验证器(verifier),其可确保仅在安全运行时才加载 eBPF 程序。

eBPF 验证和安全

由于 eBPF 允许我们在内核中运行任意代码,因此需要一种机制来确保其安全运行,而不会使用户的机器崩溃并且损害数据。该机制就是 eBPF 验证器(verifier)。

验证器分析 eBPF 程序以确保无论任何输入,程序都将始终在有限指令数量内安全地终止。例如,如果一个程序解引用一个指针,验证器会要求程序首先要检查指针以确保其不为空。解引用指针意味着“在此地址查找值”,但空值或零值都不是要查看的有效地址。如果解引用的为程序中的空指针,程序将崩溃;而在内核中解引用空指针会使整个机器崩溃,所以避免这种情况至关重要。

验证器还会确保 eBPF 程序只能访问可应访问的内存。例如,想象一个在网络栈中触发的 eBPF 程序,并传递了包含真正传输数据的内核 socket buffer。这个 eBPF 程序可以调用特定的辅助函数,如 bpf_skb_load_bytes() 来从套接字缓冲区读取数据字节。另一个由系统调用触发的 eBPF 程序,由于没有可用的套接字缓冲区,将不允许使用 bpf_skb_load_bytes() 辅助函数。验证器还需要确保程序只读取该套接字缓冲区中的数据字节——不允许访问任意内存。目的是确保 eBPF 程序从安全角度来看是安全的。

当然,你仍然可以编写恶意的 eBPF 程序。如果可以出于正当理由查看数据,那么你也可以出于不正当理由查看数据。请注意仅从可验证来源加载受信任的 eBPF 程序,并且仅将管理 eBPF 工具的权限授予信任的具有 root 权限用户。

动态加载 eBPF 程序

eBPF 程序可以动态地加载到内核中或从内核中删除。一旦 eBPF 程序附加到一个事件,无论是什么导致该事件发生,它们都会被该事件触发。例如,如果你将程序附加到系统调用以打开文件,则只要任何进程尝试打开文件,就会触发该程序。加载程序时该进程是否已经在运行并不重要。

这使得基于 eBPF 提供可观察性或安全工具的具有一大优势——eBPF 加载后就可以立即查看机器上发生的一切。

此外,如图 2-2 所示,人们还可以通过 eBPF 非常快速地创建新的内核功能,而无需每个其他 Linux 用户都接受相同的更改。

fig2-2.png

图 2-2. 使用 eBPF 为内核添加函数 (cartoon by Vadim Shchekoldin, Isovalent)

现在你已经看到了 eBPF 是如何允许对内核进行动态的、自定义的改变的,让我们来研究一下如果你想写一个 eBPF 程序会涉及到哪些内容。


1. "Linux 5.12 将达到约 2880 万行,..." Phoronix(2021 年 3 月)。
2. Yujuan Jiang 等人,"我的补丁会成功吗?以及多快?"(论文,2013 年)。根据这篇研究论文,33% 的补丁被接受,大多数需要 3-6 个月。
3. 值得庆幸的是,现有功能的安全补丁会更快地被提供。
Copyright © 2017-2022 | 基于 CC 4.0 协议发布 | ebpf.top all right reserved. Updated at 2022-09-24 13:39:22

results matching ""

    No results matching ""