free linux kernel development course
free embedded systems course
ioctl multiple arguments linux kernel
free embedded linux course
In this lecture of our free Linux device drivers course we solve a very practical problem: what happens when a single ioctl command needs more than one piece of information? A single integer argument is easy, but real drivers often need to send or receive several related values at once — for example a buffer address together with its length, or a device name together with a status flag. The clean way to handle ioctl multiple arguments in Linux kernel code is to group them inside a C structure and pass a pointer to that structure through the existing single ioctl argument slot.
What You Will Learn
- Why
ioctlonly ever takes one argument, and how structures work around that limit - How to safely copy a structure between user space and kernel space
- How to design an ioctl command that both reads and writes fields of a structure
- A complete, original kernel 6.x character driver example you can build and test yourself
Prerequisites
- Completion of the earlier lectures on
ioctl(),_IOW/_IOR/_IOWRmacros and magic numbers - Comfort with
copy_to_user()andcopy_from_user() - A Linux machine (kernel 6.x) with build essentials and kernel headers installed
Why One Argument Is Not Enough
The kernel’s ioctl file operation is defined with exactly one extra parameter beyond the file pointer and the command number. That single parameter is an unsigned long, which can hold either a plain number or a pointer. When a command needs one value — say, a size or a mode — passing that value directly, or a pointer to a single variable, is enough. But the moment a command needs several related values together, passing them one ioctl call at a time becomes clumsy, and worse, it is not atomic: another thread could change device state between the two calls.
The standard solution is to declare a structure containing every field the command needs, allocate that structure in user space, and pass its address as the ioctl argument. The driver then copies the whole structure in with a single copy_from_user() call, or copies a result structure back out with a single copy_to_user() call. This keeps the operation atomic from the caller’s point of view and keeps the driver interface tidy even as the number of fields grows.
struct ep_part_info info;
ioctl(fd, EP_PART_SET, &info);
copy_from_user(&kinfo, argp, sizeof(kinfo));
use kinfo.name, kinfo.size, kinfo.flags
Designing the Shared Structure and Header
Just like with plain ioctl commands, the structure definition and the command numbers must live in a header file that is shared between the kernel module and the user space program. This guarantees both sides agree on field order, field size and alignment. Here is an original header for a small partition-style demo driver, unrelated to any existing textbook example.
/* ep_part_ioctl.h - shared between driver and user space */
#ifndef EP_PART_IOCTL_H
#define EP_PART_IOCTL_H
#include <linux/ioctl.h>
#define EP_PART_NAME_LEN 16
struct ep_part_info {
char name[EP_PART_NAME_LEN];
__u32 size_kb;
__u32 flags;
};
#define EP_MAGIC 'P'
#define EP_PART_SET _IOW(EP_MAGIC, 1, struct ep_part_info)
#define EP_PART_GET _IOR(EP_MAGIC, 2, struct ep_part_info)
#define EP_PART_CLEAR _IO(EP_MAGIC, 3)
#endif
Notice that EP_PART_SET and EP_PART_GET both encode sizeof(struct ep_part_info) inside the command number through the macro. This lets the kernel’s ioctl dispatch validate the expected transfer size automatically when the driver uses helpers that check it, and it documents the intent clearly for anyone reading the header.
Driver-Side Handling of the Structure
The driver keeps one struct ep_part_info per open device as its internal state, protected by a mutex so concurrent opens cannot corrupt it. The unlocked_ioctl handler copies the structure in for a “set” command, or copies it out for a “get” command.
struct ep_part_dev {
struct cdev cdev;
struct mutex lock;
struct ep_part_info info;
};
static long ep_part_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct ep_part_dev *dev = filp->private_data;
struct ep_part_info tmp;
void __user *argp = (void __user *)arg;
switch (cmd) {
case EP_PART_SET:
if (copy_from_user(&tmp, argp, sizeof(tmp)))
return -EFAULT;
tmp.name[EP_PART_NAME_LEN - 1] = '\0';
mutex_lock(&dev->lock);
dev->info = tmp;
mutex_unlock(&dev->lock);
break;
case EP_PART_GET:
mutex_lock(&dev->lock);
tmp = dev->info;
mutex_unlock(&dev->lock);
if (copy_to_user(argp, &tmp, sizeof(tmp)))
return -EFAULT;
break;
case EP_PART_CLEAR:
mutex_lock(&dev->lock);
memset(&dev->info, 0, sizeof(dev->info));
mutex_unlock(&dev->lock);
break;
default:
return -ENOTTY;
}
return 0;
}
Two details matter here. First, a local tmp variable is used for the copy instead of touching dev->info directly while holding user-controlled data; this avoids exposing partially-copied state to other threads. Second, the name field is explicitly NUL-terminated after the copy, since data coming from user space must never be trusted to already be well-formed.
A Minimal User Space Test Program
The user space program below exercises all three commands. It fills a structure, sends it with EP_PART_SET, reads it back with EP_PART_GET, then clears it.
/* ep_part_test.c */
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "ep_part_ioctl.h"
int main(void)
{
int fd = open("/dev/eppart0", O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
struct ep_part_info info = {0};
strncpy(info.name, "rootfs", sizeof(info.name) - 1);
info.size_kb = 65536;
info.flags = 1;
ioctl(fd, EP_PART_SET, &info);
struct ep_part_info readback = {0};
ioctl(fd, EP_PART_GET, &readback);
printf("name=%s size_kb=%u flags=%u\n",
readback.name, readback.size_kb, readback.flags);
ioctl(fd, EP_PART_CLEAR);
close(fd);
return 0;
}
Building and Running the Example
Compile the module and load it, create the device node, build the test program, then run it.
$ make $ sudo insmod ep_part.ko $ sudo mknod /dev/eppart0 c $(grep eppart /proc/devices | cut -d' ' -f1) 0 $ gcc -o ep_part_test ep_part_test.c $ sudo ./ep_part_test
Expected output:
name=rootfs size_kb=65536 flags=1
If the printed line matches, the structure travelled correctly from user space into the kernel and back out again in a single round trip for each command.
Comparing Single-Value and Struct-Based Ioctl
| Aspect | Single Value Argument | Struct Pointer Argument |
|---|---|---|
| Number of fields | One | Any number |
| Atomicity | Fine for one field | Whole set updated together |
| Copy calls needed | One small copy | One copy of the whole struct |
| Header maintenance | Trivial | Struct must stay ABI-stable |
| Typical use | Get size, get status flag | Set configuration, read statistics block |
Common Mistakes
- Forgetting to NUL-terminate string fields copied from user space
- Copying directly into the shared device state without a local temporary and a lock
- Letting the structure contain pointers — pointers are only valid in the process that created them, never pass raw pointers inside an ioctl structure
- Changing the structure layout after user space programs have already been built against the old layout
Best Practices
- Keep the structure fixed-size and avoid padding surprises by using explicit-width types like
__u32 - Validate every field after copying it in, never trust ranges or lengths from user space
- Use a single struct per logical operation rather than many small ioctl commands
- Document the structure layout in the shared header with comments
Security Considerations
Because the whole structure arrives from an untrusted process, every field must be treated as hostile input until validated. Size fields should be checked against sane maximums before being used in allocations or loops, and string fields should always be bounded and NUL-terminated before use, exactly as shown in the example above.
Summary
When a single ioctl command needs to exchange more than one value with a driver, the correct pattern is a shared structure copied in one atomic step using copy_from_user() or copy_to_user(). This keeps the driver interface clean, keeps related fields consistent, and scales naturally as a device’s configuration grows more complex.
Frequently Asked Questions
Can an ioctl structure contain pointers to other buffers?
It can, but the pointer value itself must be treated as a user-space address and dereferenced only through copy_from_user()/copy_to_user() again, never accessed directly.
What happens if the structure sizes differ between kernel and user space?
The copy will read or write the wrong number of bytes, corrupting memory or leaking data. Always build the user program against the exact same header the module uses.
Is a struct-based ioctl faster than several separate ioctl calls?
Yes, one system call transfers all the fields at once, which is both faster and atomic compared to multiple separate calls.
Should I lock the device state while copying from user space?
Copy into a local temporary first, then lock only while updating the shared device state, so a slow or faulting user-space copy never happens while holding the lock.
Can this pattern be used for large data transfers?
For large or variable-length data, prefer read()/write() or memory mapping; ioctl structures are best kept small and fixed-size.
Continue the Free Linux Device Drivers Course
Next up: filling the file_operations structure the modern way and closing out the character driver chapter.
