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 方向位与数据流关系图
下图展示了四种方向位对应的数据流路径,以及内核在路径关键节点插入的安全校验点:
4.5 方向位选择决策流程
在定义新的 ioctl 命令时,建议按照以下决策树选择正确的方向位:
- 判断是否有用户空间数据传入:驱动是否需要从用户缓冲区读取数据?如果是,则方向位必须包含
_IOC_WRITE。 - 判断是否有内核数据传出:驱动是否需要向用户缓冲区写入数据?如果是,则方向位必须包含
_IOC_READ。 - 两者皆否:命令仅触发设备动作(如复位、中断),不涉及数据拷贝,使用
_IOC_NONE。 - 两者皆是:命令需要原子地完成读写操作(如交换状态字),使用
_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中缓存的变量。