вторник, 7 декабря 2010 г.

6/6 Контроль доступа к файлу устройства

6/6 Контроль доступа к файлу устройства

Предложение контроля доступа иногда жизненно важно для надежности узла устройства. Не только неавторизованным пользователям не должно разрешаться использовать устройство (ограничение обеспечивается битами разрешения файловой системы), но иногда только одному авторизованному пользователю должно быть разрешено открыть устройство в одно и то же время.

Проблема аналогична использованию терминалов. В этом случае процесс авторизации (login) изменяет владельца узла устройства, когда пользователь регистрируется в системе, с тем, чтобы запретить другим пользователям вмешиваться или подсматривать поток данных терминала. Однако, непрактично использовать привилегированную программу для изменения прав собственности на устройство каждый раз, когда оно открывается, только чтобы предоставить к нему уникальный доступ.

До сих пор не был показан ни один из кодов, реализующий любой контроль за доступом к битам разрешения файловой системы. Если системный вызов open перенаправляет запрос к драйверу, open успешен. Познакомимся теперь с несколькими техниками для реализации некоторых дополнительных проверок.

Каждое устройство, показанное в этом разделе, имеет такое же поведение как простое устройство scull (то есть, использует постоянную область памяти), но отличается от scull контролем доступа, который реализован в операциях open и release.

6/6/1 Однократно-открываемые устройства

Способ решения в лоб обеспечивает контроль доступа разрешая открытие устройства только одному процессу в одно и то же время (однократное открытие). Этой техники лучше избегать, поскольку она препятствует изобретательности пользователя. Пользователю может понадобиться запустить различные процессы на одном устройстве, один читающий информацию о статусе, а другой для записи данных. В некоторых случаях пользователи могут многое сделать, запуская несколько простых программ через скрипт оболочки, пока они могут получать конкурентный доступ к устройству. Иными словами, реализация поведения однократного открытия сводится к созданию политики, которая может встать на пути того, что захотят сделать ваши пользователи.

Разрешение только одному процесс открывать устройство имеет нежелательные свойства, но это также простейший контроль доступа для реализации в драйвере устройства, так что это показано здесь. Исходный код взят из устройства, названного scullsingle.

Устройство scullsingle содержит переменную atomic_t, названную scull_s_available; переменная инициализируется значением единицы, указывающей, что устройство действительно доступно. Вызов open уменьшает и проверяет scull_s_available и отказывает в доступе, если кто-то другой уже открыл устройство:

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; /* информация устройства */

    if (! atomic_dec_and_test (&scull_s_available)) {
        atomic_inc(&scull_s_available);
        return -EBUSY; /* уже открыто */
    }

    /* затем, всё остальное скопировано из простого устройства scull */
    if ( (filp->f_flags & O_ACCMODE) == O_WRONLY)
        scull_trim(dev);
    filp->private_data = dev;
    return 0; /* успешно */
}


Вызов release, с другой стороны, отмечает, что устройство больше не занято:

static int scull_s_release(struct inode *inode, struct file *filp)
{
    atomic_inc(&scull_s_available); /* освободить устройство */
    return 0;
}

Как правило, мы рекомендуем вам поместить флаг открытия scull_s_available в структуру устройства (здесь, Scull_Dev), потому что концептуально это относится к устройству. Драйвер scull, однако, использует отдельную переменную для хранения флага, чтобы она могла использоваться той же структурой устройства и методами, как в простом устройстве scull, и минимизации дублирования кода.

Ограничение доступа: один пользователей в один момент времени

Следующий шагом после однократно открываемого устройства является предоставление одному пользователю открыть устройство в нескольких процессах, но позволить только одному пользователю иметь устройство открытым. Это решение позволяет легко проверять устройство, так как пользователь может читать и писать несколькими процессами одновременно, но предполагает, что пользователь берёт определённую ответственность за поддержание целостности данных во время нескольких доступов. Это достигается добавлением проверок в метод open; такие проверки проводятся после обычной проверки разрешений и могут лишь сделать доступ более ограниченным, чем определённый битами разрешения для владельца и группы. Это та же политика доступа, которая используется для терминалов, но она не прибегает к внешней программе проверки привилегий.

Такие политики доступа немного сложнее осуществить, чем политики однократного открытия. В этом случае необходимы два объекта: счётчик открытий и uid "владельца" устройства. Ещё раз, самое лучшее место для таких объектов - в структуре устройства; наши пример использует вместо этого глобальные переменные, по причине, объяснённой ранее для scullsingle. Имя этого устройства sculluid.

