NAME

mprotect, pkey_mprotect - 메모리 영역에 보호 설정하기

SYNOPSIS

#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);

DESCRIPTION

mprotect()는 [addr, addr+len-1] 구간 주소 범위를 일부라도 담고 있는 호출 프로세스의 메모리 페이지들에 대한 접근 보호를 변경한다. addr이 페이지 경계에 맞게 정렬되어 있어야 한다.

호출 프로세스에서 그 보호를 위반하는 방식으로 메모리에 접근하려 시도하면 커널이 그 프로세스에 SIGSEGV 시그널을 생성한다.

protPROT_NONE이거나 다음 목록의 다른 값들을 비트 OR 한 것이다.

PROT_NONE
메모리에 전혀 접근할 수 없다.
PROT_READ
메모리를 읽을 수 있다.
PROT_WRITE
메모리를 변경할 수 있다.
PROT_EXEC
메모리를 실행할 수 있다.
PROT_SEM (리눅스 2.5.7부터)
메모리를 원자 연산에 쓸 수 있다. 이 플래그는 futex(2) 구현의 일부로서 (FUTEX_WAIT 같은 명령에 필요한 원자 연산 수행이 가능함을 보장하기 위해) 도입되었는데 현재는 어느 아키텍처에서도 쓰지 않는다.
PROT_SAO (리눅스 2.6.26부터)
메모리에 강한 접근 순서(strong access ordering)가 있어야 한다. 이 기능은 PowerPC 아키텍처에 한정된 것이다. (아키텍처 명세 2.06 버전에서 SAO CPU 기능을 추가하며 POWER 7이나 PowerPC A2 등에서 사용 가능하다.)

추가로 (리눅스 2.6.0부터) prot에 다음 플래그들 중 하나를 설정할 수 있다.

PROT_GROWSUP
위로 자라는 매핑의 끝점까지 보호 모드를 적용한다. (스택이 위로 자라는, 가령 HP-PARISC 같은 아키텍처에서 스택 영역에 그런 매핑을 만든다.)
PROT_GROWSDOWN
아래로 자라는 매핑의 시작점까지 보호 모드를 적용한다. (스택 세그먼트이거나 MMAP_GROWSDOWN 플래그를 설정해 맵 한 세그먼트일 것이다.)

mprotect()처럼 pkey_mprotect()addrlen으로 지정한 페이지들의 보호를 변경한다. pkey 인자는 그 메모리에 할당한 보호 키(pkeys(7) 참고)를 나타낸다. 보호 키는 pkey_alloc(2)으로 할당해서 pkey_mprotect()에게 전달해야 한다. 이 시스템 호출의 사용례는 pkeys(7)를 보라.

RETURN VALUE

성공 시 mprotect()pkey_mprotect()는 0을 반환한다. 오류 시 이 시스템 호출들은 -1을 반환하며 오류를 나타내도록 errno를 설정한다.

ERRORS

EACCES
지정한 접근권을 메모리에 줄 수 없다. 예를 들어 읽기 전용으로만 접근할 수 있는 파일을 mmap(2) 하고서 mprotect()에게 PROT_WRITE 표시를 하라고 하는 경우에 발생할 수 있다.
EINVAL
addr이 유효한 포인터가 아니거나 시스템 페이지 크기의 배수가 아니다.
EINVAL
(pkey_mprotect()) pkeypkey_alloc(2)으로 할당된 것이 아니다.
EINVAL
protPROT_GROWSUPPROT_GROWSDOWN을 모두 지정했다.
EINVAL
prot에 유효하지 않은 플래그를 지정했다.
EINVAL
(PowerPC 아키텍처) protPROT_SAO를 지정했지만 SAO 하드웨어 기능이 사용 가능하지 않다.
ENOMEM
내부 커널 구조체를 할당할 수 없다.
ENOMEM
[addr, addr+len-1] 구간 내의 주소가 프로세스의 주소 공간에서 유효하지 않거나, 한 개 이상의 맵 되지 않은 페이지를 나타낸다. (커널 2.4.19 전에서는 이 경우에 잘못해서 EFAULT 오류를 생성했다.)
ENOMEM
메모리 영역의 보호를 변경하면 상이한 속성(가령 읽기 보호와 읽기/쓰기 보호)의 매핑 총개수가 허용 최대치를 초과하게 된다. (예를 들어 현재 PROT_READ|PROT_WRITE로 보호하는 영역 중간의 어느 범위를 PROT_READ 보호로 만들면 양쪽의 읽기/쓰기 매핑과 가운데의 읽기 전용 매핑을 합쳐서 세 개 매핑이 생긴다.)

