리눅스 커널에 컨테이너 객체가 생길까 — 조셉 바식의 ‘셀(cell)’ 제안

리눅스 커널에는 “컨테이너”라는 객체가 없습니다. 커널은 네임스페이스, cgroup, seccomp 같은 부품만 제공합니다. 조립은 Docker, Podman, LXC 같은 사용자 공간 도구가 맡습니다. 그래서 커널은 “이 프로세스가 어느 컨테이너에 속하는가”라는 질문에 답할 수 없습니다.

Btrfs 개발자 조셉 바식(Josef Bacik)이 이 구조를 바꾸는 “셀(cell)” 패치를 linux-fsdevel에 올렸습니다. 이 글에서는 지금 방식의 문제를 직접 재현합니다. 그리고 셀 API가 그 문제를 어떻게 막으려 하는지 정리합니다.

지금 방식: 부품을 하나씩 조립한다

바식은 프로세스가 컨테이너에 “일부만 들어간” 상태에서 버그가 많이 나온다고 지적했습니다. 바깥 자원이 컨테이너 안으로 새어 들어가기도 쉽습니다. unshare로 컨테이너를 흉내 내면 두 문제를 모두 볼 수 있습니다. 아래 실습은 Ubuntu 24.04(util-linux 2.39.3)에서 root 없이 실행했습니다.

echo "host secret" > secret.txt
exec 7<secret.txt            # 호스트 쪽에서 fd 7을 연다

# user/pid/mount/uts만 새로 만든다. net/ipc/cgroup은 지정하지 않았다
unshare -U -r -p -f -m -u --mount-proc sh -c \
  'hostname box; echo "inside: hostname=$(hostname) pid=$$";
   echo "inherited fd7: $(cat <&7)"; sleep 30' > inside.log 2>&1 &
exec 7<&-
sleep 1
CPID=$(pgrep -n -f "sleep 30")
cat inside.log

for n in user pid mnt uts net ipc cgroup; do
  a=$(readlink /proc/self/ns/$n); b=$(readlink /proc/$CPID/ns/$n)
  [ "$a" = "$b" ] && s=공유 || s=분리
  printf "%-7s %-22s %s\n" $n "$b" $s
done
inside: hostname=box pid=1
inherited fd7: host secret
user    user:[4026532219]      분리
pid     pid:[4026532222]       분리
mnt     mnt:[4026532220]       분리
uts     uts:[4026532221]       분리
net     net:[4026531833]       공유
ipc     ipc:[4026532196]       공유
cgroup  cgroup:[4026532207]    공유

호스트에서 연 fd 7이 그대로 컨테이너 안으로 들어갔습니다. 네트워크, IPC, cgroup은 지정하지 않았으므로 호스트와 공유합니다.

이미 실행 중인 프로세스가 이 네임스페이스 중 일부에만 들어갈 수도 있습니다. nsenter로 user와 uts 네임스페이스에만 들어가 봅니다.

nsenter -t $CPID -U -u --preserve-credentials \
  sh -c 'echo "hostname=$(hostname) pid=$$"; ps -e | wc -l'
hostname=box pid=86868
39

호스트 이름은 컨테이너 것을 쓰지만 PID와 프로세스 목록은 호스트 것을 봅니다. 커널 입장에서 이 프로세스는 컨테이너 안도, 밖도 아닙니다. 네임스페이스 inode 번호를 하나씩 비교하는 것 말고는 소속을 판단할 방법이 없습니다.

셀 API: 비어 있는 상태에서 시작한다

셀은 처음 만들 때 아무것도 없습니다. 프로세스도, 파일 시스템도, 네트워크도 없습니다. 만든 쪽이 명시적으로 넣은 자원만 셀 안에 존재합니다. 셀을 만드는 데는 권한이 필요 없습니다.

int cell_create(int parent, unsigned int flags);   /* cellfd 반환 */
int cell_configure(int cellfd, unsigned int cmd, const char *key,
                   const void *value, int aux);
int cell_kill(int cellfd, unsigned int flags);
int pidfd_configure(int pidfd, unsigned int cmd, const char *key,
                    const void *value, int aux);

parent는 현재 CELL_SELF만 허용하고 flags는 0이어야 합니다. cellfd는 복제할 수 없고, 닫으면 셀과 그 안의 모든 것이 즉시 종료됩니다.

