4. 命令方向定义:_IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READWRITE 的含义与使用场景

在 Linux 内核 ioctl 命令设计中,方向(Direction) 字段用于描述用户空间与内核空间之间的数据流动方向。该字段占用命令宏定义中的 2 个比特位,由 _IOC_DIRBITS 控制。正确设置方向位不仅是对内核 copy_from_user / copy_to_user 权限的声明,更是内核安全校验机制的重要依据。

核心原则:方向位必须如实反映驱动实际执行的数据拷贝操作。虚报或漏报方向位会导致内核内存访问校验失效,轻则引发系统警告(如 KASAN 或 CONFIG_DEBUG_VM 报错),重则造成内核崩溃或安全漏洞。

4.1 方向宏定义与位域编码

Linux 内核在 <linux/ioctl.h> 中定义了四个方向常量,其本质是 2 位无符号整数的枚举值:

/* 方向位定义(基于 include/uapi/asm-generic/ioctl.h) */
#define _IOC_NONE       0U      /* 00: 无数据传输 */
#define _IOC_WRITE      1U      /* 01: 用户空间 → 内核空间(写) */
#define _IOC_READ       2U      /* 10: 内核空间 → 用户空间(读) */
#define _IOC_READWRITE  (_IOC_READ | _IOC_WRITE)  /* 11: 双向传输 */

方向位在命令宏中的位置由 _IOC_DIRSHIFT 决定(通常为 30 位偏移,即方向位占据命令整数的最高 2 位)。构建命令时,内核通过 _IOC(_IOC_DIR, type, nr, size) 宏将方向位编码到命令值中。

4.2 方向含义与使用场景对照表

方向宏 二进制值 数据流向 典型使用场景 安全校验要点
_IOC_NONE 00 无数据拷贝 设备复位、状态查询(仅返回整数)、触发中断、使能/禁能功能 禁止在 handler 中调用 copy_from_user / copy_to_user
_IOC_WRITE 01 用户 → 内核 配置参数写入、固件下载、寄存器设置、缓冲区填充 必须校验用户缓冲区可读(access_ok(VERIFY_READ, ...))
_IOC_READ 10 内核 → 用户 状态读取、统计信息获取、寄存器读取、版本查询 必须校验用户缓冲区可写(access_ok(VERIFY_WRITE, ...))
_IOC_READWRITE 11 双向传输 原子读写操作、状态交换、配置回读、复杂控制结构 必须同时校验读/写权限;注意竞态条件
常见错误:许多驱动开发者将只读操作(如获取设备版本号)错误地定义为 _IOC_NONE,然后在 handler 中调用 copy_to_user。这会导致内核 __check_object_size 校验失败,在 CONFIG_HARDENED_USERCOPY 开启时直接触发 usercopy 警告或拒绝拷贝。

4.3 方向位在内核安全校验中的角色

内核在 do_vfs_ioctl 和 __se_sys_ioctl 路径中,会通过 _IOC_DIR(cmd) 提取方向位,并与用户传入的 arg 参数进行联合校验。其核心校验逻辑如下:

/* 内核 ioctl 入口处的方向校验(简化逻辑) */
long do_vfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    unsigned int dir = _IOC_DIR(cmd);
    void __user *uarg = (void __user *)arg;

    /* 方向位校验:如果命令声明了数据传输,则 arg 不能为 NULL */
    if (dir != _IOC_NONE) {
        if (!uarg) {
            pr_warn_ratelimited("ioctl: NULL argument for dir=%u\n", dir);
            return -EINVAL;
        }
    }

    /* 写方向:校验用户空间可读 */
    if (dir & _IOC_WRITE) {
        if (!access_ok(VERIFY_READ, uarg, _IOC_SIZE(cmd))) {
            pr_warn_ratelimited("ioctl: write access denied\n");
            return -EFAULT;
        }
    }

    /* 读方向:校验用户空间可写 */
    if (dir & _IOC_READ) {
        if (!access_ok(VERIFY_WRITE, uarg, _IOC_SIZE(cmd))) {
            pr_warn_ratelimited("ioctl: read access denied\n");
            return -EFAULT;
        }
    }

    /* 调用驱动注册的 unlocked_ioctl */
    return filp->f_op->unlocked_ioctl(filp, cmd, arg);
}

从上述代码可以看出:

  • 空指针校验:只要方向位非 _IOC_NONE,内核强制要求 arg 不能为 NULL。
  • 读写权限分离:_IOC_WRITE 只校验用户空间可读(即用户有权限读取自己的缓冲区),_IOC_READ 只校验用户空间可写。这种分离设计避免了不必要的权限放大。
  • 大小校验:access_ok 结合 _IOC_SIZE(cmd) 确保用户缓冲区至少能容纳命令声明的大小。

4.4 方向位与数据流关系图

下图展示了四种方向位对应的数据流路径,以及内核在路径关键节点插入的安全校验点:

