2. Linux 内核驱动基础回顾:模块加载与卸载、file_operations 结构体、字符设备驱动框架
在深入安全驱动开发之前,必须牢固掌握 Linux 内核驱动的基础骨架。本章聚焦于三个核心支柱:模块生命周期管理、文件操作接口以及字符设备驱动框架。这些是构建任何内核态安全通道(包括 OP-TEE 驱动)的基石。
2.1 内核模块的加载与卸载
Linux 内核模块允许在运行时动态扩展内核功能,无需重新编译整个内核。每个模块都包含两个强制入口点:module_init() 和 module_exit()。
关键安全原则: 模块加载时运行在内核最高特权级(Ring 0)。任何内存访问错误、空指针解引用或资源泄漏都可能导致整个系统崩溃或产生安全漏洞。安全驱动开发必须确保 init 和 exit 函数的幂等性与原子性。
2.1.1 模块加载流程
- insmod / modprobe:用户态工具将 .ko 文件加载到内核空间。
- 内核模块加载器:执行重定位、解析符号依赖、调用
module_init()注册的函数。 - 初始化函数:分配资源、注册设备、创建 /dev 节点。
- 模块状态:从
MODULE_STATE_COMING转换为MODULE_STATE_LIVE。
2.1.2 模块卸载流程
- rmmod:用户态请求卸载模块。
- 内核检查:确认模块引用计数为 0,无进程正在使用。
- 清理函数:调用
module_exit()注册的函数,释放所有资源。 - 模块状态:转换为
MODULE_STATE_GOING,最终从内核中移除。
2.1.3 模块参数与许可证
| 宏 / 声明 | 用途 | 安全注意事项 |
|---|---|---|
module_param(name, type, perm) |
定义模块加载时可传递的参数 | 参数校验不严可能导致缓冲区溢出或内核崩溃 |
MODULE_LICENSE("GPL") |
声明模块许可证,影响符号导出 | 非 GPL 模块无法调用 EXPORT_SYMBOL_GPL 导出的符号 |
MODULE_AUTHOR() |
作者信息 | 无直接安全影响,但便于审计追踪 |
MODULE_DESCRIPTION() |
模块功能描述 | 无直接安全影响 |
// 最小安全模块示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
static int __init safe_driver_init(void)
{
pr_info("Safe Driver: Module loaded successfully\n");
// 安全实践:检查返回值,确保资源分配成功
return 0;
}
static void __exit safe_driver_exit(void)
{
pr_info("Safe Driver: Module unloaded\n");
// 安全实践:确保所有资源已被释放,无内存泄漏
}
module_init(safe_driver_init);
module_exit(safe_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Security Expert");
MODULE_DESCRIPTION("A minimal safe driver template");
常见陷阱: 在
module_exit() 中忘记释放 kmalloc 分配的内存、未注销字符设备、未销毁 mutex 或 spinlock,会导致内核内存泄漏或死锁。安全驱动必须保证 exit 路径的完整性。
2.2 file_operations 结构体
file_operations 是字符设备驱动的核心接口,它将用户空间的系统调用(如 open、read、write、ioctl)映射到内核驱动的具体实现函数。每个字段都是一个函数指针,指向驱动开发者提供的回调。
2.2.1 关键成员与安全职责
| 成员 | 系统调用 | 安全职责 |
|---|---|---|
.owner |
— | 设置为 THIS_MODULE,防止模块在使用中被卸载 |
.open |
open() |
权限检查、设备状态初始化、递增引用计数 |
.release |
close() |
资源清理、递减引用计数、确保无数据残留 |
.read |
read() |
边界检查、避免越界读取、不泄露内核栈/堆敏感数据 |
.write |
write() |
输入验证、长度检查、防止缓冲区溢出 |
.unlocked_ioctl |
ioctl() |
命令号校验、参数拷贝(copy_from_user)、权限检查 |
.mmap |
mmap() |
物理内存范围检查、禁止映射敏感内核内存区域 |
// file_operations 结构体定义示例
static struct file_operations safe_fops = {
.owner = THIS_MODULE,
.open = safe_driver_open,
.release = safe_driver_release,
.read = safe_driver_read,
.write = safe_driver_write,
.unlocked_ioctl = safe_driver_ioctl,
};
2.2.2 用户空间与内核空间的数据拷贝
安全驱动开发中最常见的漏洞来源之一就是用户空间与内核空间之间的数据交换。必须使用内核提供的专用 API,而不是直接解引用用户空间指针。
copy_from_user():从用户空间拷贝数据到内核空间。必须检查返回值(未拷贝的字节数),如果非零则说明拷贝失败。copy_to_user():从内核空间拷贝数据到用户空间。同样需要检查返回值。access_ok():在拷贝前验证用户空间地址范围是否合法(现代内核中copy_from_user内部已包含此检查,但显式调用可增加防御深度)。
安全编码规范: 永远不要使用
memcpy()、sprintf() 或 strcpy() 直接操作来自用户空间的指针。始终使用 copy_from_user/copy_to_user 系列函数,并对所有输入长度进行严格校验。
2.3 字符设备驱动框架
字符设备是 Linux 中最基本的设备类型,以字节流方式访问。注册一个字符设备需要三个核心步骤:分配设备号、初始化 cdev 结构体、将设备添加到系统。
2.3.1 完整注册流程
- 分配设备号:使用
alloc_chrdev_region()(动态分配)或register_chrdev_region()(静态指定)。 - 初始化 cdev:调用
cdev_init()并将file_operations结构体关联到cdev。 - 添加设备:调用
cdev_add()将设备注册到内核 VFS 层。 - 创建设备节点:在
/dev下创建设备文件(通常通过device_create()配合类实现)。
// 字符设备驱动框架核心代码
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "safe_char_dev"
#define CLASS_NAME "safe_class"
static int major_num;
static struct class *safe_class = NULL;
static struct device *safe_device = NULL;
static struct cdev safe_cdev;
static int __init safe_char_init(void)
{
dev_t dev_num;
// 1. 动态分配设备号
if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
pr_err("Failed to allocate device number\n");
return -1;
}
major_num = MAJOR(dev_num);
pr_info("Allocated major number: %d\n", major_num);
// 2. 初始化 cdev 并关联 file_operations
cdev_init(&safe_cdev, &safe_fops);
safe_cdev.owner = THIS_MODULE;
// 3. 添加 cdev 到内核
if (cdev_add(&safe_cdev, dev_num, 1) < 0) {
pr_err("Failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return -1;
}
// 4. 创建类并在 /dev 下创建设备节点
safe_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(safe_class)) {
pr_err("Failed to create class\n");
cdev_del(&safe_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(safe_class);
}
safe_device = device_create(safe_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(safe_device)) {
pr_err("Failed to create device\n");
class_destroy(safe_class);
cdev_del(&safe_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(safe_device);
}
pr_info("Safe char device registered successfully\n");
return 0;
}
static void __exit safe_char_exit(void)
{
dev_t dev_num = MKDEV(major_num, 0);
// 逆序释放资源
device_destroy(safe_class, dev_num);
class_destroy(safe_class);
cdev_del(&safe_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("Safe char device unregistered\n");
}
module_init(safe_char_init);
module_exit(safe_char_exit);
2.3.2 字符设备驱动架构图
下图展示了用户空间应用程序通过 VFS 层与内核字符设备驱动交互的完整路径,以及安全边界所在的位置。
2.4 关键安全实践总结
| 组件 | 安全风险 | 缓解措施 |
|---|---|---|
| 模块加载/卸载 | 资源泄漏、引用计数错误、竞态条件 | 使用 THIS_MODULE、确保 exit 路径完整、使用 mutex 保护共享资源 |
| file_operations | 空指针解引用、越界访问、权限绕过 | 所有回调函数检查 filp->private_data、验证 count 和 offset、使用 capable() 检查权限 |
| 数据拷贝 | 内核内存泄露、栈/堆溢出、信息泄露 | 始终使用 copy_from_user/copy_to_user、检查返回值、使用 kzalloc 避免未初始化数据 |
| 设备注册 | 设备号冲突、cdev 未正确初始化、udev 节点权限错误 | 使用动态分配、检查 cdev_add 返回值、设置合理的 devnode 回调控制访问权限 |