3. 驱动中的用户态与内核态:系统调用入口、copy_from_user/copy_to_user 安全使用

3.1 用户态与内核态:权限分界

Linux 系统通过 CPU 特权级将运行空间划分为用户态(Ring 3)和内核态(Ring 0)。驱动程序运行在内核态,拥有最高权限,可以直接访问硬件和系统全局资源;而应用程序运行在用户态,受限于页表隔离和指令集限制,无法直接访问内核地址空间。

安全要点:驱动开发者必须时刻牢记——所有来自用户态的数据都是不可信的。内核不能直接解引用用户态指针,必须通过专用 API 进行安全拷贝。
用户态与内核态隔离模型 用户态 (Ring 3) — 应用程序 受限指令集 · 独立页表 · 无法访问内核地址 通过系统调用陷入内核 数据必须通过 copy_from_user / copy_to_user 传递 系统调用边界 — 权限检查点 内核态 (Ring 0) — 驱动程序 完全特权 · 直接访问硬件 · 全局内核地址空间 必须验证用户传入的指针和长度 禁止直接解引用用户空间指针 系统调用入口 返回结果 ⚠ 内核态代码必须对用户态传入的所有参数进行合法性校验

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,避免直接解引用)

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 安全拷贝流程

copy_from_user 安全拷贝流程 用户态应用程序调用 write() VFS 层调用驱动 .write 回调 参数校验:长度、指针、权限 copy_from_user() 执行安全拷贝 成功:返回拷贝字节数 失败:返回 -EFAULT

3.6 常见安全陷阱与最佳实践

  • 陷阱1:使用 __copy_from_user()(不带下划线前缀的变体)——跳过地址检查,仅在确定指针安全时使用
  • 陷阱2:未检查 copy_from_user 返回值——部分拷贝可能导致数据不完整
  • 陷阱3:在 ioctl 中直接使用 arg 作为指针——必须使用 copy_from_user 或 get_user
  • 陷阱4:未验证用户态传入的长度——可能导致缓冲区溢出
最佳实践清单:
✅ 始终使用 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 进行细粒度控制