ioctl 方向位与数据流安全校验图 ioctl 方向位与数据流安全校验 用户空间 (User Space) 内核空间 (Kernel Space) 内核安全校验层 ① 方向位提取 _IOC_DIR(cmd) ② 空指针校验 dir != _IOC_NONE → arg != NULL ③ 写权限校验 dir & _IOC_WRITE → access_ok(READ) ④ 读权限校验 dir & _IOC_READ → access_ok(WRITE) ⑤ 驱动 handler 执行 unlocked_ioctl / compat_ioctl _IOC_NONE 无数据流 _IOC_WRITE 用户 → 内核 _IOC_READ 内核 → 用户 _IOC_READWRITE 双向传输

4.5 方向位选择决策流程

在定义新的 ioctl 命令时,建议按照以下决策树选择正确的方向位:

  1. 判断是否有用户空间数据传入:驱动是否需要从用户缓冲区读取数据?如果是,则方向位必须包含 _IOC_WRITE。
  2. 判断是否有内核数据传出:驱动是否需要向用户缓冲区写入数据?如果是,则方向位必须包含 _IOC_READ。
  3. 两者皆否:命令仅触发设备动作(如复位、中断),不涉及数据拷贝,使用 _IOC_NONE。
  4. 两者皆是:命令需要原子地完成读写操作(如交换状态字),使用 _IOC_READWRITE。
最佳实践:即使驱动内部只使用 put_user / get_user 而非 copy_to_user / copy_from_user,方向位也必须如实设置。内核的 access_ok 校验在 put_user 路径中同样生效。

4.6 方向位误用的典型后果

误用场景 实际方向 声明的方向 内核行为 安全风险
只读操作声明为 _IOC_NONE 读 无 跳过 access_ok(WRITE) 校验 可能写入只读内存区域
只写操作声明为 _IOC_READ 写 读 只校验写权限,未校验读权限 用户传入只读缓冲区时内核崩溃
双向操作声明为 _IOC_WRITE 读写 写 未校验用户缓冲区可写 内核数据泄露到不可写内存
无数据操作声明为 _IOC_READ 无 读 强制要求 arg 非 NULL 用户传入 NULL 时返回 -EINVAL,功能异常

4.7 驱动代码中的方向位使用示例

以下是一个字符设备驱动中正确使用方向位的完整示例:

/* 头文件定义 */
#define MY_IOCTL_RESET       _IO('M', 0)           /* _IOC_NONE */
#define MY_IOCTL_SET_CFG     _IOW('M', 1, struct my_cfg)   /* _IOC_WRITE */
#define MY_IOCTL_GET_STAT    _IOR('M', 2, struct my_stat)  /* _IOC_READ */
#define MY_IOCTL_SWAP        _IOWR('M', 3, struct my_swap) /* _IOC_READWRITE */

/* 驱动 handler 实现 */
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    void __user *uarg = (void __user *)arg;
    struct my_cfg cfg;
    struct my_stat stat;
    struct my_swap swap;

    switch (cmd) {
    case MY_IOCTL_RESET:
        /* _IOC_NONE:不访问用户空间,直接操作硬件 */
        my_device_reset();
        return 0;

    case MY_IOCTL_SET_CFG:
        /* _IOC_WRITE:从用户空间拷贝数据 */
        if (copy_from_user(&cfg, uarg, sizeof(cfg)))
            return -EFAULT;
        return my_device_set_config(&cfg);

    case MY_IOCTL_GET_STAT:
        /* _IOC_READ:向用户空间拷贝数据 */
        my_device_get_stat(&stat);
        if (copy_to_user(uarg, &stat, sizeof(stat)))
            return -EFAULT;
        return 0;

    case MY_IOCTL_SWAP:
        /* _IOC_READWRITE:先读后写,原子操作 */
        if (copy_from_user(&swap, uarg, sizeof(swap)))
            return -EFAULT;
        my_device_swap(&swap);
        if (copy_to_user(uarg, &swap, sizeof(swap)))
            return -EFAULT;
        return 0;

    default:
        return -ENOTTY;
    }
}
注意:上述示例中 MY_IOCTL_SWAP 使用了 _IOWR,这意味着内核在进入 handler 前已经同时校验了用户缓冲区的读权限和写权限。驱动开发者无需在 handler 中重复调用 access_ok,但必须确保 copy_from_user 和 copy_to_user 的调用顺序与方向位声明一致。

4.8 方向位与 compat_ioctl 的兼容性

在 64 位内核上运行 32 位应用程序时,compat_ioctl 路径会重新解析方向位。由于方向位始终位于命令整数的最高 2 位,不受字节序和结构体对齐影响,因此方向位的校验在 compat 路径中完全透明。但需要注意:

  • _IOC_SIZE(cmd) 在 compat 路径中可能返回不同的值(如果命令涉及 compat_ptr_t 转换)。
  • 驱动在 compat_ioctl 中应使用 _IOC_DIR(cmd) 重新提取方向位,而非依赖 unlocked_ioctl 中缓存的变量。