Powered By Blogger

rfLinux

Sunday, January 31, 2021

Оптимизация: frame pointer omission

История началась с вынужденной переустановки Gentoo. Благодаря когда-то давно где-то увиденной рекомендации из раза в раз в make.conf перекочёвывает переменная CFLAGS с аргументом -fomit-frame-pointer. Но что, собственно, он делает и нужен ли там вообще? Конец года 2020 хорошее время, чтобы в этом разобраться.

Чтобы понять, что делает -fomit-frame-pointer, вспомним кое-что о стеке. Рассмотрим простой код:

#include <stdio.h>

int main(int argc, char** argv)
{
        printf("Hello %s\n", "world");
}
Что происходит при вызове функции printf() (да и самой main() тоже)?

В точности следующее:

00001199 <main>:
    1199:       8d 4c 24 04             lea    0x4(%esp),%ecx
    119d:       83 e4 f0                and    $0xfffffff0,%esp
    11a0:       ff 71 fc                pushl  -0x4(%ecx)
    11a3:       55                      push   %ebp
    11a4:       89 e5                   mov    %esp,%ebp
    11a6:       53                      push   %ebx
    11a7:       51                      push   %ecx
    11a8:       e8 2f 00 00 00          call   11dc <__x86.get_pc_thunk.ax>
    11ad:       05 53 2e 00 00          add    $0x2e53,%eax
    11b2:       83 ec 08                sub    $0x8,%esp
    11b5:       8d 90 08 e0 ff ff       lea    -0x1ff8(%eax),%edx
    11bb:       52                      push   %edx
    11bc:       8d 90 0e e0 ff ff       lea    -0x1ff2(%eax),%edx
    11c2:       52                      push   %edx
    11c3:       89 c3                   mov    %eax,%ebx
    11c5:       e8 66 fe ff ff          call   1030 <printf@plt>
    11ca:       83 c4 10                add    $0x10,%esp
    11cd:       b8 00 00 00 00          mov    $0x0,%eax
    11d2:       8d 65 f8                lea    -0x8(%ebp),%esp
    11d5:       59                      pop    %ecx
    11d6:       5b                      pop    %ebx
    11d7:       5d                      pop    %ebp
    11d8:       8d 61 fc                lea    -0x4(%ecx),%esp
    11db:       c3                      ret
В строках 11bb и 11c2 в стек запихиваются аргументы для printf() и на момент после исполнения инструкции call стек принимает следующий вид:

... ...
0xXXXXfffb arg1 [ebp + 12]
0xXXXXfff7 arg0 [ebp + 8]
0xXXXXfff3 return addr [ebp + 4]
0xXXXXffef previous ebp [ebp + 0]
... ...

Если кому-то интересно, что происходит в строках 11a8-11ad, то это работа PIC (position-independent code), в котором данные адресуются относительно текущей инструкции кода. В x86-64 возможна непосредственная адресация относительно rip, но в х86 такого режима не было. Да-да, это 32-битный код для х86. Так было проще продемонстрировать передачу аргументов через стек. Потом поясним, почему. PIC в gcc включен по умолчанию. Запустим gcc с флагом -fno-pic, и вывод будет выглядеть уже иначе:
    11ad:       68 08 20 00 00          push   $0x2008
    11b2:       68 0e 20 00 00          push   $0x200e
    11b7:       e8 fc ff ff ff          call   11b8 <main+0x1f>
что воспринимается заметно проще. Но вернёмся назад к нашей printf(). Наверняка, она использует какие-то локальные переменные. Много локальных переменных. Регистров общего назначения у процессоров x86 не сказать, чтобы много. Едва ли их хватит на все нужды printf() и эти переменные будут размещены на стеке. В самом начале выполнения printf() на вершине стека находится адрес, куда будет передано управление после того, как в теле printf() процессор выполнит инструкцию ret. Для того, чтобы зарезервировать на стеке место для локальных переменных printf(), надо вычесть нужное число байт из esp - указателя на вершину стека, сдвинув его таким образом вперёд. Далее, обращаться к локальным переменным printf() сможет, например, по смещению относительно esp. Но это не всегда удобно, ведь по ходу дела программа может затолкать в стек ещё какие-то данные, не связанные с вызовом подпрограмм, значение esp изменится и смещения поплывут. Однако есть ещё один регистр ebp. Как правило, в прологе функции в нём сохраняется содержимое регистра esp, которое printf() "унаследует" от вызывающего кода. На протяжении выполнения printf() содержимое ebp не изменится. Вот относительно него-то и можно адресовать локальные переменные. Пролог типичной функции выглядит очень просто:
    XXXX:       55                      push   %ebp
    XXXX:       89 e5                   mov    %esp,%ebp
Действия, вполняемые этим кодом, отменяются единственной инструкцией 32-разрядных процессоров x86 leave, которая эквивалентна этому коду:
    XXXX:       89 ec                   mov    %ebp,%esp
    XXXX:       5d                      pop    %ebp
Невероятно, но факт: существует инструкция enter, которая настраивает фрейм, то есть, соответствует прологу. Однако этот факт тщательно скрывается её эффективность оказалась ниже, чем хотелось бы. Так что она не используется в коде, генерируемом gcc. А с leave всё в порядке. Потому и видим мы её, а не комбинацию мув-поп.

Казалось бы, к чему всё это? Именно к самой сути флага -fomit-frame-pointer, ведь содержимое ebp и есть тот самый указатель на фрейм, а наш флаг просто подавляет генерацию пролога и, соответственно, эпилога. Имеет ли такая оптимизация смысл? Ведь в сумме мы экономим самое большее 6 байтов и какое-то мизерное число тактов на грани статистической погрешности. Этот аргумент звучит не очень убедительно, но вряд ли разработчики компиляторов стали бы заморачиваться, если смысла совсем не было - да, MSVC тоже умеет элиминировать указатель на фреймы ключом /Oy. Действительно, если функция достаточно мала, настолько, что пролог и эпилог составляют ощутимый процент её тела, и в то же время вызывается она очень часто, выигрыш может стоить свеч. Хотя тут уже впору задуматься об инлайнинге. Может показаться, что эта оптимизация в любом случае не навредит, хотя бы и толку от неё было не особо много. На самом деле она усложняет отладку. Для компилятора заменить все ссылки на содержимое стека относительно ebp смещениями от esp не особо-то большое дело. Но при этом будет утрачена или существенно затруднена возможность следовать за выполнением кода по стеку вызовов. Отладчик всегда может без проблем найти ссылку на вызывающий код по содержимому ebp. Ведь как мы помним на вершине стека хранится адрес, принадлежащий функции, которой надлежит вернуть управление по окончании работы. Так как содержимое ebp больше не указывает гарантированно на вершину стека на момент вызова данной функции, то отладчик поставлен в тупик. Палка о двух концах. Но, кстати, вдобавок мы ещё получили свободный регистр... Когда их не так много и ещё один дополнительный регистр - это плюс 16% к объёму регистровой памяти, то возможно результат уже более ощутим. На процессорах x86 это близко к истине. Но не на amd64, где регистры широкие и их уже побольше.

Ещё один нюанс флага -fomit-frame-pointer состоит в том, что он включает оптимизацию, но не гарантирует её. Вот что по этому поводу можно прочесть в первоисточнике, man-странице gcc:

-fomit-frame-pointer
           Omit the frame pointer in functions that don't need one.  This avoids the
           instructions to save, set up and restore the frame pointer; on many
           targets it also makes an extra register available.

           On some targets this flag has no effect because the standard calling
           sequence always uses a frame pointer, so it cannot be omitted.

           Note that -fno-omit-frame-pointer doesn't guarantee the frame pointer is
           used in all functions.  Several targets always omit the frame pointer in
           leaf functions.

           Enabled by default at -O and higher.
В частности, пролог функции не может быть элиминирован в том случае, когда функция вызывает alloca(). alloca() не особо популярна и обладает неоднозначной репутацией, но тем не менее. Между тем, alloca(), выделяя память на стеке, делает это куда быстрее, чем malloc(), не требует парного вызова free(), так как память освобождается автоматически при выходе из стекового фрейма. Впрочем, alloca() не может использоваться в inline-функциях и пригодна для выделения лишь небольших блоков памяти на несколько сотен байтов максимум при гарантии, что указатель на блок не будет использован после возврата из функции, в которой был выделен данный блок. С точки зрения безопасности элиминация прологов функци сокращает attack surface для перехвата управления, когда злонамеренный код перезаписывает пролог инструкциями ветвления, так называемый трамплин (trampoline)[1].

Условия, при которых компилятор не будет генерировать пролог и эпилог функции, могут в частности, включать следующее:

  • Данная функция - тупиковая (leaf function). То есть, она находится на вершине стека вызовов и в свою очередь не вызывает другие функции;
  • Не используются исключения;
  • Данная функция не содержит вызовов других функций, параметры которых передаются на стеке;
  • Данная функция не принимает аргументов.

Возвращаясь к вопросу, почему в целях демонстрации использовался 32-разрядный код, можно сказать следующее. Linux ABI на amd64 требует передачи параметров в регистрах. Как уже упоминалось, в amd64 регистров общего назначения больше. Добиться от кода использования стека, соответственно, несколько проблематичнее, в противоположность ABI IA-32.

Ссылки:

  1. Learning Linux binary analysis. Ryan O'Neill. 2016
  2. Agner Vog's optimization manuals
  3. System V Application Binary Interface. Intel386 Architecture Processor Supplement, Version 1.1

Monday, December 1, 2014

Объекты ядра - kobject

По мотивам http://lwn.net/Articles/51437/ и Documentation/kobject.txt.
Всё, что вы никогда не хотели знать об объектах, множествах и типах объектов ядра.

Отчасти сложность понимания модели драйверов - и объектов ядра (kobject), абстракции, на которой построена эта модель - в том, что здесь нет ясного подхода, с которого можно было бы начать. Работа с объектами требует понимания других типов ядра, которые взаимосвязаны и ссылаются друг на друга. Чтобы упростить объяснение, мы попытаемся рассмотреть многосторонний подход и начнём с определения терминов, по ходу дела проясняя детали. Вот краткие определения сущностей, с которыми мы будем иметь дело:

  • kobject (объект ядра) - это объект типа struct kobject (внезапно :)). Объект имеет имя и счётчик ссылок. Также у него есть указатель на родительский объект, что позволяет огранизовать объекты в иерархию, определённый тип и, обычно, некоторое представление в виртуальной файловой системе sysfs.

    В общем и в целом, сами по себе объекты интереса не представляют. Но они внедряются в другие структуры, с которыми код драйвера имеет дело

    Подобного рода структура НИКОГДА не должна содержать более одного внедрённого объекта. Если нарушить это правило, подсчёт ссылок объекта наверняка окажется поломанным и ваш код будет работать неправильно. Так что не делайте этого.

  • ktype - тип объекта. Каждая структура, с которой ассоциирован объект ядра (далее, если явно не оговорено иное, просто объект для краткости, kobject), должна принадлежать какому-то типу ktype. Принадлежность объекта к типу ktype определяет, что происходит с объектом при его создании или уничтожении.

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

    Когда вы видите директорию в sysfs, забитую другими директориями, в общем случае эти директории представляют объекты, принадлежащие одному и тому же множеству.

Далее мы увидим, как создавать эти типы и манипулировать ими. Мы начнём с простого и выйдем снова на объекты.

Внедрение объектов

Ситуация, когда код ядра создаёт самостоятельные объекты, достаточно редка за исключением одного случая, который будет рассмотрен ниже. Вместо этого объекты используются для управления доступом к более сложным подсистемно-специфичным структурам. Таким образом, объект встраивается в управляемую структуру. Если вы привыкли думать в манере объектно-ориентированной парадигмы, kobject - это базовый абстрактный класс, которому наследуют другие классы. kobject реализует некоторые вещи, которые не очень нужны им самим, но очень полезные другим объектам. Язык C не позволяет напрямую пользоваться наследованием и чтобы компенсировать это ограничение, используется внедрение одних структур в другие.

