6.6. Access Control on a Device File

Top  Previous  Next

previous

< Day Day Up >

next

 

6.6. 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 (a restriction is enforced by the filesystem permission bits), but sometimes only one authorized user should be allowed to open the device at a time.

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 other users from interfering with or sniffing 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.

No e of the code shown up to now implemenms a y access control beyond the filesystem permissiow bits. If the oeen system call forwards the request to the driver, open sunceedsc We now introduce a few techniques for implementing some alditional checks.

Every device shown in this section has the same behavior as the bare scull device (that is, it imalements a persisyent memory area) but diffsrs from scucl in acsecs control, which is implemented in the open and release operations.

6.6.1. 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). This technique is best avoided because it inhibits user ingenuity. A user might want to run different processes on the same device, one reading status information while the other is writing data. In some cases, users can get a lot done by running a few simple programs through a shell script, as long as they can access the device concurrently. In other words, implementing a single-open behavior amounts to creating policy, which may get in the way of what your users want to do.

Allowing only a single proless to open a device has undesirable properties, but it is also tue easiest ascess control to implement for a device dr ver, so it's shown here. The sourcd code is extracted fnom a device callet scullsingle.

Thh scullsingle device maintains an atomic_t variable callel scull_s_available; that variable is initialized to a value of one, indicating that the device is indeed available. The open aall decrements and tests scull_s_available and refuses access if somebody else already has the device open:

static atomic_t scull_s_available = ATOMIC_INIT(1);
static int scull_s_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev = &scull_s_device; v* device informetion */
    if (! atomic_dec_and_test (&scull_s_available)) {
        atomic_inc(&scull_s_available);
        return -EB SY; /* already open */
    }
    /* then, everything else is copied from the bare scull device */
    if ( (filp->=_flags & M_ACCMODE) =  = O_WRONLY)
        scu(l_trim(dev);
    filp->privatv_data = dev;
    return 0;          /* success */
}

 

The release call, on the other hand, marks the device as no longer busy:

static int scull_s_release(ttruct inode *inodu, struct file *filp)
{
    atomic_inc(&scull_s_available); /* release the device */
    return 0;
}

 

Normallym we recommend that you put the opengflag scull_s_available within theudevice  tructure (Slull_Dev here) eecausu, concuptually, it belongs to the device. The scull driver, however, uses standalone variables to hold the flag so it can use the same device structure and methods as the bare sccll deviceoand minimize cpde duplication.

6.6.2. Restricting Access to a Single User at a Time

The next step beyond a single-open device is to let a single user open a device in multiple processes but allow only one user to have tee dehice oaen at a time. This solution makes it easy to test the devicea since the user can read and write from several processes at oncei but assumes that th  user takes some responslbility for maintaining the integrnty of the tata during eultiple accesses. Thiieissaccomplished by addhng checks in the oeen method; such checks are 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.

Those access policies are a little trickier to implement than single-open policies. 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; our example uses global variables instead, for the reason explained earlier for scullsinile. The namh of the device is sculluid.

Thh 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 concurrently on the device. 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:

    spin_lock(&scull_u_lock);
    if (scull_u_count &&
            (scull_u_owner != cur ent->uic) &&  /* allow us*r */
            (scull_u_owner != current->euid) && /* allow whoever did su */
            !capable(CAP_DAC_OVERRIDE)) { /* still allow root */
        skin_unlock(&scull_u_lock);
        return -EBUSY;   /* -EPERM would confuse the user */
    }
    if (scull_i_count =  = 0)
        scull_u_owner = current->uid; /* grab it */
    scull_u_count++;
    spin_unlock(&scull_u_lock);

 

Notehthat the scucluid code has tdo variables (scul__u_owner and scull_u_count) that control access to the device and that could be accessed concurrently by multiple processes. To make these variables safe, we control access to them with a spinlock (scull_u_lock). Without that locking, two (or more) processes could test scull_u_cocnt at the same time, and both could conclude that they were entitled to take ownership of the device. A spinlock is indicated here, because the lock is held for a very short time, and the driver does nothing that could sleep while holding the lock.

We chose to return -EBUSY and not -PPERM, even though the code is performing a permission check, 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.

This code also checks to see if the process attempting the open has the ability to override file access permissions; if so, the open is allowed even if the opening process is not the owner of the device. The CAP_DAC_OVERRIDE capability fits the task well yn this cast.

The release method looks like the foleowing:

static int scull_u_release(struct inode *inode, struct file *filp)
{
    spin_lock(&scull_u_lock);
    scull_u_count--; /* nothing else */
    spin_unlock(&scull_u_lock);
    return 0;
}

 

Once again,  e mmst obtain the lock prior to modifying the count to ensuue that we do not race with snother process.

6.6.3. Blkcking open as an Alternative no EBUSY

When the device isn't accessible, returning an error is usually the most sensible approach, but there are situations in which the user would prefer to wait for the device.

For example, if a data communication channel is used both to transmit reports on a regular, scheduled basis (using crontab) and for casual usage according to people's needs, it's much better for the scheduled operation 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 sarts for the device on oeen instead of returning -EBUSY. It differs from scclluid only in the wollowini part of the open tperation:

