Cloudflare Workers AI Güvenlik Postmortem'i: Kiracılar Arası KV Önbellek Sızıntıları için Runbook
Özet olarak
Çok kiracılı KV önbellek sızıntılarını tespit edin, düzeltin ve önleyin: Cloudflare GLM-5.3 postmortem'inden oyun backend'lerine kapsamlı runbook.
Oyuncuya yönelik bir yapay zeka özelliği, başka bir stüdyonun prompt metnini içeren bir yanıt döndürdü. Bir çökme değil. Bir 500 değil. HTTP 200 ile teslim edilen, konuyla ilgili tutarlı bir yanıt ve çağıranın kendi isteğiyle eşleşen bir token sayısı.
Çok kiracılı bir hatanın alabileceği en kötü biçim budur: çıktı makul görünür, kimse ticket açmaz. Cloudflare tam olarak bunu 27 Ağustos 2026'da yaşadı; GLM-5.3 Workers AI modelinden gelen 155 yanıt, diğer müşterilerin oturumlarından veri içeriyordu. Etkilenen her istek 681.280 token'dan uzundu. Düzeltme iki değişiklik gerektirdi — biri yapılandırma, biri motor yaması — ve tespit hikayesi düzeltmenin kendisinden daha öğretici.
İşte runbook: ne bozuldu, aynı hata sınıfını kendi stack'inizde nasıl tespit edersiniz, nasıl giderirsiniz ve tekrarlanmasını nasıl durdurursunuz.
Gerçekte Ne Bozuldu
Birbiriyle örtüşen iki bağımsız sorun vardı. İkisinden herhangi biri tek başına atlatılabilirdi.
Birinci sorun: kimsenin doğrulamadığı bir kapasite uyumsuzluğu
Cloudflare, GLM-5.3'ü 1.048.576 token'lık bir context penceresiyle devreye aldı. Prefill GPU'larındaki KV önbellek havuzu 681.280 token tutuyordu.
Bu fark önemli çünkü havuz boyutu bir yapılandırma değeri değil. SGLang, model ağırlıklarını yükledikten sonra kalan GPU belleğinden bunu başlangıçta boyutlandırır. Reklamı yapılan context uzunluğu ayrı bir yapılandırma değeridir. Sistemde ikisini aynı olmaya zorlayan hiçbir şey yoktur ve birbirlerinden saptıklarında sizi uyaran hiçbir şey yoktur.
Lansman öncesi testler geçti. HTTP durum kodları 200'dü, streaming gövdelerinde hata frame'i yoktu, throughput normal görünüyordu. Testler taşımayı doğruladı, içeriği değil.
İkinci sorun: reddedilmesine rağmen kendini canlı raporlayan istek
Bir prompt havuzu aştığında, SGLang'ın zamanlayıcısı onu reddetti. Reddetme yolu, hatayı istemci için kaydetti ve prompt'u, isteğin bellek ayırmadan batch mekanizmasından akabilmesi için tek token'lık bir yer tutucuyla değiştirdi. Tek bir düğümde bu sorunsuz çalıştı: yer tutucu çalıştı, batch bitti, istemci bir hata aldı.
Ayrıştırılmış prefill/decode modunda — prefill GPU'larının KV önbelleğini oluşturup RDMA üzerinden token üreten decode GPU'larına gönderdiği modda — sıra şöyle işledi:
- Bootstrap. Yönlendirici decode'a prompt'un 8 sayfa uzunluğunda olduğunu söyledi. Decode 8 KV sayfası ayırdı.
- Prefill kabulü. Kuyruk,
req.finished()False döndüren her isteği kabul etti. Reddedilen bir istek reddinito_finishiçinde sakladı,finished_reasoniçinde değil; bu yüzdenfinished()hâlâ False döndürdü. Reddedilen istek kabul edildi. - Prefill forward pass. Prompt zaten tek token'lık bir yer tutucuyla değiştirilmişti. Prefill, bu tek token üzerinde önemsiz bir forward pass çalıştırdı.
- KV transferi. Prefill, decode'a bir sayfa KV gönderdi ve "transfer tamam" sinyali verdi.
- Decode kabulü. Decode'un kuyruğu aynı
finished()kontrolünü kullandı ve devri kabul etti. - Decode attention'ı. Decode, ayrılmış 8 sayfanın tamamına dikkat ederek token üretti. Bu istek tarafından yalnızca sayfa 0 yazılmıştı. Sayfa 1 ile 7 arası hâlâ diğer kiracıların prompt'larını ve kısmi üretimlerini tutuyordu.
- Yanıt. İstemci HTTP 200, eşleşen bir
prompt_tokenssayısı ve bayat önbellek sayfalarından derlenmiş tutarlı bir yanıt aldı.
681.280 token eşiğinin üzerindeki her istek bu şekilde davrandı. Hata, 6 Temmuz 2026'da upstream'te issue #30233 olarak kaydedilmişti ve etkilenen dağıtım henüz düzeltmeyi almamıştı.
Oyuncularınızdan Önce Tespit Edin
Durum kodlarıyla izolasyonu test ederek elde edemezsiniz. Üç sinyale ihtiyacınız var.
Sinyal 1: havuz boyutu ile context uzunluğu, çalışan motordan okunmalı
Bunu başlangıçta ve CI'da kontrol edin, bir yapılandırma dosyasından değil. Havuz, artan bellekten türetilir; bu yüzden bir niceleme değişikliği, bir tensor-paralellik değişikliği veya bir CUDA grafik boyutu ayarı onu sessizce hareket ettirir.
# deploy_gate.py - run before promoting any disaggregated inference config
import json, os, sys, urllib.request
CONTEXT_LEN = 1_048_576 # the model's advertised window
HEADROOM = 1.25 # pool must exceed context, not merely match it
def server_info(base_url: str) -> dict:
with urllib.request.urlopen(f"{base_url}/get_server_info", timeout=10) as r:
return json.load(r)
def kv_pool_tokens(info: dict) -> int:
# SGLang reports the pool it sized at startup; field names vary by version
for key in ("max_total_num_tokens", "kv_pool_tokens", "max_total_tokens"):
if key in info:
return int(info[key])
raise RuntimeError("KV pool size not exposed by this build - fail closed")
def main(prefill_url: str) -> None:
pool = kv_pool_tokens(server_info(prefill_url))
required = int(CONTEXT_LEN * HEADROOM)
print(f"kv_pool={pool} required={required} context={CONTEXT_LEN}")
if pool < required:
print("FAIL: pool smaller than context window; over-length prompts reach the rejection path")
sys.exit(1)
print("OK: pool covers the full context window")
if __name__ == "__main__":
main(os.environ.get("PREFILL_URL", "http://localhost:30000"))
Her prefill düğümünde, load balancer trafiği görmeden önce çalıştırın. Sıfır olmayan bir çıkış, dağıtımı engeller.
Sinyal 2: KV önbelleğinde canary işaretleri
Uzun bir prompt'a benzersiz bir sentinel dizisi yerleştirin, isteğin bitmesini bekleyin, ardından onu asla içermemesi gereken ikinci bir uzun prompt gönderin. Dönen metni kontrol edin, durum kodunu değil.
import uuid, requests
def canary_check(base_url: str, token: str, long_prompt_tokens: int) -> None:
sentinel = f"CANARY-{uuid.uuid4()}"
filler = "lorem ipsum " * (long_prompt_tokens // 2)
requests.post(base_url, json={
"prompt": f"{sentinel} {filler}",
"max_tokens": 16,
}, headers={"Authorization": f"Bearer {token}"}, timeout=300)
# A fresh, unrelated request must never surface the previous sentinel
r = requests.post(base_url, json={
"prompt": f"Summarize this: {filler}",
"max_tokens": 16,
}, headers={"Authorization": f"Bearer {token}"}, timeout=300)
assert sentinel not in r.text, f"KV cache leak: {sentinel} surfaced in a fresh response"
Bunu kabul etmeyi düşündüğünüz en uzun prompt'a, tam sınıra ve sınır + 1 token'a karşı çalıştırın. Sınır durumu, reddetme yolunun devreye girdiği yerdir.
Sinyal 3: fazla iyi olan gecikme
Tam uzunluktaki bir prefill ölçülebilir zaman alır. 8 sayfalık bir prompt, 1 sayfalık bir promptun alması gereken sürede dönüyorsa, başka birinin önbelleğini okuyorsunuz. Prefill süresini prompt uzunluğuna göre indeksleyerek günlüğe kaydedin ve uzun-context kovasının p5'inde uyarı verin. Anormal derecede hızlı bir uzun prompt, bir kazanım değil, sızıntı imzasıdır.
Sırasıyla Düzeltme
1. Havuzun tüm context'i tutmasını sağlayın
Cloudflare, GLM-5.3 yapılandırmasını GLM-5.2 ile eşleşecek şekilde değiştirdi ve prefill havuzunun artık yaklaşık 2,04 milyon token tutması için yeterli GPU belleği boşalttı — bu, 1.048.576 token'lık context'in yaklaşık iki katı. Context penceresine sığan bir prompt artık her zaman havuza sığar; böylece geçerli bir istek için reddetme yoluna asla ulaşılmaz.
2. Reddetme yolunu yine de güvenli hale getirin
Yalnızca havuz boyutlandırması kırılgandır. Niceleme, tensor paralelliği, CUDA grafik boyutları veya bir model sürümündeki herhangi bir değişiklik, havuzu gözden kaçırılması kolay bir şekilde hareket ettirir.
Motor yaması, bekleyen bir reddi taşıyan isteği, herhangi bir bootstrap, ayırma veya KV transferi gerçekleşmeden önce, ayrıştırma kabul kuyruğunda orijinal hatasıyla birlikte sonlandırır. Decode düğümü ayrıca aktarılan token sayısının ayrılan token sayısıyla eşleştiğini doğrular. Bu, upstream'te PR #38704 olarak yayınlandı, #38935 ile takip edildi ve SGLang v0.5.20 ile v0.5.22 arasında dağıtıldı.
SGLang'ı herhangi bir yerde çalıştırıyorsanız, v0.5.22 veya sonrasına sabitleyin. Bu, "gelecek sprint'e planlayın" türü bir yükseltme değil.
3. Reproducer ile doğrulayın, hislerle değil
Cloudflare, modeli geri yüklemeden önce her iki değişikliği de araştırmacının repro script'ine ve kendi canary işareti testlerine karşı doğruladı. Biri adversarial, biri dahili iki bağımsız test. Bir modeli veya bir fleet'i yeniden etkinleştirmeden önce doğru çıta budur.
"200 Döndü" Neden Bir Test Değildir
Kök test hatası dardı ve tamamen sıradandı: ekip, uzun context uzunluklarında HTTP yanıt kodlarını ve motor performansını doğruladı; model çıktısını ve tutarlılığını doğrulamadı. Benchmark'lar geçti çünkü benchmark'lar "yanıt verdi mi?" diye sorar, "doğru yanıt verdi mi?" diye değil.
Barındırılan bir yapay zeka özelliği için doğruluk kontrolü ucuzdur. Kendi oyun backend'iniz için eşdeğer boşluk genellikle daha geniştir. İşte aktarılabilir kural: herhangi bir paylaşılan kaynak havuzu, havuzun boyutunu sistemin kabul edeceği en büyük isteğin boyutuna bağlayan bir doğrulamaya ihtiyaç duyar ve bu doğrulama canlı sürece karşı çalışmalıdır, bir yapılandırma dosyasına karşı değil.
Aynı Hata Sınıfı Oyun Backend'lerinde Yaşıyor
"KV önbellek sayfaları" yerine "oturum slotları" koyun ve olay neredeyse satır satır yeniden üretilir.
Sabit bir havuz ayırırsınız: eşleşme slotları, oyuncu durumu slotları veya aktör tamponları. Bir istek 8 slot ayırır. Bir kabul kontrolü state != Finished yerine state == Valid test eder; bu yüzden reddedilen bir ayırma yine de kabul edilir. Slot, yalnızca slot 0 başlatılmış olarak bir sonraki lobiye devredilir. Slot 1 ile 7 arası hâlâ önceki maçın oyuncu kimliklerini, envanterini ve konum verilerini tutar.
Bu, güvenlik ihlallerine dayanacak şekilde oyun backend'leri mimarileme yazısında ele aldığımız başarısızlık moduyla aynı: sızıntının karmaşık bir saldırı olması gerekmez, yalnızca kodun gerçekte uygulamadığı bir sahiplik modeli yeterlidir.
İşte hatalı desen ve düzeltmesi, C# ile:
public sealed class SessionSlotPool
{
private readonly SessionSlot[] _slots;
private readonly Queue<int> _free = new();
// BUG: release without invalidation leaves stale state for the next tenant
public void ReleaseUnsafe(int[] indices)
{
foreach (var i in indices) _free.Enqueue(i);
}
// FIX: invalidate on release, stamp a generation, verify on read
public void Release(int[] indices)
{
foreach (var i in indices)
{
_slots[i].Reset(); // zero player list, inventory, match id
_slots[i].OwnerId = null;
_slots[i].Generation++; // stale handles now fail the check
_free.Enqueue(i);
}
}
public bool TryRead(int index, long expectedGeneration, out SessionSlot slot)
{
slot = _slots[index];
return slot.Generation == expectedGeneration && slot.OwnerId != null;
}
}
Nesil sayacı ucuz kısımdır. "Umarız slot üzerine yazılmıştır"ı sert, test edilebilir bir redde dönüştürür. Bayat bir handle'a karşı yapılan herhangi bir okuma, sessizce başka bir oyuncunun verisini döndürmek yerine başarısız olur.
İzolasyon Kontrol Listesi
- Havuz boyutunu context uzunluğuna karşı başlangıçta, çalışan süreçten doğrulayın. Config'ten değil. Sıfır olmayan bir çıkış dağıtımı engeller.
- "Reddedildi"yi "bitti"den ayrı bir terminal durum olarak ele alın. Herhangi bir ayırma veya devirden önce, kabulde iptal edin ve orijinal hatayı döndürün.
- Transfer sayılarını her iki tarafta doğrulayın. Decode 8 sayfa ayırdıysa, 8 sayfanın geldiğini onaylamalıdır. "Tamam" raporlayan tek sayfalık bir transfer tam olarak bu hatadır.
- Başarısızlık yolunu içerik doğrulamalarıyla test edin. Canary işaretleri, sentinel dizileri, sınır-artı-bir prompt'lar. Yanıt gövdesini kontrol edin, asla durum kodunu.
- Kapasite değişikliklerinde kapıyı yeniden çalıştırın. Niceleme, tensor paralelliği, batch boyutu ve model sürümü havuzu hareket ettirir. Her birini izolasyonla ilgili bir değişiklik olarak ele alın.
Madde 5, ekiplerin atladığı maddedir. Her kapasite değişikliği aynı zamanda bir güvenlik değişikliğidir, çünkü havuz beyan edilmek yerine artan bellekten türetilir. Aynı sıfır toplamlı düşünce, elle boyutlandırdığınız herhangi bir kaynak havuzu için geçerlidir — sıfır atık sunucu hibernasyon analizi içindeki trade-off matematiği aynı aritmetiktir.
Bunun Maliyeti Nedir
İzolasyon bedava değildir ve trade-off dürüsttür.
KV havuzunu 681.280'den yaklaşık 2,04 milyon token'a ikiye katlamak, aksi halde daha fazla eşzamanlı istek tutacak GPU belleği tüketir. Daha az eşzamanlı istek, üretilen token başına daha yüksek maliyet demektir. Bir prefill fleet'inde bu headroom, satamayacağınız kapasitedir.
Daha ucuz bir alternatif var: küçük havuzu koruyun ve aşırı uzun prompt'ları, zamanlayıcıya ulaşmadan önce uçta açık bir HTTP 400 ile reddedin. Bu, throughput'u korur ancak meşru uzun-context isteklerini başarısız kılar — ki bu, bir ajan kodlama modeli için ürünün çoğudur.
Cloudflare, kapasite maliyeti pahasına doğruluğu seçti. Oyuncuya yönelik bir özellik için bu neredeyse her zaman doğrudur. Alternatif — başka bir oyuncunun verisini içeren hızlı, ucuz, makul bir yanıt — bir postmortem'de savunabileceğiniz bir trade-off değildir.
İzolasyon Yükü Aslında Nerede Duruyor
Her çok kiracılı sistem bu kararı verir: hangi paylaşılan kaynakları havuzlayıp yeniden kullanıyorsunuz ve geçersiz kılmanın sahibi kim? GPU KV sayfaları için yanıt "çıkarım motoru, kötü bir şekilde, v0.5.22'ye kadar".
Oyununuzun oyuncu durumu için yanıt genellikle "veritabanınız ve bu sizin sorumluluğunuzda". Havuz anlambilimine hiç sahip olmamayı tercih ederseniz, hesaba bağlı durum hizmetleri yüzeyi değiştirir: horizOn, istemcinin çakışmaları çözdüğü, sunucularınızın hangi slotun bayat olduğunu tahmin etmek yerine, revizyon farkında okuma ve yazma içeren hesaba bağlı Cloud Save sağlar.
Bu izolasyon işini ortadan kaldırmaz — hiçbir şey kaldırmaz. Onu gerçekten kontrol ettiğiniz sınırlara daraltır.
Bu Hafta Ne Yapmalısınız
Sahip olduğunuz her paylaşılan havuzu denetleyin ve üç soru sorun. Havuz boyutu, sistemin kabul ettiği en büyük isteği kanıtlanabilir şekilde karşılıyor mu? Reddedilen bir istek herhangi bir şey ayırmadan önce sonlanıyor mu? Alan taraf, gelenin ayırdığıyla eşleştiğini doğruluyor mu?
Herhangi bir yanıt "öyle varsayıyoruz" ise, yeterince uzun bir isteği bekleyen aynı hataya sahipsiniz. İçeriği başka bir oyuncunun yanıtında en zarar verici olacak havuzdan başlayın.
Oyuncu durumu için bu havuzun bakımını yapmayı bırakmaya hazırsanız, horizOn'u ücretsiz deneyin — kimlik doğrulama, hesaba bağlı Cloud Save, lider tabloları, uzak yapılandırma ve çökme raporları; sıfırdan bir veritabanı ve oturum modeli kurmadan. API dokümanları entegrasyon yüzeyini yaklaşık on dakikada gösterir.
Kaynak: GLM-5.3 Workers AI modelinde istekler arası KV önbellek maruziyeti ve SGLang ile gidermemiz