(Для тех, кто знаком с реализацией списков в ядре, это аналогично тому, как сама по себе структура "list_head" обычно бесполезна, но для создания списков объектов её внедряют в другие структуры.)

Итак, для примера код в подсистеме пользовательского ввода-вывода UIO из drivers/uio/uio.c пользуется такой структурой, определяющей регион памяти, присвоенный UIO-устройству:

    struct uio_map {
            struct kobject kobj;
            struct uio_mem *mem;
    };

Работая с объектом типа struct uio_map structure, для доступа к ассоциированному объекту ядра вы просто используете поле kobj. В коде, который работает с самими объектами ядра, а не инкапсулирующими их типами возникает проблема, как по указателю на внедрённый объект выяснить, кому принадлежит данный объект? Вы должны избегать трюков и не подразумевать, например, что структура kobject находится в начале той структуры, в которую этот kobject внедрён. Вместо этого используйте макрос container_of() из <linux/kernel.h>:

    container_of(pointer, type, member)
где:
  • "pointer" указатель на внедрённый kobject,
  • "type" тип инкапсулирующего объекта и
  • "member" имя поля структуры, на которую указывает "pointer".

Значение, возвращаемое container_of() - это указатель на соответствующий тип-контейнер. Например, указатель "kp" на struct kobject, внедрённый внутрь struct uio_map может быть преобразован к типу-контейнеру struct uio_map таким образом:

    struct uio_map *u_map = container_of(kp, struct uio_map, kobj);

Для удобства программисты часто определяют небольшие макросы "обратного приведения к типу" указателя на объект ядра к инкапсулирующему типу:

    struct uio_map {
            struct kobject kobj;
            struct uio_mem *mem;
    };

    #define to_map(map) container_of(map, struct uio_map, kobj)

Здесь "map" это указатель на поле типа struct kobject в нашей структуре. Вот как этот макрос используется:

    struct uio_map *map = to_map(kobj);

Инициализация объектов

Код, создающий объект, должен его инициализировать. Некоторые внутренние поля инициализируются обязательным вызовом kobject_init():

    void kobject_init(struct kobject *kobj, struct kobj_type *ktype);

Для объекта должен быть создан тип ktype, так как каждый объект должен иметь тип kobj_type. После вызова kobject_init(), чтобы зарегистрировать объект в sysfs, необходимо вызвать функцию kobject_add():

    int kobject_add(struct kobject *kobj, struct kobject *parent, const char *fmt, ...);

Этим вызовом объекту присваивается родительский объект и имя. Если объект должен входить в какое-то множество, то перед вызовом kobject_add() поле kobj->kset должно быть заполнено соответствующим образом. Если объект сам ассоциирован со множеством kset то указатель на родительский объект может быть NULL, а родительским объектом станет само множество kset.

С момента регистрации объекта в ядре его имя не должно изменяться напрямую. Если необходимо переименовать объект, используйте функцию kobject_rename():

    int kobject_rename(struct kobject *kobj, const char *new_name);

kobject_rename не держит блокировок и не определяет, как должно выглядеть имя объекта, так что синхронизацию и прочие проверки должен обеспечивать пользователь.

Есть ещё одна функция, kobject_set_name(), но она устарела и удаляется из кода. Если ваш код содержит этот вызов, вам стоит исправить код.

Чтобы получить доступ к имени объекта, используйте функцию kobject_name():

    const char *kobject_name(const struct kobject * kobj);

Для того, чтобы проинициализировать и зарегистрировать объект в ядре, предусмотрена вспомогательная функция с достаточно неожиданным (уроки искромётного юмора от К-Х - перев.) именем kobject_init_and_add():

    int kobject_init_and_add(struct kobject *kobj, struct kobj_type *ktype,
                             struct kobject *parent, const char *fmt, ...);

Смысл аргументов тот же, что для функций kobject_init() и kobject_add(), описанных выше.

События uevent

После того, как объект был зарегистрирован, необходимо уведомить всех о том, что мы создали объект. Сделать это можно с помощью функции kobject_uevent():

    int kobject_uevent(struct kobject *kobj, enum kobject_action action);

При добавлении объекта используйте флаг KOBJ_ADD в качестве аргумента action. Это должно делаться только после того, как были проинициализированы атрибуты дочерних объектов, потому что при уведомлении о новом объекте юзерспейс сразу же обратиться к этим данным.

Когда объект удаляется из ядра (детали, как это делается, см. ниже), создаётся событие uevent для соответствующего действия KOBJ_REMOVE. Пользоателю нет нужды об этом волноваться, т.к. всю работу берёт на себя подсистема объектов.

Подсчёт ссылок

Одна из ключевых функций объектов - это подсчёт ссылок на то, во что он внедрён. До тех пор, пока есть ссылки на данный объект, объект (и код, который его поддерживает) должен существовать. Низкоуровневые функции для манипуляций счётчиком ссылок на объект:

    struct kobject *kobject_get(struct kobject *kobj);
    void kobject_put(struct kobject *kobj);

Успешный вызов kobject_get() увеличит счётчик ссылок на объект и вернёт указатель на него.

Когда ссылка больше не нужна, вызов kobject_put() уменьшит счётчик и, вероятно, освободит объект. Обратите внимание, что kobject_init() устанавливает счётчик ссылок в 1, так что код, который проинициализировал объект в конечном счёте должен будет вызвать kobject_put(), чтобы освободить объект.

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

Если всё, что вам нужно, это подсчёт ссылок на вашу структуру, используйте kref, а не kobject. Последний будет избыточен для одного лишь аудита объектов. Чтобы узнать больше о kref, прочтите файл Documentation/kref.txt в дереве кода ядра Linux.

Создание "простых" объектов

Иногда программисту нужно просто создать директорию в иерархии sysfs без использования множеств, функций чтения/сохранения и прочих деталей. Это единственное исключение, когда нужно создавать самостоятельный объект. Чтобы создать его, используйте следующую функцию:

    struct kobject *kobject_create_and_add(char *name, struct kobject *parent);

Эта функция создаст объект и поместит в sysfs директорию под директорией, которая соответствует родительскому объекту. Чтобы создать простые атрибуты объекта, используйте вызов:

    int sysfs_create_file(struct kobject *kobj, struct attribute *attr);
или
    int sysfs_create_group(struct kobject *kobj, struct attribute_group *grp);

Оба типа атрибутов, которые здесь используются, для объекта, созданного с помощью kobject_create_and_add(), могут иметь тип kobj_attribute, так что создавать новые пользовательские атрибуты необязательно.

См. примеры реализации простого объекта и атрибутов в модулях в samples/kobject/kobject-example.c.

Типы ktype и деструкторы объекта

До сих пор мы не касались одного важного момента. Что происходит с объектом, когда его счётчик ссылок обнуляется? Обычно код, создавший объект, не знает, когда наступит такой момент; если бы это было возможно, то особого смысла в использовании объектов просто не было бы. Когда мы имеем дело с sysfs даже объекты с предсказуемым жизненным циклом становятся сложными в этом плане, потому что какая-то другая часть ядра может получить ссылку на любой объект, зарегистрированный в системе.

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

Создав свой объект с помощью kobject_add() вы никогда не должны использовать на нём kfree(), чтобы напрямую осводобить объект. Единственный безопасный способ сделать это - использовать kobject_put(). Вызов kobject_put() сразу после kobject_init() является хорошим правилом, которое позволит предотвратить возможные ошибки.

Уведомление об обнулении счётчика ссылок происходит с помощью метода объекта - release(). Обычно это выглядит как-то так:

    void my_object_release(struct kobject *kobj)
    {
         struct my_object *mine = container_of(kobj, struct my_object, kobj);

     /* Perform any additional cleanup on this object, then... */
     kfree(mine);
    }

Важный момент, которым нельзя пренебречь: каждый объект должен иметь метод release() и объект должен находиться в непротиворечивом состоянии ровно до тех пор, пока не вызван этот метод. Код, нарушающий эти требования нарушены, дефективен. ОБратите внимание, ядро предупредит вас, если объект не имеет метода release(). Не пытайтесь избавиться от этих предупреждений, создавая пустые заглушки вместо реально работающего метода; если вы попытаетесь провернуть такой трюк, вам придётся иметь дело с беспощадным мэйнтейнером подсистемы kobject.

Имейте в виду, имя объекта доступно из метода-деструктора, но оно НЕ ДОЛЖНО изменяться внутри кода деструктора. В противном случае это может привести к утечке памяти в подсистеме управления объектов, а такие вещи печалят пользователей.

Любопытным является то обстоятельство, что метод release() не хранится внутри самого объекта; вместо этого деструктор ассоциирован с соответствующим типом объектов - ktype. Давайте посмотрим на структуру struct kobj_type:

    struct kobj_type {
     void (*release)(struct kobject *kobj);
     const struct sysfs_ops *sysfs_ops;
     struct attribute **default_attrs;
     const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
     const void *(*namespace)(struct kobject *kobj);
    };

Эта структура описывает определённый тип объектов (если быть более точным, тип инкапсулирующего kobject объекта). Каждый объект должен принадлежать к какому-то типу, описанному структурой kobj_type; указатель на структуру этого типа должен быть передан при вызове функции kobject_init() или kobject_init_and_add().

Поле release в struct kobj_type это, конечно, указатель на метод release() для данного типа объектов. Два других поля (sysfs_ops и default_attrs) определяют, как объекты данного типа представляются в sysfs; эти поля мы рассматривать не будем, потому что их описание выходит за рамки данного документа.

Указатель default_attrs указывает на список атрибутов по умолчанию, которые будут создавать для любого объекта данного типа.

kset

kset - это, в общем-то, множество объектов, которые должны быть ассоциированы друг с другом. Объекты, принадлежащие одному множеству, не обязаны быть одного типа. Но когда множество гетерогенно, будьте осторожны.

Множества выполняют следующие функции:

  • Множество - это своеобразный мешок, куда складываются объекты. Оно может использоваться для отслеживания "всех блочных устройств" или "всех драйверов PCI-устройств"."
  • Множество - это также поддиректория в sysfs, где отображаются объекты, ассоциированные с данным множеством. Каждое множество содержит объект, который может быть родительским объектом для других объектов; в иерархии sysfs директории верхнего уровня устроены именно так.
  • Множества могут поддерживать "горячее подключение" ("hotplugging") объектов и влиять на то, как генерируются события uevent для юзерспейса.

С точки зрения объектно-ориентированной парадигмы множество "kset" это класс-контейнер верхнего уровня; множество имеет собственный kobject, который, однако, управляется подсистемой управления множествами и не должен напрямую манипулироваться другими пользователями.

Объекты, принадлежащие множеству, организованы в обычный связный список ядра. Объекты указывают на содержащее их множество с помощью своего поля kset. Почти во всех случаях объекты, принадлежащие данному множеству, указывают на соответствующий kset, как на своего родителя (точнее, внедрённый kobject данного множества kset).

Поскольку множество имеет свой внедрённый объект, оно всегда должно создаваться динамически, а не объявляться статически или размещаться на стеке. Чтобы создать новое множество, используйте следующие функции:

  struct kset *kset_create_and_add(const char *name,
       struct kset_uevent_ops *u,
       struct kobject *parent);

Когда множество вам уже больше не нужно, вызовите:

    void kset_unregister(struct kset *kset);
чтобы уничтожить его. Таким образом множество будет удалено из sysfs, а его счётчик ссылок будет уменьшен (ну, вероятно множество исчезнет из sysfs только когда его счётчик ссылок дропнется до нуля - перев.) Когда счётчик ссылок обнулится, множество будет уничтожено. Т.к. могут существовать другие ссылки на данное множество, то вполне возможно, что множество будет уничтожено после того, как kset_unregister() возвратит управление.

