Ключи защиты памяти

Ключи защиты памяти обеспечивают механизм повышения надежности программ.

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

Поскольку доступ к защищенным ключами страницам является атрибутом выполняемой нити, этот механизм естественным образом распространяется на приложения с несколькими нитями, но с ограничением на использование этими приложениями только нитей pthread 1:1 (или области системы). Подход, реализуемый командой mprotect, не обеспечивает надежную работу в среде с несколькими нитями, поскольку приходится удалять защиту для всех нитей, если необходимо предоставить доступ для какой-либо одной нити. Можно одновременно использовать оба механизма; в этом случае, программа не может выполнить запись в защищенную от записи страницу даже если ключ защиты разрешает запись.

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

Можно упростить процесс отладки, если разрабатывать приложение, учитывая защиту ключами. Установка ключа защиты страницы и задание набора ключей активного пользователя являются системными вызовами, и поэтому являются относительно затратными операциями. Следует разрабатывать программу таким образом, чтобы частота этих операций не была избыточной.

Пользовательские ключи защиты

При работе с ключами защиты применяются следующие принципы и соглашения:
  • Страницы, экспортированные из ядра только для чтения, по-прежнему будут видны для вашей программы. Эти страницы имеют ключ защиты UKEY_SYSTEM. Этот ключ защиты не находится под управлением программы, однако программа всегда имеет к нему доступ.
  • Все страницы памяти вашей программы изначально имеют присвоенный им пользовательский общий ключ. Как было указано выше, доступ к памяти с ключом 0 предоставляется всегда, что делает этот ключ пользовательским общим ключом.
  • Можно задать ключи защиты только для обычных и общих данных. Нельзя защитить, например, данные библиотеки, общую с ядром низшую память или текст программы.
  • В зависимости от базового аппаратного обеспечения и административного выбора, доступно только ограниченное число пользовательских ключей защиты (обычно один). Когда программа присваивает ключ защиты одной или нескольким страницам, по умолчанию данные на этих страницах становятся недоступными. Необходимо явным образом предоставить права доступа к этим данным на чтение или запись для окружающих кодовых путей, которым требуется доступ, путем вызова новой службы для управления действующим пользовательским набором ключей.
  • Физическое аппаратное обеспечение, вероятно, поддерживает дополнительные ключи защиты, которые не доступны для использования в качестве пользовательских ключей защиты.
  • Для присвоения странице ключей защиты не требуются специальные права доступа. Единственным требованием является наличие текущих прав на запись для этой страницы.
  • Ключи защиты не управляют правами на выполнение.

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

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

Области, защищенные пользовательскими ключами

Пользовательские ключи защиты могут использоваться для защиты страниц в следующих областях:
  • Область данных
  • Область стека по умолчанию
  • Области mmap
  • Общая память, присоединенная с помощью команды shmat(), за исключением перечисленных ниже случаев
  • Для следующих категорий страниц нельзя использовать ключи защиты:
    • Файлы shmat и закрепленная общая память
    • Большие (нелистаемые) страницы
    • Текст программы
    • Общая с ядром нижняя область память с разрешением чтения

Системные предварительные требования для защиты ключами

Защита памяти ключами представляет собой зависящий от аппаратного обеспечения механизм защиты страниц, который предоставляется ядром AIX для применения в прикладных программах. Для использования этой функции к системе предъявляются следующие требования:
  • Система должна выполняться на физическом аппаратном обеспечении, предоставляющем защиту памяти ключами
  • Система должна выполняться в 64-разрядном ядре
  • В системе должно быть включено применение пользовательских ключей защиты

Предварительные требования к программе для защиты ключами

Для использования пользовательских ключей к программе предъявляются следующие требования:
  • Программа должна объявить себя поддерживающей пользовательские ключи и с помощью команды ukey_enable определить, сколько пользовательских ключей защиты доступно.
  • Программа должна организовать защищенные данные в пределах страницы.
  • Программа должна с помощью команды ukey_protect присвоить частный ключ для каждой страницы, которую необходимо защитить.
  • Если данные malloc'd защищены, то перед высвобождением необходимо снять защиту.
  • Программа должна с помощью команды ukeyset_init подготовить один или несколько наборов ключей.
  • Программа, возможно, должна добавить необходимые ключи в набор ключей с помощью команды ukeyset_add_key, для включения в будущем прав на чтение или запись.
  • Программа должна с помощью команды ukeyset_activate активировать набор ключей, чтобы предоставить права доступа, определенные в наборе.
Программа не должна:
  • Включать какие либо нити pthread M:N (области процесса)
  • Иметь возможность выполнения контрольной точки (например, при условии CHECKPOINT=yes в среде)