cell_configure() 명령키의미
CELL_SET_STRINGhostname셀 안의 호스트 이름
CELL_SET_STRINGnetnew: 만든 쪽이 설정할 새 네트워크 네임스페이스. own: 셀이 직접 설정. 지정하지 않으면 AF_UNIX만 쓸 수 있다
CELL_SET_BINARYids1: 만든 사용자 UID로 실행(root는 불가). 65536: 32비트 UID 공간 상위에서 고유 범위를 할당
CELL_SET_FDroot / net / budget루트 파일 시스템 네임스페이스, 네트워크 네임스페이스, cgroup 디렉터리. budget은 필수
CELL_CMD_SEAL–설정을 끝내고 잠근다. 이후 변경 불가

프로세스는 셀 안에서 태어난다

셀에는 setns()로 들어갈 수 없습니다. 위 실습의 nsenter 같은 동작이 원천적으로 막힙니다. 프로세스는 셀 안에서 새로 만들어야 하고, 한 번 들어가면 나올 수 없습니다.

순서호출설명
1pidfd_open(cellfd, PIDFD_EMPTY)셀 안에 아직 실행되지 않은 “배아” 프로세스를 만든다
2PIDFD_SET_STRING exec셀의 마운트 네임스페이스 기준 실행 파일 경로
3PIDFD_SET_BINARY argv/envp/cred인자, 환경 변수, 셀 안의 UID/GID
4PIDFD_SET_FD넘길 fd를 하나씩 지정한다. 지정하지 않은 fd는 없다
5PIDFD_CMD_START실행을 시작한다. 첫 프로세스가 셀의 init이 된다

4단계가 위 실습의 fd 7 누수에 대한 답입니다. 셀 안의 모든 프로세스에는 no_new_privs가 설정됩니다. Unix 소켓으로 셀 경계를 넘어 fd를 보내는 것도 양방향으로 막힙니다.

기존 방식과 비교

항목네임스페이스 조립셀
시작 상태호스트를 복제한 뒤 차단빈 상태에서 추가
소속 판단네임스페이스 inode 비교커널이 셀 객체로 관리
기존 프로세스 진입setns()/nsenter로 일부만 가능불가
fd 전달열린 fd가 상속됨PIDFD_SET_FD로 지정한 것만
네트워크 기본값지정하지 않으면 호스트와 공유AF_UNIX만 가능
cgroup선택셀 전용 cgroup 필수
중첩가능현재 불가(바식은 개발 중이라고 답함)

커뮤니티 반응

바식은 이 패치가 완성품이 아니라고 밝혔습니다. “논쟁할 코드를 만들려고” 올렸다는 설명입니다. 코드는 GitHub 브랜치에 설계 문서와 함께 공개돼 있습니다. LWN 댓글에서 나온 지적은 다음과 같습니다.

쟁점내용
시스템 콜 횟수키-값을 한 번에 하나씩 설정하므로 컨테이너를 대량으로 만들 때 비용이 크다. 배열로 넘기자는 제안이 나왔다
문자열 키ABI 의존성은 그대로이면서 seccomp로 걸러내기 어려워진다
fd 전달 금지Flatpak, Toolbox, 브라우저 샌드박스는 Wayland·PipeWire·D-Bus에서 fd 전달을 쓴다. 강하게 격리된 컨테이너에만 쓸 수 있게 된다
기존 설계 재사용FreeBSD jail API를 가져오지 않은 이유가 무엇인가
필요성하위 부품이 남아 있는 한 버그는 줄지 않고 시스템 콜만 늘어난다

주의사항

  • 셀 API를 지금 코드에 쓰지 마십시오. 메인라인에 머지되지 않았고, 시스템 콜 이름과 인자는 바뀔 수 있습니다.
  • 지금 런타임을 직접 만든다면 상속되는 fd를 먼저 정리하십시오. close_range()나 O_CLOEXEC를 쓰지 않으면 위 실습처럼 호스트 fd가 컨테이너로 넘어갑니다.
  • unshare로 만든 환경을 격리된 환경으로 보지 마십시오. 지정하지 않은 네임스페이스는 호스트와 공유합니다.

마무리

커널 커뮤니티는 오랫동안 컨테이너 객체를 거부했고, 이전 시도도 멀리 가지 못했습니다. 셀은 “빈 상태에서 필요한 것만 넣는다”는 원칙으로 같은 문제에 다시 도전합니다. 아직 linux-fsdevel에서만 논의 중이어서 반응은 많지 않습니다. 더 넓은 논의가 시작되면 API 모양이 크게 바뀔 가능성이 높습니다.

참고

답글 남기기