Пример использования множества можно найти в файле samples/kobject/kset-example.c дерева исходного кода ядра.

Если множество должно управлять генерацией событий uevent, соответствующие операции можно определить в структуре struct kset_uevent_ops:

    struct kset_uevent_ops {
            int (*filter)(struct kset *kset, struct kobject *kobj);
            const char *(*name)(struct kset *kset, struct kobject *kobj);
            int (*uevent)(struct kset *kset, struct kobject *kobj,
                      struct kobj_uevent_env *env);
    };

Функция filter позволяет множеству отфильтровать события, посылаемые юзерспейсу, для некоторого объекта. Если указатель равен 0, события не будут генерироваться.

Функция name будет вызывана для переопределения имени множества, которое посылает событие. По умолчанию сохраняется имя множетва-источника события, но с помощью данной функции это имя можно изменить.

uevent вызывается, когда событие готово к отправке в юзерспейс и позволяет передать дополнительные переменные окружения в событии uevent.

Может возникнуть вопрос, а как, собственно, добавить в множество объекты, если нет специальной функции, которая бы это делала? (но мы-то с вами уже знаем ответ - перев.) Ответ таков: это делает функция kobject_add(). Когда мы передаём kobject_add() объект, его поле kset должно указывать на то множество, в которое этот объект должен быть включён. Остальную работу выполняет сама kobject_add().

Если родительский объект для объекта, принадлежащего данному множеству, не установлен, то он будет добавлен в директорию множества. Не все объекты множества обязательно "сидят" в директории, закреплённой за данным множеством. Если перед добавлением объекта в множество его указатель на родителя определён явным образом, объект регистрируется во множестве, но в файловой иерархии он появится в директории объекта-родителя.

Удаление объектов

После того, как подсистема управления объектами успешно зарегистрировала объект, он должен быть удалён, когда в нём больше нет нужды. Чтобы сделать это, используйте kobject_put(). Этот вызов автоматически освободит ресурсы, занимаемые объектом. Если при регистрации объекта генерируется событие KOBJ_ADD, то при его удалении - KOBJ_REMOVE. Попутно будет выполнена необходимая работа на уровне sysfs.

В случае, если вы хотите удалить объект в два шага (например, ваш код не может спать во время уничтожения объекта), воспользуйтесь вызовом kobject_del(), который разрегистрирует объект в sysfs. Сам объект становится "невидимым", но он продолжает существовать и число ссылок на него остаётся неизменным. Позже вызовите kobject_put(), чтобы освободить память, выделенную под объект.

kobject_del() может быть использована, чтобы убрать ссылку на родительский объект, если созданы циркулярные ссылки. В некоторых случаях это нормальное явление, что родительский объект ссылается на дочерний. Циркулярные ссылки должны устраняться явным вызовом функции kobject_del() так, что будут вызваны деструкторы и объекты, которые были циклически связаны, освободят друг друга.

Где взять код примеров

Чтобы иметь более полные примеры корректного использования объектов и можеств, изучите файлы samples/kobject/{kobject-example.c,kset-example.c}, которые будут скомпонованы как загружаемые модули ядра, если при конфигурировании ядра была активирована опция CONFIG_SAMPLE_KOBJECT.

Aftermath

Мы быстро пробежались по API объектов, вкратце ознакомились с устройством самих объектов и связанных с ними структур. Пришло время спросить, а зачем, собственно, всё это нужно? На самом деле, в тексте К-Х уже есть частичный ответ. Там упоминались события uevent. Хотя бы вот для этого. Но обо всё по порядку.

Как мы увидели, объекты предоставляют средство подсчёта ссылок, они внедряются в тот объект, которым управляют. У объектов одного типа - один деструктор. Объекты могут собираться во множества (или наборы, коллекции - как угодно). Что, помимо учёта ссылок и универсальных деструкторов дают нам объекты? Прежде всего, это структуризация. Если мы рассмотрим некое реальное устройство, то легко увидеть, что жёсткий диск подсоединён к порту SATA, порт SATA сидит на SATA-шине, SATA-шина управляется SATA-контроллером... Иерархия. Объекты позволяют нам выстроить иерархию устройств, подсоединённых к системе. Объекты ядра тесно взаимосвязаны с псевдо-ФС sysfs. Вся та лапша, которую можно увидеть под /sys - это отображения картины подключения и взаимосвязей устройств в системе, как её видит ядро. На самом верхнем уровне в /sys мы можем найти такие директории, как class, devices, bus, power, например. Если вы сталкивались с программированием драйверов для OS X, то имеете преставление об IORegistry и планах устройств. Все устройства в системе могут быть классифицированы по разным признакам. Например, является устройство блочным или символьным, каковы его потребности в питании и откуда оно это питание берёт, на какой шине сидит устройство. На самом деле, директории верхнего уровня в /sys в известной степени выполняют ту же задачу, что планы IORegistry и поэтому одно устройство может быть представлено во многих директориях. Например, тот же диск у нас будет в devices/, а симлинк на него будет и в bus/ и в block/. Кроме того, sysfs экспортирует также атрибуты устройств. Эти атрубуты являются атрибутами объектов и раз уж мы затронули эту тему, то наверно пробежимся по ней быстро. На основе текста, с сокращениями и некоторыми вольностями взятого отсюда: http://lwn.net/Articles/54651/

Как объекты получают своё воплощение в sysfs

Как мы видели ранее, при использовании kobject_init() мы получим самостоятельный объект, не представленный в sysfs. Если же воспользоваться API kobject_register() (или kobject_add()), в sysfs будет создана соответствующая этому объекту директория; других усилий со стороны программиста не требуется.

Именем директории будет имя объекта. Место директории в структуре sysfs определяется его позицией в иерархии объектов. Короче, директория объекта будет находиться в директории, редставляющей родительский объект данного объекта, т.е., того объекта, на который указывает поле parent. Если поле parent вашего объекта указывает на kset, то родителем этого объекта станет объект соответствующего множества kset. Если указатель parent не указфвает ни на kset, ни на другой объект, то директория объекта окажется в sysfs на самом верхнем уровне.

Заполнение директории объекта

Получить представление объекта в sysfs легко, как мы убедились. Однако, эта директория будет пустой и пользы от неё будет не много. Для связи объекта ядра с пользовательским окружением было бы лучше, если директория содержит какие-то атрибуты.Создание атрибутов требует дополнительных шагов, но в целом, это несложно.

Основные атрибуты в sysfs - это атрибуты по умолчанию, которые описываются в типе объекта kobj_type. Эти атрибуты присущи каждому объекту данного типа (см. default_attrs выше). Поле default_attrs - это указатель на массив указателей атрибутов со следующей структурой:

    struct attribute {
 char   *name;
 struct module   *owner;
 mode_t   mode;
    };

name - это, собственно, имя атрибута (имя файла в sysfs), owner - указатель на модуль ядра (если применимо), который непосредственно имеет дело с этими атрибутами и, наконец, mode - режим доступа к файлу (обычная триада rwx-битов для владельца, группы и остальных). Как правило, режим - S_IRUGO для read-only атрибутов; если атрибут изменяемый (для пользователя из юзерспейса), то режим может быть S_IWUSR, что даст суперпользователю доступ к файлу в режиме записи. Последним элементом в массиве default_attrs должен быть NULL.

Поле default_attrs указывает на то, какие атрибуты есть в принципе, но они ещё никак не реализованы на уровне sysfs. Поле kobj_type->sysfs_ops определяет, как атрибуты отображаются или считываются:

    struct sysfs_ops {
 ssize_t (*show)(struct kobject *kobj, struct attribute *attr, 
                        char *buffer);
 ssize_t (*store)(struct kobject *kobj, struct attribute *attr, 
   const char *buffer, size_t size);
    };

Эти функции будут вызываться дл каждой операции чтения или записи соответственно для каждого объекта данного типа. В каждлом случае kobj - это указатель на тот объект, чьи атрибуты изменяются или читаются, attr - указатель на переменную типа struct attribute, описывающую атрибут и буфер размером в страницу для данных атрибута.

Задача функции show() закодировать полное значение атрибута, при этом не выходя за пределы PAGE_SIZE. Имейте в виду, что по соглашению атрибут sysfs содержит только одно значение или, по крайней мере, массив значений одного типа, так что ограничени на размер буфера не должно быть проблемой. Возвращаемое значение - это число байт, записанных в буфер или -1 в случае ошибки. Рассматривать store() нет особого смысла, суть аргументов та же, только обратное направление (чтение из буфера, а не запись в буфер) и дополнительный аргумент size, определяющий длину входных данных.

Итак, для работы с одним выбранным атрибутом данного типа kobj_type нужна своя пара функций show() и store().

Собственные атрибуты объектов

Часто default_attrs в kobj_type исчерпывает собой все атрибуты, которые имеет объект. Но это вовсе не обязательно всегда так; атрибуты объекта могут быть добавлены или удалены по желанию. Если вам нужно задать новый атрибудт объекта, просто заполните структуру, описывающую атрибут, и передаёте указатель на неё следующему вызову:

    int sysfs_create_file(struct kobject *kobj, struct attribute *attr);

Если всё прошло гладко, будет создан файл см именем, заданным соответствующим полем структуры struct attribute и функция возвратит 0; в противном случае код возврата будет меньше нуля.

Как это ни удивительно, но удаляется атрибут таким вызовом:

    int sysfs_remove_file(struct kobject *kobj, struct attribute *attr);

Кроме того, атрибуты могут связываться симлинками, они могут быть бинарными. Но эти детали мы опустим, потому что и так уже увлеклись. Суть должна быть уже ясна. Что дальше? Чтобы понять, что дальше, попробуем ответить на вопрос, а как, собственно, пользовательское окружение узнаёт о том, что к сиетеме подключено то или иное устройство? Когда-то, когда деревья были большими, а компьютеры - большими и глупыми в Linux были статичные файлы устройств. О недостатках этого способа работы с железом говорить не будем. Статический /dev сменился динамической файловой системой devfs и демоном devfsd, который заведовал всем этим хозяйством - такой подход был принят в FreeBSD. Ещё позже на смену devfsd пришёл udev (о нём я тоже писал, точнее, переводил :)) Не будем углубляться в то, как был устроен hotplug раньше и вообще, опустим все эти исторические детали. Сейчас udev - это хитрый демон, работающий в режиме пользователя, и слушающий netlink-сокет. Когда в ядре регистрируется или удаляется объект, ядро посылает uevent сообщение в сокет (вспомнили kobject_uevent()?). udev проделывает всю нужную работу по созданию спецфайла в /dev, подгрузке модуля, если необходимо, и созданию ополнительных симлинков. Таким образом, объекты - основа работы хотплага. Но не только его. На этапе, когда ядро уже загрузилось, но udev ещё не запустился, надо сделать так, чтобы и подключенные ранее устройства были учтены, однако посылать сообщения в netlink-сокет нет смысла. Его никто не слушает. Но вот смонтировать и заполнить sysfs мы вполне можем безо всякого udev. Когда udev будет запущен, он сможет пройтись по иерархии sysfs и создать файлы устройств. Вот зачем нам нужна sysfs и атрибуты. В частности. В общем, это нечто более глубокое, чем просто механизм поддержки хотплага. Реализация объектной модели драйверов в Linux делает возможным лёгкую реализацию хотплага, но суть именно в отношениях различных классов устройств, шин, портов и всего такого. Однако, это уже другая и определённо долгая история. Нашей целью было лишь слегка потыкать палочкой в объекты, а не заниматься всеобъемлющим описанием их анатомии, физиологии и экологии. Возможно, как-нибудь уделим внимание и этому.

Tuesday, August 12, 2014