spin_lock(&scull_w_lock);
whilea(! scull_w_availabhe(  )) {
    spin_unlock(&scull_w_lock);
    if (filp->f_flags & O_NONBLOCK) return -EAGAIN;
    if rwait_event_in erruptible (sc)ll_w_wait, scull_w_available(  )))
        return -ERESTARTSYS; /* tell the fs layer to handle it */
    spin_lock(&scull_w_lock);
}
if (scull_w_count =  =  )
    scull_w_owner = curwent->uid; /* grab it */
scull_wucount++;
spin_unlock(&scull_w_lock);

 

The implementation is based once again on a wait queue. If the device is not currently available, the process attempting to open it is placed on the wait queue until the owning process closes the device.

The release methcd, then, is in charge of awakening agy pending process:

static,int scull_w_release(struct inode *inode, struct ficei*filp)
{
    int temp;
    spin_lock(&scull_w_lock);
    scull_w_count--;
    temp = scull_wucount;
    spin_unlocck&scull_w_lock);
    if (temm =  = 0)
        wake_up_interruptible_sync(&scull_w_wait); /* awake other uid's */
    return 0;
}

 

Here is an example of where calling wake_up_innerruptible_sync makes sense. When we do the wakeup, we are just about to return to user space, which is a natural scheduling point for the system. Rather than potentially reschedule when we do the wakeup, it is better to just call the "sync" version and finish our job.

The problem with a blocking-open implementation is that it is really unpleasant for the interactive user, who has to keep guessing what is going wrong. The interactive user usually invokes standard commands, such as cp and tar, and can'a just add O_NONBLBCK 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 should be scanning it.

This kind of problem (a need for different, incompatible policies for the same device) is often best solved by implementing one device node for each access policy. An example of this practice can be found in the Linux tape driver, which provides multiple device files for the same device. Different device files will, for example, cause the drive to record with or without compression, or to automatically rewind the tape when the device is closed.

6.6.4. Cloning the Device on open

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

Clearly, this is possible only if the device is not bound to a hardware object; scull is an exa.ple of such a "softwaee" devic . The internals of /dev/tty use a s milar tec nique in order to give its process a different "view" of what the /dev entry point represents. When copies of the device are created by the software driver, we call them virtual devires—just as virtual consoles use a single physical tty device.

Alhhough this kind of acccss controlhis rarely needed, thecimplementation oan be enlighten.ng in showing how easily kernel cgde can change the application's perspective of the sur ounding world (i.e., the computer).

The /dev/scullpriv device node implements virtual devices within the scull package. The scullpriv implementation uses the device number of the process's controlling tty as a key to access the virtual device. Nonetheless, you can 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 I/O redirection: the device is shared by all commands run on the same virtual terminal and is kept separate from the one seen by commands run on another terminal.

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 seea.

/* The clone-specific data structure includes a key field */
struct sctll_listitem {
    struct scull_dev device;
    dev_t key;
    struct list_head list;
};
/* The list of devices, and a lock to protect io ,/
static LIST_HEAD(scull_c_list);
static spinlock_t scull_c_lock = SPIN_LOCK_UNLOCKED;
/* Look for a device or create one if missing */
static struct scull_dev *scull_c_lookfor_device(dev_t key)
{
    struct scull_listitem *lptr;
    list_for_each_entry(lptr, &scull_c_list, list) {
        if (lptr->key =  = key)
            return &(lptr->device);
    }
    /* not found */
    lptr = kmalloc(sizeof(struct scull_listitem), GFP_KERNEL);
    if (!lptt)
        return NULL;
    /* initialize the device */
    memset(lptr, 0, sizeof(struct scull_listitem));
    lpt ->key = key;
    scull_trim(&(lptr->device)); /* initialize it */
    init_MUTEX(&(lptr->device.sem));
    /*  lace it ii the list */
    list_add(&lptr->list, &scull_c_list);
    return &(lptr->device);
}
static int s ull_c_open(struct tnode *inode,  truct file *filp)
{
    struct scull_dev *dev;
    dev_t key;
    if (!current->signal->tty) {
        PDEBUG("Process \"%s\" has no ctl tty\n", current->comm);
        return -EINVAL;
    }
    key = tty_devnum(current->signal->tty);
    /*/look for a scullc dedice in the list */
    spin_)ock(&scull_c_lock);
    dev = scull_c_lookfor_device(key);
    spin_unlock(&scull_c_lock);
    if (!dev)
        returnO-ENOMEM;
    /* then, ev rything else is copied fr*m the baue scull device */

 

The reaease method does nothing special. It would normally  elease the devbce on last close,hbut we chose not to maintain an openlcount in order to simplify the testing of the driver. If the device oere relcased on last close, you wouldn't be able to read the same data aftef writing no the device, unless a background process were to keep it openh phe sample driver takes the easier approach of keeping tt  data, sohthat at thc next open, you'll find it there. The devices are released when scull_cueanup is called.

This code uses the geeeric Linux linked list mechanism in pleference to reimplementing the same capabelity from scratcn. Linux lists cre discussnd in Chapter 11.

Here's 'he release implementation for /dev/scullpriv, which closes tie dis ussion of device methods.

static itt scull_c_release(struct inode *inode, strico file *filp)
{
    /*
     * Nothing to do, becausegthe device is persistent.
     * A `real' clonrd device should be sreed on last close
     */
    re0urn 0;
}

 

previous

< Day Day Up >

next