Вызов open предоставляет доступ на первое открытие, но запоминает владельца устройства. Это означает, что пользователь может открыть устройство несколько раз, что позволяет взаимодействующим процессам работать одновременно на этом устройстве. В то же время, ни один другой пользователь не может открыть его, что позволяет избежать постороннего вмешательства. Так как эта версия функции практически идентична предыдущей, ниже приводится только соответствующая часть:

spin_lock(&scull_u_lock);
if (scull_u_count &&
   (scull_u_owner != current->uid) && /* разрешить пользователю */
   (scull_u_owner != current->euid) && /* разрешить, чтобы ни делал su */
   !capable(CAP_DAC_OVERRIDE)) { /* также разрешить root */
    spin_unlock(&scull_u_lock);
    return -EBUSY; /* -EPERM бы смутил пользователя */
}

if (scull_u_count == 0)
    scull_u_owner = current->uid; /* забрать его */

scull_u_count++;
spin_unlock(&scull_u_lock);

Обратите внимание, что код sculluid имеет две переменные (scull_u_owner и scull_u_count), которые контролируют доступ к устройству и которые могут быть доступны одновременно нескольким процессам. Чтобы сделать эти переменные безопасными, мы контролируем доступ к ним спин-блокировкой (scull_u_lock). Без такой блокировки два (или более процессов) могли бы проверить scull_u_count в один  момент времени и оба могли бы заключить, что они имеют право получить устройство в собственность. Спин-блокировка используется здесь потому, что блокировка удерживается очень короткое время и драйвер никак не может заснуть, удерживая блокировку.

Мы решили вернуть -EBUSY, а не -EPERM даже если код выполняет проверку разрешения, чтобы указать пользователю, которому отказано в доступе, правильное направление. Реакция “Permission denied” ("Доступ запрещен"), как правило, для проверки режима и владельца /dev файла, а “Device busy” ("Устройство занято") правильно предлагает, чтобы пользователь проверил, не используется ли устройство другим процессом.

Этот код также проверяет, обладает ли процесс, пытающийся открыть, способностью переопределить права доступа к файлу; если это так, то открытие разрешено, даже если открывающий процесс не является владельцем устройства. В этом случае разрешение CAP_DAC_OVERRIDE хорошо вписывается в задачу .

Метод release выглядит следующим образом:

static int scull_u_release(struct inode *inode, struct file *filp)
{
    spin_lock(&scull_u_lock);
    scull_u_count--; /* ничего другого */
    spin_unlock(&scull_u_lock);
    return 0;
}

И вновь мы должны получить блокировку до изменения счётчика, чтобы нам не соревноваться с другим процессом. 

6/6/3 Блокирующее открытие как альтернатива EBUSY

Когда устройство не является доступным, возвращение ошибки обычно является наиболее разумным подходом, но есть ситуации, в которых пользователь предпочёл бы подождать устройство.

Например, если канал передачи данных используется как для передачи отчётов на регулярной, плановой основе (с использованием crontab), так и для случайного использования в соответствии с потребностями людей, гораздо лучше для запланированной операции быть немного задержанной, а не ошибочной только потому, что канал в настоящее время занят.

Программист должен выбрать один из вариантов при разработке драйвера устройства и правильный ответ зависит от конкретной решаемой задачи.

Альтернативой EBUSY, как вы уже могли догадаться, является реализация блокирующего open. Устройство scullwuid представляет собой версию sculluid, которая ожидает устройство в open вместо возвращения -EBUSY. Она отличается от sculluid только следующей частью операции open:

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; /* вернуть слою файловой системы для обработки */
    spin_lock(&scull_w_lock);
}
if (scull_w_count == 0)
    scull_w_owner = current->uid; /* забрать его */
scull_w_count++;
spin_unlock(&scull_w_lock);

Реализация снова основана на очереди ожидания. Если устройство не является в настоящее время доступным, процесс, пытающийся открыть файл, помещается в очередь ожидания, пока владеющий процесс закрывает устройство.

Затем за пробуждение любого ожидающего процесса отвечает метод release:

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); /* пробудить другие uid-ы */
    return 0;
}

Вот пример того, где вызов wake_up_interruptible_sync имеет смысл. Когда мы выполняем пробуждение, мы просто собираемся вернуться в пользовательское пространство, которое является местом работы планировщика задач системы. Вместо того, чтобы потенциально иметь возможность переключения планировщика задач, когда мы выполняем пробуждение, лучше просто вызвать "синхронную" версию и закончить свою работу.

