Ioctl Multiple Arguments in Linux Kernel Drivers- Free Linux Device Drivers Course

 

PREV_LEC | NEXT_LEC

Ioctl Multiple Arguments in Linux Kernel Drivers
Free Linux Device Drivers Course — Character Devices, Part 13
Kernel 6.x Ready
Original Driver Code
Hands-On Lab
free linux device drivers course
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 ioctl only 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/_IOWR macros and magic numbers
  • Comfort with copy_to_user() and copy_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-Based Ioctl Data Flow
User Space
struct ep_part_info info;
ioctl(fd, EP_PART_SET, &info);
Kernel Space
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.

 

PREV_LEC | NEXT_LEC

 

Leave a Reply

Your email address will not be published. Required fields are marked *