MPROTECT(2)

ИМЯ
mprotect, pkey_mprotect - контролирует доступ к области памяти

ОБЗОР
#include <sys/mman.h>

int mprotect(void *addr, size_t len, int prot);
#define _GNU_SOURCE             /* смотрите feature_test_macros(7) */
#include <sys/mman.h>

int pkey_mprotect(void *addr, size_t len, int prot, int pkey);

ОПИСАНИЕ
Вызов mprotect() изменяет параметры доступа страниц памяти вызывающего процесса, которые содержатся, даже частично, в адресном диапазоне [addr, addr+len-1]. Значение addr должно быть выровнено на границу страницы.

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

Значение prot представляет собой комбинацию следующих флагов доступа: PROT_NONE или побитово сложенные другие значения из следующего списка:

PROT_NONE
    Доступ к памяти запрещён.

PROT_READ
    Память можно читать.

PROT_WRITE
    Память можно изменять.

PROT_EXEC
    Память можно выполнять.

PROT_SEM (начиная с Linux 2.5.7)
    Память можно использовать для атомарных операций. Этот флаг появился как часть реализации futex(2) (для гарантии способности выполнять атомарные операции, требуемые таким командам как FUTEX_WAIT), но пока не используется ни в одной архитектуре.

PROT_SAO (начиная с Linux 2.6.26)
    Память должна иметь строгий порядок доступа. Это свойство есть только в архитектуре PowerPC (в спецификации архитектуры версии 2.06 добавлено свойство ЦП SAO и оно доступно, например, на POWER 7 или PowerPC A2).

Также (начиная с Linux 2.6.0), prot может содержать один из следующих установленных флагов:

PROT_GROWSUP
    Применить режим защиты до конца отображения, которое растёт вверх (такие отображения создаются для области стека например в архитектуре HP-PARISC, где стек растёт вверх).

PROT_GROWSDOWN
    Применить режим защиты до начала отображения, которое растёт вниз (которое должно быть сегментом стека или сегментом, отображённым с установленным флагом MAP_GROWSDOWN).

Подобно mprotect(), вызов pkey_mprotect() изменяет защиту страниц, указанных addr и len. Аргумент pkey содержит ключ защиты (смотрите pkeys(7)), назначаемый памяти. Ключ защиты должен быть выделен с помощью pkey_alloc(2) до передачи в pkey_mprotect(). Пример использования этого системного вызова смотрите в pkeys(7).

ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ
При успешном выполнении mprotect() и pkey_mprotect() возвращают 0. В случае ошибки возвращается -1, а errno устанавливается в соответствующее значение.

ОШИБКИ

EACCES
    Нельзя задать этот вид доступа. Например, это может случиться, если при вызове mmap(2) файл доступен только на чтение, а запрос mprotect() был PROT_WRITE.

EINVAL
    Значение addr не является правильным указателем или не кратен размеру системной страницы.

EINVAL
    (pkey_mprotect()) pkey не был выделен с помощью pkey_alloc(2).

EINVAL
    В prot указаны оба флага, PROT_GROWSUP и PROT_GROWSDOWN.

EINVAL
    Указано неверное значение в prot.

EINVAL
    (архитектура PowerPC ) В prot указан PROT_SAO, но недоступно аппаратное свойство SAO.

ENOMEM
    Не удалось выделить место под внутренние структуры ядра.

ENOMEM
    Адреса в диапазоне [addr, addr+len-1] некорректны для адресного пространства процесса, или одна или более указанных страниц не отображена (до ядра версии 2.4.19 в этих случаях некорректно возвращалась ошибка EFAULT).

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

ВЕРСИИ
Вызов pkey_mprotect() впервые появился в Linux 4.9; поддержка в библиотеке glibc добавлена в версии 2.27.

СООТВЕТСТВИЕ СТАНДАРТАМ
mprotect(): В POSIX.1-2001, POSIX.1-2008, SVr4 сказано, что поведение mprotect() не определено, если переданная область памяти не получена через mmap(2).

Вызов pkey_mprotect является непереносимым расширением Linux.

ЗАМЕЧАНИЯ
В Linux всегда можно вызвать mprotect() с любым адресом из адресного пространства процесса (за исключением области ядра vsyscall). В частности, это можно использовать для изменения отображений существующего кода на записываемые.

Отличается ли действие PROT_EXEC от PROT_READ зависит от архитектуры процессора, версии ядра и состояния процесса. Если в флагах специализаций процессора установлен READ_IMPLIES_EXEC (смотрите personality(2)), то указание PROT_READ подразумевает добавление PROT_EXEC.

На некоторых аппаратных архитектурах (например, i386) PROT_WRITE подразумевает PROT_READ.

В POSIX.1 сказано, что реализация может разрешить доступ отличный от указанного в prot, но для доступа на запись должен быть обязательно установлен флаг PROT_WRITE, и любой доступ должен быть запрещён, если установлен флаг PROT_NONE.

В приложениях нужно осторожно использовать mprotect() и pkey_mprotect() вместе. На x86, если mprotect() используется с установленным в prot значением PROT_EXEC, то pkey может быть выделен и установлен ядром в память неявным образом, но только если до этого pkey был равен 0.

В системах без аппаратной поддержки ключей защиты pkey_mprotect() всё ещё можно использовать, но значение pkey должно быть равно -1. При таком вызове операция pkey_mprotect() эквивалентна mprotect().

ПРИМЕР

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

Результат работы программы:

    $ ./a.out
    Начало области:             0x804c000
    Получен SIGSEGV при адресе: 0x804e000

  Исходный код программы
    #include <unistd.h>
    #include <signal.h>
    #include <stdio.h>
    #include <malloc.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <sys/mman.h>
    #define handle_error(msg) \
        do { perror(msg); exit(EXIT_FAILURE); } while (0)
    static char *buffer;
    static void
    handler(int sig, siginfo_t *si, void *unused)
    {
        /* Замечание: вызов printf() из обработчика сигнала небезопасен
           (и не должен выполняться в готовых программах), так как
           printf() не async-signal-safe; смотрите signal-safety(7).
           Тем не менее, здесь мы используем printf(), так как это простой
           способ показать когда вызывается обработчик. */
        printf("Получен SIGSEGV при адресе: 0x%lx\n",
                (long) si->si_addr);
        exit(EXIT_FAILURE);
    }
    int
    main(int argc, char *argv[])
    {
        char *p;
        int pagesize;
        struct sigaction sa;
        sa.sa_flags = SA_SIGINFO;
        sigemptyset(&sa.sa_mask);
        sa.sa_sigaction = handler;
        if (sigaction(SIGSEGV, &sa, NULL) == -1)
            handle_error("sigaction");
        pagesize = sysconf(_SC_PAGE_SIZE);
        if (pagesize == -1)
            handle_error("sysconf");
        /* выделить буфер с выравниванием на границу страницы;
           начальная защита: PROT_READ | PROT_WRITE */
        buffer = memalign(pagesize, 4 * pagesize);
        if (buffer == NULL)
            handle_error("memalign");
        printf("Начало области:             0x%lx\n", (long) buffer);
        if (mprotect(buffer + pagesize * 2, pagesize,
                    PROT_READ) == -1)
            handle_error("mprotect");
        for (p = buffer ; ; )
            *(p++) = 'a';
        printf("Цикл завершён\n");     /* никогда не должно случиться */
        exit(EXIT_SUCCESS);
    }

СМОТРИТЕ ТАКЖЕ
mmap(2), sysconf(3), pkeys(7)