In continuation of the previous text第6章:字符设备驱动的高级操作4:The Return Value And The Predefined Commands, let's GO ahead.
Another point we need to cover before looking at the ioctl code for the scull driver is how to use the extra argument. If it is an integer, it's easy: it can be used directly. If it is a pointer, however, some care must be taken.
在查看 scull 驱动的 ioctl 代码之前,我们还需要说明如何使用附加参数。如果它是一个整数,那么很简单:可以直接使用。但是,如果它是一个指针,则必须格外小心。
When a pointer is used to refer to user space, we must ensure that the user address is valid. An attempt to access an unverified user-supplied pointer can lead to incorrect behavior, a kernel oops, system corruption, or security problems. It is the driver's responsibility to make proper checks on every user-space address it uses and to return an error if it is invalid.
当使用指针指向用户空间时,我们必须确保用户地址是合法有效的。尝试访问一个未经验证的、由用户提供的指针,可能会导致行为异常、内核崩溃(oops)、系统损坏甚至安全漏洞。驱动程序有责任对它使用的每一个用户空间地址进行正确检查,如果地址无效,则返回一个错误。
In Chapter 3, we looked at the copy_from_user and copy_to_user functions, which can be used to safely move data to and from user space. Those functions can be used in ioctl methods as well, but ioctl calls often involve small data items that can be more efficiently manipulated through other means. To start, address verification (without transferring data) is implemented by the function access_ok, which is declared in <linux/uaccess.h>:
在第 3 章中,我们学习了 copy_from_user 和 copy_to_user 函数,它们可以用来安全地与用户空间交换数据。这些函数同样可以用于 ioctl 方法中,但是 ioctl 调用通常只涉及少量数据,这些数据可以通过其他方式更高效地操作。首先,仅进行地址验证(不传输数据) 是由函数 access_ok 实现的,该函数声明在 <linux/uaccess.h> 中:
cpp
int access_ok(int type, const void *addr, unsigned long size);
The first argument should be either VERIFY_READ or VERIFY_WRITE, depending on whether the action to be performed is reading the user-space memory area or writing it. The addr argument holds a user-space address, and size is a byte count. If ioctl, for instance, needs to read an integer value from user space, size is sizeof(int). If you need to both read and write at the given address, use VERIFY_WRITE, since it is a superset of VERIFY_READ.
第一个参数应当是 VERIFY_READ 或 VERIFY_WRITE,具体取决于要执行的操作是读取用户空间内存区域还是写入。addr 参数保存的是一个用户空间地址,size 是字节数。例如,如果 ioctl 需要从用户空间读取一个整数值,那么 size 就是 sizeof(int)。如果你需要对给定地址既读又写 ,请使用 VERIFY_WRITE,因为它是 VERIFY_READ 的超集。
Unlike most kernel functions, access_ok returns a boolean value: 1 for success (access is OK) and 0 for failure (access is not OK). If it returns false, the driver should usually return -EFAULT to the caller.
与大多数内核函数不同,access_ok 返回一个布尔值 :1 表示成功(访问合法),0 表示失败(访问不合法)。如果它返回 0,驱动通常应该向调用者返回 -EFAULT。
There are a couple of interesting things to note about access_ok. First, it does not do the complete job of verifying memory access; it only checks to see that the memory reference is in a region of memory that the process might reasonably have access to. In particular, access_ok ensures that the address does not point to kernel-space memory. Second, most driver code need not actually call access_ok. The memory-access routines described later take care of that for you. Nonetheless, we demonstrate its use so that you can see how it is done.
The scull source exploits the bitfields in the ioctl number to check the arguments before the switch:
关于 access_ok,有几点值得注意:
- 它并不完成 内存访问验证的全部工作;它仅仅检查该内存引用是否位于进程合理有权访问 的内存区域。特别地,
access_ok确保该地址没有指向内核空间内存。 - 大多数驱动代码实际上不需要 调用
access_ok。后面会讲到的内存访问函数会为你处理这件事。尽管如此,我们还是演示它的用法,让你明白其工作原理。
scull 源码利用 ioctl 命令号中的位域,在 switch 语句之前检查参数:
cpp
int err = 0, tmp;
int retval = 0;
/*
* extract the type and number bitfields, and don't decode
* wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok( )
*/
if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;
if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;
/*
* the direction is a bitmask, and VERIFY_WRITE catches R/W
* transfers. `Type' is user-oriented, while
* access_ok is kernel-oriented, so the concept of "read" and
* "write" is reversed
*/
if (_IOC_DIR(cmd) & _IOC_READ)
err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
else if (_IOC_DIR(cmd) & _IOC_WRITE)
err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
if (err) return -EFAULT;
After calling access_ok, the driver can safely perform the actual transfer. In addition to the copy_from_user and copy_to_user functions, the programmer can exploit a set of functions that are optimized for the most used data sizes (one, two, four, and eight bytes). These functions are described in the following list and are defined in <linux/uaccess.h> :
调用 access_ok 之后,驱动就可以安全地执行实际的数据传输了。除了 copy_from_user 和 copy_to_user 函数之外,程序员还可以使用一组为最常用数据大小 (1、2、4、8 字节)优化的函数。这些函数定义在 <linux/uaccess.h> 中,说明如下:
- put_user(datum, ptr) __put_user(datum, ptr)
These macros write the datum to user space; they are relatively fast and should be called instead of copy_to_user whenever single values are being transferred. The macros have been written to allow the passing of any type of pointer to put_user, as long as it is a user-space address. The size of the data transfer depends on the type of the ptr argument and is determined at compile time using the sizeof and typeof compiler builtins. As a result, if ptr is a char pointer, one byte is transferred, and so on for two, four, and possibly eight bytes. put_user checks to ensure that the process is able to write to the given memory address. It returns 0 on success, and -EFAULT on error. __put_user performs less checking (it does not call access_ok), but can still fail if the memory pointed to is not writable by the user. Thus, __put_user should only be used if the memory region has already been verified with access_ok. As a general rule, you call __put_user to save a few cycles when you are implementing a read method, or when you copy several items and, thus, call access_ok just once before the first data transfer, as shown above for ioctl.
这些宏将数据写入用户空间;它们速度相对较快,在传输单个值 时,应优先调用它们,而不是 copy_to_user。这些宏允许传递任意类型的指针给 put_user,只要它是用户空间地址。数据传输的大小取决于 ptr 参数的类型,并在编译时通过 sizeof 和 typeof 内置函数确定。因此,如果 ptr 是 char 指针,就传输 1 个字节,以此类推传输 2、4 甚至 8 个字节。
-
put_user会检查确保进程能够写入给定的内存地址。成功返回 0,失败返回-EFAULT。 -
__put_user执行的检查更少(它不调用access_ok),但如果指向的内存对用户不可写,它仍然可能失败。 -
因此,只有在内存区域已经通过
access_ok验证过的情况下,才能使用__put_user。 -
通常规则:当你实现
read方法,或者复制多个数据项(因此在第一次传输前只调用一次access_ok,就像上面 ioctl 的示例)时,可以调用__put_user来节省几个时钟周期。 -
get_user(local, ptr) __get_user(local, ptr)
These macros are used to retrieve a single datum from user space. They behave like put_user and __put_user, but transfer data in the opposite direction. The value retrieved is stored in the local variable local; the return value indicates whether the operation succeeded. Again, __get_user should only be used if the address has already been verified with access_ok.
If an attempt is made to use one of the listed functions to transfer a value that does not fit one of the specific sizes, the result is usually a strange message from the compiler, such as "conversion to non-scalar type requested." In such cases, copy_to_user or copy_from_user must be used.
这些宏用于从用户空间获取单个数据 。它们的行为类似于 put_user 和 __put_user,只是数据传输方向相反。获取到的值存储在局部变量 local 中;返回值表示操作是否成功。同样,只有在地址已经通过 access_ok 验证过的情况下,才能使用 __get_user。
如果尝试使用这些函数传输不满足特定大小 的值,编译器通常会报出奇怪的错误,例如 "conversion to non-scalar type requested"。在这种情况下,必须使用 copy_to_user 或 copy_from_user。
补充说明:
- 为什么必须严格校验用户态指针
用户态传入的指针完全不可信:地址可能非法、越界、指向内核空间、或是恶意构造的伪造地址。驱动若直接裸访问用户指针,会直接触发内核 Oops、段错误、系统宕机 ,还会带来越界读写、内核信息泄露、提权等严重安全漏洞。因此 Linux 驱动强制要求:所有用户态指针,必须经过内核安全接口校验与拷贝,绝不直接解引用。
- access_ok 函数核心要点
- 头文件依赖:定义于
<linux/uaccess.h>,必须包含该头文件才能使用。 - 作用范围:只做区间合法性粗校验 ,判断该地址是否属于当前进程合法用户态地址区间,不做实际内存读写检查,也不校验页表权限。
- 参数含义
VERIFY_WRITE:最高权限校验,代表该内存允许读 + 写,可覆盖双向传输场景;VERIFY_READ:仅校验读权限,只适用于单纯读取用户内存。
- 返回值逻辑:返回 1 代表地址合法,返回 0 代表非法;代码中常通过
!access_ok(...)判断错误,非法则返回-EFAULT。 - 局限性:不能单独依赖它保证安全,它只是前置过滤,真正安全的内存访问仍要搭配
get_user / put_user / copy_xx_user。
- ioctl 方向位与 access_ok 方向颠倒的核心原因
_IOC_READ:站在用户态视角 ,表示用户要从设备读数据;对应内核行为:驱动需要写入用户缓冲区 → 内核需使用VERIFY_WRITE校验。_IOC_WRITE:站在用户态视角 ,表示用户要向设备写数据;对应内核行为:驱动需要读取用户缓冲区 → 内核需使用VERIFY_READ校验。这是内核固定设计规范,也是新手最容易写错、引发内存错误的关键点。
- 两套用户态读写接口的区别与选型
(1)get_user / put_user
- 自带安全检查,内部隐含基础地址合法性判断;
- 专门针对 char/short/int/long 等基础定长单变量 优化,指令更少、效率更高;
- 适合 ioctl 中传递简单整型、单个基础数据类型的场景;
- 适用:不确定地址是否提前校验、常规驱动开发通用场景。
(2)__get_user / __put_user
- 去掉了冗余安全检测,运行开销更低、性能更好;
- 不主动调用 access_ok,完全依赖开发者手动提前完成地址校验;
- 强制使用前提:必须先通过
access_ok验证过用户地址合法; - 适用:高频调用路径、批量拷贝、追求极致性能的驱动代码。
(3)copy_from_user / copy_to_user
- 通用性最强,支持任意长度、结构体、数组、复杂大数据块;
- 性能略低,因为包含循环拷贝、完整权限校验;
- 适用:结构体传输、数组、大块数据、长度不固定的缓冲区交互。
- 常见编译错误场景说明
当强行用 get_user/put_user 拷贝结构体、嵌套联合体、非标量复杂类型时,编译器会抛出:
conversion to non-scalar type requested原因:这类宏依靠编译期
sizeof、typeof自动推导数据宽度,仅支持基础标量类型 。解决方式:复杂多字节复合数据,必须改用copy_from_user/copy_to_user。
- 内核标准错误码规范
-ENOTTY:非法命令、当前设备不支持该 ioctl 指令;-EFAULT:用户态指针非法、地址越界、内存访问无效;-EINVAL:参数数值错误、配置非法、参数大小不匹配。严格使用标准错误码,能让用户态程序统一识别错误类型,便于上层开发与问题排查。
-
工程编码最佳实践
-
ioctl 处理流程固定顺序:校验幻数 → 校验命令序号 → 校验用户地址 → 数据读写 → 业务逻辑;
-
小数据优先使用
get_user/put_user,减少拷贝函数开销; -
批量 / 频繁 ioctl 场景,提前统一执行一次 access_ok,后续使用
__get_user/__put_user提速; -
绝不直接强制解引用
arg用户指针,杜绝所有裸指针访问用户内存的写法。