Question

I'm running a Linux 3.2 kernel with the following ioctl prototype:

long ioctl(struct file *f, unsigned int cmd, unsigned long arg);

I noticed that arg is always unsigned long regardless of the actual data type passed to ioctl from the respective userspace function. Examples for ioctl typically show the following implementation (source):

typedef struct
{
    int status, dignity, ego;
} query_arg_t;

#define QUERY_GET_VARIABLES _IOR('q', 1, query_arg_t *)

static long my_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
        query_arg_t q;

        switch (cmd)
        {
                case QUERY_GET_VARIABLES:
                        q.status = status;
                        q.dignity = dignity;
                        q.ego = ego;
                        if (copy_to_user((query_arg_t *)arg, &q, sizeof(query_arg_t)))
                        {
                                return -EACCES;
                        }
                        break;
                default:
                        return -EINVAL;
        }

    return 0;
}

Notice that a query_arg_t * type is expected, so a cast is applied: (query_arg_t *)arg

Doesn't this break the strict aliasing rule though, since arg is of type unsigned long while the cast is query_arg_t *?

Was it helpful?

Solution

Pointers are integral types. It's entirely fine to store a pointer in any integral type that is large enough to contain it. For example, the following is perfectly valid C:

double f()
{
    double a = 10.5;

    uintptr_t p = (uintptr_t)(&a);

    double * q = (double *)p;

    return *q;
}

By contrast, the following is a clear aliasing violation:

short buf[100] = {};

double x = *(double*)(buf + 13);

The point is that it doesn't matter how you store your pointer values. What matters is that you must only treat those pointers as pointers to an object that are actually pointers to an object of the correct type.

In the first example, p does really store the pointer to a double, although it is not itself a double *. In the second example, buf + 13 is simply not a pointer to a double, so dereferencing it as such is type punning, and an aliasing violation.

(Pointers and casts are one of the reasons that C is not a safe language: The correctness of an operation can depend on the value of a variable, rather than just its type.)

OTHER TIPS

This does not break the aliasing rules.

Object arg is accessed only here in the my_ioctl function:

(query_arg_t *)arg

And the object is accessed only through its type unsigned long. The cast only converts the value of the object from an unsigned long value to a query_arg_t * value.

Keep in mind that while these are aliases, they exist in entirely different execution contexts - the user space and the kernel. The strict aliasing rule is meant to prevent issues that can arise when the compiler deals with possible aliases in a unit of code. This never happens for user space and kernel space code. They are separate units of code, that never mix together in ways that can cause issues related to what the strict aliasing rule is meant to address.

unsigned long (or some other integer type which is cast-compatible with unsigned long) is the underlying type of uintptr_t in all Linux ABIs.

$ grep -rw uintptr_t /usr/include/stdint.h
typedef unsigned long int uintptr_t;

C99 says that any pointer type can be cast to uintptr_t (or its underlying type) and back to the original pointer type without loss of information or violation of the strict-aliasing rules. So as long as the user space code that called ioctl(fd, QUERY_GET_ARGS, ptr) passed a query_arg_t * as the ptr argument, the program-as-a-whole is conformant.

Note also that the ioctl prototype you show is the in-kernel, driver-side interface. In user space, the prototype is

extern int ioctl(int fd, unsigned long int request, void *arg);

which makes it more apparent that the third argument is some concrete but unspecified pointer type, and that caller and (ultimate) callee had better agree on the actual type of the pointer. (That being the normal use pattern for void * in C.)

(Further note for pedants: the actual user space prototype is

extern int ioctl(int fd, unsigned long int request, ...);

This is a compatibility kludge for programs that pass numeric constants as the third argument without a cast. You may be beginning to understand why ioctl is not considered a well-designed API anymore.)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top