Cloudflare Containers가 64KiB 블록을 지우지 않고 재사용해 이웃 테넌트 데이터가 그대로 읽혔다

Cloudflare Containers와 Sandboxes에서 Linux dm-thin의 skip_block_zeroing 설정 때문에 4KiB 쓰기가 새로 할당한 64KiB 블록의 나머지 60KiB에 남은 다른 테넌트 데이터가 그대로 읽혔습니다. 5,614개 디렉터리 블록과 2,700개 외부 inode 실측치, 9월 19일에 끝난 두 단계 조치, 그리고 샌드박스를 운영할 때 점검할 항목을 정리합니다.

Cloudflare Containers가 64KiB 블록을 지우지 않고 재사용해 이웃 테넌트 데이터가 그대로 읽혔다 대표 이미지

핵심 요약

  • 2026년 9월 4일 Oren Yomtov와 Accomplish 연구팀이 Cloudflare Containers와 Sandboxes의 교차 테넌트 데이터 노출을 신고했고, Cloudflare는 9월 19일까지 전 함대 정리를 마쳤습니다.
  • 원인은 Linux dm-thin 씬 풀의 skip_block_zeroing 설정입니다. 새로 할당한 64KiB 블록을 0으로 지우지 않아, 4KiB 쓰기로 그 블록을 차지한 컨테이너가 나머지 93.75%(60KiB)를 원시 읽기로 그대로 볼 수 있었습니다.
  • 프로덕션 배치 6곳에서 검사한 디렉터리 블록 5,614개 중 연구팀 자신의 파일시스템 블록은 0개였고, 체크섬으로 식별한 외부 디렉터리 inode는 2,700개였습니다.
  • 잔여 데이터는 24개 배치 중 18개(75%), 22개 노드 중 20개(90.9%), 4개 대륙에서 확인됐습니다. 회수된 블록에는 디렉터리 구조, 데이터베이스 페이지, 구조가 온전한 SQLite 데이터베이스가 섞여 있었습니다.
  • Cloudflare는 보관 중인 텔레메트리에서 자사와 연구팀의 검증 활동 외에 악용 흔적을 찾지 못했다고 밝혔습니다.

Cloudflare Containers와 Sandboxes가 쓰는 저장 구조

Cloudflare Containers는 Cloudflare Workers 위에서 컨테이너 이미지를 실행하는 관리형 컴퓨팅 서비스입니다. 각 컨테이너는 Firecracker 가상 머신 안에서 돌고, 그 VM에는 전용 루트 디스크가 /dev/vdc로 붙습니다. 그 위에 올라간 Sandbox SDK는 신뢰할 수 없는 코드를 격리 실행하는 제품으로, 공식 문서는 각 샌드박스가 "자체 격리 컨테이너에서 전체 Linux 환경"을 실행하며 "강한 보안 경계"를 제공한다고 소개합니다(Sandbox SDK 문서). AI 에이전트가 생성한 코드를 실행하는 용도로 권장되는 제품이고, Workers Paid 요금제에서 동작합니다.

디스크는 Linux device mapper의 씬 프로비저닝(dm-thin)으로 만듭니다. 실제 저장 공간은 가상 디스크가 아직 매핑되지 않은 영역에 쓸 때만 할당되고, 할당 단위는 64KiB입니다. 문제가 된 저장 풀에는 skip_block_zeroing 옵션이 켜져 있었습니다. 커널 문서는 이 옵션을 "새로 프로비저닝한 블록의 제로화를 건너뛴다"고 한 줄로 설명합니다(커널 dm-thin 문서). 컨테이너가 삭제되면 그 물리 블록은 여러 고객 계정이 함께 쓰는 풀로 돌아갔고, 다음 컨테이너가 지워지지 않은 블록을 물려받았습니다.

신고부터 공개까지

연구팀은 9월 4일 Cloudflare의 버그바운티 프로그램을 통해 제보했습니다. Cloudflare는 같은 달 안에 조치를 끝냈고, 연구팀은 9월 14일 자신들의 개념증명이 더는 동작하지 않는다고 확인했습니다. 함대 전체 정리는 9월 19일에 마무리됐고, 양측의 공동 글은 9월 24일에 나왔습니다. 신고에서 완화 완료까지 15일, 공개까지 20일이 걸린 셈입니다.

4KiB 쓰기 하나로 60KiB가 남은 이유

