CoW로 구현하는 1ms 미만 VM 샌드박스
Copy-on-Write 메모리 포크로 1ms 이내에 뜨는 VM 샌드박스가 서버리스와 보안 격리를 어떻게 바꾸는지 살펴봅니다.

Docker 컨테이너를 시작하는 데는 약 500밀리초가 걸립니다. Firecracker microVM은 약 125밀리초, V8 아이솔레이트는 약 5밀리초 정도 걸리죠. 그런데 Copy-on-Write 메모리 포킹을 쓰는 새로운 경량 샌드박스들은 격리된 실행 환경을 1밀리초 미만, 종종 50~200마이크로초 범위에서 띄울 수 있습니다. 이 정도면 함수 호출 한 번마다 새 샌드박스를 만들어도 될 만큼 빠릅니다.
이건 단순한 점진적 개선이 아닙니다. 샌드박스가 할 수 있는 일 자체가 질적으로 달라지는 겁니다. 샌드박스 생성에 500ms가 든다면 아껴서 만들고 재사용하게 됩니다. 50μs면 신뢰할 수 없는 입력, 플러그인 호출, 사용자 요청마다 만들 수 있죠. 보안 모델도 '테넌트를 격리한다'에서 '개별 작업을 격리한다'로 바뀝니다.
Copy-on-Write의 실제 의미
Copy-on-Write(CoW)는 운영체제 기법으로, 데이터를 실제로 복사하지 않고 메모리 영역의 '복사본'을 만듭니다. 원본과 복사본이 같은 물리 메모리 페이지를 가리키며 읽기 전용으로 표시되어 있어서, 둘 다 똑같은 데이터를 봅니다. 실제 복사는 둘 중 하나가 페이지에 쓰기를 시도할 때 일어납니다. 그 순간 커널이 쓰기를 가로채서 해당 페이지 하나만 복사하고, 쓰기는 복사본에 반영됩니다.
유닉스의 fork() 시스템 콜은 1990년대부터 이 기법을 써왔습니다. 프로세스를 fork하면 자식은 부모 메모리의 완전한 복사본을 받지만, CoW 덕분에 실제로 복사되는 데이터는 없습니다. 자식이 (보통 그렇듯) 곧바로 exec()를 호출하면 메모리를 통째로 갈아엎고 CoW 페이지는 그냥 해제됩니다. 사실상 fork 비용은 거의 공짜였던 셈입니다.
// Classic fork() — copy-on-write in action
pid_t pid = fork();
// At this point:
// - Parent and child share ALL physical memory pages
// - Both have identical virtual address spaces
// - Pages are marked read-only in both
// - Zero data has been copied
if (pid == 0) {
// Child process
// Reading shared_data is free — same physical page
int value = shared_data[0];
// Writing triggers CoW — only THIS page gets copied
shared_data[0] = 42;
// Now child has its own copy of this one page
// Parent's page is unchanged
}
fork에서 샌드박스까지
CoW 샌드박스의 핵심 아이디어는 이렇습니다. VM이나 컨테이너를 처음부터 새로 부팅하는 대신, 런타임과 라이브러리, 초기 상태까지 올려둔 '템플릿' 환경을 미리 부팅해 둡니다. 그다음 CoW로 이를 포크해서 즉시 복사본을 만듭니다. 각 복사본은 템플릿이 멈춘 바로 그 상태, 즉 초기화가 끝나고 바로 실행 가능한 상태에서 시작하지만, 메모리 공간은 각자 독립적입니다.
성능 차이는 엄청납니다. 기존 VM 시작에는 커널 로드, 하드웨어 초기화, 파일시스템 마운트, init 시스템 기동, 애플리케이션 코드 로드, 런타임 초기화가 필요합니다. Firecracker처럼 공격적으로 최적화해 군더더기를 많이 덜어내도 여전히 수백 밀리초짜리 초기화 작업이 남습니다.
CoW 포킹은 이 과정을 통째로 건너뜁니다. 초기화는 템플릿이 이미 끝내 놨고, 포크는 초기화된 복사본을 마이크로초 단위로 만듭니다. '시작 비용'이라고 해봐야 새 주소 공간을 만들고 페이지 테이블 엔트리를 복제하는 커널 작업뿐입니다. 템플릿이 메모리를 얼마나 쓰든 이 작업은 대략 수천 번의 연산 정도입니다.
판도를 바꾸는 영역들
서버리스 함수
콜드 스타트는 서버리스의 골칫거리입니다. AWS Lambda는 콜드 스타트에 100~500ms가 걸리고, JVM 기반 런타임은 더 깁니다. 지연에 민감한 워크로드에서는 이걸 감당하기 어려워서, 인스턴스를 계속 따뜻하게 유지하거나(서버리스의 취지를 무색하게 만드는) 예측 불가능한 지연을 받아들여야 합니다.
CoW 샌드박스를 쓰면 콜드 스타트가 1ms 미만으로 떨어집니다. 콜드 스타트 자체가 사실상 공짜이므로 모든 호출이 '콜드 스타트'가 될 수 있습니다. 웜 풀도 필요 없고, 유휴 인스턴스가 메모리를 낭비하지도 않으며, 호출 사이에 남는 오래된 상태도 없습니다. 함수 실행마다 초기화 비용을 치르지 않고도 깨끗하고 격리된 환경을 얻게 됩니다.
플러그인 및 확장 시스템
신뢰할 수 없는 플러그인을 안전하게 실행하는 건 소프트웨어에서 가장 어려운 문제 중 하나입니다. 브라우저는 V8 아이솔레이트로 JavaScript에 대해 이 문제를 풀었습니다. 하지만 컴파일된 확장, 스크립트 언어, 바이너리 플러그인 같은 임의의 코드에 대해서는 격리 옵션이 제한적이었습니다. 컨테이너는 요청마다 격리하기엔 너무 느리고, WebAssembly는 생태계와 언어 지원이 부족합니다.
CoW 샌드박스는 그 중간 지점을 제시합니다. 호스트 환경의 격리된 복사본에서 임의의 코드를 실행하며, 설정과 해제는 1ms 미만입니다. 플러그인은 완전한 OS 환경(파일시스템, 네트워크, 라이브러리)을 보지만, 변경 사항은 격리됩니다. 샌드박스가 끝나면 모든 변경이 사라집니다. CI 시스템의 사용자 제출 코드, 노트북 환경, 빌드 도구에 특히 잘 맞습니다.
보안 격리
신뢰할 수 없는 입력을 처리할 때, 예를 들어 업로드된 PDF를 파싱하거나 사용자가 넣은 HTML을 렌더링하거나 데이터베이스 쿼리를 실행할 때, 이 작업을 격리된 샌드박스에서 돌리면 익스플로잇의 피해 범위가 제한됩니다. PDF 파서에 버퍼 오버플로가 있어도 공격자가 얻는 건 곧 파기될 일회용 샌드박스이지, 애플리케이션 서버가 아닙니다.
신뢰할 수 없는 작업마다 프로세스 격리를 하는 방식은 기존 샌드박싱으로는 오버헤드가 처리 시간을 넘어서서 현실적이지 않았습니다. PDF 파싱에 10ms가 걸린다면 컨테이너 생성에 500ms를 쓰는 건 말이 안 되죠. 하지만 CoW 샌드박스 생성에 100μs를 쓰는 건 사실상 무시할 수 있는 비용입니다.
구현의 세부 사항
실용적인 CoW 샌드박스 시스템을 만들려면 fork()를 호출하는 것 이상으로 여러 문제를 풀어야 합니다.
- 메모리 계정. CoW 때문에 메모리 사용량을 계산하기 애매해집니다. 템플릿이 1GB를 쓰고 각자 10MB씩 수정하는 복사본을 100개 포크하면, 실제 물리 메모리 사용량은 100GB가 아니라 약 2GB(공유 1GB + 고유 100 × 10MB)입니다. 커널은 공유 페이지와 개별 페이지를 추적하지만, 샌드박스별로 정확한 사용량을 얻으려면
/proc/[pid]/smaps를 파싱해야 합니다. - 파일시스템 격리. CoW는 메모리는 해결하지만 파일 쓰기는 별도의 격리가 필요합니다. 오버레이 파일시스템(overlayfs)은 파일에 대해 CoW 의미를 제공합니다. 샌드박스는 템플릿의 파일시스템을 보지만 쓰기는 별도의 레이어로 갑니다. 샌드박스가 끝나면 오버레이는 버려집니다.
- 네트워크 격리. 각 샌드박스는 간섭을 막기 위해 자체 네트워크 네임스페이스가 필요합니다. 리눅스 네임스페이스로 가능하지만 네트워크 네임스페이스 생성에는 측정 가능한 오버헤드가 있습니다. 일부 시스템은 미리 만들어 둔 네임스페이스 풀을 재사용합니다.
- 리소스 제한. 메모리를 무제한으로 할당하거나 CPU를 무제한으로 쓰는 샌드박스는 서비스 거부 공격의 통로가 됩니다. cgroups로 메모리, CPU, I/O를 제한할 수 있지만 cgroup 생성·소멸에도 오버헤드가 있습니다. 여기서도 풀링이 도움이 됩니다.
- 확실한 정리. 샌드박스가 끝나면 메모리, 파일 디스크립터, 네트워크 연결, IPC 객체 등 모든 리소스를 확실하게 정리해야 합니다. PID 네임스페이스가 도움이 됩니다. 네임스페이스의 init 프로세스를 종료하면 모든 하위 프로세스가 함께 종료됩니다.
CoW vs WebAssembly 샌드박스
WebAssembly(Wasm)는 또 다른 주요 경량 샌드박싱 기술입니다. 두 기술은 근본적으로 다른 트레이드오프를 가지고 있어서 비교해 볼 만합니다.
Wasm 샌드박스는 선형 메모리 모델을 가진 메모리 안전 가상 머신에서 코드를 실행합니다. 샌드박스는 선형 메모리 밖의 어떤 것에도 접근할 수 없습니다. 파일시스템도, 네트워크도, (WASI로 명시적으로 제공하지 않는 한) 시스템 콜도 없습니다. 매우 안전하지만 제약이 많습니다. 기존 코드를 Wasm으로 다시 컴파일해야 하고, 모든 언어가 Wasm으로 효과적으로 컴파일되는 것도 아닙니다.
CoW 샌드박스는 격리된 OS 환경에서 네이티브 코드를 실행합니다. 전체 OS 인터페이스에 접근할 수 있고(seccomp 필터로 제한할 수도 있음), 어떤 바이너리든 실행할 수 있으며, 일반 시스템 라이브러리를 사용합니다. 덜 제약적이지만 덜 안전합니다. 격리 경계가 OS 프로세스 모델이라서, Wasm의 최소한의 VM보다 공격 표면이 더 넓습니다.
Wasm을 고를 때: 샌드박싱할 코드를 직접 제어하고, 워크로드가 Wasm으로 깔끔하게 컴파일되며, 가능한 가장 강한 격리가 필요할 때입니다. CoW 샌드박스를 고를 때: 임의의 기존 바이너리를 실행해야 하고, 워크로드가 파일시스템, 네트워크, 자식 프로세스 같은 OS 수준 기능을 필요로 하며, 최소 공격 표면보다 호환성을 우선할 때입니다.
함정: 멀티스레드 프로그램에서의 fork
fork()에는 잘 알려진 함정이 있습니다. 호출한 스레드만 복사된다는 점입니다. 부모에 스레드가 20개 있다면 자식에는 하나만 남습니다. 나머지 19개 스레드가 잡고 있던 뮤텍스는 자식 메모리에서 여전히 잠긴 상태로 표시되지만, 그것을 잡고 있던 스레드는 존재하지 않습니다. 자식은 그 뮤텍스를 처음 획득하려는 순간 데드락에 걸립니다.
CoW 샌드박스 시스템은 포크하는 순간 템플릿 프로세스가 단일 스레드가 되도록 해서 이 문제를 피합니다. 보통은 이렇게 합니다. 템플릿에서 모든 것을 초기화하고(라이브러리 로드, 런타임 설정, 초기 상태 준비), 메인 스레드를 제외한 모든 스레드를 멈춘 뒤 포크하고, 각 자식이 필요에 따라 스레드를 다시 생성하게 합니다. 초기화 비용은 한 번만 치르고, 포크는 스레딩 위험을 피합니다.
일부 최신 접근법은 userfaultfd나 커스텀 페이지 폴트 핸들러를 사용해 fork()에 전혀 의존하지 않고 CoW와 유사한 동작을 구현합니다. 멀티스레딩 문제는 피하지만 복잡도가 늘고 커널 수준의 조율이 더 필요합니다.
지켜볼 것들
1밀리초 미만 샌드박싱은 아직 초기 단계지만, 기반 기술은 탄탄합니다(fork, 네임스페이스, cgroups, overlayfs 모두 성숙한 기술입니다). 이 위에 만들어지는 서버리스 컴퓨팅, CI/CD, 안전한 코드 실행 시스템들은 작업 단위 격리가 대규모로도 실용적이라는 것을 보여주고 있습니다. 이런 도구들이 성숙해지면서, 샌드박싱은 비싸다는 가정은 가비지 컬렉션이 실시간 애플리케이션에 너무 느리다는 가정만큼 낡은 것이 될 겁니다. 오버헤드는 사라지고 있고, '모든 것을 샌드박스에' 넣는 보안상의 이점도 무시하기 어려워지고 있습니다.


