Linux Device Drivers (3rd Edition) [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Linux Device Drivers (3rd Edition) [Electronic resources] - نسخه متنی

Jonathan Corbet, Greg Kroah-Hartman, Alessandro Rubini

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید








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.

None of the code shown up to now implements any access control beyond
the filesystem permission bits. If the open
system call forwards the request to the driver,
open succeeds. We now introduce a few techniques
for implementing some additional checks.

Every device shown in this section has the same behavior as the bare
scull device (that is, it implements a
persistent memory area) but differs from scull
in access 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 process to open a device has undesirable
properties, but it is also the easiest access control to implement
for a device driver, so it's shown here. The source
code is extracted from a device called
scullsingle.


The
scullsingle

device maintains an atomic_t variable called
scull_s_available; that variable is initialized to
a value of one, indicating that the device is indeed available. The
open call 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; /* device information */
if (! atomic_dec_and_test (&scull_s_available)) {
atomic_inc(&scull_s_available);
return -EBUSY; /* already open */
}
/* 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;
return 0; /* success */
}


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

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

Normally, we recommend that you put the open flag
scull_s_available within the device structure
(Scull_Dev here) because, conceptually, 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 scull device
and minimize code 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
the device open at a time. This solution makes it easy to test the
device, since the user can read and write from several processes at
once, but assumes that the user takes some responsibility for
maintaining the integrity of the data during multiple accesses. This
is accomplished by adding checks in the open
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 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 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 != current->uid) && /* allow user */
(scull_u_owner != current->euid) && /* allow whoever did su */
!capable(CAP_DAC_OVERRIDE)) { /* still allow root */
spin_unlock(&scull_u_lock);
return -EBUSY; /* -EPERM would confuse the user */
}
if (scull_u_count = = 0)
scull_u_owner = current->uid; /* grab it */
scull_u_count++;
spin_unlock(&scull_u_lock);

Note that the
sculluid

code has two variables (scull_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_count 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
-EPERM, 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 in
this case.

The release method looks like the following:

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, we must obtain the lock prior to modifying the count to
ensure that we do not race with another process.


6.6.3. Blocking open as an Alternative to 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 waits for the device on
open instead of returning
-EBUSY. It differs from
sculluid only in the following part of the
open operation:

spin_lock(&scull_w_lock);
while (! scull_w_available( )) {
spin_unlock(&scull_w_lock);
if (filp->f_flags & O_NONBLOCK) return -EAGAIN;
if (wait_event_interruptible (scull_w_wait, scull_w_available( )))
return -ERESTARTSYS; /* tell the fs layer to handle it */
spin_lock(&scull_w_lock);
}
if (scull_w_count = = 0)
scull_w_owner = current->uid; /* grab it */
scull_w_count++;
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
method, then, is in charge of awakening any pending process:

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

Here is an example of where calling
wake_up_interruptible_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'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 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 example of such a
"software" device. The internals of
/dev/tty use a similar technique 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 devicesjust as virtual consoles
use a single physical tty device.

Although this kind of access control is rarely needed, the
implementation can be enlightening in showing how easily kernel code
can change the application's perspective of the
surrounding 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 seen.

/* The clone-specific data structure includes a key field */
struct scull_listitem {
struct scull_dev device;
dev_t key;
struct list_head list;
};
/* The list of devices, and a lock to protect it */
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 (!lptr)
return NULL;
/* initialize the device */
memset(lptr, 0, sizeof(struct scull_listitem));
lptr->key = key;
scull_trim(&(lptr->device)); /* initialize it */
init_MUTEX(&(lptr->device.sem));
/* place it in the list */
list_add(&lptr->list, &scull_c_list);
return &(lptr->device);
}
static int scull_c_open(struct inode *inode, struct 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 device in the list */
spin_lock(&scull_c_lock);
dev = scull_c_lookfor_device(key);
spin_unlock(&scull_c_lock);
if (!dev)
return -ENOMEM;
/* then, everything else is copied from the bare scull device */


The release method
does nothing special. It would normally release the device on last
close, but we chose not to maintain an open count in order to
simplify the testing of 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. 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
scull_cleanup

is called.

This code uses the generic Linux linked list mechanism in preference
to reimplementing the same capability from scratch. Linux lists are
discussed in Chapter 11.

Here's the release
implementation for /dev/scullpriv, which closes
the discussion of device methods.

static int 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
*/
return 0;
}


    / 202