Driving Me Nuts - Kernel 中绝对不该做的事
在面向 Linux 内核编程新手开发者的邮件列表中(请参阅在线资源),会提出许多常见问题。几乎每次有人提出这些问题时,得到的回复总是“不要那样做!”,这让困惑的提问者感到不解,并疑惑自己究竟进入了一个什么样的奇怪的开发社区。本文是不定期系列文章的第一篇,旨在解释为什么通常不建议这样做。接下来,为了弥补之前的批评,我们将打破所有规则,并向您展示如何才能 *真的* 做到。
在这个“不要那样做”类别中最常见的问题是:“我如何在内核模块中读取文件?” 大多数新的内核开发者都来自用户空间编程环境或其他操作系统,在这些环境中,读取文件是将配置信息引入程序的自然且必要的部分。然而,在 Linux 内核中,从文件中读取数据以获取配置信息被认为是禁止的。这是因为如果开发者尝试这样做,可能会导致各种不同的问题。
最常见的问题是解释数据。从内核内部编写文件解释器是一个容易出现问题的过程,并且该解释器中的任何错误都可能导致灾难性的崩溃。此外,解释器中的任何错误都可能导致缓冲区溢出。这些溢出可能允许非特权用户接管机器或访问受保护的数据,例如密码文件。
试图保护内核免受愚蠢的编程错误的影响并不是不允许驱动程序读取文件的最重要原因。最大的问题是策略。Linux 内核程序员试图尽可能快地逃离“策略”这个词。他们几乎从不希望内核强制用户空间执行可以避免的策略。让模块从文件系统中特定位置读取文件会强制设置该文件位置的策略。如果 Linux 发行商决定处理系统所有配置文件的最简单方法是将它们放在 /var/black/hole/of/configs 中,则必须修改此内核模块以支持此更改。这对于 Linux 内核社区来说是不可接受的。
尝试从内核内部读取文件的另一个大问题是试图弄清楚文件到底在哪里。Linux 支持文件系统命名空间,这允许每个进程包含其自己的文件系统视图。这允许某些程序仅看到整个文件系统的一部分,而其他程序则在不同的位置看到文件系统。这是一个强大的功能,试图确定您的模块位于正确的文件系统命名空间中是一项不可能完成的任务。
如果这些大问题还不够,那么如何将配置信息放入内核的最终问题也是一个策略决策。通过强制内核模块每次都读取文件,作者就强制做出了这个决定。但是,某些发行版可能会认为最好将系统配置存储在本地数据库中,并让辅助程序在适当的时候将数据输入内核。或者,他们可能希望以某种方式连接到外部机器,以确定当时的正确配置。无论用户决定采用哪种方法来存储配置数据,通过强制将其放在特定文件中,他或她都在用户身上强制执行该策略决策,这是一个坏主意。
在最终理解 Linux 内核程序员对策略决策的反感,并认为那些理想主义者都疯了之后,您仍然面临着如何将配置数据输入内核模块的实际问题。如何在不招致愤怒的电子邮件口水战的情况下完成此操作?
向特定内核模块发送数据的常用方法是使用字符设备和 ioctl 系统调用。这允许作者向内核发送几乎任何类型的数据,用户空间程序在初始化过程中的适当时间发送数据。然而,ioctl 命令已被确定具有许多不良副作用,并且通常不赞成在内核中创建新的 ioctl。此外,尝试正确处理 32 位用户空间程序对 64 位内核进行 ioctl 调用,并以正确的方式转换所有数据类型,这是一项可怕的任务。
由于不允许使用 ioctl,因此可以使用 /proc 文件系统将配置数据输入内核。通过将数据写入内核模块创建的文件系统中的文件,内核模块可以直接访问它。然而,最近,proc 文件系统受到了内核开发人员的限制,因为程序员长期以来严重滥用它来包含几乎任何类型的数据。这个文件系统正在慢慢清理,只包含进程信息,例如文件系统状态的名称。
对于更结构化的文件系统,sysfs 文件系统为任何设备和任何驱动程序提供了一种创建文件的方法,可以将配置数据发送到这些文件。此接口优于 ioctl 和使用 /proc。有关如何在内核模块中创建和使用 sysfs 文件的信息,请参阅本专栏之前的文章。
现在您了解了禁止从内核模块读取文件背后的原因,您当然可以跳过本文的其余部分。它与您无关,因为您正忙于将内核模块转换为使用 sysfs。
还在这里?好吧,所以您仍然想知道如何从内核模块读取文件,并且任何劝说都无法让您改变主意。您保证永远不会在将要提交以包含到主内核树中的代码中尝试这样做,并且我从未描述过如何这样做,对吗?
实际上,读取文件非常简单,一旦解决了一个小问题。许多内核系统调用都导出供模块使用;这些系统调用以 sys_ 开头。因此,对于 read 系统调用,应使用 sys_read 函数。
读取文件的常用方法是尝试如下所示的代码
fd = sys_open(filename, O_RDONLY, 0); if (fd >= 0) { /* read the file here */ sys_close(fd); }
但是,当在内核模块中尝试这样做时,sys_open() 调用通常会返回错误 -EFAULT。这会导致作者在邮件列表中发布问题,从而引出上面描述的“不要从内核读取文件”的回复。
作者忘记考虑的主要事情是内核期望传递给 sys_open() 函数调用的指针来自用户空间。因此,它会对指针进行检查,以验证它是否在正确的地址空间中,以便尝试将其转换为内核指针,供内核的其余部分使用。因此,当我们尝试将内核指针传递给函数时,会发生错误 -EFAULT。
为了处理这种地址空间不匹配,请使用函数 get_fs() 和 set_fs()。这些函数会根据调用者的需要修改当前进程的地址限制。对于 sys_open(),我们想告诉内核来自内核地址空间的指针是安全的,所以我们调用
set_fs(KERNEL_DS);
set_fs() 函数的唯一两个有效选项是 KERNEL_DS 和 USER_DS,分别大致代表内核数据段和用户数据段。
要确定在修改地址限制之前的当前地址限制是什么,请调用 get_fs() 函数。然后,当内核模块完成滥用内核 API 后,它可以恢复正确的地址限制。
因此,有了这些知识,编写上述代码片段的正确方法是
old_fs = get_fs(); set_fs(KERNEL_DS); fd = sys_open(filename, O_RDONLY, 0); if (fd >= 0) { /* read the file here */ sys_close(fd); } set_fs(old_fs);
下面可以看到一个完整模块的示例,该模块读取 /etc/shadow 文件并将其转储到内核系统日志中,证明这样做可能很危险
#include <linux/kernel.h> #include <linux/init.h> #include <linux/module.h> #include <linux/syscalls.h> #include <linux/fcntl.h> #include <asm/uaccess.h> static void read_file(char *filename) { int fd; char buf[1]; mm_segment_t old_fs = get_fs(); set_fs(KERNEL_DS); fd = sys_open(filename, O_RDONLY, 0); if (fd >= 0) { printk(KERN_DEBUG); while (sys_read(fd, buf, 1) == 1) printk("%c", buf[0]); printk("\n"); sys_close(fd); } set_fs(old_fs); } static int __init init(void) { read_file("/etc/shadow"); return 0; } static void __exit exit(void) { } MODULE_LICENSE("GPL"); module_init(init); module_exit(exit);
现在,您掌握了如何滥用内核系统调用 API 并惹恼内核程序员的新知识,您真的可以试试运气,从内核内部写入文件。启动您最喜欢的编辑器,然后编写类似以下内容的代码
old_fs = get_fs(); set_fs(KERNEL_DS); fd = sys_open(filename, O_WRONLY|O_CREAT, 0644); if (fd >= 0) { sys_write(data, strlen(data); sys_close(fd); } set_fs(old_fs);
该代码似乎可以正确构建,没有编译时警告,但是当您尝试加载模块时,您会收到这个奇怪的错误
insmod: error inserting 'evil.ko': -1 Unknown symbol in module
这意味着您的模块尝试使用的符号尚未导出,并且在内核中不可用。通过查看内核日志,您可以确定该符号是什么
evil: Unknown symbol sys_write
因此,即使函数 sys_write 存在于 syscalls.h 头文件中,它也没有导出以供内核模块使用。实际上,在三个不同的平台上导出了此符号,但是谁真的使用 parisc 架构呢?为了解决这个问题,我们需要利用内核模块可用的内核函数。通过阅读 sys_write 函数的实现代码,可以绕过缺少导出符号的问题。以下内核模块展示了如何通过不使用 sys_write 调用来完成此操作
#include <linux/kernel.h> #include <linux/init.h> #include <linux/module.h> #include <linux/syscalls.h> #include <linux/file.h> #include <linux/fs.h> #include <linux/fcntl.h> #include <asm/uaccess.h> static void write_file(char *filename, char *data) { struct file *file; loff_t pos = 0; int fd; mm_segment_t old_fs = get_fs(); set_fs(KERNEL_DS); fd = sys_open(filename, O_WRONLY|O_CREAT, 0644); if (fd >= 0) { sys_write(fd, data, strlen(data)); file = fget(fd); if (file) { vfs_write(file, data, strlen(data), &pos); fput(file); } sys_close(fd); } set_fs(old_fs); } static int __init init(void) { write_file("/tmp/test", "Evil file.\n"); return 0; } static void __exit exit(void) { } MODULE_LICENSE("GPL"); module_init(init); module_exit(exit);
正如您所看到的,通过使用函数 fget、fput 和 vfs_write,我们可以实现我们自己的 sys_write 功能。
总之,从内核内部读取和写入文件是一件非常非常糟糕的事情。永远不要这样做。永远。本文中的两个模块以及用于编译它们的一个 Makefile 都可以在 Linux Journal FTP 站点上找到,但我们希望在日志中看不到任何下载。而且,我也从未告诉过您如何这样做。您是从其他人那里学到的,而其他人是从他姐姐最好的朋友那里学到的,而她是从她的同事那里听说如何做的。
本文的资源: /article/8130。
Greg Kroah-Hartman 是 Linux 设备驱动程序,第 3 版 的作者之一,并且是比他愿意承认的更多的驱动程序子系统的内核维护者。他在 SuSE Labs 工作,从事各种特定于内核的工作,如有与本文无关的问题,可以通过 greg@kroah.com 与他联系。