컨테이너 하나를 새 PID/mount namespace로 격리하고 cgroup으로 메모리 상한까지 걸어두면 안전하다고 믿기 쉽다. Linux namespace는 무엇을 볼 수 있는지, cgroup v2는 얼마나 쓸 수 있는지를 정하지만, 그 프로세스가 어떤 커널 syscall을 호출할 수 있는지는 둘 다 건드리지 않는다. unshare로 새 namespace에 들어가도 ptrace()나 mount()를 부르는 syscall 자체는 여전히 막히지 않는다 — 커널 취약점과 결합하면 격리를 우회할 수 있는 지점이다.
이 빈틈을 막는 게 seccomp-BPF다. 그런데 "seccomp이 syscall을 막는다"는 말을 들으면 흔히 open("/etc/shadow", ...)처럼 경로까지 검사할 거라 기대한다. 실제로 그런 규칙을 걸면 절대 동작하지 않는다. 왜 그런지, 그리고 이게 버그가 아니라 의도된 설계라는 걸 따라가 본다.
seccomp을 SECCOMP_MODE_FILTER로 켜면 커널은 매 syscall 진입 시 프로세스가 등록해둔 classic BPF(cBPF) 프로그램을 실행해 허용·거부를 판정한다. Docker·containerd·Kubernetes가 컨테이너에 기본으로 씌우는 프로파일도 이 모드로 동작한다.
cBPF는 A(누산기)·X(보조 레지스터)·M[0..15](스크래치 메모리) 세 저장소만 가진 단순한 레지스터 머신이다. 필터에 넘어오는 입력은 struct seccomp_data 하나뿐인데, 커널 문서(userspace-api/seccomp_filter.rst)는 이 구조체가 "system call number, arguments, and other metadata"를 담는다고 설명한다.
"인자를 담는다"는 말 때문에 open()의 두 번째 인자인 경로 문자열도 검사 대상이라고 오해하기 쉽다. 그런데 같은 문서는 곧이어 이렇게 못 박는다: "BPF programs may not dereference pointers ... does not contain pointers to memory." open(path, flags)을 필터가 볼 때 path는 유저 공간 메모리를 가리키는 정수(포인터값)일 뿐이고, 그 주소가 가리키는 문자열 내용은 필터가 읽을 방법이 없다. 레지스터에 담긴 정수 — syscall 번호, flags, 길이 — 만 평가 대상이다.
이건 성능을 아끼려는 제약이 아니다. 필터가 포인터를 따라가 인자 내용을 검사할 수 있었다면 "검사 시점의 메모리"와 "실행 시점의 메모리"가 달라질 수 있는 TOCTOU(time-of-check-to-time-of-use) 창이 생긴다. 멀티스레드 프로세스라면 한 스레드가 안전한 경로로 syscall을 걸어 필터를 통과시킨 뒤, 필터 평가와 실제 syscall 실행 사이의 짧은 틈에 다른 스레드가 같은 메모리를 위험한 경로로 덮어쓸 수 있다. seccomp은 그 능력 자체를 주지 않음으로써 이 공격 클래스를 구조적으로 차단한다. 경로 기반 통제는 대신 AppArmor·SELinux 같은 LSM의 몫으로 남는다.
직접 확인해보는 가장 쉬운 방법은 raw cBPF 대신 libseccomp으로 필터를 짜보는 것이다. libseccomp은 규칙을 사람이 읽을 수 있는 API로 표현하고 내부에서 cBPF로 컴파일한다.
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
// 가능: open()의 정수 플래그 인자(O_CREAT)를 검사해서 차단
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EACCES), SCMP_SYS(open), 1,
SCMP_A1(SCMP_CMP_MASKED_EQ, O_CREAT, O_CREAT));
// 불가능: "경로가 /etc/shadow일 때만" 조건은 표현할 방법이 없다.
// path는 포인터일 뿐이고, BPF는 그 안의 문자열을 읽지 못한다.
seccomp_load(ctx);
SCMP_A1로 두 번째 레지스터 인자(flags)는 정수라서 검사할 수 있지만, 첫 번째 인자(path)를 문자열로 비교하는 API는 라이브러리에 존재하지 않는다 — 커널이 그 기능 자체를 지원하지 않기 때문이다. 이 대조를 코드로 놓고 보면 "인자를 본다"는 말이 실제로 무엇을 뜻하는지 분명해진다.
seccomp-BPF는 레지스터에 담긴 정수만 보는 대가로 TOCTOU 공격을 원천적으로 차단하는 설계다. 경로 기반 접근 제어가 필요하면 seccomp이 아니라 LSM을 봐야 한다는 결론도 이 제약에서 바로 나온다.
다음에 파고들 만한 지점은 필터를 여러 개 겹쳐 걸었을 때(런타임 기본 프로파일 위에 사용자 프로파일을 추가하는 경우처럼)의 최종 판정이다 — seccomp은 필터를 append-only로 쌓고, 그중 가장 강한 조치를 채택하는 우선순위 규칙으로 이걸 결정한다.