Access Control on a Device File

Offering access control is sometimes vital for the reliability of a device node. Not only should unauthorized users not be permitted to use the device (which is enforced by the filesystem permission bits), but sometimes only one authorized user should be allowed to open the device at a time.

None of the code shown up to now implements any access control in addition to the filesystem permission bits. If the open system call forwards the request to the driver, open will succeed. I’m now going to introduce a few techniques for implementing some additional checks.

The problem is similar to that of using ttys. In that case, the login process changes the ownership of the device node whenever a user logs into the system, in order to prevent intrusion in the tty data flow. However, it’s impractical to use a privileged program to change the ownership of a device every time it is opened, just to grant unique access to it.

Every device shown in this section has the same behavior as the bare scull device (that is, it implements a persistent memory area); it differs from scull only in access control, which is implemented in the open and close operations.

Single-Open Devices

The brute-force way to provide access control is to permit a device to be opened by only one process at a time (single-openness). I personally dislike this technique, because it inhibits user ingenuity. A user might well want to run different processes on the same device, one reading status information while the other is writing data. Often a handful of simple programs and a shell script can accomplish a lot. In other words, single-openness is more like policy than mechanism (at least to my way of thinking).

Despite my aversion to single-openness, it’s the easiest implementation for a device driver, so it’s shown here. The source code is extracted from a device called scullsingle.

The open call refuses access based on a global integer flag:

int scull_s_open (struct inode *inode, struct file *filp)
{
    Scull_Dev *dev = &scull_s_device; /* device information */
    int num = NUM(inode->i_rdev);

    if (num > 0) return -ENODEV; /* 1 device only */
    if (scull_s_count) return -EBUSY; /* already open */
    scull_s_count++;


    /* then, everything else is copied from the bare scull device */

    if ( (filp->f_flags & O_ACCMODE) == O_WRONLY)
        scull_trim(dev);
    filp->private_data = dev;
    MOD_INC_USE_COUNT;
    return 0;          /* success */
}

The close call, on the other hand, marks the device as no longer busy.

void scull_s_release (struct inode *inode, struct file *filp)
{
    scull_s_count--; /* release the device */
    MOD_DEC_USE_COUNT;
    return;
}

The best place to put the open flag (scull_s_count) is within the device structure (Scull_Dev here) because, conceptually, it belongs to the device.

The scull driver, however, uses a standalone variable to hold the open flag in order to use the same device structure and methods as the bare scull device and minimize code duplication.

Restricting Access to a Single User at a Time

A more sensible implementation of access control is granting access to a user only if nobody else has control of the device. This kind of check is performed after the normal permission checking and can only make access more restrictive than that specified by the owner and group permission bits. This is the same access policy as that used for ttys, but it doesn’t resort to an external privileged program.

Sensible features are a little trickier to implement than single-open. In this case, two items are needed, an open count and the uid of the ``owner'' of the device. Once again, the best place for such items is within the device structure; the samples use global variables instead, for the reason explained previously for scullsingle. The name of the device is sculluid.

The open call grants access on first open, but remembers the owner of the device. This means that a user can open the device multiple times, thus allowing cooperating processes to work flawlessly. At the same time, no other user can open it, thus avoiding external interference. Since this version of the function is almost identical to the preceding one, only the relevant part is reproduced here:

if (scull_u_count && 
    (scull_u_owner != current->uid) &&  /* allow user */
    (scull_u_owner != current->euid) && /* allow whoever did su */
    !suser()) /* still allow root */
        return -EBUSY;   /* -EPERM would confuse the user */


if (scull_u_count == 0)
    scull_u_owner = current->uid; /* grab it */

scull_u_count++;

I made the decision to return -EBUSY and not -EPERM, even if the code performs permission checks, in order to point a user who is denied access in the right direction. The reaction to ``Permission denied'' is usually to check the mode and owner of the /dev file, while ``Device Busy'' correctly suggests that the user should look for a process already using the device.

The code for close is not shown, since all it does is decrement the usage count.

Blocking Open as an Alternative to EBUSY

Returning an error when the device isn’t accessible is usually the most sensible approach, but there are situations when you’d prefer to wait for the device.

For example, if a data communication channel is used both to transmit reports on a timely basis (using crontab) and for casual usage according to people’s needs, it’s much better for the timely report to be slightly delayed rather than fail just because the channel is currently busy.

This is one of the choices that the programmer must make when designing a device driver, and the right answer depends on the particular problem being solved.

The alternative to EBUSY, as you may have guessed, is to implement blocking open.

The scullwuid device is a version of sculluid that waits for the device on open instead of returning -EBUSY. It differs from sculluid only in the following part of the open operation:

while (scull_w_count && 
       (scull_w_owner != current->uid) &&  /* allow user */
       (scull_w_owner != current->euid) && /* allow whoever did su */
       !suser()) {
    if (filp->f_flags & O_NONBLOCK) return -EAGAIN; 
    interruptible_sleep_on(&scull_w_wait);
    if (current->signal & ~current->blocked) /* a signal arrived */
        return -ERESTARTSYS; /* tell the fs layer to handle it */
    /* else, loop */
}
if (scull_w_count == 0)
    scull_w_owner = current->uid; /* grab it */
scull_w_count++;

The release method, then, is in charge of awakening any pending process:

void scull_w_release (struct inode *inode, struct file *filp)
{
    scull_w_count--;
    if (scull_w_count == 0)
        wake_up_interruptible(&scull_w_wait); /* awake other uid's */
    MOD_DEC_USE_COUNT;
    return;
}

The problem with a blocking-open implementation is that it is really unpleasant to the interactive user, who has to keep guessing what is going wrong. The interactive user usually invokes precompiled commands like cp and tar and can’t just add O_NONBLOCK to the open call. Someone who’s making a backup using the tape drive in the next room would prefer to get a plain ``device or resource busy'' message, instead of being left to guess why the hard drive is so silent today while tar is scanning it.

This kind of problem (different incompatible policies for the same device) is best solved by implementing one device node for each access policy, similar to the way /dev/ttyS0 and /dev/cua0 act on the same serial port in different ways, or /dev/sculluid and /dev/scullwuid offer two different policies for accessing a memory area.

Cloning the Device on Open

Another technique to manage access control is creating different private copies of the device depending on the process opening it.

Clearly this is only possible if the device is not bound to a hardware object; scull is an example of such a ``software'' device. The kmouse module also uses this technique so that every virtual console appears to have a private pointing device. When copies of the device are created by the software driver, I call them ``virtual devices''--just as ``virtual consoles'' use a single physical tty device.

While a requirement for this kind of access control is unusual, the implementation can be enlightening in showing how easily kernel code can change the applications’ perspective of the surrounding world (i.e., the computer). The topic is quite exotic, actually, so if you aren’t interested, you can jump directly to the next chapter.

The /dev/scullpriv device node implements virtual devices within the scull package. The scullpriv implementation uses the minor number of the process’s controlling tty as a key to access the virtual device. You can nonetheless easily modify the sources to use any integer value for the key; each choice leads to a different policy. For example, using the uid leads to a different virtual device for each user, while using a pid key creates a new device for each process accessing it.

The decision to use the controlling terminal is meant to enable easy testing of the device using input/output redirection.

The open method looks like the following code. It must look for the right virtual device and possibly create one. The final part of the function is not shown because it is copied from the bare scull, which we’ve already seen.

struct scull_listitem {
    Scull_Dev device;
    int key;
    struct scull_listitem *next;
};

struct scull_listitem *scull_c_head;

int scull_c_open (struct inode *inode, struct file *filp)
{
    int key;
    int num = NUM(inode->i_rdev);
    struct scull_listitem *lptr, *prev;

    if (num > 0) return -ENODEV; /* 1 device only */

    if (!current->tty) {
        PDEBUG("Process "%s" has no ctl tty
",current->comm);
        return -EINVAL;
    }
    key = MINOR(current->tty->device);

    /* look for a device in the linked list; if missing create it */
    prev = NULL;
    for (lptr = scull_c_head; lptr && (lptr->key != key); 
         lptr = lptr->next)
        prev=lptr;
    if (!lptr) { /* not found */
        lptr = kmalloc(sizeof(struct scull_listitem), GFP_KERNEL);
        if (!lptr)
            return -ENOMEM;
        memset(lptr, 0, sizeof(struct scull_listitem));
        lptr->key = key;
        scull_trim(&(lptr->device)); /* initialize it */
        if (prev)
            prev->next = lptr;
        else
            scull_c_head = lptr; /* the first one */
    }

    /* then, everything else is copied from the bare scull device */

The close method does nothing special. It could release the device on last close, but I chose not to maintain an open count in order to simplify testing the driver. If the device were released on last close, you wouldn’t be able to read the same data after writing to the device unless a background process were to keep it open at least once. The sample driver takes the easier approach of keeping the data, so that at the next open, you’ll find it there. The devices are released when cleanup_module is called.

Here’s the close implementation for /dev/scullpriv, which closes the chapter as well.

void scull_c_release (struct inode *inode, struct file *filp)
{
    /*
     * Nothing to do, because the device is persistent.
     * A "real" cloned device should be freed on last close
     */
    MOD_DEC_USE_COUNT;
    return;
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.221.52