Прим.: Если программа поддерживает пользовательские ключи, существует связанный с ней дополнительный контекст для представления действующего пользовательского набора ключей. Это отображается в следующем:
  • Обработчики сигналов, получающие структуру ucontext_t. Предыдущий действующий пользовательский набор ключей находится в ucontext_t.__extctx.__ukeys, массиве из двух элементов, содержащем 64-разрядное значение пользовательского набора ключей
  • Пользовательские контекстные структуры, откомпилированные с определенным __EXTABI__ (которое применяется setcontext, getcontext, makecontext, swapcontext)

Функции

Для работы с ключами защиты предоставляются следующие новые функции ядра AIX:
Функция Описание
sysconf _SC_AIX_UKEYS позволяет определить число поддерживаемых пользовательских ключей (эта функция может быть вызвана в предыдущих версиях AIX)
ukey_enable Включает для процесса программную среду, поддерживающую пользовательские ключи, и сообщает, сколько пользовательских ключей доступно
ukeyset_init Инициализирует пользовательский набор ключей, который будет представлять набор прав доступа для частного ключа (ключей)
ukeyset_add_key Добавляет в набор ключей права на чтение или запись для указанного ключа
ukeyset_remove_key Удаляет из набора ключей права на чтение и/или запись для указанного ключа
ukeyset_add_set Добавляет все права доступа из одного набора ключей в другой
ukeyset_remove_set Удаляет все права доступа одного набора ключей из другого
ukeyset_activate Применяет права доступа набора ключей к выполняемой нити
ukeyset_ismember Проверяет, содержатся ли данные права доступа в наборе ключей
ukey_setjmp Расширенная форма setjmp, которая сохраняет действующий набор ключей (использует структуру ukey_jmp_buf)
pthread_attr_getukeyset_np Получает атрибут набора ключей нити pthread
pthread_attr_setukeyset_np задает атрибут набора ключей нити pthread
ukey_protect Задает пользовательский ключ защиты для страничного диапазона пользовательской памяти
ukey_getkey Получает пользовательский ключ защиты для указанного адреса

Отладка

Команда dbx добавляет ограниченную поддержку ключей защиты:
  • При отладке выполняемой программы:
    • Команда ukeyset отображает действующий набор ключей.
    • Команда ukeyvalue отображает ключ защиты, связанный с данным расположением памяти.
  • При отладке файла дампа команда ukeyexcept сообщает действующий набор ключей, эффективный адрес исключительной ситуации ключа и ключ памяти.

Сведения об аппаратном обеспечении

Действующий пользовательский набор ключей в выполняемом контексте поддерживающей ключи нити переводит действующий аппаратный регистр маски прав доступа (AMR) в формат, представленный абстрактным типом данных ukeyset_t. Эта информация предоставляется только для целей отладки. Для установки действующего пользовательского набора ключей следует применять только определенные программные службы.
  • AMR - это 64-разрядный регистр, содержащий 32-разрядные пары (одна пара на ключ) для максимум 32 ключей, пронумерованных от 0 до 31.
    • Первый бит каждой пары представляет права на запись в соответствующий ключ.
    • Аналогично, второй бит каждой пары представляет права на чтение соответствующего ключа.
  • Значение бита, равное 0, предоставляет соответствующий доступ, а значение, равное 1, запрещает доступ.
  • Пара битов, предоставляющая права доступа для ключа 0, не управляется программой. Пользовательский ключ 0 является пользовательским общим ключом, и все нити всегда имеют полный доступ к данным в этом ключе, независимо от параметров действующего пользовательского набора ключей.
  • Все остальные пары битов представляют пользовательские частные ключи, которые (если доступны) можно использовать для защиты ваших данных.

Пример программы

Ниже приведен пример программы,поддерживающей пользовательские ключи:

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <sys/ukeys.h>
#include <sys/syspest.h>
#include <sys/signal.h> 
#include <sys/vminfo.h>

#define ROUND_UP(size,psize)    ((size)+(psize)-1 & ~((psize)-1))

/*
 * Это пример каркаса программы, поддерживающей пользовательские ключи.
 *
 * Структура private_data_1 отображает защищенную ключом область памяти,
 * к которой основная программа имеет свободный доступ, а "незащищенная"
 * команда имеет только права на чтение.
 */
struct private_data_1 {
        int some_data;
};
struct private_data_1 *p1;      /* указатель на структуру защищенных данных */

ukeyset_t keyset1RW;            /* набор ключей для предоставления доступа обработчику сигнала */

/*
 * Незащищенная функция должна успешно прочитать защищенные данные.
 *
 * Если значение счетчика равно 0, оно просто возвращается, так что инициатор может записать
 * увеличенное на единицу значение обратно в защищенное поле.
 *
 * Если значение счетчика равно 1, он пытается самостоятельно обновить защищенное поле.
 * Результатом должно являться SIGSEGV.
 */