공격 절차는 짧습니다. 게스트 파일시스템의 빈 공간에 해당하는 64KiB 정렬 영역마다 4KiB 블록 하나를 씁니다. 그 쓰기가 아직 매핑되지 않은 씬 블록에 닿으면 dm-thin은 풀에서 64KiB 물리 블록을 새로 가져옵니다. 제로화가 꺼져 있으니 4KiB만 덮어쓰고, 나머지 60KiB는 이전 소유자의 바이트를 그대로 유지합니다. 이후 /dev/vdc를 원시 수준으로 읽으면 새 컨테이너가 한 번도 쓰지 않은 데이터가 나옵니다.

매핑되지 않은 영역을 그냥 읽는 것만으로는 아무것도 나오지 않습니다. dm-thin이 물리 블록을 할당하지 않고 0을 돌려주기 때문입니다. 그래서 작은 쓰기로 할당을 유발한 다음 큰 읽기를 하는 순서가 필요했습니다. 연구팀은 이 순서를 4KiB 정렬 쓰기와 원시 읽기로 구현했고, 같은 64KiB 블록을 4KiB 쓰기로 완전히 덮으려면 16번을 써야 한다는 점이 이 기법의 출발점이었습니다.

실측 숫자로 본 노출 규모

연구팀은 ext4 디렉터리 블록 체크섬(metadata_csum)으로 블록의 소유자를 가렸습니다. 프로덕션 배치 6곳에서 검사 가능한 디렉터리 블록 5,614개를 확인했고, 그중 연구팀 자신의 파일시스템에 속한 블록은 0개였습니다. 체크섬 분석으로 식별한 외부 디렉터리 inode는 2,700개입니다. 방법의 정확도는 그들이 만들고 지운 통제 블록 162개를 전부 자기 파일시스템으로 귀속시키는지로 검증했습니다.

잔여 데이터는 24개 배치 중 18개(75%), 22개 노드 중 20개(90.9%)에서 나왔고 배치는 4개 대륙에 흩어져 있었습니다. 회수된 블록 종류는 디렉터리 구조, 데이터베이스 페이지, 구조가 온전한 SQLite 데이터베이스였습니다. 연구팀 쪽 설명에는 디렉터리 목록, SQLite 데이터베이스, Chromium 프로필, .env 파일, 자격증명 파일이 포함됩니다(Accomplish 글, BleepingComputer 보도).

한 가지 분명히 해둘 조건이 있습니다. 공격자는 특정 피해자나 호스트를 고를 수 없고, 활성 상태로 붙어 있는 디스크를 읽을 수도 없습니다. 노출 여부는 Cloudflare의 워크로드 배치와 어떤 블록이 재할당되는지에 달려 있었고, 잔여 데이터가 항상 존재하는 것도 아니었습니다. 대신 배치를 반복하면 4개 대륙 어디서든 확률이 붙었습니다.

Cloudflare의 두 단계 조치와 "악용 증거 없음"

첫 조치는 풀 설정에서 skip_block_zeroing을 빼는 것이었습니다. 새로 할당되는 블록이 다시 0으로 지워지면서 신고된 기법은 막혔습니다. 다만 이 변경은 이미 매핑된 블록을 정리하지 못합니다. 실행 중인 컨테이너 디스크와 호스트가 캐시해 둔 dm-thin 스냅샷(OCI 이미지 레이어)에 남은 매핑은 새 컨테이너가 상속할 수 있었고, 상속한 블록의 빈 영역은 여전히 원시 읽기로 보였습니다.

그래서 Cloudflare는 실행 중이던 컨테이너 디스크를 모두 폐기하고, 한밤중에 호스트를 드레인해 VM을 재시작하고 이미지 캐시를 비웠습니다. 이 정리는 9월 19일에 끝났습니다.

악용 여부 조사에는 한계가 남습니다. Cloudflare는 보관 중인 디스크 I/O 텔레메트리에 자사·연구팀의 검증 활동 외에 같은 패턴이 없었다고 밝혔지만, 보관 기간과 문제 설정이 언제부터 켜져 있었는지는 공개하지 않았습니다. The Hacker News도 노출이 얼마나 오래 지속됐는지 이 설명만으로는 알 수 없다고 짚었습니다(The Hacker News). 공격자가 피해자를 고를 수 없다는 점은 위험의 크기를 낮추는 근거지만, 회수된 데이터에 자격증명 파일이 섞여 있었다는 사실을 없애지는 못합니다.