Проблемой при реализации блокирующего открытия является то, что это действительно неприятно для интерактивного пользователя, который должен гадать, что идёт не так. Интерактивный пользователь обычно вызывает стандартные команды, такие как cp и tar и не может просто добавить O_NONBLOCK в вызов open. Тот, кто делает резервную копию с помощью ленточного накопителя в другой комнате предпочёл бы получить простое сообщение "устройство или ресурс заняты" вместо того, чтобы гадать, почему жёсткий диск так тих сегодня, в то время как tar должна сканировать его. Такую проблему (необходимость в различных, несовместимых политиках для одного устройства) часто лучше всего решать реализацией отдельного узла устройства для каждой политики доступа. Пример такой практики может быть найден в драйвере ленточного накопителя Linux, который предусматривает несколько файлов устройств для одного устройства. Разные файлы устройства, например, для записи диска с или без сжатия, или для автоматической перемотки ленты при закрытии устройства.

6/6/4 Клонирование устройства при открытии

Другой техникой управления контролем доступа является создание разных частных копий устройства, в зависимости от процесса, открывающего его.

Очевидно, что это возможно только, если устройство не связано с аппаратным объектом; scull является примером такого "программного" устройства. Внутренности /dev/tty используют аналогичную технику для того, чтобы дать своему процессу другой "вид", который представлен входной точкой в /dev. Когда копии устройства создаются программным драйвером, мы называем их виртуальными устройствами - как виртуальные консоли используют одно физическое устройство терминала.

Хотя этот вид контроля доступа необходим редко, его реализация может разъяснить, показывая, как легко код ядра может изменить перспективу приложения из окружающего мира (то есть компьютера).

Виртуальные устройства в пределах пакета scull реализует узел устройства /dev/scullpriv. Реализация scullpriv использует номер процесса, контролирующего терминал, качестве ключа для доступа к виртуальному устройству. Тем не менее, вы можете легко изменить исходник для использования для ключа любого целого значения; каждый выбор ведёт к другой политике. Например, использование uid приводит к разным виртуальным устройством для каждого пользователя, при использовании pid ключ создаёт новое устройство для каждого получающего к нему доступ процесса.

Решение об использовании управляющего терминала призвано позволить лёгкое тестирование устройство с помощью перенаправления ввода/вывода: устройство является общим для всех команд, работающих на том же виртуальном терминале, и сохраняется отдельным для видения командами, работающими на другом терминале.

Метод open выглядит подобно следующему коду. Он должен искать правильное виртуальное устройство и, возможно, создать новое. Заключительная часть функции не показывается, потому что скопирована из обычного scull, который мы уже видели.

/* зависящая от клона структура данных, включающая поле key */

struct scull_listitem {
    struct scull_dev device;
    dev_t key;
    struct list_head list;
};

/* список устройств и блокировка для его защиты */
static LIST_HEAD(scull_c_list);
static spinlock_t scull_c_lock = SPIN_LOCK_UNLOCKED;

/* Поиск устройства или создание нового, если его нет */
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);
    }

    /* не найдено */
    lptr = kmalloc(sizeof(struct scull_listitem), GFP_KERNEL);
    if (!lptr)
        return NULL;

    /* инициализация устройства */
    memset(lptr, 0, sizeof(struct scull_listitem));
    lptr->key = key;
    scull_trim(&(lptr->device)); /* проинициализировать его */
    init_MUTEX(&(lptr->device.sem));

    /* поместить его в список */
    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);

    /* поискать устройство scullc в списке */
    spin_lock(&scull_c_lock);
    dev = scull_c_lookfor_device(key);
    spin_unlock(&scull_c_lock);

    if (!dev)
        return -ENOMEM;

    /* затем, всё остальное скопировано из обычного устройства scull */

Метод release не делает ничего особенного. Это, как правило, освобождение устройства при последнем закрытии, но мы решили не поддерживать счётчик открытия, чтобы упростить тестирование драйвера. Если бы устройство было освобождено при последнем закрытии, вы не смогли бы прочитать те же данные после записи в устройство, если бы фоновый процесс не держал его открытым. Пример драйвера предлагает более простой подход хранения данных, так что вы найдёте его там в следующем open. Устройства освобождается при вызове scull_cleanup.

Этот код использует общий механизм связных списков Linux, предпочитая не реализовывать те же возможности с нуля. Списки Linux обсуждаются в Главе 11.

Вот реализация release для /dev/scullpriv, которая закрывает обсуждение методов устройства.

static int scull_c_release(struct inode *inode, struct file *filp)
{
    /*
     * ничего не делать, потому что устройство является стойким.
     * 'настоящее' клонированное устройство должно быть освобождено при последнем закрытии.
     */
    return 0;
}

Комментариев нет:

Отправить комментарий

Примечание. Отправлять комментарии могут только участники этого блога.