NAME

setns - 스레드를 네임스페이스로 재연계하기

SYNOPSIS

#define _GNU_SOURCE         /* feature_test_macros(7) 참고 */
#include <sched.h>

int setns(int fd, int nstype);

DESCRIPTION

nsetns() 시스템 호출을 통해 호출 스레드가 다른 네임스페이스로 이동할 수 있다. fd 인자는 다음 중 하나이다.

fd/proc/[pid]/ns/ 링크를 가리킴

fd/proc/[pid]/ns/ 링크를 가리키는 경우 setns()는 호출 스레드를 그 링크에 연계된 네임스페이스로 다시 연계한다. 그때 nstype 인자별 제약이 있으면 적용을 받는다. 이렇게 사용할 때 각 setns() 호출은 호출자의 네임스페이스 소속 중 한 가지만 바꾼다.

nstype 인자는 호출 스레드를 어떤 종류의 네임스페이스로 재연계할 수 있는지 나타낸다. 이 인자는 다음 값들 중 하나일 수 있다.

0
어떤 종류의 네임스페이스라도 참여를 허용한다.
CLONE_NEWCGROUP (리눅스 4.6부터)
fd가 cgroup 네임스페이스를 가리켜야 한다.
CLONE_NEWIPC (리눅스 3.0부터)
fd가 IPC 네임스페이스를 가리켜야 한다.
CLONE_NEWNET (리눅스 3.0부터)
fd가 네트워크 네임스페이스를 가리켜야 한다.
CLONE_NEWNS (리눅스 3.8부터)
fd가 마운트 네임스페이스를 가리켜야 한다.
CLONE_NEWPID (리눅스 3.8부터)
fd가 자손 PID 네임스페이스를 가리켜야 한다.
CLONE_NEWTIME (리눅스 5.8부터)
fd가 시간 네임스페이스를 가리켜야 한다.
CLONE_NEWUSER (리눅스 3.8부터)
fd가 사용자 네임스페이스를 가리켜야 한다.
CLONE_NEWUTS (리눅스 3.0부터)
fd가 UTS 네임스페이스를 가리켜야 한다.

fd가 어떤 종류의 네임스페이스를 가리키는지 호출자가 알고 있다면 (또는 신경쓰지 않는다면) nstype을 0으로 지정하면 충분하다. nstype을 0 아닌 값으로 지정하는 게 유용한 경우는 fd가 가리키는 네임스페이스의 종류를 호출자가 알지 못하고 그 네임스페이스가 특정 종류임을 보장하고 싶을 때이다. (다른 프로세스가 파일 디스크립터를 열고서 가령 유닉스 도메인 소켓을 통해 호출자에게 전달했다면 fd가 가리키는 네임스페이스 종류를 호출자가 알지 못할 수도 있다.)

fd가 PID 파일 디스크립터

리눅스 5.8부터 fdpidfd_open(2)이나 clone(2)으로 얻은 PID 파일 디스크립터를 가리킬 수 있다. 이렇게 사용할 때 setns()fd가 가리키는 스레드와 같은 한 가지 이상의 네임스페이스로 호출 스레드를 원자적으로 옮긴다.

nstype 인자는 위에 나열한 CLONE_NEW* 네임스페이스 상수들을 하나 이상 OR 해서 지정하는 비트 마스크다. nstype에 지정된 대상 스레드의 네임스페이스 각각으로 호출자가 옮겨진다. 나머지 네임스페이스들에서 호출자의 소속은 그대로 유지된다.

예를 들어 다음 코드는 PID 1234와 같은 사용자, 네트워크, UTS 네임스페이스로 호출자를 옮기되, 호출자의 다른 네임스페이스 소속은 그대로 두게 된다.

int fd = pidfd_open(1234, 0);
setns(fd, CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWUTS);

각 네임스페이스 종류의 세부 사항

각 네임스페이스 종류로 재연계할 때 다음 세부 사항 및 제약에 유의해야 한다.

사용자 네임스페이스