두 달 동안 여섯 번 탈출한 샌드박스들

이번 건은 같은 연구팀이 7월 이후 공개한 여섯 번째 샌드박스 탈출입니다. 7월 23일 Claude Cowork의 VM을 벗어난 SharedRoot, 9월 11일 Claude Code의 macOS 샌드박스를 뚫은 Beltdown, 9월 12일 Cursor CLI의 Beltdown2, 9월 15일 OpenAI Codex 샌드박스에서 나온 두 가지 경로, 9월 19일 Docker 하이퍼바이저 탈출(CVE-2026-77179, Docker Desktop 4.88.0에서 수정), 그리고 이번 Cloudflare Containers입니다.

여섯 건 중 다섯 건이 AI 코딩 에이전트가 코드를 실행하는 샌드박스를 겨눴고, 실패한 지점은 매번 달랐습니다. macOS 샌드박스 프로필, 컨테이너 런타임, 하이퍼바이저, 그리고 이번에는 저장 계층입니다. 에이전트 샌드박스를 도입하는 속도가 그 격리를 검증하는 속도보다 빠르다는 사실이 이 목록에서 가장 눈에 띄는 부분입니다. 격리 경계는 프롬프트나 도구 권한만이 아니라 블록 할당 같은 하위 계층에도 걸려 있습니다.

샌드박스를 운영한다면 점검할 항목

점검 항목 확인 방법 이유
씬 풀 제로화 dmsetup status 또는 씬 풀 설정에서 제로화 옵션을 확인 재할당 블록에 이전 데이터가 남는 경로를 끊습니다
풀 분리 테넌트별 씬 풀과 노드 배치 여부를 확인 한 풀을 공유하면 잔여 블록이 곧 교차 노출입니다
디스크 암호화 dm-crypt/LUKS 또는 공급자 볼륨 암호화 적용 여부를 확인 잔여 블록이 남아도 키가 다르면 평문으로 읽히지 않습니다
볼륨 폐기 삭제 시 blkdiscard/fstrim 실행과 이미지 레이어 캐시 정리 캐시된 스냅샷이 매핑을 상속하는 경로를 막습니다
자격증명 배치 샌드박스 안 .env 파일과 장기 토큰 존재 여부를 확인 코드 실행 환경에 장기 비밀이 있으면 유출 피해가 커집니다
자체 검증 빈 볼륨에 4KiB 정렬 쓰기를 한 뒤 원시 읽기로 잔여를 확인 재현 테스트가 유일한 확인 수단입니다

결론

이번 사건에서 특정 벤더의 실수로만 읽을 부분은 크지 않습니다. 다중 테넌트 저장 계층에서 성능을 위해 끄는 옵션 하나가 테넌트 격리 경계를 그대로 무너뜨린 사례이고, 같은 구조는 셀프호스팅 가상화, CI 러너, 에이전트 샌드박스 어디에나 있습니다. 실제로 블록을 재할당받는 순서를 반복하면 이웃의 자격증명 파일까지 읽힐 수 있다는 점을 실측으로 보여준 것이 이 연구의 값입니다.

Cloudflare의 대응은 빠른 편이었습니다. 제보 15일 만에 함대 정리를 끝냈고, 신고 기법이 이미 매핑된 블록에는 통하지 않는다는 구멍까지 스스로 찾아 캐시를 폐기했습니다. 반면 "악용 증거 없음"은 보관된 텔레메트리 범위 안에서의 결론이며, 설정이 언제부터 그 상태였는지는 공개되지 않았습니다. 이 문장을 무해 판정으로 읽기보다 확인 범위가 명시된 관찰로 읽는 편이 안전합니다.

운영자라면 세 가지를 순서대로 처리하길 권한다. 첫째, 씬 풀 설정에서 블록 제로화가 켜져 있는지 확인하라. 성능 측정에서 이 옵션이 이득으로 보이더라도 격리 비용을 함께 계산하라. 둘째, 테넌트 간에 물리 풀과 이미지 캐시를 공유하고 있다면 dm-crypt 같은 저장 암호화로 잔여 블록의 가독성을 끊어라. 셋째, 샌드박스 안에 장기 자격증명을 두지 말고, 빈 볼륨에 작은 정렬 쓰기를 넣은 뒤 원시 읽기로 잔여 데이터를 확인하는 재현 테스트를 정기 점검에 넣어라.

참고 링크