prctl 사용법

프로세스 이름 표시, 부모가 죽었을 때 자식 정리, 권한 상승 차단, 코어 덤프 생성 여부처럼 프로세스 자체의 커널 레벨 동작을 세밀하게 제어하고 싶을 때가 있다. 리눅스는 이런 잡다한 제어를 위해 옵션마다 별도 시스템 콜을 만드는 대신 prctl() 하나에 PR_* 옵션 상수를 넘기는 방식을 쓴다. 이 글에서는 실무에서 자주 쓰는 옵션 네 가지를 실제 C 코드로 테스트한 결과와 함께 정리한다.

prctl 기본 형태

#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3,
          unsigned long arg4, unsigned long arg5);

옵션에 따라 arg2~arg5의 의미가 완전히 달라진다. 자주 쓰는 옵션은 다음과 같다.

옵션대상효과
PR_SET_NAME / PR_GET_NAME현재 스레드16바이트(널 포함) 이름 설정/조회, ps·top에 표시됨
PR_SET_PDEATHSIG현재 스레드부모 프로세스 종료 시 받을 시그널 지정
PR_SET_NO_NEW_PRIVS현재 스레드(비가역)execve로 setuid/setgid·capability를 통한 권한 상승 차단
PR_SET_DUMPABLE현재 프로세스코어 덤프 생성 여부, /proc/pid/mem 접근 권한에 영향

스레드 이름 지정 (PR_SET_NAME)

스레드가 여러 개인 프로그램을 ps -Ttop으로 볼 때 전부 실행 파일 이름으로만 보이면 어떤 스레드가 뭘 하는지 구분하기 어렵다. PR_SET_NAME으로 현재 스레드에 이름을 붙일 수 있다.

#include <stdio.h>
#include <sys/prctl.h>
#include <unistd.h>

int main(void) {
    char before[16] = {0};
    prctl(PR_GET_NAME, before);
    printf("before: %s\n", before);

    prctl(PR_SET_NAME, "worker-1");

    char after[16] = {0};
    prctl(PR_GET_NAME, after);
    printf("after:  %s\n", after);
    pause();
    return 0;
}
$ ./prctl_name &
before: prctl_name
after:  worker-1
$ ps -p $! -o pid,comm
    PID COMMAND
   7192 worker-1

16바이트 제한(널 종료 포함 15자)이라 긴 이름은 잘린다. 이름은 스레드 단위 속성이라, 멀티스레드 프로그램이면 스레드마다 각자 prctl을 호출해야 한다.

부모 종료 시 시그널 받기 (PR_SET_PDEATHSIG)

부모가 자식을 fork만 해두고 wait이나 종료 처리를 안 하면, 부모가 먼저 죽었을 때 자식이 고아 프로세스로 계속 남는다. PR_SET_PDEATHSIG를 걸어두면 부모가 죽는 즉시 커널이 자식에게 지정한 시그널을 보내준다.

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/prctl.h>

void handler(int sig) {
    printf("child: received signal %d, exiting\n", sig);
    _exit(0);
}

int main(void) {
    pid_t pid = fork();
    if (pid == 0) {
        signal(SIGTERM, handler);
        prctl(PR_SET_PDEATHSIG, SIGTERM);
        printf("child: pid=%d, parent=%d, waiting...\n", getpid(), getppid());
        pause();
        return 0;
    }
    printf("parent: pid=%d, child=%d, exiting now\n", getpid(), pid);
    return 0; /* wait/kill 없이 바로 종료 */
}
$ ./prctl_pdeathsig
parent: pid=7233, child=7234, exiting now
child: pid=7234, parent=7233, waiting...
child: received signal 15, exiting

부모가 자식을 kill하지 않고 그냥 종료했는데도 자식이 SIGTERM(15)을 받고 스스로 정리된 것이 확인된다.

권한 상승 차단 (PR_SET_NO_NEW_PRIVS)

seccomp 필터나 샌드박스를 구현할 때, 이후 execve로 setuid/setgid 바이너리나 파일 capability를 통해 권한이 올라가는 걸 원천 차단하고 싶으면 PR_SET_NO_NEW_PRIVS를 켠다. 커널의 /proc/[pid]/status NoNewPrivs 필드에도 그대로 반영된다.

#include <stdio.h>
#include <unistd.h>
#include <sys/prctl.h>

int main(void) {
    printf("before: PR_GET_NO_NEW_PRIVS = %d\n",
           prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0));
    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    printf("after:  PR_GET_NO_NEW_PRIVS = %d, pid=%d\n",
           prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0), getpid());
    pause();
    return 0;
}
$ ./prctl_nnp &
before: PR_GET_NO_NEW_PRIVS = 0
after:  PR_GET_NO_NEW_PRIVS = 1, pid=7304
$ grep NoNewPrivs /proc/7304/status
NoNewPrivs:	1

코어 덤프 생성 여부 제어 (PR_SET_DUMPABLE)

민감한 데이터를 다루는 프로세스가 크래시 났을 때 메모리 내용이 코어 파일로 디스크에 남는 걸 막고 싶다면 PR_SET_DUMPABLE을 0으로 설정한다. SIGSEGV를 직접 유발해서 코어 덤프 생성 여부를 비교했다.

#include <stdio.h>
#include <sys/prctl.h>

int main(int argc, char **argv) {
    if (argc > 1 && argv[1][0] == '0') {
        prctl(PR_SET_DUMPABLE, 0);
    }
    printf("dumpable=%d, crashing now\n", prctl(PR_GET_DUMPABLE));
    int *p = NULL;
    *p = 1; /* SIGSEGV */
    return 0;
}
$ ulimit -c unlimited
$ ./prctl_dumpable_crash 1     # dumpable=1 (기본값)
dumpable=1, crashing now
Segmentation fault (core dumped)

$ ./prctl_dumpable_crash 0     # PR_SET_DUMPABLE(0) 호출
dumpable=0, crashing now
Segmentation fault

같은 SIGSEGV인데도 dumpable=0일 때는 셸이 “core dumped” 표시를 하지 않는다 — 실제로 코어가 생성되지 않았다는 뜻이다.

주의사항

  • PR_SET_PDEATHSIG는 시그널을 보낸 스레드 기준으로 설정된다. 그 스레드가 아니라 다른 스레드가 먼저 죽는 경우, 또는 setuid 바이너리를 execve한 경우처럼 예외적으로 리셋되는 케이스가 있으니 정확한 조건은 man 페이지를 확인한다.
  • PR_SET_NO_NEW_PRIVS는 한 번 켜면 그 프로세스와 이후 자식들에게 계속 상속되며 되돌릴 수 없다.
  • PR_SET_DUMPABLE은 setuid/setgid 실행 파일을 execve하면 커널이 보안을 위해 자동으로 0으로 리셋한다. 직접 0으로 설정하지 않아도 이런 경우 코어 덤프가 안 남을 수 있다.

마무리

prctl은 옵션 하나하나가 사실상 별개의 기능이라 필요한 옵션만 그때그때 man 페이지에서 찾아 쓰게 된다. 그래도 이름 지정·부모 사망 처리·권한 상승 차단·코어 덤프 제어 네 가지는 데몬이나 워커 프로세스를 만들 때 반복적으로 마주치는 조합이라 기본으로 알아두면 좋다.

참고

답글 남기기