스스로를 사용자 네임스페이스에 재연계하려는 프로세스는 대상 사용자 네임스페이스 내에서 CAP_SYS_ADMIN 역능이 있어야 한다. (이 때문에 자손 사용자 네임스페이스에만 참여가 가능하다.) 프로세스가 사용자 네임스페이스에 성공적으로 참여하면 사용자 ID 및 그룹 ID와 상관없이 그 네임스페이스 내의 모든 역능이 인가된다.

다중 스레드 프로세스는 setns()로 사용자 네임스페이스를 바꿀 수 없다.

setns()를 이용해 호출자의 현재 사용자 네임스페이스로 재진입하는 것이 허용되지 않는다. 이는 역능을 버린 호출자가 setns() 호출을 통해 그 역능을 다시 얻는 것을 막는다.

보안적 이유로 한 프로세스가 다른 프로세스와 파일 시스템 관련 속성(clone(2) CLONE_FS 플래그로 공유를 제어하는 속성들)을 공유하고 있으면 새 사용자 네임스페이스에 참여할 수 없다.

사용자 네임스페이스에 대한 더 자세한 내용은 user_namespaces(7)를 보라.

마운트 네임스페이스

마운트 네임스페이스를 바꾸기 위해선 호출자 프로세스가 자기 사용자 네임스페이스에서 CAP_SYS_CHROOTCAP_SYS_ADMIN 역능을 가지고 있고 대상 마운트 네임스페이스를 소유한 사용자 네임스페이스에서 CAP_SYS_ADMIN 역능을 가지고 있어야 한다.

다른 프로세스와 파일 시스템 관련 속성(clone(2)CLONE_FS 플래그로 공유를 통제하는 속성들)을 공유 중이면 새 마운트 네임스페이스에 참여할 수 없다.

사용자 네임스페이스와 마운트 네임스페이스의 상호작용에 대한 자세한 내용은 user_namespaces(7)를 보라.

PID 네임스페이스

새 PID 네임스페이스에 재연계하기 위해선 호출자가 자기 사용자 네임스페이스와 대상 PID 네임스페이스를 소유한 사용자 네임스페이스 모두에서 CAP_SYS_ADMIN 역능을 가지고 있어야 한다.

PID 네임스페이스 재연계는 다른 네임스페이스들과 좀 다르다. 호출 스레드에 PID 네임스페이스를 재연계하면 이후 생성되는 호출자의 자식 프로세스들이 들어갈 PID 네임스페이스를 바꿀 뿐이다. 즉, 호출자 자체의 PID 네임스페이스는 바뀌지 않는다.

대상 PID 네임스페이스가 호출자의 현재 PID 네임스페이스와 같거나 그 자손(자식, 손자 등)인 경우에만 PID 네임스페이스 재연계가 허용된다.

PID 네임스페이스에 대한 더 자세한 내용은 pid_namespaces(7)를 보라.

cgroup 네임스페이스

새 cgroup 네임스페이스에 재연계하기 위해선 호출자가 자기 사용자 네임스페이스와 대상 cgroup 네임스페이스를 소유한 사용자 네임스페이스 모두에서 CAP_SYS_ADMIN 역능을 가지고 있어야 한다.

setns()로 호출자의 cgroup 네임스페이스를 바꾸어도 호출자의 cgroup 멤버십은 바뀌지 않는다.

네트워크, IPC, 시간, UTS 네임스페이스
새 네트워크 내지 IPC, 시간, UTS 네임스페이스에 재연계하기 위해선 호출자가 자기 사용자 네임스페이스와 대상 네임스페이스를 소유한 사용자 네임스페이스 모두에서 CAP_SYS_ADMIN 역능을 가지고 있어야 한다.

RETURN VALUE

성공 시 setns()는 0을 반환한다. 실패 시 -1을 반환하며 오류를 나타내도록 errno를 설정한다.

ERRORS