VERSIONS

리눅스 4.9에서 pkey_mprotect()가 처음 등장했다. glibc 2.27에서 라이브러리 지원이 추가되었다.

CONFORMING TO

mprotect(): POSIX.1-2001, POSIX.1-2008, SVr4. POSIX에서는 mmap(2)을 통해 얻은 것이 아닌 메모리 영역에 적용 시 mprotect()의 동작 방식이 명세되어 있지 않다고 한다.

pkey_mprotect()는 이식성 없는 리눅스 확장이다.

NOTES

리눅스에서는 프로세스 주소 공간 내의 (커널 vsyscall 영역을 제외한) 어느 주소에도 mprotect() 호출을 항상 허용한다. 특히 이를 이용해 기존의 코드 매핑을 쓰기 가능으로 바꿀 수 있다.

PROT_EXECPROT_READ와는 다른 어떤 효력이 있는지 여부는 프로세서 아키텍처, 커널 버전, 프로세스 상태에 따라 정해진다. 프로세스 인격 플래그(personality(2) 참고)에 READ_IMPLIES_EXEC가 설정돼 있으면 PROT_READ를 지정할 때 PROT_EXEC가 암묵적으로 추가된다.

일부 하드웨어 아키텍처(가령 i386)에서는 PROT_WRITEPROT_READ를 함의한다.

POSIX.1에서는 구현에서 prot에 지정된 것 이외의 접근을 허용할 수도 있되, 최소한으로는 PROT_WRITE가 설정된 경우에만 쓰기 접근을 허용할 수 있고 PROT_NONE이 설정된 경우에는 어떤 접근도 허용해선 안 된다고 한다.

응용에서 mprotect()pkey_mprotect()를 섞어서 사용할 때는 주의해야 한다. x86에서 protPROT_EXEC로 설정해서 mprotect()를 쓰면 커널에서 암묵적으로 pkey를 할당해서 메모리에 설정할 수도 있다. 단 pkey가 앞서 0이었을 때만 그렇게 한다.

하드웨어에서 보호 키를 지원하지 않는 시스템에서도 pkey_mprotect()를 쓸 수는 있지만 pkey를 -1로 설정해야 한다. 그렇게 호출할 때 pkey_mprotect()의 동작은 mprotect()와 동등하다.

EXAMPLES

아래 프로그램은 mprotect() 사용 방식을 보여 준다. 프로그램에서 메모리 페이지 4개를 할당해서 그 중 세 번째를 읽기 전용으로 만든 다음 할당 영역을 위로 훑으면서 바이트를 변경하는 루프를 실행한다.

다음은 프로그램 실행 시 볼 수 있을 출력 예이다.

$ ./a.out
Start of region:        0x804c000
Got SIGSEGV at address: 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()가 비동기 시그널 안전이 아니기 때문이다.
       signal-safety(7) 참고. 그럼에도 불구하고 핸들러가 호출된
       것을 간단히 보여 주기 위해 여기에 printf()를 사용한다. */

    printf("Got SIGSEGV at address: %p\n", si->si_addr);
    exit(EXIT_FAILURE);
}

int
main(int argc, char *argv[])
{
    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("Start of region:        %p\n", buffer);

    if (mprotect(buffer + pagesize * 2, pagesize,
                PROT_READ) == -1)
        handle_error("mprotect");

    for (char *p = buffer ; ; )
        *(p++) = 'a';

    printf("Loop completed\n");     /* 절대 여기까지 오지 않음 */
    exit(EXIT_SUCCESS);
}

SEE ALSO

mmap(2), sysconf(3), pkeys(7)


2021-03-22