crossdev на funtoo

Ждём ебилдов


Я уже очень давно не писал о вещах насущных и приземлённых и наверно не стал бы этого делать впредь, если бы не одно происшествие. Небольшая предыстория. Просто чтобы как-то занять место. Когда-то я пользовался ubuntu, но потом перестал и перешёл на gentoo. Сей дистрибутив пришёлся мне зело по нраву. Нет, дело отнюдь не в мифических процентах производительности. Просто gentoo позволяет вертеть пакетами по своему желанию и усмотрению в довольно широких пределах. Чуть позже передо мной встал вопрос, на что заменить уже на другой системе старый, заваленный всяким хламом Debian, который было проще убить, чем пытаться дальше шаманить с обновлениями (и не зря, как убедительно показали страсти по systemd какое-то время спустя - выхаживать Debian ради того, чтобы однажды к тебе приехало вышеупомянутое чудо поттерингомысли...) Кое-кто из знакомых пользовался форком gentoo - funtoo, ну и не то мне посоветовали попробовать, не то самому стало интересно, в общем, я решился на эксперимент. Тем более, что это практически та же gentoo, только в профиль и с git'ом вместо rsync'а для обновлений, никаких проблем возникнуть не должно было. Так в целом и получилось. Дела шли неплохо, пока однажды я подумал, а не поставить ли мне mingw-w64 там, как и на gentoo? Всё удобнее, чем всякую мелочь при необходимости собирать на соседнем хосте. Вытянул я требуемый для сего crossdev, повелеваю системе

crossdev -S -t i686-w64-mingw32
И продолжаю заниматься своими делами, благо не близкий свет - пока всё соберётся. Что поделаешь, плата за гибкость.

И тут началось... Дело в том, что crossdev берёт на себя весь труд утащить из интернетов всё, что нужно и расквартировать это у вас на диске. Всё, что нужно, включает binutils, mingw64-runtime конкретно для таргета i686-w64-mingw32, собственно gcc. binutils собирается без проблем, gcc тоже... Кажется. mingw64-runtime стоит колом, хоть ты тресни. Давайте разбираться. По умолчанию crossdev приволочёт вам последний стабильный ебилд, в моём случае, это был gcc-4.8.2, в то время, как хост пользуется gcc-4.7.3, 4.8 для хоста я замаскировал. Честно признаться, не вдаваясь особо в детали, я сначала подумал было, что косяк с версией. На gentoo последний ебилд из серии >4.8.2 категорически не собирается под i686-w64-mingw32, мотивируя этом неразрешёнными символами. Ну что ж, я вполне счастлив и с 4.8.2. Там я просто замаскировал до поры до времени то, что не собирается, просто чтобы не тратить время на пустые попытки и не стопорить прочие обновления. Здесь решил поступить так же, с той лишь разницей, что под маской должны были скрыться все компиляторы >4.8, т.о., ко мне должен был приехать компилятор той же версии, что у хоста - 4.7.3-r2. В общем, командую:

crossdev -S -t i686-w64-mingw32 --g 4.7.3-r2
и дальше по своим делам.

Итог - file collision. То, что должно быть установлено этим ебилдом, каким-то образом уже есть в системе. Забавная петрушка. Но и правда, emerge пытается скопировать файлы с именем i686-pc-linux-*-4.7.3, при том, что такие файлы уже есть в /usr и совершенно внезапно принадлежат они моему законному хост-компилятору. Коллизия вполне очевидная. И что ещё смешнее, будь это уже упомянутый gcc-4.8.2, у вас и окажется i686-pc-linux-*-4.8.2! А вот i686-w64-mingw32-*-4.8.2 вы ни в жисть не увидите. И в этом вы будете не одиноки. Скрипт конфигурирования mingw64-runtime тоже его не увидит, потому что его нет.

Улавливаете логику? Ваш crossdev становится лазейкой для контрабанды замаскированных версий gcc! Вместо того, чтобы собрать и поставить в систему компилятор под таргет i686-w64-mingw32, он соберёт вам ещё один компилятор для того же таргета, что и ваш хост (CHOST = CTARGET, unconditionally) и поставит его рядом, не забыв сделать компилятором по умолчанию. Вот это проливает свет на причину. А вот здесь немножко обсуждения, самхау рилейтед. Похоже, местный Патрегбох нещадно поломал ебилды компиляторов. Вот, собственно, и всё недолга. Все мы люди - хотелось как лучше, а получилось не очень. Фикс есть, хотя достаточно костыльный, но действенный. Можно утащить ебилд gcc из gentoo, иных путей, кроме тотального переписывания ебилда или отката коммитов я лично не увидел. Но первое слишком муторно, а второе уже не в нашей власти, если говорить о глобальных изменениях. Хотя crossdev собирает инструментарий для кросскомпиляции в оверлее, есть одна хитрость, ебилды он берёт системные, что почему-то особого удивления и не вызывает, так что можно утащить ебилд откуда-нибудь отсюда или из /usr/portage/sys-devel/gcc на работающей gentoo-системе и кинуть его в аналогичное место на funtoo. Есть подводный камень, если ебилд, устанавливаемый crossdev'ом имеет ту же версию, что ваш хост-компилятор. С вероятностью, равной единице, ваш хост-компилятор очень захочет пересобраться при следующей проверке обновлений системы (emerge -uDNpv world) из-за разницы в флагах. Поэтому стратегически выгоднее ставить версию компилятора где-то рядом с вашей, а не такую же. Например, если у вас хост-компилятор gcc-4.7.3-r2, то попросите crossdev поставить gcc-4.7.3-r1. Топорный путь, но что поделаешь. Зато быстрый и работает. В общем, ждём-с.

Monday, June 9, 2014

Пользовательские привилегии в Linux

В свете уже поднятой и рассмотренной здесь темы встаёт закономерный вопрос: всё это конечно хорошо и замечательно, но как можно наделить конкретными привилегиями не отдельный процесс, а пользователя, чтобы можно было создавать своих root'ов и при этом ограничить множество доступных пользователю привилегий? Ответ на этот вопрос - Pluggable Authentication Modules, также известные, как подключаемые модули аутентификации или попросту PAM, а именно, модуль pam_cap.so. Вместо того, чтобы ассоциировать разрешения с конкретными файлами, мы могли бы наделить конкретного пользователя нужной привилегией. При успешной авторизации шелл пользователя получит необходимое разрешение в множество наследуемых разрешений, которое будет унаследовано всеми его дочерними процессами, а это значит всеми процессами данного пользователя.

Сперва добавим модуль авторизации:

# cat /etc/pam.d/system-login
auth     required     pam_cap.so

Теперь определим, что и кому мы разрешаем:

# cat /etc/security/capability.conf
cap_kill,cap_setpcap luser
none *

Пользователь luser получает право на привилегии cap_kill - обход проверок безопасности при посылке сигналов другим процессам, не принадлежащим данному пользователю, и разрешение на манипуляции привилегиями. По умолчанию модуль pam_cap.so обычно не подключается, но если бы подключался, то обычный "бесправный" пользователь описывался бы строкой "none *".

Итак, luser получает свои разрешения cap_kill и cap_setpcap, а все остальные не будут иметь никаких разрешённых привилегий. Кстати, особенность состоит в том, что присваиваемые таким образом привилегии не являются активными. Они лишь наследуются и могут быть разрешены - далее мы увидим, как, а пользовательский процесс, требующий, допустим, привилегию cap_net_raw, сможет её активировать. Изначально же она будет присутствовать лишь во множестве наследуемых (inheritable) привилегий. Чем-то это напоминает Windows, где пользователи из группы администраторов могут иметь в токене безопасности привилегию отладки процесса (т.е., практически полного доступа к нему, включая операции над содержимым его памяти), что ещё не означает автоматически возможность каждого администраторского процесса воспользоваться ею, ведь её ещё надо активировать (что обычно происходит после принятия консента на системах с UAC - т.е. всей линейки Vista+). Однако здесь у нас всё чуть-чуть сложнее.

Проверим, что у нас получилось:

$ grep ^Cap /proc/$$/status
CapInh: 0000000000000120
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000001fffffffff

$ capsh --decode=0000000000000120
0x0000000000000120=cap_kill,cap_setpcap

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

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <unistd.h>

int cap_enable (cap_value_t cap)
{
        cap_t _caps;

        _caps = cap_get_proc ();

        if (_caps == NULL)
                return -1;

        if (cap_set_flag (_caps, CAP_EFFECTIVE, 1, &cap, CAP_SET) != 0)
                return -1;

        if (cap_set_proc (_caps) != 0)
                return -1;

        return 0;
}

void dump_caps (const char* img_name, const char* m)
{
        if (m)
                printf (m);

        printf (" file caps %s\n", cap_to_text (cap_get_file (img_name), NULL));
        printf (" process caps %s\n", cap_to_text (cap_get_proc (), NULL));

        return;
}

int main(int argc, char ** argv)
{
        dump_caps (argv [0], "before enabling...\n");
        sleep (30);
        cap_enable (CAP_KILL);
        dump_caps (argv [0], "after enabling...\n");
        sleep (60);
}
Его сборка и выполнение должны дать такие результаты:
$ ./cap_test       
before enabling...
 file caps (null)
 process caps = cap_kill,cap_setpcap+i
after enabling...
 file caps (null)
 process caps = cap_kill,cap_setpcap+i
То есть, привилегии недоступны. Чтобы понять, в чём дело, вспомним уже упоминавшиеся формулы, по которым вычисляются разрешения:
P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset)
P'(effective) = F(effective) ? P'(permitted) : 0
P'(inheritable) = P(inheritable)
где F - разрешения, ассоциированные с файлом, а P - с процессом. Штрихом обозначены разрешения после exec(). Иными словами, для того, чтобы наша программа получила привилегию в свой набор доступных разрешений, необходимо либо чтобы для файла было установлено наследование сообветствующей привилегии, либо, чтобы она изначально входила в множество доступных привилегий файла. Т.е., либо cap_kill+i, либо cap_kill+p. Остановимся на первом варианте:
# setcap cap_kill+i ./cap_test
$ ./cap_test
before enabling...
 file caps = cap_kill+i
 process caps = cap_kill+ip cap_setpcap+i
after enabling...
 file caps = cap_kill+i
 process caps = cap_kill+eip cap_setpcap+i
 
[ в другом терминале между паузами ]

