3. 驱动中的用户态与内核态
3.1 用户态与内核态:权限分界
Linux 系统通过 CPU 特权级将运行空间划分为用户态(Ring 3)和内核态(Ring 0)。驱动程序运行在内核态,拥有最高权限,可以直接访问硬件和系统全局资源;而应用程序运行在用户态,受限于页表隔离和指令集限制,无法直接访问内核地址空间。
安全要点:驱动开发者必须时刻牢记——所有来自用户态的数据都是不可信的。内核不能直接解引用用户态指针,必须通过专用 API 进行安全拷贝。
3.2 系统调用入口:驱动与用户态的桥梁
驱动程序通过注册 file_operations 结构体向用户态暴露操作接口。当应用程序执行 open()、read()、write()、ioctl() 等系统调用时,VFS 层会调用驱动对应的回调函数。
| 系统调用 | 驱动回调 | 典型用途 | 安全关注点 |
|---|---|---|---|
open() |
.open |
设备初始化、权限检查 | 检查打开模式与设备权限 |
read() |
.read |
从设备读取数据 | 缓冲区长度校验、copy_to_user |
write() |
.write |
向设备写入数据 | 缓冲区长度校验、copy_from_user |
ioctl() |
.unlocked_ioctl |
设备控制命令 | 命令号校验、指针参数验证 |
mmap() |
.mmap |
内存映射 | 物理地址范围限制、权限位检查 |
关键原则:系统调用入口是用户态进入内核态的第一道关卡。驱动必须在入口处完成以下检查:
1. 参数合法性(指针非空、长度合理)
2. 权限验证(是否有权执行该操作)
3. 数据拷贝(使用安全 API,避免直接解引用)
1. 参数合法性(指针非空、长度合理)
2. 权限验证(是否有权执行该操作)
3. 数据拷贝(使用安全 API,避免直接解引用)
3.3 copy_from_user / copy_to_user 安全使用
copy_from_user() 和 copy_to_user() 是内核提供的安全数据拷贝 API,它们会执行以下关键操作:
- 地址空间检查:验证目标/源地址是否属于用户空间
- 页错误处理:处理用户态页面可能不在内存中的情况
- 访问权限验证:确保不会读取或写入内核地址
- 部分拷贝处理:返回未拷贝成功的字节数
// 安全拷贝示例:从用户态读取数据
ssize_t my_driver_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
char *kernel_buf;
ssize_t ret;
// 1. 参数校验:检查长度是否合理
if (count == 0 || count > MAX_BUF_SIZE)
return -EINVAL;
// 2. 分配内核缓冲区
kernel_buf = kmalloc(count, GFP_KERNEL);
if (!kernel_buf)
return -ENOMEM;
// 3. 安全拷贝:使用 copy_from_user
if (copy_from_user(kernel_buf, buf, count)) {
ret = -EFAULT;
goto out;
}
// 4. 处理数据...
ret = count;
out:
kfree(kernel_buf);
return ret;
}
常见错误:直接解引用用户态指针
// ❌ 错误做法:直接访问用户态指针
char *user_ptr = (char *)arg;
printk("%c\n", user_ptr[0]); // 可能导致内核崩溃或安全漏洞
// ✅ 正确做法:使用 copy_from_user
char kernel_buf;
if (copy_from_user(&kernel_buf, user_ptr, 1))
return -EFAULT;
3.4 安全使用对比表
| API | 方向 | 返回值含义 | 错误处理 |
|---|---|---|---|
copy_from_user(to, from, n) |
用户 → 内核 | 0 成功;非0 未拷贝字节数 | 返回 -EFAULT |
copy_to_user(to, from, n) |
内核 → 用户 | 0 成功;非0 未拷贝字节数 | 返回 -EFAULT |
get_user(x, ptr) |
用户 → 内核 | 0 成功;非0 错误 | 返回 -EFAULT |
put_user(x, ptr) |
内核 → 用户 | 0 成功;非0 错误 | 返回 -EFAULT |
3.5 安全拷贝流程
3.6 常见安全陷阱与最佳实践
- 陷阱1:使用
__copy_from_user()(不带下划线前缀的变体)——跳过地址检查,仅在确定指针安全时使用 - 陷阱2:未检查
copy_from_user返回值——部分拷贝可能导致数据不完整 - 陷阱3:在
ioctl中直接使用arg作为指针——必须使用copy_from_user或get_user - 陷阱4:未验证用户态传入的长度——可能导致缓冲区溢出
最佳实践清单:
✅ 始终使用
✅ 检查返回值是否为 0,非 0 表示部分拷贝失败
✅ 验证用户传入的长度不超过内核缓冲区大小
✅ 使用
✅ 在
✅ 始终使用
copy_from_user / copy_to_user 系列 API✅ 检查返回值是否为 0,非 0 表示部分拷贝失败
✅ 验证用户传入的长度不超过内核缓冲区大小
✅ 使用
access_ok() 进行前置地址范围检查(可选,copy_* 内部已包含)✅ 在
ioctl 中优先使用 _IOR/_IOW 宏定义命令号
3.7 权限检查与 SELinux 的交互
当驱动通过系统调用入口接收用户态请求时,SELinux 会在 VFS 层进行安全上下文检查。驱动开发者需要理解:
- SELinux 在
file_permission()和inode_permission()中插入检查点 - 驱动可以通过
selinux_check_access()进行自定义权限检查 - 对于
ioctl命令,SELinux 使用file_ioctl权限和命令号进行细粒度控制
// 在驱动中集成 SELinux 权限检查示例
#include <linux/security.h>
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int ret;
// 自定义 SELinux 权限检查
ret = selinux_check_access(filp->f_cred,
current_cred(),
"my_device",
"control",
cmd);
if (ret < 0)
return -EACCES;
// 继续处理 ioctl 命令...
switch (cmd) {
case MY_IOCTL_READ:
// 使用 copy_to_user 返回数据
break;
case MY_IOCTL_WRITE:
// 使用 copy_from_user 读取数据
break;
}
return 0;
}
注意:SELinux 检查不能替代驱动内部的数据验证。即使 SELinux 允许了操作,驱动仍需对用户态传入的数据进行完整性校验。安全是分层防御的。
3.8 本章小结
| 知识点 | 关键要点 |
|---|---|
| 用户态 vs 内核态 | CPU 特权级隔离,驱动运行在 Ring 0 |
| 系统调用入口 | file_operations 回调,第一道安全检查点 |
| copy_from_user | 安全拷贝用户态数据到内核,必须检查返回值 |
| copy_to_user | 安全拷贝内核数据到用户态,必须检查返回值 |
| 安全原则 | 永远不要直接解引用用户态指针 |
| SELinux 集成 | 在驱动中可调用 selinux_check_access 进行细粒度控制 |