«نشت KV cache در Cloudflare Workers AI: وقتی جواب LLM شما حاوی داده کاربر دیگه بود»
نویسنده: آرش لطیفی
تحلیل حادثه امنیتی GLM-5.3 در Workers AI — ۱۵۵ پاسخ با داده سایر مشتریها، ریشه باگ در SGLang، و درسهایی برای هر کسی که LLM خودمیزبان داره.
TL;DR — Cloudflare دیروز فاش کرد مدل GLM-5.3 روی Workers AI بهخاطر دو باگ همزمان (کانفیگ اشتباه + باگ SGLang) جوابهایی برگردونده که حاوی داده کاربران دیگه بود؛ اگر سلفهاست LLM داری، این پست فنی مرجع خوندنه.
چی شده؟
دیروز (۲۶ اکتبر ۲۰۲۶) Cloudflare گزارش کامل یه حادثه امنیتی رو با هماهنگی محقق و تیم SGLang منتشر کرد. خلاصه:
- از ۲۷ اوت تا ۹ سپتامبر ۲۰۲۶، تعداد کمی از مشتریهای مدل
@cf/zai-org/glm-5.3روی Workers AI، پاسخهایی دریافت کردن که حاوی داده از درخواستهای کاربران دیگه بود. - در کل ۱۵۵ پاسخ تحت تاثیر قرار گرفت؛ همه به پرامپتهای بیشتر از ۶۸۱,۲۸۰ توکن.
- همه چیز با گزارش یه پژوهشگر امنیتی (Gadi Evron از Knostic) از طریق HackerOne در ۸ سپتامبر لو رفت. Cloudflare صبح فردا بازتولید کرد، ۴۰ دقیقه بعد از اعلام حادثه مدل رو آفلاین کرد، و همون روز با کانفیگ اصلاحشده برگردوند.
- شواهدی از سوءاستفاده عمدی پیدا نشد؛ الگوی درخواستها با استفاده معمولی از کانتکست بلند سازگار بود.
ریشه: دو باگ که روی هم افتادن
این یه باگ ساده نبود؛ ترکیب یه اشتباه کانفیگ و یه باگ موتور inference بود.
۱) کانفیگ اشتباه: GLM 5.3 با پنجره کانتکست ۱,۰۴۸,۵۷۶ توکنی deploy شده بود، ولی pool کش KV روی GPUهای prefill فقط جا برای ۶۸۱,۲۸۰ توکن داشت. Cloudflare از این عدم تطابق قبل از عرضه خبر داشت! چون تستهایشون فقط کد HTTP موفق و بدنه استریم بدون فریم خطا رو چک کرده بود — نه منسجم بودن خروجی مدل.
۲) باگ SGLang در حالت disaggregated: وقتی پرامپت در pool جا نمیشد، scheduler اون رو reject میکرد و پرامپت رو با یه placeholder تک-توکنی عوض میکرد. مشکل: صفهای admission روی prefill و decode چک میکردند که آیا درخواست finished() هست، نه اینکه reject شده. درخواست reject شده خودش رو «زنده» گزارش میکرد، پس:
- prefill یه forward pass بیاهمیت روی یک توکن انجام میداد.
- فقط یک صفحه KV به decode منتقل میشد و سیگنال «انتقال کامل» میرفت.
- decode روی همه صفحات رزروشده attention میکرد — صفحه P0 نوشته شده بود، ولی صفحات P1 تا P7 هنوز حاوی داده درخواستهای قبلی (پرامپت و خروجی کاربران دیگه) بودن.
- نتیجه: کلاینت بهجای HTTP 400، یه HTTP 200 منسجم میگرفت که سریعتر از انتظار هم بود و محتوای کاربران دیگه توش بود.
هر درخواستی بالای اون آستانه این رفتار رو داشت. جالبه که این باگ از ۶ جولای ۲۰۲۶ بهعنوان issue #30233 به SGLang گزارش شده بود.
جدول خط زمانی حادثه
| تاریخ (۲۰۲۶) | اتفاق | |---|---| | ۲۷ اوت | عرضه GLM 5.3 با pool کوچکتر از کانتکست؛ نششت از همون روز | | ۸ سپتامبر، ۲۳:۱۱ | گزارش از طریق HackerOne | | ۹ سپتامبر، ۱۳:۲۲ | اعلام حادثه؛ بازتولید با دو اکانت مستقل در ۲۵ دقیقه | | ۹ سپتامبر، ۱۴:۰۰ | مدل خصوصی شد؛ درخواستها HTTP 400 | | ۹ سپتامبر، ۱۹:۴۰ | مدل با کانفیگ اصلاحشده + پچ موتور برگشت | | ۲ اکتبر | SGLang v0.5.21 منتشر شد | | ۱۹ اکتبر | SGLang v0.5.22 منتشر شد (شامل همه فیکسها) | | ۲۶ اکتبر | انتشار هماهنگ گزارش با پژوهشگر و RadixArk |
فیکسها
سمت کانفیگ: pool الان حدود ۲.۰۴ میلیون توکن نگه میداره — تقریبا دو برابر کانتکست ۱,۰۴۸,۵۷۶. یعنی درخواست معتبر هرگز به مسیر reject نمیرسه.
سمت موتور: مسیر reject الان امن شده — درخواستِ reject شده قبل از هر bootstrap یا انتقال KV با خطای اصلیاش retire میشه. فیکس اصلی در #36988 (v0.5.20) و #38935 رفت، و Cloudflare توصیه میکنه همه اپراتورها به v0.5.22 یا بعدتر آپدیت کنن. decode هم حالا تعداد توکن منتقلشده رو با تعداد رزروشده مقایسه میکنه.
توصیههای Cloudflare به اپراتورهای LLM
اگه SGLang رو در حالت disaggregated prefill/decode اجرا میکنی (یا هر سرویس inference سلفهاست داری):
- سایز pool رو اندازه بگیر و با کانتکست مقایسه کن — روی هر نود، از لاگ استارتاپ موتور، بعد از هر تغییر weights/کوانتیزیشن/parallelism. اگه pool کوچیکتره، کانتکست رو کوچیک کن. وابسته به پراکسی نباش.
- به v0.5.22+ آپدیت کن تا درخواست بلندتر از pool قبل از handoff رد بشه.
- deferred KV release رو روی هر دو نقش روشن کن و batchهای انتقال Mooncake رو محدود کن.
- تشخیص خرابی رو فعال کن جایی که مدل پشتیبانی میکنه:
--disaggregation-enable-kv-checksumو--kv-canary(هنوز همه معماریها رو پوشش نمیدن؛ کار ادامه داره). - پراکسی رو طوری تنظیم کن که abort بیهوده تولید نکنه — لودبالانسر باید اجازه بده درخواست تا اولین توکن جلو بره.
درس اصلی برای بقیه ما
چند نکته که فراتر از این حادثه خاصه:
- چند-اجارهای بودن (shared tenancy) یه مدل حمله واقعیه. یکی از کامنتگذارهای HN (نظر ۲۳۳ امتیازی زیر پست) نکته مهمی گفت: خیلیها فرض میکنن سرویس مشترک بهطور جادویی توکنهاشون رو ایزوله میکنه، در حالی که cache مشترک کلاسهای کاملی از حمله داره. دادههای خیلی حساس رو روی میزبانهای غیرمشترک ببر.
- چک کردن کد HTTP کافی نیست. گلوگاه Cloudflare دقیقا این بود که تستها فقط status code و پرفورمنس رو دیدن، نه منسجم بودن خروجی. تست سیستمهای inference باید خروجی مدل رو هم validate کنه.
- پاسخهای «غیرعجیب» LLM رو جدی بگیر. جوابهای بیربط از یه API میتونه فقط باگ باشه — ولی یه کامنتگذار HN گفت این حادثه ممکنه توضیح بده چرا چند بار از یه ارائهدهنده جوابهای کاملا نامرتبط گرفته.
- اگه روی Cloudflare Workers خودت اپ اجرا میکنی و نگران مرزهای ایزولاسیون هستی، این با راهنمای ایجنت سلفهاست روی Cloudflare ما هم ببین که کجاها مرز رایگان/سلف-هاست عوض میشه. و برای یه مثال دیگه از امنیت مرزهای inference، تحلیل ما روی صحهگذاری رسمی LLM در Anthropic مفیده.
Q: داده من لو رفته؟
فقط اگر بین ۲۷ اوت و ۹ سپتامبر ۲۰۲۶ از مدل @cf/zai-org/glm-5.3 با پرامپت بلندتر از ۶۸۱,۲۸۰ توکن استفاده کرده باشی. Cloudflare ۱۵۵ پاسخ تحت تاثیر رو شناسایی کرده و میگه سایر مدلها یا کانتکستهای کوتاهتر تحت تاثیر نبودن. شواهدی از سوءاستفاده عمدی هم نیست.
Q: این باگ چیه به زبان ساده؟
حافظه کش مدل (KV cache) بین درخواستها اشتباهی بین نودها جابهجا میشد؛ وقتی پرامپت در حافظه جا نمیشد، سیستم بهجای خطا، روی صفحات حافظهی قبلی (داده کاربر دیگه) attention میکرد و پاسخ میداد.
Q: چطور مطمئن شم سرویس LLM من امنه؟
برای سرویسهای مدیریتشده، لاگهای تغییرات و اعلانهای امنیتی ارائهدهنده رو دنبال کن. برای سلف-هاست، SGLang رو به v0.5.22+ برسون، سایز pool رو با کانتکست مقایسه کن، و خروجی مدل رو توی تستها validate کن نه فقط کدهای HTTP.