int untrusted(struct private_data_1 *p1) {
        int count = p1->some_data;      /* Можно прочитать защищенные данные */
        if (count == 1)
                p1->some_data = count;  /* Однако запись запрещена */
        return count + 1;
}

/*
 * Обработчик сигнала обнаруживает умышленное нарушение защиты в
 * незащищенной функции выше при значении count == 1.
 * Обратите внимание, что обработчик вводится БЕЗ прав доступа к частным данным.
 */
void handler(int signo, siginfo_t *sip, void *ucp) {
        printf("siginfo: signo %d code %d\n", sip->si_signo, sip->si_code);
        (void)ukeyset_activate(keyset1RW, UKA_REPLACE_KEYS);
        exit(1);
}

main() {
        int nkeys;
        int pagesize = 4096;            /* аппаратный размер страницы данных */
        int padded_protsize_1;          /* размер страницы защищенных данных */
        struct vm_page_info page_info;

        ukey_t key1 = UKEY_PRIVATE1;
        ukeyset_t keyset1W, oldset;
        int rc;
        int count = 0;

        struct sigaction sa;

        /*
         * Попытка узнать о пользовательском ключе.
         */
        nkeys = ukey_enable();
        if (nkeys == -1) {
                perror("ukey_enable");
                exit(1);
        }
        assert(nkeys >= 2);

        /*
         * Определяется размер страницы области данных.
         */
        page_info.addr = (long)&p1;             /* адрес в области данных */
        rc = vmgetinfo(&page_info, VM_PAGE_INFO, sizeof(struct vm_page_info));

        if (rc)
                perror("vmgetinfo");
        else
                pagesize = page_info.pagesize;  /* получаем фактический размер страницы */

        /*
         * Необходимо выделить соответствующий странице объем памяти
         * для области, которую необходимо защитить ключом.
         */
        padded_protsize_1 = ROUND_UP(sizeof(struct private_data_1), pagesize);
        rc = posix_memalign((void **)&p1, pagesize, padded_protsize_1);
        if (rc) {
                perror("posix_memalign");
                exit(1);
        }

        /*
         * Инициализация частных данных.
         * Это можно сделать до установки защиты.
         *
         * Обратите внимание, что указатель на частные данные находится в общей памяти.
         * Мы защищаем только сами данные.
         */
        p1->some_data = count;

        /*
         * Создание наборов ключей для применения для доступа к защищенной структуре.
         * Обратите внимание, что эти наборы ключей будут размещены в общей памяти.
         */
        rc = ukeyset_init(&keyset1W, 0);
        if (rc) {
                perror("ukeyset_init");
                exit(1);
        }

        rc = ukeyset_add_key(&keyset1W, key1, UK_WRITE);        /* WRITE */
        if (rc) {
                perror("ukeyset_add_key 1W");
                exit(1);
        }

        keyset1RW = keyset1W;
        rc = ukeyset_add_key(&keyset1RW, key1, UK_READ);        /* R/W */
        if (rc) {
                perror("ukeyset_add_key 1R");
                exit(1);
        }

        /*
         * Доступ к частным данным запрещен с помощью применения частного ключа
         * к содержащей их странице (страницам).
         */
        rc = ukey_protect(p1, padded_protsize_1, key1);
        if (rc) {
                perror("ukey_protect");
                exit(1);
        }

        /*
         * Разрешить всему основному коду ссылаться на чтение/запись защищенных данных.
         */
        oldset = ukeyset_activate(keyset1RW, UKA_ADD_KEYS);
        if (oldset == UKSET_INVALID) {
                printf("ukeyset_activate failed\n");
                exit(1);
        }

        /*
         * Задается обработчик сигнала для SIGSEGV, для обнаружения преднамеренного
         * нарушения ключа в незащищенном коде.
         */
        sa.sa_sigaction = handler;
        SIGINITSET(sa.sa_mask);
        sa.sa_flags = SA_SIGINFO;
        rc = sigaction(SIGSEGV, &sa, 0);
        if (rc) {
                perror("sigaction");
                exit(1);
        }

        /*
         * Основной цикл выполнения программы.
         */
        while (count < 2) {
                /*
                 * Когда необходимо выполнить "незащищенный" код, изменяем права доступа
                 * к частным данным на R/O, удалив права на запись.
                 */
                (void)ukeyset_activate(keyset1W, UKA_REMOVE_KEYS);

                /*
                 * Вызов незащищенной команды.  Она может только читать
                 * переданные ей защищенные данные.
                 */
                count = untrusted(p1);

                /*
                 * Восстанавливается полный доступ к частным данным.
                 */
                (void)ukeyset_activate(keyset1W, UKA_ADD_KEYS);

                p1->some_data = count;
        }
ukey_protect(p1, padded_protsize_1, UKEY_PUBLIC);
        free(p1);
        exit(0);
}