$ grep ^Cap /proc/`pgrep cap_test`/status
CapInh: 0000000000000120
CapPrm: 0000000000000020
CapEff: 0000000000000000
CapBnd: 0000001fffffffff
$ grep ^Cap /proc/`pgrep cap_test`/status
CapInh: 0000000000000120
CapPrm: 0000000000000020
CapEff: 0000000000000020
CapBnd: 0000001fffffffff
На первый взгляд ненамного удобнее. Более того, мы всё ещё зависим от разрешений, ассоциированных с файлами. Однако именно в этом и состоит гибкость. Во-первых, при распределении привилегий среди пользователей, мы наделяем их лишь некоторыми полномочиями. И именно пользователей. Кто-то сможет посылать сигналы чужим процессам, а кто-то нет. Во-вторых, с помощью дисковых разрешений, мы определяем, что пользователям можно запускать с данными привилегиями, а что нет, потому что позволять пользователю запускать вообще всё, что попало при данном повышенном уровне привилегий - это тоже не лучшее решение. Так что наделив пользователя небольшим кусочком власти, мы всё ещё контролируем, как он сможет этой властью воспользоваться. Однако приведённый пример имеет одну небольшую особенность. Программа в курсе относительно привилегий и сама изменяет свои разрешения на основе заданных значений. Для того, чтобы применять привилегии к любым программам, в том числе таким, которые могут ничего не знать о привилегиях, необходимо снова использовать форсированные разрешения, которые ассоциированы с файлами на диске. Например, так setcap cap_kill+ei ./cap_test (при логине пользователь получает унаследованное разрешение cap_kill, для файла cap_test cap_kill тоже помечено, как наследуемое разрешение - в соответствие с правилом, приведённом выше, при запуске cap_test разрешение cap_kill будет занесено в его множество доступных привилегий - P'(permitted) = P(inheritable) & F(inheritable); если для данного файла данное разрешение входит в множество эффективных разрешений, то оно заносится и в множество эффективных, или действительных, разрешений выполняемого процесса: P'(effective) = F(effective) ? P'(permitted) : 0). Допустим, что наш cap_test имеет такие разрешения:
$ getcap ./cap_test
./cap_test = cap_kill,cap_net_admin+ei
Однако, процесс не получит привилегию cap_net_admin в какое-либо из своих множеств, если эта привилегия не была предоставлена пользователю через PAM:
$ ./cap_test
before enabling...
 file caps = cap_kill,cap_net_admin+ei
 process caps = cap_kill+eip cap_setpcap+i
after enabling...
 file caps = cap_kill,cap_net_admin+ei
 process caps = cap_kill+eip cap_setpcap+i
Таким образом, это скорее механизм ограничения и разграничения привилегий, чем наделения ими :) На первый взгляд всё несколько запутанно, но на деле, привилегии не так страшны. Напоследок, если вдруг по какой-то причине вы не можете воспользоваться библиотекой libcap, активировать привилегию можно вот так, напрямую используя сисколлы (не совсем напрямую, ибо libc, но уже без посредничества libcap):
#include <sys/capability.h>

int main (int argc, char **argv)
{
        struct __user_cap_header_struct hdr;
        struct __user_cap_data_struct data;

        memset (&hdr, 0, sizeof(hdr));
        hdr.version = _LINUX_CAPABILITY_VERSION;

        if (capget (&hdr, &data) < 0)
                return 1;

        data.effective |= CAP_TO_MASK(CAP_KILL);
        data.effective |= CAP_TO_MASK(CAP_SETPCAP);

        if (capset (&hdr, &data) < 0)
                return 2;

        return 0;
}
Но разумеется, лучше пользоваться libcap, как более портабельным API.

Sunday, June 8, 2014

Разрешения процессов в Linux

Терминологический казус



То, о чём сейчас пойдёт речь, по-английски называется capabilities. Дословно "возможности", "способности". Семантически, это то, что называется привилегиями. Как ни странно, хоть тема и не нова, устоявшегося русского перевода этого термина я не видел. Точнее даже так, я никакого русского перевода не видел. И этот момент несколько смущает. Но ничего не поделаешь, придётся выкручиваться своими силами. Мне кажется, что наиболее адекватный перевод в данном случае - "разрешения", хотя по сути речь как раз о возможностях. Но раз обсуждаемый предмет - это то, что процессу разрешено делать, а не какие-то сугубо процесс-специфические возможности, им самим и создаваемые, я счёл более корректной приведённую версию перевода: разрешения процессов или привилегии. В дальнейшем я буду применять эти два понятия, как синонимы. Ещё один казуистический момент, так как с точки зрения ядра поток и процесс описываются единым дескриптором, дескриптором задачи (thread - это аппаратный контекст, являющийся частью дескриптора задачи), я не делаю разницы между потоком и процессом, так как здесь она не имеет особого значения. Важно лишь уточнить, что разрешения являются такой же частью дескриптора задачи, как и аппаратный контекст выполнения. И раз уж речь зашла о ядре (впрочем она и дальше пойдёт во многом именно о ядре), то сразу определимся, что речь идёт о версии 3.12. Система разделения власти суперпользователя на множество привилегий появилась в доисторической с точки зрения сегодняшнего дня ветке ядра 2.2. За эти годы многое изменилось самым кардинальным образом. Поэтому даже между ветками 2.6 и 3 будут расхождения в коде. Но общие принципы остаются в целом теми же.

Постановка задачи



В общем и в целом, всё просто. Кто пользуется таким замечательным инструментом, как wireshark, наверняка сталкивался уже с разрешениями процессов. Суть идеи такова: есть масса различных операций, результаты которых имеют критическое значение для всей системы, или же могут сказаться на отдельных пользователях этой самой системы, если оных много. Дабы свести к минимуму риск от потенциально деструктивных операций, система не даёт их выполнять обычным пользователям. А для административных задач есть всемогущий root и подразумевается, что человек, знающий пароль root'а на данной системе знает, что делает. Всё просто и вроде бы даже эффективно. Противопоставление "root vs обычный пользователь" простое, понятное, как чёрное и белое, и прозрачное. В идеале, пользователи имеют лишь минимально необходимый набор прав, они не могут навредить друг другу, так как доступ к данным защищён правами владения и доступа, не могут нанести вред системе и всё ещё могут добровольно делиться друг с другом доступом к данным. Идиллия. Но не всё так просто. Нынче Unix-подобные ОС уже совсем не обязательно работают лишь на системах, обслуживающих уйму пользователей. И тем не менее, вхождение в массы не изменило одного обстоятельства. Сидеть под root'ом всё так же моветон и категорически не рекомендуется без настоятельной нужды. А что делать, если пользователь на своей системе хочет изменить время? И пусть даже он единственный живой пользователь системы и никому навредить не сможет, кроме самого себя, операция изменения системного времени всё ещё требует привилегий. Что же делать? Тем более, если наш пользователь, скажем, путешествует туда-сюда по всему земному шару и меняет время чуть ли не каждый день. Прикажете ему сидеть под root'ом? Есть sudo, а вместе с этим и проблема. Когда наш пользователь сделает sudo date, процесс сможет не только изменять системное время, но делать всё то, что может сделать root. А что если злобные хацкеры подменили образ date на диске, пока наш пользователь плевал вниз с Эйфелевой башни, например? Вот тут-то и появилось осознание, что чего-то не хватает. Этот пробел призваны восполнить разрешения процессов в Linux. Если кратко, то существует целый класс операций, требующих привилегий. Однако специализированной программе, устанавливающей системное время, требуется право лишь на одну такую операцию. Чтобы избежать вероятности злоупотребления привилегиями, которые получает процесс с EUID=0, мы наделим нашу программу разрешением ровно на одну операцию. Всё остальное будет запрещено и наши злобные горе-хакеры останутся ни с чем. И хотя не стоит воспринимать приведённый выше пример всерьёз, проблема гранулярности традиционной модели безопасности Unix назрела давно. Более удачный и жизненный пример всё тот же wireshark - программа с графическим интерфейсом пользователя, которая позволяет следить за сетевым трафиком. Для того, чтобы успешно справляться со своей задачей, wireshark должен иметь право на создание "сырых" сокетов (raw sockets). При этом, GUI такие полномочия совершенно ни к чему и напротив, чем меньше кода выполняется с EUID=0, тем лучше. Ещё один пример из этой же области - ping. Раньше это решалось либо установкой suid-бита либо с помощью sudo. Решения, прямо скажем, далёкие от идеала и гибкости. Проблемы, связанные с подходами вроде sudo или suid-бита, были осознаны многими и в каждом потомке Unix они решались по-своему. В Solaris, к примеру, есть RBAC - разделение привилегий на основе ролей. Хотя это не то же самое, что разрешения процессов, но с RBAC тесно связана идея привилегий. В Linux же у нас есть process capabilities. И появились они, кстати, уже довольно давно, но по разным причинам широкого применения не нашли. Кажется, ситуация начинает медленно меняться, так что разберёмся с этим механизмом получше.

Кровавые детали

Контроль привилегий осуществляет ядро. В ядре Linux привилегии являются ничем иным, как битовыми картами. Чтобы убедиться в этом, проследите за корнями соответствующих структур в ядре Linux (интересующие нас поля выделены жирным курсивом, дескриптор задачи - task_struct, здесь сильно сокращён по понятным причинам):

Начнём с include/linux/sched.h:

struct task_struct {
        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
        void *stack;
        atomic_t usage;
        unsigned int flags;     /* per process flags, defined below */
        unsigned int ptrace;

 ...

/* process credentials */
        const struct cred __rcu *real_cred; /* objective and real subjective task
                                         * credentials (COW) */
        const struct cred __rcu *cred;  /* effective (overridable) subjective task
                                         * credentials (COW) */
        char comm[TASK_COMM_LEN]; /* executable name excluding path
                                     - access with [gs]et_task_comm (which lock
                                       it with task_lock())
                                     - initialized normally by setup_new_exec */
 ...
};

Далее include/linux/cred.h:

struct cred {
        atomic_t        usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
        atomic_t        subscribers;    /* number of processes subscribed */
        void            *put_addr;
        unsigned        magic;
#define CRED_MAGIC      0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
        kuid_t          uid;            /* real UID of the task */
        kgid_t          gid;            /* real GID of the task */
        kuid_t          suid;           /* saved UID of the task */
        kgid_t          sgid;           /* saved GID of the task */
        kuid_t          euid;           /* effective UID of the task */
        kgid_t          egid;           /* effective GID of the task */
        kuid_t          fsuid;          /* UID for VFS ops */
        kgid_t          fsgid;          /* GID for VFS ops */
        unsigned        securebits;     /* SUID-less security management */
        kernel_cap_t    cap_inheritable; /* caps our children can inherit */
        kernel_cap_t    cap_permitted;  /* caps we're permitted */
        kernel_cap_t    cap_effective;  /* caps we can actually use */
        kernel_cap_t    cap_bset;       /* capability bounding set */
#ifdef CONFIG_KEYS
        unsigned char   jit_keyring;    /* default keyring to attach requested
                                         * keys to */
        struct key __rcu *session_keyring; /* keyring inherited over fork */
        struct key      *process_keyring; /* keyring private to this process */
        struct key      *thread_keyring; /* keyring private to this thread */
        struct key      *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
        void            *security;      /* subjective LSM security */
#endif
        struct user_struct *user;       /* real user ID subscription */
        struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
        struct group_info *group_info;  /* supplementary groups for euid/fsgid */
        struct rcu_head rcu;            /* RCU deletion hook */
};

И наконец include/linux/capability.h:

 typedef struct kernel_cap_struct {
         __u32 cap[_KERNEL_CAPABILITY_U32S];
 } kernel_cap_t;

