2026년 9월 8일, 저희는 보안 연구원인 Knostic의 Gadi Evron으로부터 GLM-5.3 Workers AI 모델(@cf/zai-org/glm-5.3)에 영향을 미치는 테넌트 격리(tenant isolation) 문제에 대한 경고를 받았습니다. 이 문제는 2026년 8월 27일부터 발생했습니다. 저희는 해당 문제를 해결했으며, 9월 9일에 모델을 복구했습니다. Workers AI에서 호스팅되는 다른 어떤 모델도 영향을 받지 않았음을 확인했습니다.
8월 27일과 9월 9일 사이에 GLM 5.3 Workers AI 모델을 사용한 소수의 고객들이 자신보다 직전에 같은 모델로 요청을 보낸 다른 고객의 데이터를 포함하는 응답을 받았습니다. 총 155개의 응답이 영향을 받았으며, 이들은 모두 681,280 토큰을 초과하는 프롬프트에 대한 것이었습니다.
저희 자체 테스트와 연구원의 보고서를 제외하고 식별된 요청들은 누군가 버그를 반복적으로 악용한 경우라기보다는 일반적인 장문 컨텍스트 사용 패턴과 시간 및 양 면에서 일치했습니다. 저희는 고의적인 악용에 대한 증거를 발견하지 못했습니다.
오늘 저희는 이 문제를 공조적으로 공개하기 위해 SGLang(저희가 사용하는 오픈 소스 추론 엔진)의 게시물과 함께 이 보고서를 발표합니다. 이 게시물에는 이 문제의 원인이 된 버그, 저희의 대응 타임라인, 그리고 이후 저희 프로덕션 SGLang 배포판에 적용한 강화 조치들이 설명되어 있습니다. 또한 이 문제와 유사한 버그를 해결하기 위한 SGLang 업스트림 유지 관리자들과의 지속적인 노력도 상세히 다룹니다.
발생한 일
GLM은 Z.ai에서 개발한 텍스트 생성 모델로, 에이전트 코딩 워크플로우(agentic coding workflows)를 위해 구축되었으며 Workers AI를 통해 이용 가능한 여러 모델 중 하나입니다. Gadi Evron은 저희의 HackerOne 프로그램을 통해 9월 8일, Workers AI에서 @cf/zai-org/glm-5.3로 전송된 매우 긴 프롬프트가 때때로 다른 사용자 세션에 속하는 텍스트를 반환한다는 보고서를 제출했습니다. Cloudflare는 다음 날 아침 이 문제를 재현했으며, 사고 선언 후 40분 이내에 해당 모델을 오프라인 처리했습니다. 이후 저희는 수정된 구성과 패치된 추론 엔진(inference engine)을 배포하여 당일 서비스가 복구되었습니다.
이 특정 요청 간 KV 캐시 노출 보고는 두 가지 중첩된 문제의 결과였습니다. 첫 번째는 Cloudflare가 사용하는 오픈 소스 추론 엔진인 SGLang의 잘못된 설정이었습니다. 우리는 1,048,576 토큰 컨텍스트 창을 가진 GLM 5.3을 배포했지만, 우리의 prefill GPU에 있는 KV 캐시 풀은 681,280 토큰만 수용할 수 있었습니다. 우리는 출시 전에 이러한 불일치를 알고 있었습니다. 우리는 엔진이 처리할 수 없는 모든 프롬프트에 대해 오류를 반환할 것으로 예상했고, 장문 컨텍스트 테스트에서 성공적인 응답을 받았기 때문에 이것이 문제가 아니라고 생각했습니다.
두 번째는 SGLang이 분리된(disaggregated) prefill/decode 모드에서 발생하는 초과 길이 요청을 처리하는 방식에 있던 버그였습니다. 풀에 맞지 않는 프롬프트는 내부적으로 거부된 것으로 표시되고 1 토큰 자리 표시자(placeholder)로 대체되었지만, 요청을 중단하고 클라이언트 오류를 반환하는 대신 prefill은 해당 자리 표시자 프롬프트의 KV 캐시를 생성하여 decode 노드로 전송했습니다. decode 노드는 '전송 완료(transfer complete)' 메시지를 받았고, prefill이 이전 요청의 KV 캐시를 현재 요청의 전체 프롬프트로 덮어썼다고 가정했지만, 실제로는 이전 상태의 대부분을 그대로 남겨두었습니다. 그 결과, decode 노드는 이전 요청의 데이터를 여전히 담고 있는 오래된 캐시 페이지(stale cache pages)를 참조하여 응답을 생성했습니다. 그 결과, 681,280 토큰보다 긴 프롬프트에 대한 응답은 다른 사용자의 GLM 5.3 세션 데이터가 포함될 수 있었습니다.
모델을 비활성화(disabling the model)한 후, 우리는 이 취약점을 수정하고 대중에게 다시 접근을 허용하기 전에 다음과 같은 조치를 취했습니다:
설정 수정: 풀(pool)이 전체 컨텍스트를 보유하도록 변경
우리는 GLM 5.3 구성을 GLM 5.2 구성과 일치시키도록 조정하여 설정을 수정했습니다. 이 과정에서 충분한 GPU 메모리가 확보되었고, 그 결과 프리필 KV 풀은 이제 약 204만 개의 토큰을 보유하게 되었습니다. 이는 컨텍스트 길이인 1,048,576 토큰의 대략 두 배에 해당합니다. 컨텍스트 창(context window)에 맞는 프롬프트는 이제 항상 풀에 들어가므로, 유효한 요청에 대해서는 거부 경로(rejection path)가 절대 도달되지 않습니다. 이 것만으로도 버그가 발생하지 않도록 막을 수 있습니다.
엔진 수정: 거부 경로를 안전하게 만듦
엔진을 수정하기 위해, 우리는 또한 거부 경로를 안전하게 만들었습니다. 풀 크기에만 의존하는 것은 취약합니다. 양자화(quantization), 텐서 병렬성(tensor parallelism), CUDA 그래프 크기(CUDA graph sizes) 또는 향후 모델 버전의 변경은 놓치기 쉬운 방식으로 풀 크기를 변화시킵니다. 게다가, 프리필 스케줄러가 요청을 거부하고 동일한 버그를 유발할 수 있는 다른 상황들이 있을 수 있습니다. 이를 방지하기 위해, 우리는 또한 엔진을 패치하여 보류 중인 거부를 포함하는 요청이 부트스트랩(bootstrap), 할당(allocation) 또는 KV 전송(KV transfer)이 발생하기 전에 분산 입고 큐(disaggregation admission queue)에서 원래의 오류와 함께 폐기되도록 했습니다.
우리는 이를 상위 레벨(upstream)에서 __#38704__로 제안했으며, 이에는 디코드 노드(decode node)가 전송된 토큰 수가 예약한 수와 일치하는지 확인하는 기능도 포함되어 있었습니다. 해당 수정 사항은 이미 SGLang main의 __#36988__에 있었으며, 관련 실패 경로를 위해 9월 6일에 병합되었으나 아직 출시되지는 않았습니다. 유지 관리자들은 또한 우리의 접근 방식을 따라 이러한 요청을 더 일찍 거부하는 __#38935__도 병합했습니다.
우리는 모델을 복원하기 전에 두 가지 변경 사항 모두를 연구원의 재현 스크립트와 자체 카나리 마커 테스트(canary-marker tests)를 통해 검증했습니다.
배경: GLM 5.3 서비스 방식
Workers AI는 SGLang을 사용하여 GLM 5.3 모델을 분리형(disaggregated) 구성으로 실행합니다. 이 방식에서는 프리필 GPU 풀이 프롬프트를 처리하고 모델의 KV 캐시를 생성한 다음, 그 캐시를 Mooncake을 통해 원격 직접 메모리 접근(RDMA) 방식으로 디코드 GPU 풀에 전달하여 응답을 생성합니다. 저희는 이 아키텍처에 대해 __고성능 LLMs__와 더 작고 빠르며 안전한 모델들 관련 포스트에서 이전에 작성한 적이 있습니다.
GPU 메모리 내의 KV 캐시는 고정 크기의 페이지가 공유되는 풀입니다. 각 요청은 진입할 때 페이지를 할당받고 완료되면 이를 해제합니다. 현재 페이지는 요청 간에 0으로 초기화되지 않으며, 다음 페이지를 받는 요청은 단순히 해당 페이지를 덮어씁니다. 이러한 접근 방식은 추론 엔진(inference engine)에서 흔하며 최고의 성능을 달성하는 데 필수적이지만, 그만큼 추론 엔진이 이 메모리 페이지의 소유권을 추적할 책임이 있다는 것을 의미합니다. KV 캐시 페이지 소유권 및 수명 주기 추적은 노드 내부에서의 공유 소유권(KV 캐시 오프로드와 토큰 생성 모두 서로 다른 수명 주기로 메모리에 접근)뿐만 아니라, 원격 프리필 노드와 기술적으로 공유되는 소유권(해당 페이지에 대한 동시적인 변경 가능한 참조를 보유함)까지 다루어야 합니다.
게다가, 이 풀의 크기는 설정 값이 아닙니다. SGLang은 모델 가중치를 로드한 후 남아 있는 GPU 메모리로부터 시작 시점에 그 크기를 결정합니다. 모델이 광고하는 컨텍스트 길이(context length)는 별도의 설정 값입니다. 두 값이 일치하도록 강제하는 것은 아무것도 없으며, 그것을 확인하는 것이 우리의 임무입니다.
문제 1: 잘못된 구성 (misconfiguration)
우리 설정으로 GLM 5.3 prefill 노드를 사용했을 때, 풀(pool)은 681,280 토큰이었지만 모델의 컨텍스트 길이(context length)는 1,048,576 토큰이었습니다. 선택된 KV pool 크기는 SGLang이 시작 시 로그에 출력하고 메트릭으로 노출합니다. 우리는 이러한 크기 불일치(size mismatch)를 인지하고 있었지만, 사전 출시 테스트 중에는 벤치마킹 과정에서 오류 프레임 없이 성공적인 HTTP status codes와 스트리밍 바디(streaming body)가 관찰되었습니다. 우리는 엔진이 스트리밍이나 다른 메커니즘을 통해 KV cache pool과 요청된 컨텍스트 창(context window) 간의 차이를 우아하게 처리할 수 있다고 가정했습니다.
이것은 우리의 테스트에서 중요한 간극이었습니다: 우리는 모델 출력과 일관성(coherence)을 검증하지 않은 채, 장문 컨텍스트에서의 HTTP 응답 코드와 엔진 성능만을 검증했습니다. 이제는 응답 코드를 통해 추론하는 대신, 분리 배포(disaggregated deployment) 변경 사항이 배포되기 전에 실행 중인 엔진으로부터 KV pool 크기를 컨텍스트 길이와 직접 비교하여 확인합니다.
문제 두 가지: SGLang 버그
v0.5.21 버전에서 SGLang 팀은 KV pool 크기보다 긴 프롬프트를 처리하는 방식에 대한 이 버그를 수정했습니다. 이전에는 다음과 같은 문제가 발생했습니다.
KV pool이 담을 수 있는 크기보다 긴 프롬프트가 도착하면 SGLang의 스케줄러(scheduler)가 이를 거부했습니다. 거부 경로(Req.set_finish_with_abort)는 클라이언트에 반환될 오류를 기록하고, 요청이 실제 메모리를 할당하지 않고도 정상적인 배치 메커니즘을 통해 처리될 수 있도록 프롬프트를 단일 토큰 플레이스홀더(placeholder)로 교체했습니다. 단일 노드 배포 환경에서는 이것이 작동했습니다: 플레이스홀더가 실행되고, 배치가 완료되었으며, 클라이언트는 오류를 받았습니다.
우리가 운영하는 것과 같은 분리 배포(disaggregated mode) 환경에서는 prefill과 decode 양쪽의 진입 큐(admission queues)가 요청이 거부되었는지(rejected) 여부가 아니라 완료되었는지(finished)를 확인했습니다. 거부된 요청은 자신이 살아있는 것처럼 보고했습니다. 따라서:
부트스트랩(Bootstrap). 라우터는 디코드 노드에 프롬프트 길이(다이어그램에서 8 페이지)를 알렸습니다. 디코드는 해당 요청을 위해 8개의 KV 페이지를 예약했습니다.프리필 수용(Prefill admission). 프리필 수용 큐는 req.finished()가 False인 모든 요청을 수용했습니다. 거부된 요청은 finished_reason이 아닌 to_finish에 거부를 저장했기 때문에, finished()는 여전히 False를 반환했습니다. 따라서 해당 거부 요청도 수용되었습니다.프리필 포워드 패스(Prefill forward pass). 거부로 인해 프롬프트는 이미 단일 토큰 플레이스홀더로 대체되었습니다. 프리필은 이 단일 토큰에 대해 사소한(trivial) 포워드 패스를 실행했습니다.KV 전송(KV transfer). 프리필은 디코드에 하나의 KV 페이지를 보내고 전송이 완료되었음을 신호했습니다.디코드 수용(Decode admission). 디코드의 수용 큐도 동일한 finished() 검사를 했기 때문에, 이 인계(handoff)를 받아들였습니다.디코드 어텐션(Decode attention). 디코드가 생성을 시작하고 어텐션은 예약된 8개 페이지를 모두 읽었습니다. 이 요청에 의해 쓰여진 것은 P0 페이지뿐이었습니다. P1부터 P7까지의 페이지에는 이전 요청들이 남긴 내용, 즉 다른 사용자의 프롬프트와 부분적인 생성 결과가 여전히 남아 있었습니다.응답(Response). 클라이언트는 HTTP 400 대신 HTTP 200을 받았습니다. prompt_tokens는 긴 프롬프트와 일치했고 답변은 일관성이 있었습니다. 이 응답은 8페이지 프리필이 걸릴 시간보다 빠르게 도착했으며, 다른 사용자의 KV 페이지에서 온 내용도 포함하고 있었습니다.
임계값을 초과하는 모든 요청이 이런 방식으로 동작했습니다. 이 버그는 다른 사용자에 의해 2026년 7월 6일자로 SGLang에 이슈 __#30233__으로 보고되었으며, Cloudflare가 이 사건을 조사하던 중 발견되었습니다. 저희는 v0.5.10 이후부터 SGLang에서 이러한 유형의 문제를 지적해 왔으며, 이번 보안 사고는 Cloudflare가 이미 SGLang 팀과 협력하여 시작했던 작업을 가속화했습니다. 이 문제에 대한 패치는 SGLang v0.5.20에서 출시되었고, v0.5.21 및 v0.5.22에서 후속 조치가 이루어졌으며, SGLang에 의존하는 모든 다른 운영자가 사용할 수 있습니다. 모든 운영자에게 SGLang 종속성을 v0.5.22 이상으로 업데이트할 것을 강력히 권장합니다. 특정 PR에 대한 더 자세한 내용은 아래를 참조하십시오.
GLM 5.3만이 이 버그의 영향을 받은 모델이었습니다. Workers AI에서 분리 배포(disaggregated deployment)된 다른 모든 배포는 prefill과 decode 모두에서 컨텍스트 길이보다 큰 KV 풀을 가지고 있었기 때문에, 어떤 요청도 거부 경로에 도달할 수 없었습니다. 저희는 SGLang으로 제공하는 모든 모델에 아래 설명된 엔진 수정(engine fix)과 강화 조치(hardening)를 적용했으며, 배포 검사에서는 모든 새로운 배포에 이를 요구합니다.
타임라인
모든 시간은 UTC 기준입니다.