EBADF
fd가 유효한 파일 디스크립터가 아니다.
EINVAL
fd가 가리키는 네임스페이스의 종류가 nstype에 지정한 것과 일치하지 않는다.
EINVAL
지정한 네임스페이스로 스레드를 재연계하는 데 문제가 있다.
EINVAL
호출자가 선조(부모, 조부모, 등) PID 네임스페이스에 참여하려고 했다.
EINVAL
호출자가 이미 참여해 있는 사용자 네임스페이스에 참여하려고 시도했다.
EINVAL
호출자가 다른 프로세스와 파일 시스템(CLONE_FS) 상태를 (특히 루트 디렉터리를) 공유하면서 새 사용자 네임스페이스에 참여하려고 했다.
EINVAL
호출자가 다중 스레드인데 새 사용자 네임스페이스에 참여하려고 했다.
EINVAL
fd가 PID 파일 디스크립터인데 nstype이 유효하지 않다. (가령 0이다.)
ENOMEM
지정한 네임스페이스를 바꾸는 데 필요한 메모리를 할당할 수 없다.
EPERM
호출 스레드가 이 동작에 필요한 역능을 가지고 있지 않다.
ESRCH
fd가 PID 파일 디스크립터인데 가리키는 프로세스가 더이상 존재하지 않는다. (즉 종료되어 wait이 이뤄졌다.)

VERSIONS

리눅스 커널 3.0에서 setns() 시스템 호출이 처음 등장했다. glibc 버전 2.14에서 라이브러리 지원이 추가되었다.

CONFORMING TO

setns() 시스템 호출은 리눅스 전용이다.

NOTES

마법 링크 /proc/[pid]/ns/에 대한 자세한 설명은 namespaces(7)를 보라.

clone(2)으로 새 스레드를 생성할 때 공유할 수 있는 속성들을 모두 setns()로 바꿀 수 있는 것은 아니다.

EXAMPLES

아래 프로그램은 둘 이상의 인자를 받는다. 첫 번째 인자는 기존 /proc/[pid]/ns/ 디렉터리 내의 네임스페이스 파일 경로를 나타낸다. 나머지 인자들은 명령과 그 인자들을 지정한다. 프로그램은 네임스페이스 파일을 열고, setns()를 이용해 그 네임스페이스에 참여하고, 지정한 명령을 그 네임스페이스 안에서 실행한다.

다음 셸 세션은 (ns_exec라는 바이너리로 컴파일 한) 이 프로그램을 (newuts라는 바이너리로 컴파일 한) clone(2) 맨 페이지의 CLONE_NEWUTS 예시 프로그램과 함께 사용하는 것을 보여 준다.

먼저 clone(2)의 예시 프로그램을 배경으로 실행한다. 그 프로그램은 별도의 UTS 네임스페이스에서 자식을 생성한다. 자식이 자기 네임스페이스에서 호스트명을 바꾼 후 두 프로세스 모두 자기 UTS 네임스페이스 내의 호스트명을 표시하며, 그래서 둘이 어떻게 다른지 볼 수 있다.

$ su                   # 네임스페이스 작업에 특권 필요함
Password:
# ./newuts bizarro &
[1] 3549
clone() returned 3550
uts.nodename in child:  bizarro
uts.nodename in parent: antero
# uname -n             # 셸에서 호스트명 확인
antero

다음으로 아래와 같이 프로그램을 실행해서 셸을 실행하게 한다. 그 셸 내에서 호스트명이 첫 번째 프로그램이 만든 자식이 설정한 것과 같은지 확인한다.

# ./ns_exec /proc/3550/ns/uts /bin/bash
# uname -n             # ns_exec가 시작한 셸에서 실행
bizarro

프로그램 소스

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)

int
main(int argc, char *argv[])
{
    int fd;

    if (argc < 3) {
        fprintf(stderr, "%s /proc/PID/ns/FILE cmd args...\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 네임스페이스 파일 디스크립터 얻기. 이후 실행되는 프로그램으로
       파일 디스크립터가 상속되지 않도록 하기 위해 O_CLOEXEC를 써서
       연다. */

    fd = open(argv[1], O_RDONLY | O_CLOEXEC);
    if (fd == -1)
        errExit("open");

    if (setns(fd, 0) == -1)       /* 그 네임스페이스에 참여 */
        errExit("setns");

    execvp(argv[2], &argv[2]);    /* 네임스페이스에서 명령 실행 */
    errExit("execvp");
}

SEE ALSO

nsenter(1), clone(2), fork(2), unshare(2), vfork(2), namespaces(7), unix(7)


2020-08-13