Т.е., в текущей версии реализации одно множество разрешений представлено двумя 32-битными словами или 64 битами (строка "#define _LINUX_CAPABILITY_U32S_3 2" в файле include/uapi/linux/capability.h). Всего таких множеств 3 (см. выше определение структуры struct cred): разрешённые (permitted), эффективные (effective), наследуемые (inheritable). В общем-то названия этих множеств и краткие комментарии в коде говорят сами за себя. Множество разрешённых привилегий - это те разрешения, которые наш процесс может активировать в принципе, но которые не обязательно действуют в данный момент времени. Если процесс исключает привилегию из множества доступных (privilege drop), то он больше не может ею воспользоваться, в том числе, он не сможет вернуть себе право на данную привилегию (reacquire privilege) Множество эффективных привилегий - это те элементы множества разрешённых привилегий, которые были активированы (запрошены) процессом и действуют в данный момент. Именно это поле используется ядром для проверки права на данную операцию. И, наконец, множество наследуемых привилегий - это те разрешения, с которыми может работать дочерний процесс, порождённый нашим процессом, т.е., множество привилегий, которые присваиваются процессу, порождённому с помощью exec*() (все они в конечном счёте сходятся на execve()). Процесс, созданный в результате fork(), получает полные копии множеств привилегий родительского процесса. Эти три множества привилегий свойственны и выполняющимся процессам, и файлам (о файлах и привилегиях чуть ниже). Ещё одно множество - ограничивающее (bounding set). Если с 3 другими множествами всё ясно, то на этом стоит остановиться чуть подробнее. Во-первых, это множество в отличие от уже рассмотренных свойственно только процессам, но не файлам. Во-вторых, это ещё один ограничительный механизм, но в отличие от множества доступных процессу (permitted) привилегий, устанавливаемого при запуске исполняемого образа или при посредничестве другого процесса, это множество устанавливается для процессов-потомков, порождённых вызовом execve(). Происходит это следующим образом: во время выполнения exec*() к привилегиям из ограничивающего множества (bounding set) процесса, вызывающего exec*(), и множества доступных привилегий (permitted), ассоциированного с файлом исполняемого образа на диске, применяется логическое И, а результат заносится в множество разрешённых привилегий дочернего процесса. Таким образом, потомку будут доступны лишь те операции, которые есть и в ограничивающем множестве, и во множестве разрешённых привилегий, прочитанном с диска. Начиная с версии Linux 2.6.25 процесс не может добавить привилегию в множество наследуемых привилегий (inheritable), если она не входит в ограничивающее множество, даже не смотря на то, что данная привилегия доступна вызывающему потоку (т.е., она может быть в его permitted-множестве). Ограничивающее множество является маской разрешённых привилегий, определённых для файла, но не для множества уже унаследованных привилегий. Таким образом, это механизм контроля наследования разрешений, а не их использования уже существующим процессом. Следует отметить важный технический момент: операция ограничения привилегий в соответствии с маской ограничивающего множества не выполняется непосредственно над атрибутами файла, а над разрешениями свежесозданного процесса. Так что хоть ограничивающее множество действует в первую очередь на те разрешения, которые ассоциированы с файлом, вычисления происходят на множествах разрешений процесса, не файла. Создаваемый процесс получит в точности те разрешения, которые свойственны файлу исполняемого образа, но позже они будут откорректированы в соответствии с маской родительского процесса. Звучит запутанно, но надеюсь, всё станет понятнее далее, когда мы дойдём до файловых атрибутов. В зависимости от версии ядра ограничивающее множество - это либо системный атрибут, либо свойство конкретного процесса (http://lwn.net/Articles/251666/). До версии 2.6.25 маска задаётся через файл /proc/sys/kernel/cap-bound в виде целого со знаком в десятичной системе. Эту маску может установить только init. Процессы c EUID=0, имеющие разрешение CAP_SYS_MODULE, могут удалять отдельные привилегии из этой маски. Обычно на таких системах маска не включает разрешение CAP_SETPCAP. Чтобы снять это ограничение, достаточно изменить определение CAP_INIT_EFF_SET в файле include/linux/capability.h и пересобрать ядро. Начиная с версии 2.6.25 ограничивающее множество больше не является глобальным общесистемным атрибутом. Оно наследуется при вызове fork() и exec*(). Поток может удалять привилегии из своего ограничивающего множества. Детали см. в capabilities(2)

Итак, отдельно взятая привилегия - это ни много, ни мало бит в битовой карте, представляющей множество разрешённых, эффективных или наследуемых привилегий. Приведём наконец некоторые примеры конкретных привилегий:

CAP_SETPCAP Манипуляции привилегиями и, соответственно, разрешение использовать вызов capset() в ядрах, где поддержка файловых разрешений не включена.
Если же файловые разрешения поддерживаются, эта привилегия позволяет добавлять разрешения в ограничивающее множество потока, а также удалять их оттуда (prctl(2)); манипуляции с флагами безопасности (http://lwn.net/Articles/368600/)
CAP_NET_ADMIN Различные операции, относящиеся к сетевому администрированию, как то: конфигурирование сетевых интерфейсов, администрирование файрволла, изменение таблицы маршрутизации, включение "неразборчивого" режима на интерфейсе и тому подобное
CAP_CHOWN Манипуляции с идентификаторами пользователя/группы объекта файловой системы (chown(2))
CAP_DAC_OVERRIDE Обход проверок безопасности при операциях ввода-вывода
CAP_KILL Обход проверок безопасности при посылке сигналов (kill(2))
CAP_SETFCAP (начиная с Linux 2.6.24) Манипуляции с файловыми разрешениями
CAP_SYSLOG (начиная с Linux 2.6.37) Привилегированные операции с системным логом; отображение адресов ядра в /proc, если атрибут /proc/sys/kernel/kptr_restrict установлен в 1
CAP_SYS_TIME Изменение системного времени
CAP_SYS_BOOT Использование вызовов reboot() и kexec_load() - перезагрузка системы и "горячая" загрузка ядра, соответственно (reboot(2) kexec_load(2))
CAP_SYS_ADMIN Широкий спектр административных операций, как то: управление дисковыми квотами, активация и деактивация устройств своппинга и много чего ещё
CAP_SETGID
CAP_SETUID
Манипуляции с идентификаторами пользователя/группы процесса - вызовы setuid(2), setreuid(2), setresuid(2), setfsuid(2)

Данный список далеко не полный, т.к. я не ставил целью переписывание соответствующей man-страницы, и лишь демонстрирует некоторые более или менее типичные привилегии. Все доступные для данного ядра привилегии можно посмотреть в заголовочном файле include/uapi/linux/capability.h или в man-странице capabilities(2).

О флагах безопасности (securebits). Это флаги, управляющие наследованием и использованием привилегий. Этими флагами манипулируют сами потоки. Для этого, впрочем, им нужно иметь привилегию CAP_SETPCAP, как уже было упомянуто.

SECBIT_KEEP_CAPS Поток, имеющий UID 0, сохраняет все разрешения при смене идентификатора пользователя (не root)

По умолчанию этот флаг очищен и даже если он установлен потоком, он принудительно очищается при вызове exec*() с целью предотвращения утечки пивилегий
SECBIT_NO_SETUID_FIXUP Множества разрешеий не будут корректироваться ядром при смене UID между 0 и любым другим значением
SECBIT_NOROOT Программа с suid-битом не получает автоматически все привилегии, которые имеет root

Управление флагами возможно с помощью prctl(2) и "операторов" PR_SET_SECUREBITS / PR_GET_SECUREBITS.

Так в общих чертах выглядит объект нашего интереса. Но встаёт вопрос, а кто и как назначает привилегии конкретным процессам? Когда поддержки файловых разрешений ещё не было, единственным способом использования привилегий был некий вспомогательный процесс, который мог бы раздавать их другим процессам. Этакий сервер безопасности. Либо сама программа, запущенная изначально с правами суперпользователя, могла добровольно отказаться от ненужных ей привилегий. Оба способа явно довольно вычурны. Вероятно это в немалой степени послужило фактором, сдерживающим внедрение использования привилегий.

Файловые разрешения

Вот мы и подошли, пожалуй, к самому интересному. Действительно, без файловых разрешений реализация привилегий в Linux так и осталась бы ущербной. Говоря выше о множествах привилегий, мы узнали, что множества доступных (permitted), наследуемых (inheritable) и эффективных (effective) привилегий свойственны как файлам, так и процессам. Что это значит? Ровно то, что значит. Привилегии могут быть ассоциированы не только с выполняющимся процессом, но и с файлом, в котором хранится исполняемый образ. Какой-то особой поддержки привилегий со стороны файловой системы не требуется. Всё делом в том, что разрешения хранятся в расширенных атрибутах файлов, а это значит, что любая файловая система, которая поддерживает расширенные атрибуты (а это, в общем-то, все современные файловые системы, которые широко используются в Linux - ext, reiserfs, xfs, например), автоматически поддерживает и файловые разрешения. Linux разделяет расширенные атрибуты файлов по пространствам имён. В частности, например, определены такие пространства, как "user" - для пользовательских атрибутов, "security" - для атрибутов безопасности. Нас интересует именно последнее. Разрешения хранятся в атрибуте capability. Имя атрибута определено в заголовке include/uapi/linux/xattr.h таким образом:

#define XATTR_SECURITY_PREFIX   "security."
...
#define XATTR_CAPS_SUFFIX "capability"
#define XATTR_NAME_CAPS XATTR_SECURITY_PREFIX XATTR_CAPS_SUFFIX

Проведём небольшой эксперимент:

$ attr -l /usr/bin/dumpcap
Attribute "capability" has a 20 byte value for /usr/bin/dumpcap

# getcap /usr/bin/dumpcap
/usr/bin/dumpcap = cap_dac_read_search,cap_net_admin,cap_net_raw+ep

Что и требовалось продемонстрировать. dumpcap является частью пакета wireshark. Именно этот исполняемый файл "хватает" пакеты с сетевого интерфейса. Для того, чтобы мы могли использовать wireshark из-под обычного пользовательского аккаунта, этому файлу присвоены соответствующие разрешения: операции сетевого администрирования, право на создание символьных ("сырых", raw) сокетов во множествах доступных и эффективных привилегий. Каким образом всё это облечено в код ядра? Вся чёрная магия скрыта в файле security/commoncap.c

/*
 * Extract the on-exec-apply capability sets for an executable file.
 */
int get_vfs_caps_from_disk(const struct dentry *dentry, struct cpu_vfs_cap_data *cpu_caps)
{
        struct inode *inode = dentry->d_inode;
        __u32 magic_etc;
        unsigned tocopy, i;
        int size;
        struct vfs_cap_data caps;

        memset(cpu_caps, 0, sizeof(struct cpu_vfs_cap_data));

        if (!inode || !inode->i_op->getxattr)
                return -ENODATA;

        size = inode->i_op->getxattr((struct dentry *)dentry, XATTR_NAME_CAPS, &caps,
                                   XATTR_CAPS_SZ);
        if (size == -ENODATA || size == -EOPNOTSUPP)
                /* no data, that's ok */
                return -ENODATA;
        if (size < 0)
                return size;

        if (size < sizeof(magic_etc))
                return -EINVAL;

        cpu_caps->magic_etc = magic_etc = le32_to_cpu(caps.magic_etc);

        switch (magic_etc & VFS_CAP_REVISION_MASK) {
        case VFS_CAP_REVISION_1:
                if (size != XATTR_CAPS_SZ_1)
                        return -EINVAL;
                tocopy = VFS_CAP_U32_1;
                break;
        case VFS_CAP_REVISION_2:
                if (size != XATTR_CAPS_SZ_2)
                        return -EINVAL;
                tocopy = VFS_CAP_U32_2;
                break;
        default:
                return -EINVAL;
        }

        CAP_FOR_EACH_U32(i) {
                if (i >= tocopy)
                        break;
                cpu_caps->permitted.cap[i] = le32_to_cpu(caps.data[i].permitted);
                cpu_caps->inheritable.cap[i] = le32_to_cpu(caps.data[i].inheritable);
        }

        return 0;
}

/*
 * Attempt to get the on-exec apply capability sets for an executable file from
 * its xattrs and, if present, apply them to the proposed credentials being
 * constructed by execve().
 */
static int get_file_caps(struct linux_binprm *bprm, bool *effective, bool *has_cap)
{
        struct dentry *dentry;
        int rc = 0;
        struct cpu_vfs_cap_data vcaps;

        bprm_clear_caps(bprm);

        if (!file_caps_enabled)
                return 0;

        if (bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID)
                return 0;

        dentry = dget(bprm->file->f_dentry);

        rc = get_vfs_caps_from_disk(dentry, &vcaps);
        if (rc < 0) {
                if (rc == -EINVAL)
                        printk(KERN_NOTICE "%s: get_vfs_caps_from_disk returned %d for %s\n",
                                __func__, rc, bprm->filename);
                else if (rc == -ENODATA)
                        rc = 0;
                goto out;
        }

        rc = bprm_caps_from_vfs_caps(&vcaps, bprm, effective, has_cap);
        if (rc == -EINVAL)
                printk(KERN_NOTICE "%s: cap_from_disk returned %d for %s\n",
                       __func__, rc, bprm->filename);

out:
        dput(dentry);
        if (rc)
                bprm_clear_caps(bprm);

        return rc;
}

/**
 * cap_bprm_set_creds - Set up the proposed credentials for execve().
 * @bprm: The execution parameters, including the proposed creds
 *
 * Set up the proposed credentials for a new execution context being
 * constructed by execve().  The proposed creds in @bprm->cred is altered,
 * which won't take effect immediately.  Returns 0 if successful, -ve on error.
 */
int cap_bprm_set_creds(struct linux_binprm *bprm)
{
        const struct cred *old = current_cred();
        struct cred *new = bprm->cred;
        bool effective, has_cap = false;
        int ret;
        kuid_t root_uid;

        effective = false;
        ret = get_file_caps(bprm, &effective, &has_cap);
        if (ret < 0)
                return ret;

        root_uid = make_kuid(new->user_ns, 0);

        if (!issecure(SECURE_NOROOT)) {
                /*
                 * If the legacy file capability is set, then don't set privs
                 * for a setuid root binary run by a non-root user.  Do set it
                 * for a root user just to cause least surprise to an admin.
                 */
                if (has_cap && !uid_eq(new->uid, root_uid) && uid_eq(new->euid, root_uid)) {
                        warn_setuid_and_fcaps_mixed(bprm->filename);
                        goto skip;
                }
                /*
                 * To support inheritance of root-permissions and suid-root
                 * executables under compatibility mode, we override the
                 * capability sets for the file.
                 *
                 * If only the real uid is 0, we do not set the effective bit.
                 */
                if (uid_eq(new->euid, root_uid) || uid_eq(new->uid, root_uid)) {
                        /* pP' = (cap_bset & ~0) | (pI & ~0) */
                        new->cap_permitted = cap_combine(old->cap_bset,
                                                         old->cap_inheritable);
                }
                if (uid_eq(new->euid, root_uid))
                        effective = true;
        }
skip:

        /* if we have fs caps, clear dangerous personality flags */
        if (!cap_issubset(new->cap_permitted, old->cap_permitted))
                bprm->per_clear |= PER_CLEAR_ON_SETID;


        /* Don't let someone trace a set[ug]id/setpcap binary with the revised
         * credentials unless they have the appropriate permit.
         *
         * In addition, if NO_NEW_PRIVS, then ensure we get no new privs.
         */
        if ((!uid_eq(new->euid, old->uid) ||
             !gid_eq(new->egid, old->gid) ||
             !cap_issubset(new->cap_permitted, old->cap_permitted)) &&
            bprm->unsafe & ~LSM_UNSAFE_PTRACE_CAP) {
                /* downgrade; they get no more than they had, and maybe less */
                if (!capable(CAP_SETUID) ||
                    (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) {
                        new->euid = new->uid;
                        new->egid = new->gid;
                }
                new->cap_permitted = cap_intersect(new->cap_permitted,
                                                   old->cap_permitted);
        }

        new->suid = new->fsuid = new->euid;
        new->sgid = new->fsgid = new->egid;

        if (effective)
                new->cap_effective = new->cap_permitted;
        else
                cap_clear(new->cap_effective);
        bprm->cap_effective = effective;

        /*
         * Audit candidate if current->cap_effective is set
         *
         * We do not bother to audit if 3 things are true:
         *   1) cap_effective has all caps
         *   2) we are root
         *   3) root is supposed to have all caps (SECURE_NOROOT)
         * Since this is just a normal root execing a process.
         *
         * Number 1 above might fail if you don't have a full bset, but I think
         * that is interesting information to audit.
         */
        if (!cap_isclear(new->cap_effective)) {
                if (!cap_issubset(CAP_FULL_SET, new->cap_effective) ||
                    !uid_eq(new->euid, root_uid) || !uid_eq(new->uid, root_uid) ||
                    issecure(SECURE_NOROOT)) {
                        ret = audit_log_bprm_fcaps(bprm, new, old);
                        if (ret < 0)
                                return ret;
                }
        }

        new->securebits &= ~issecure_mask(SECURE_KEEP_CAPS);
        return 0;
}

Ключевой здесь является функция get_vfs_caps_from_disk(const struct dentry *dentry, struct cpu_vfs_cap_data *cpu_caps), принимающая в качестве аргументов указатель на элемент каталога, связанный с файлом исполняемого образа и указатель на структуру, которая принимает прочитанные из атрибута разрешения. struct cpu_vfs_cap_data - то же самое, что struct vfs_cap_data из include/uapi/linux/capability.h, но при этом числа представлены в специфичном для процессора порядке следования байтов (endianness), в то время, как для struct vfs_cap_data представление little endian. Атрибуты непосредственно считываются с диска при помощи метода getxattr() объекта, представляющего индексный узел:

size = inode->i_op->getxattr((struct dentry *)dentry, XATTR_NAME_CAPS, &caps, XATTR_CAPS_SZ);
Если таковой метод реализован для индексного узла в данной файловой системе, конечно же. Эта функция непосредственно читает данные с диска и проверяет их валидность. Прежде всего, ревизия реализации разрешений должна соответствовать размеру дисковых данных для этой реализации. В свою очередь get_vfs_caps_from_disk() используется функцией get_file_caps(), которая вычисляет множество доступных процессу разрешений основываясь на данных, прочитанных с диска из расширенных атрибутов файла и том, что мы наследуем от родителя через execve(). Собственно последняя часть задачи ложится на функцию bprm_caps_from_vfs_caps():

/*
 * Calculate the new process capability sets from the capability sets attached
 * to a file.
 */
static inline int bprm_caps_from_vfs_caps(struct cpu_vfs_cap_data *caps,
                                          struct linux_binprm *bprm,
                                          bool *effective,
                                          bool *has_cap)
{
        struct cred *new = bprm->cred;          /* Creds we inherit from the parent and simultaneously container for new creds */
        unsigned i;
        int ret = 0;

        if (caps->magic_etc & VFS_CAP_FLAGS_EFFECTIVE)
                *effective = true;

        if (caps->magic_etc & VFS_CAP_REVISION_MASK)
                *has_cap = true;

        CAP_FOR_EACH_U32(i) {
                __u32 permitted = caps->permitted.cap[i];          /* Those we have read from disk */
                __u32 inheritable = caps->inheritable.cap[i];

                /*
                 * pP' = (X & fP) | (pI & fI)
                 */
                new->cap_permitted.cap[i] =
                        (new->cap_bset.cap[i] & permitted) |
                        (new->cap_inheritable.cap[i] & inheritable);

                if (permitted & ~new->cap_permitted.cap[i])
                        /* insufficient to execute correctly */
                        ret = -EPERM;
        }

        /*
         * For legacy apps, with no internal support for recognizing they
         * do not have enough capabilities, we return an error if they are
         * missing some "forced" (aka file-permitted) capabilities.
         */
        return *effective ? ret : 0;
}}

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

P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset)
P'(effective) = F(effective) ? P'(permitted) : 0
P'(inheritable) = P(inheritable)    [не изменяются]
Здесь F - файловые разрешения, P - привилегии родительского процесса, P' - результирующие привилегии. Ещё раз посмотрите на код функции bprm_caps_from_vfs_caps() и без труда поймёте, что к чему. Через один из своих аргументов, effective, bprm_caps_from_vfs_caps() также возвращает флаг, уведомляющий о наличии среди файловых разрешений таких, которые должны быть активированы - эффективных файловых разрешений. Вопреки ожиданию наличие этих разрешений определяется не их фактическим присутствием, а специальным флагом, VFS_CAP_FLAGS_EFFECTIVE. Старые разрешения передаются через блок параметров исполняемого файла - переменную типа struct linux_binprm. Поднимаясь снизу вверх, мы обнаружим, что get_file_caps() - это ещё отнюдь не верхушка айсберга. Эта функция в свою очередь вызывается из cap_bprm_set_creds(). Последняя не объявлена, как статичная, что указывает на простой факт - она используется где-то за пределами данной единицы компиляции. Проявив такие чудеса дедукции и порывшись вокруг, обнаруживаем, что на cap_bprm_set_creds() "ссылается" static int selinux_bprm_set_creds(struct linux_binprm *bprm) из security/selinux/hooks.c, напрямую последняя не вызывается. Что же касается cap_bprm_set_creds(), то именно она и является основным хабом, где происходит вся обработка, связанная с разрешениями. Говоря об этой функции, как о хабе, я имею в виду, что её вызывает execve() с проинициализированным блоком параметров исполняемого образа, отсюда же тракт управления идёт к считыванию данных с диска, и частичной обработке полученных данных и здесь же потом происходит окончательная обработка множеств разрешений. Результаты своей работы эта функция предоставляет в распоряжение execve() опять же через блок параметров файла. Переход к этому коду происходит через указатель на операции безопасности (security ops).

Чуть ранее, прежде чем управление будет передано bprm_caps_from_vfs_caps(), в локальной переменной new, куда bprm_caps_from_vfs_caps() запишет вычисленные разрешения, кто-то должен был уже что-то записать. Это понятно уже из самого кода bprm_caps_from_vfs_caps(), ибо в самом деле, довольно странный способ вычисления разрешений, применяя их к чему-то неопределённому. Разрешения передаются между разными участками кода через параметры исполняемого образа, struct linux_binprm. Эти параметры заполняются опять же, в do_execve_common().

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

/*
 * sys_execve() executes a new program.
 */
static int do_execve_common(const char *filename,
                                struct user_arg_ptr argv,
                                struct user_arg_ptr envp)
{
...
        retval = prepare_bprm_creds(bprm);
        if (retval)
                goto out_free;
...
        retval = prepare_binprm(bprm);
...
Инициализация множеств разрешений нового процесса:
/*
 * Prepare credentials and lock ->cred_guard_mutex.
 * install_exec_creds() commits the new creds and drops the lock.
 * Or, if exec fails before, free_bprm() should release ->cred and
 * and unlock.
 */
int prepare_bprm_creds(struct linux_binprm *bprm)
{
        if (mutex_lock_interruptible(&current->signal->cred_guard_mutex))
                return -ERESTARTNOINTR;

        bprm->cred = prepare_exec_creds();
        if (likely(bprm->cred))
                return 0;

        mutex_unlock(&current->signal->cred_guard_mutex);
        return -ENOMEM;
}
/*
 * Prepare credentials for current to perform an execve()
 * - The caller must hold ->cred_guard_mutex
 */
struct cred *prepare_exec_creds(void)
{
        struct cred *new;

        new = prepare_creds();
        if (!new)
                return new;

#ifdef CONFIG_KEYS
        /* newly exec'd tasks don't get a thread keyring */
        key_put(new->thread_keyring);
        new->thread_keyring = NULL;

        /* inherit the session keyring; new process keyring */
        key_put(new->process_keyring);
        new->process_keyring = NULL;
#endif

        return new;
}
/**
 * prepare_creds - Prepare a new set of credentials for modification
 *
 * Prepare a new set of task credentials for modification.  A task's creds
 * shouldn't generally be modified directly, therefore this function is used to
 * prepare a new copy, which the caller then modifies and then commits by
 * calling commit_creds().
 *
 * Preparation involves making a copy of the objective creds for modification.
 *
 * Returns a pointer to the new creds-to-be if successful, NULL otherwise.
 *
 * Call commit_creds() or abort_creds() to clean up.
 */
struct cred *prepare_creds(void)
{
        struct task_struct *task = current;
        const struct cred *old;
        struct cred *new;

        validate_process_creds();

        new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
        if (!new)
                return NULL;

        kdebug("prepare_creds() alloc %p", new);

        old = task->cred;
        memcpy(new, old, sizeof(struct cred));

        atomic_set(&new->usage, 1);
        set_cred_subscribers(new, 0);
        get_group_info(new->group_info);
        get_uid(new->user);
        get_user_ns(new->user_ns);

#ifdef CONFIG_KEYS
        key_get(new->session_keyring);
        key_get(new->process_keyring);
        key_get(new->thread_keyring);
        key_get(new->request_key_auth);
#endif

#ifdef CONFIG_SECURITY
        new->security = NULL;
#endif

        if (security_prepare_creds(new, old, GFP_KERNEL) < 0)
                goto error;
        validate_creds(new);
        return new;

error:
        abort_creds(new);
        return NULL;
}
EXPORT_SYMBOL(prepare_creds);
Данные просто копируются из дескриптора текущей задачи, т.е. вызвавшей exec(), в блок параметров образа, который будет запущен. Теперь весь остальной код может сделать свою работу.

Далее, считывание разрешений с диска и вычисление новых разрешений, там же в fs/exec.c

/* 
 * Fill the binprm structure from the inode. 
 * Check permissions, then read the first 128 (BINPRM_BUF_SIZE) bytes
 *
 * This may be called multiple times for binary chains (scripts for example).
 */
int prepare_binprm(struct linux_binprm *bprm)
{
        umode_t mode;
        struct inode * inode = file_inode(bprm->file);
        int retval;

        mode = inode->i_mode;
        if (bprm->file->f_op == NULL)
                return -EACCES;

        /* clear any previous set[ug]id data from a previous binary */
        bprm->cred->euid = current_euid();
        bprm->cred->egid = current_egid();

        if (!(bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID) &&
            !current->no_new_privs &&
            kuid_has_mapping(bprm->cred->user_ns, inode->i_uid) &&
            kgid_has_mapping(bprm->cred->user_ns, inode->i_gid)) {
                /* Set-uid? */
                if (mode & S_ISUID) {
                        bprm->per_clear |= PER_CLEAR_ON_SETID;
                        bprm->cred->euid = inode->i_uid;
                }

                /* Set-gid? */
                /*
                 * If setgid is set but no group execute bit then this
                 * is a candidate for mandatory locking, not a setgid
                 * executable.
                 */
                if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
                        bprm->per_clear |= PER_CLEAR_ON_SETID;
                        bprm->cred->egid = inode->i_gid;
                }
        }

        /* fill in binprm security blob */
        retval = security_bprm_set_creds(bprm);
        if (retval)
                return retval;
        bprm->cred_prepared = 1;

        memset(bprm->buf, 0, BINPRM_BUF_SIZE);
        return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);
}
Далее, в security/security.c
int security_bprm_set_creds(struct linux_binprm *bprm)
{
        return security_ops->bprm_set_creds(bprm);
}
Структура, содержащая указатели на операции безопасности, инициализируется в security/selinux/hooks.c. Здесь без труда находим наш selinux_bprm_set_creds()
static struct security_operations selinux_ops = {
        .name =                         "selinux",
...
        .bprm_set_creds =               selinux_bprm_set_creds,
...
};
Резюмируя, получим такой вот флоучарт:

