PKEYS(7)

ИМЯ
pkeys - обзор ключей защиты памяти

ОПИСАНИЕ
Ключи защиты памяти (pkeys) — это расширение существующих постраничных прав на память. Для обычных прав на страницу используются страничные таблицы, требующие для изменении прав затратных системных вызовов и аннулирования TLB. Ключи защиты памяти предоставляют механизм изменения защиты без необходимости изменять страничные таблицы при каждом изменении прав.

Чтобы использовать pkeys, ПО сначала должно «пометить» (tag) страницу в страничных таблицах значением pkey. После размещения этой метки для удаления прав на запись или весь доступ к помеченной странице приложению нужно изменить только содержимое регистра.

Ключи защиты вместе с существующими правами PROT_READ/ PROT_WRITE/ PROT_EXEC передаются в системные вызовы, такие как mprotect(2) и mmap(2), но всегда считаются как дополнительное ограничение к существующим традиционным механизмам прав доступа.

Если процесс осуществляет доступ, нарушающий ограничения pkey, то он получает сигнал SIGSEGV. Подробную информацию об этом сигнале смотрите в sigaction(2).

Чтобы использовать свойство pkeys, это должен поддерживать процессор, а ядро должно включать поддержку этого свойства для этого процессора. К началу 2016 года это относится только к будущим процессорам Intel x86, и данная аппаратура поддерживает 16 ключей защиты на каждый процесс. Однако pkey 0 используется как ключ по умолчанию, поэтому для приложения доступно только 15. Ключ по умолчанию назначается любой области памяти, для которой pkey не был назначен явным образом с помощью pkey_mprotect(2).

Потенциально, ключи защиты могут добавить уровень безопасности и надежности приложений. Но, прежде всего, они не разрабатывались как средство защиты. Например, WRPKRU — это полностью непривилегированная инструкция, поэтому pkeys бесполезны, когда атакующий контролирует регистр PKRU или может выполнять любые инструкции.

Приложения должны следить за тем, чтобы их ключи защиты не «не утекли». Например, перед вызовом pkey_free(2) приложение должно проверить, что pkey не назначен памяти. Если приложение оставит назначенным освобождённый pkey, то будущий пользователь этого pkey может непреднамеренно изменить права на не относящуюся к делу структуру данных, что может привести к проблемам с безопасностью или стабильностью. В настоящее время ядро позволяет вызывать pkey_free(2) для задействованных pkeys, так как выполнение дополнительных проверок повлияло бы на производительность процессора или памяти. Реализация необходимых проверок переложена на приложение. Приложения могут найти области памяти, которым назначен pkey, в файле /proc/[pid]/smaps. Дополнительная информация представлена в proc(5).

Приложение, которое хочет воспользоваться ключами защиты, может работать и без них. Ключи могут быть недоступны из-за отсутствия аппаратной поддержки системе, где запускается приложение, в коде ядра может не быть поддержки, поддержка в ядре может быть выключена или все ключи уже задействованы библиотекой, которую использует приложение. В приложениях рекомендуется просто вызывать pkey_alloc(2) и проверять успешность выполнения, а не пытаться проверять наличие поддержки каким-то другим образом.

Хотя и необязательно, поддержку ключей защиты в аппаратуре можно определить помощью инструкции cpuid. От том, как это сделать, смотрите в программном руководстве разработчика Intel. Ядро определяет наличие поддержки и выводит информацию в /proc/cpuinfo в поле «flags». Строка «pku» в этом поле означает, что аппаратура поддерживает ключи защиты, а строка «ospke» означает, что ядро содержит включённую поддержку защиты.

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

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

Данное поведение сигнала необычно из-за того, что регистр x86 PKRU (который хранит права доступа ключа защиты) управляется тем же аппаратным механизмом (XSAVE) что и регистры плавающей запятой. Поведение сигнала такое же как у регистров плавающей запятой.

  Системные вызовы ключей защиты
В ядре Linux реализованы следующие системные вызовы для работы с pkey: pkey_mprotect(2), pkey_alloc(2) и pkey_free(2).

Системные вызовы Linux pkey доступны только, если ядро было собрано с включённым параметром CONFIG_X86_INTEL_MEMORY_PROTECTION_KEYS.

ПРИМЕР

Программа, представленная далее, выделяет страницу памяти с правами на чтение и запись. Затем она записывает кусок данных в памяти и читает его. После этого она пытается выделить ключ защиты и запретить доступ к странице с помощью инструкции WRPKRU. Далее она пытается получить доступ к странице, что, как мы ожидаем, вызовет сигнал завершения приложения.

    $ ./a.out
    буфер содержит: 73
    читаем буфер снова...
    Segmentation fault (core dumped)

  Исходный код программы
    #define _GNU_SOURCE
    #include <unistd.h>
    #include <sys/syscall.h>
    #include <stdio.h>
    #include <sys/mman.h>
    static inline void
    wrpkru(unsigned int pkru)
    {
        unsigned int eax = pkru;
        unsigned int ecx = 0;
        unsigned int edx = 0;
        asm volatile(".byte 0x0f,0x01,0xef\n\t"
                     : : "a" (eax), "c" (ecx), "d" (edx));
    }
    int
    pkey_set(int pkey, unsigned long rights, unsigned long flags)
    {
        unsigned int pkru = (rights << (2 * pkey));
        return wrpkru(pkru);
    }
    int
    pkey_mprotect(void *ptr, size_t size, unsigned long orig_prot,
                  unsigned long pkey)
    {
        return syscall(SYS_pkey_mprotect, ptr, size, orig_prot, pkey);
    }
    int
    pkey_alloc(void)
    {
        return syscall(SYS_pkey_alloc, 0, 0);
    }
    int
    pkey_free(unsigned long pkey)
    {
        return syscall(SYS_pkey_free, pkey);
    }
    #define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                               } while (0)
    int
    main(void)
    {
        int status;
        int pkey;
        int *buffer;
        /*
         * выделяем страницу памяти
         */
        buffer = mmap(NULL, getpagesize(), PROT_READ | PROT_WRITE,
                      MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
        if (buffer == MAP_FAILED)
            errExit("mmap");
        /*
         * пишем произвольные данные в страницу (чуть)
         */
        *buffer = __LINE__;
        printf("буфер содержит: %d\n", *buffer);
        /*
         * выделяем ключ защиты:
         */
        pkey = pkey_alloc();
        if (pkey == -1)
            errExit("pkey_alloc");
        /*
         * запрещаем доступ к памяти, на которой будет установлен «pkey»,
         * хотя пока ничего не запрещено
         */
        status = pkey_set(pkey, PKEY_DISABLE_ACCESS, 0);
        if (status)
            errExit("pkey_set");
        /*
         * установим ключ защиты на «буфер»
         * заметим, что он доступен пока не применён mprotect()
         * и ключ не заменен созданным ранее pkey_set()
         */
        status = pkey_mprotect(buffer, getpagesize(),
                               PROT_READ | PROT_WRITE, pkey);
        if (status == -1)
            errExit("pkey_mprotect");
        printf("читаем буфер снова...\n");
        /*
         * приложение падает, так как мы запретили доступ
         */
        printf("буфер содержит: %d\n", *buffer);
        status = pkey_free(pkey);
        if (status == -1)
            errExit("pkey_free");
        exit(EXIT_SUCCESS);
    }

СМОТРИТЕ ТАКЖЕ
pkey_alloc(2), pkey_free(2), pkey_mprotect(2), sigaction(2)