Надеюсь, это головокружительное путешествие помогло понять, каким образом реализованы привилегии Linux и что делает их привлекательными, надеюсь, вскользь коснувшись кода, вы поймёте, какую роль в реализации поддержки привилегий играет VFS. К сожалению, написано уже и так больше, чем я предполагал изначально, а во все детали заглянуть всё равно не представляется возможным в заметке сколь-нибудь вменяемого объёма. Поэтому напоследок только взглянем краем глаза, как на практике реализуется контроль привилегий и на этом остановимся. Рассмотрим самый простой пример, с которого мы и начали, с настройкой системных часов. Наша программа вызывает stime(). Так как представить себе использование этого системного вызова совсем не сложно, текст самой программы не приведён:

kernel/time.c
 /*
  * sys_stime() can be implemented in user-level using
  * sys_settimeofday().  Is this for backwards compatibility?  If so,
  * why not move it into the appropriate arch directory (for those
  * architectures that need it).
  */
 
 SYSCALL_DEFINE1(stime, time_t __user *, tptr)
 {
         struct timespec tv;
         int err;
 
         if (get_user(tv.tv_sec, tptr))
                 return -EFAULT;
 
         tv.tv_nsec = 0;
 
         err = security_settime(&tv, NULL);
         if (err)
                 return err;
 
         do_settimeofday(&tv);
         return 0;
 }
include/linux/security.h
static inline int security_settime(const struct timespec *ts,
                                   const struct timezone *tz)
{
        return cap_settime(ts, tz);
}
security/commoncap.c
int cap_settime(const struct timespec *ts, const struct timezone *tz)
{
        if (!capable(CAP_SYS_TIME))
                return -EPERM;
        return 0;
}
kernel/capability.c
/**
 * capable - Determine if the current task has a superior capability in effect
 * @cap: The capability to be tested for
 *
 * Return true if the current task has the given superior capability currently
 * available for use, false if not.
 *
 * This sets PF_SUPERPRIV on the task if the capability is available on the
 * assumption that it's about to be used.
 */
bool capable(int cap)
{
        return ns_capable(&init_user_ns, cap);
}
/**
 * ns_capable - Determine if the current task has a superior capability in effect
 * @ns:  The usernamespace we want the capability in
 * @cap: The capability to be tested for
 *
 * Return true if the current task has the given superior capability currently
 * available for use, false if not.
 *
 * This sets PF_SUPERPRIV on the task if the capability is available on the
 * assumption that it's about to be used.
 */
bool ns_capable(struct user_namespace *ns, int cap)
{
        if (unlikely(!cap_valid(cap))) {
                printk(KERN_CRIT "capable() called with invalid cap=%u\n", cap);
                BUG();
        }

        if (security_capable(current_cred(), ns, cap) == 0) {
                current->flags |= PF_SUPERPRIV;
                return true;
        }
        return false;
}
EXPORT_SYMBOL(ns_capable);
security/selinux/hooks.c
/*
* (This comment used to live with the selinux_task_setuid hook,
* which was removed).
*
* Since setuid only affects the current process, and since the SELinux
* controls are not based on the Linux identity attributes, SELinux does not
* need to control this operation.  However, SELinux does control the use of
* the CAP_SETUID and CAP_SETGID capabilities using the capable hook.
*/

static int selinux_capable(const struct cred *cred, struct user_namespace *ns,
                          int cap, int audit)
{
       int rc;

       rc = cap_capable(cred, ns, cap, audit);
       if (rc)
               return rc;

       return cred_has_capability(cred, cap, audit);
}
security/commoncap.c
 /**
  * cap_capable - Determine whether a task has a particular effective capability
  * @cred: The credentials to use
  * @ns:  The user namespace in which we need the capability
  * @cap: The capability to check for
  * @audit: Whether to write an audit message or not
  *
  * Determine whether the nominated task has the specified capability amongst
  * its effective set, returning 0 if it does, -ve if it does not.
  *
  * NOTE WELL: cap_has_capability() cannot be used like the kernel's capable()
  * and has_capability() functions.  That is, it has the reverse semantics:
  * cap_has_capability() returns 0 when a task has a capability, but the
  * kernel's capable() and has_capability() returns 1 for this case.
  */
int cap_capable(const struct cred *cred, struct user_namespace *targ_ns,
                 int cap, int audit)
{
         struct user_namespace *ns = targ_ns;

         /* See if cred has the capability in the target user namespace
          * by examining the target user namespace and all of the target
          * user namespace's parents.
          */
         for (;;) {
                 /* Do we have the necessary capabilities? */
                 if (ns == cred->user_ns)
                         return cap_raised(cred->cap_effective, cap) ? 0 : -EPERM;

                 /* Have we tried all of the parent namespaces? */
                 if (ns == &init_user_ns)
                         return -EPERM;

                 /* 
                  * The owner of the user namespace in the parent of the
                  * user namespace has all caps.
                  */
                 if ((ns->parent == cred->user_ns) && uid_eq(ns->owner, cred->euid))
                         return 0;

                /*
                 * If you have a capability in a parent user ns, then you have
                 * it over all children user namespaces as well.
                 */
                ns = ns->parent;
        }

        /* We never get here */
}

В общем-то, не то, чтобы всё это было очень сложно. Идея довольно проста. Запутанность привносит модульная модель безопасности. Но за стенами кода скрыт достаточно простой механизм. В нашем примере security_settime() не имеет никакого отношения к реальной настройке часов. Всё, что делает эта функция, проверяет наличие соответствующей привилегии. Через хуки SELinux управляющий тракт в конечном итоге приводит нас в код, находящийся в файле security/commoncap.c, где макрос cap_raised() производит тривиальную проверку (логическое И между имеющимся у процесса разрешением и требуемой привелегией - соответствующая привилегия из эффективного множества должна быть установлена в единицу). В качестве упражнения оставляю вам удовольствие найти, что за security_settime() и как мы выходим на описанный выше тракт :)

Почти живой пример

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

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char ** argv)
{
 printf ("cap_setuid and cap_setgid: %d\n", prctl (PR_CAPBSET_READ, CAP_SETUID | CAP_SETGID, 0, 0, 0));
 printf (" %s\n", cap_to_text (cap_get_file (argv [0]), NULL));
 printf (" %s\n", cap_to_text (cap_get_proc (), NULL));

 if (setresuid (0, 0, 0));
  printf ("setresuid(): %s\n", strerror (errno));

 execve ("/bin/sh", NULL, NULL);
}
Эта программка должна изменить свой UID и дать нам шелл суперпользователя. Что ж, попытаем счастья:
$ gcc -o cap_test cap_test.c -lcap
$ ./cap_test
cap_setuid and cap_setgid: 1
 =
 =
setresuid(): Operation not permitted
Не повезло :( Но мы-то уже знаем, дело вовсе не в везении, а в том, что наш файл не имеет соответствующих разрешений. Попробуем теперь вот так:
# setcap cap_setuid,cap_setgid+ep ./cap_test
$ ./cap_test
cap_setuid and cap_setgid: 1
 = cap_setgid,cap_setuid+ep
 = cap_setgid,cap_setuid+ep
setresuid(): Success
# exit
exit
Успех! Да, кстати, на всякий случай, не забывайте выходить из шеллов, т.к. даже при безуспешном вызове setresuid() эта программа очевидным образом спавнит новый шелл каждый раз, только без привилегий :) В первой строке она показывает файловые разрешения, во второй - разрешения выполняющегося процесса. Так что при желании можете поэкспериментировать с этим примером больше. Информацию о разрешениях выполняющегося процесса можно посмотреть через /proc:
$ grep ^Cap /proc/$$/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000001fffffffff
Причём, не имея прав суперпользователя, вы ничего интересного не увидите, кроме маски. Вот так - уже интереснее:
# grep ^Cap /proc/$$/status
CapInh: 0000000000000000
CapPrm: 0000001fffffffff
CapEff: 0000001fffffffff
CapBnd: 0000001fffffffff
Значения можно декодировать с помощью capsh:
$ capsh --decode=0000001fffffffff
Также обратите внимание на утиль pscap, который показывает разрешения выполняющихся процессов. Всё сказанное наталкивает на мысль, что теоретически root, как таковой, уже не сильно нужен и вместо него можно ввести пользователя с полным набором привилегий. Кроме того, такая модель позволяет создавать промежуточных, недо-root'ов, с усечёнными полномочиями. Правда это требует обязательного включения поддержки некоторых параметров ядра при сборке, которые могут быть и отключены в пользовательских ядрах, так что модель root'а всё же пока актуальна, так как остаётся универсальным запасным вариантом для всех вариантов сборки ядра Linux.

PS: в заметке довольно много кода. Изначально я хотел спрятать всё это безобразие под ковриккат, но он работал не совсем так, как мне бы хотелось и я решил, что пусть уж лучше будет так, чем сломанный функционал. Надеюсь, к следующему разу я найду какой-нибудь рабочий вариант.

Источники

[1] How Linux Capability Works in 2.6.25
[2] Linux Capabilities: making them work

ПОСЕТИТЕЛИ

free counters