2 पॉइंट द्वारा GN⁺ 2025-10-09 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Cloudflare ने arm64 प्लेटफ़ॉर्म पर चलने वाले Go कंपाइलर में होने वाला एक दुर्लभ race condition बग बड़े पैमाने के ट्रैफ़िक मॉनिटरिंग के दौरान खोजा
  • यह बग stack unwinding के दौरान सेवा के अप्रत्याशित रूप से panic state में जाने या memory access error होने के रूप में सामने आया
  • कारण का पता लगाते हुए यह पुष्टि हुई कि समस्या Go runtime की asynchronous preemption और कंपाइलर द्वारा बनाए गए stack pointer adjustment के दो निर्देशों के बीच उत्पन्न हो रही थी
  • minimal reproduction code के माध्यम से यह साबित किया गया कि यह बग Go runtime की अपनी समस्या है, और इससे यह स्पष्ट हुआ कि stack pointer एक instruction जितनी छोटी race window में अधूरा बदल सकता है
  • यह issue go1.23.12, go1.24.6, go1.25.0 संस्करणों में patch किया गया, और नई पद्धति में ऐसे stack pointer manipulation से बचा गया है जिन्हें तुरंत atomic तरीके से बदलना संभव नहीं, जिससे race condition मूल रूप से बंद हो जाती है

Cloudflare द्वारा मिले Go ARM64 कंपाइलर बग का विश्लेषण

Cloudflare के data center दुनिया के 330 से अधिक शहरों में हर सेकंड 8.4 करोड़ HTTP requests संभालते हैं, और इस तरह के बड़े पैमाने के ट्रैफ़िक वातावरण में दुर्लभ बग भी अपेक्षाकृत बार-बार सामने आ जाते हैं। यह लेख arm64 प्लेटफ़ॉर्म पर Go कंपाइलर द्वारा generated code में होने वाली race condition समस्या का वास्तविक उदाहरणों के साथ विस्तार से विश्लेषण करता है।

अजीब panic घटना की जांच

  • Cloudflare नेटवर्क में Magic Transit और Magic WAN जैसे प्रोडक्ट ट्रैफ़िक प्रोसेसिंग को kernel में configure करने वाली सेवाएँ चल रही थीं
  • arm64 मशीनों पर कभी-कभी लेकिन बार-बार fatal panic संदेश मॉनिटरिंग सिस्टम में दिखाई दिए
  • शुरुआती विश्लेषण में stack unwinding के दौरान integrity violation का पता चला (पुराने कोड में, जो panic/recover pattern का उपयोग करता था, panic अधिक बार हो रहे थे)
  • अस्थायी रूप से panic/recover संरचना हटाकर panic की आवृत्ति कम की गई, लेकिन बाद में संदिग्ध fatal panic और अधिक बार होने लगे
  • इसके बाद यह निष्कर्ष निकाला गया कि साधारण pattern tracking से आगे बढ़कर गहराई से root cause analysis करना आवश्यक है

Go runtime और scheduler data structure का अवलोकन

  • Go हल्के user-space scheduler के साथ M:N scheduling संरचना अपनाता है (कई goroutine को कम संख्या वाले kernel thread पर map किया जाता है)
  • scheduler की मुख्य संरचनाएँ g(goroutine), m(machine/kernel thread), p(processor) पर आधारित हैं
  • stack unwinding failure या memory access error तब हो सकते हैं जब stack pointer या return address असामान्य रूप से बदल जाए

stack unwinding के दौरान त्रुटि का संरचनात्मक कारण

  • कई backtrace के विश्लेषण से पता चला कि सभी घटनाएँ (*unwinder).next फ़ंक्शन में stack unwinding प्रक्रिया के दौरान हो रही थीं
  • एक मामले में return address null था, इसलिए उसे असामान्य stack मानकर fatal error के साथ बंद किया गया; दूसरे मामले में stack frame के भीतर Go scheduler structure m के field (incgo) तक पहुँचते समय segmentation fault हुआ
  • crash वास्तविक बग के उत्पन्न होने वाले बिंदु से काफी दूर दिखाई देता था, इसलिए कारण का पता लगाना कठिन था

देखे गए पैटर्न और Go Netlink लाइब्रेरी से संबंध

  • stack trace की समीक्षा से पता चला कि सभी crash Go Netlink लाइब्रेरी के NetlinkSocket.Receive फ़ंक्शन में preemption होने के समय पर केंद्रित थे
  • इसके बाद दो परिकल्पनाएँ बनाई गईं
    • Go Netlink के unsafe.Pointer उपयोग से उत्पन्न बग होने की संभावना
    • Go runtime की asynchronous preemption और stack unwinding में स्वयं बग होने की संभावना
  • code audit किया गया, लेकिन सीधे memory corruption जैसे पैटर्न नहीं मिले, इसलिए अनुमान लगाया गया कि समस्या का मूल runtime और stack handling strategy में है

asynchronous preemption और race condition

  • Go 1.14 से शुरू हुई asynchronous preemption सुविधा लंबे समय तक चलने वाले goroutine के लिए OS thread को signal (SIGURG) भेजकर मजबूरन scheduling point बनाती है
  • यदि यह preemption stack frame pointer को समायोजित करने वाले दो assembly निर्देशों के बीच हो जाए, तो stack pointer बीच की अवस्था में रह सकता है
  • garbage collection, panic handling, या stack trace generation के लिए stack unwind करते समय गलत स्थान पढ़ा जा सकता है, जिससे गलत function address या data interpretation हो सकती है

minimal reproduction code बनाना

  • stack frame allocation size को नियंत्रित करके, और stack को स्पष्ट रूप से adjust करने वाले फ़ंक्शन (big_stack) तथा लगातार garbage collection कॉल करने वाले कोड को लिखकर race condition को reproduce किया गया
  • वास्तव में assembly code में stack pointer दो ADD निर्देशों से adjust हो रहा था, और इनके बीच asynchronous preemption होने पर stack unwinding के दौरान crash हो रहा था
  • यह दोष केवल standard library code से भी reproduce किया जा सका, जिससे यह सिद्ध हुआ कि यह Go कंपाइलर द्वारा generated code में निहित एक instruction-size vulnerability window है

ARM64 कंपाइलर स्तर की race window का कारण

  • ARM64 architecture की fixed-length instruction और immediate value limits के कारण stack pointer adjustment के लिए दो या अधिक निर्देशों की आवश्यकता पड़ सकती है
  • Go के internal intermediate representation (IR) में इन immediate value limits की जानकारी नहीं होती, और split instructions केवल actual machine code generation के समय डाले जाते हैं
  • इसी वजह से stack frame return (ADD RSP, RSP) के लिए दो निर्देश उपयोग में आते हैं, और preemption के लिए संवेदनशील single-instruction window बन जाती है
  • unwinder को stack pointer की पूर्ण शुद्धता की आवश्यकता होती है, लेकिन यदि execution किसी instruction के बीच रुक जाए तो गलत value interpretation और fatal failure हो सकती है
  • वास्तविक crash flow इस प्रकार था:
    1. दो ADD निर्देशों के बीच asynchronous preemption होता है
    2. GC या किसी अन्य कारण से stack unwinding routine चलती है
    3. असामान्य stack pointer स्थिति खोजी जाती है और गलत function address की व्याख्या होती है
    4. runtime crash हो जाता है

बग fix और मूलभूत सुधार

  • Cloudflare टीम ने minimal reproduction code और विस्तृत विश्लेषण के आधार पर Go के आधिकारिक repository में रिपोर्ट किया, और issue को तेज़ी से patch कर release किया गया
  • go1.23.12, go1.24.6, go1.25.0 और उसके बाद के संस्करणों में पहले temporary register में पूरा offset compute किया जाता है, फिर एक single instruction से stack pointer बदला जाता है, जिससे preemption वाली कमजोरी हट जाती है
  • अब stack pointer हमेशा वैध स्थिति में बना रहता है, इसलिए race condition संरचनात्मक रूप से रोकी जाती है
LDP -8(RSP), (R29, R30)
MOVD $32, R27
MOVK $(1<<16), R27
ADD R27, RSP, RSP
RET

निष्कर्ष और संकेत

  • यह बग विशिष्ट architecture के compiler code generation और concurrency management (asynchronous preemption) के अप्रत्याशित टकराव का उदाहरण है
  • केवल बड़े पैमाने के वातावरण में सामने आने वाली अत्यंत दुर्लभ instruction-level race condition को वास्तविक डेटा और वैज्ञानिक तर्क से ट्रेस करना इस मामले को खास बनाता है
  • यदि आप नवीन Go environment और ARM64 architecture आधारित सेवाएँ चला रहे हैं, तो संबंधित Go संस्करण पर upgrade करना महत्वपूर्ण है

1 टिप्पणियां

 
GN⁺ 2025-10-09
Hacker News राय
  • यह वाकई कमाल की खोज लगी, और assembly code देखते ही debugging path को फॉलो करने का मन हुआ। असल में यह तरीका सिर्फ assembly में ही संभव नहीं है; IR चरण में भी संभव हो सकता है, लेकिन कई कारणों से ऐसा नहीं होता। ARM assembly पढ़ पाने की क्षमता यहां बड़ा फायदा है। instruction count घटाने के लिए stack size को push या pop करने का तरीका भी सोचा था, लेकिन GC ठीक-ठीक क्या जांचता है यह पता न होने से पक्का नहीं कह सकता। इस पर और लोगों की राय सुनना चाहूंगा
    • आम तौर पर ARM का pseudo-instruction “LDR Rd, =expr” इस्तेमाल किया जाता है। जो constant सीधे नहीं बनाया जा सकता, उसके लिए constant को PC-relative स्थान पर रखकर PC के आधार पर register में load किया जाता है। इससे “SP में constant जोड़ना” प्रक्रिया 2 executable instructions में बदली जा सकती है, और 8 bytes code तथा 4 bytes data area (17-bit constant के लिए) मिलाकर कुल 12 bytes की जरूरत होती है। संबंधित दस्तावेज़: LDR pseudo-instruction विवरण
    • immediate value को RSP में जोड़ने वाले इस अजीब special case के लिए assembler में इस bug का special handling न होना हैरान करता है। अगर patch सिर्फ compiler side पर लगा है, तो aarch64 assembly के दूसरे हिस्सों में भी यही समस्या बची हो सकती है
    • ARM assembly syntax में dollar sign वाला यह अजीब expression standard AArch64 assembly नहीं है, और अच्छा होता अगर लेख में यह नियम भी बताया जाता कि “stack को सिर्फ एक बार move होना चाहिए”
    • Java या .NET जैसे runtime safepoint को स्पष्ट रूप से रखते हैं ताकि instruction set के बीच context switch न हो
    • लगता है सही समाधान यह है कि compiler constant को दो हिस्सों में register में डाले और फिर एक add से SP को atomically adjust करे। हां, इससे एक instruction बढ़ेगा, लेकिन atomicity मिल जाएगी। या temporary register में operation करके फिर वापस move करने का तरीका भी हो सकता है
  • जल्दी में पढ़ने वालों के लिए fix commit का लिंक साझा कर रहा हूं: golang/go commit link
    • issue देखते हुए यह जिज्ञासा हुई कि Go team natural-language bot इस्तेमाल करती है, या comments में सिर्फ “backport” keyword चेक करती है। संबंधित comment: github issue comment
  • तकनीकी रूप से यह बेहद शानदार ब्लॉग है, और इसकी explanation इतनी साफ है कि समझना आसान हो गया, बल्कि खुद को और smart महसूस किया। x86 assembly के बाद काफी समय बाद फिर assembly देखी, फिर भी साथ-साथ चलना आसान था। और ऐसी टीम पर यह भरोसा भी बनता है कि वह ऐसे issue कभी भी सुलझा सकती है और उसकी quality control मजबूत है। server scaling के लिए Ampere Altra पर भी विचार किया था, लेकिन जगह पर्याप्त होने के कारण आखिर में Epyc इस्तेमाल किया
  • अगर Go में ऐसा कोई mode हो जिसमें हर instruction को single-step किया जाए और हर instruction पर GC interrupt trigger हो, तो ऐसे bug शायद ज्यादा आसानी से मिल सकते हैं
  • जिज्ञासा है कि ARM64 server का उपयोग कहां हो रहा है। पिछले साल AMD EPYC आधारित Gen 12 server launch करने की बात हुई थी, लेकिन ARM64 का जिक्र नहीं था; क्या अभी ARM64 production में इस्तेमाल हो रहा है?
    • मैं Cloudflare का कर्मचारी नहीं हूं, लेकिन ब्लॉग बहुत पढ़ता हूं, इसलिए जहां तक जानता हूं, secure boot वगैरह को देखते हुए वे कुछ सालों से Ampere को AMD के साथ parallel में deploy कर रहे हैं। लगता है operational purpose edge efficiency है, हालांकि और उपयोग भी हो सकते हैं। अधिक जानकारी यहां: edge server design लेख , Ampere Altra vs AWS Graviton2 , और Qualcomm ARM मूल्यांकन
    • याद है कि Cloudflare कुछ non-edge computing public cloud पर host करता है, जैसे control plane आदि, तो संभव है ऐसा ही हो
  • मैं समझता था कि Cloudflare आजकल 100% Rust और x86(EPYC) ही इस्तेमाल करता है; Go और ARM का उपयोग होना दिलचस्प है
  • हर बार Cloudflare ब्लॉग मुझे ऐसा बेहतरीन content लगता है जो infra या ML की जादूगरी के बिना engineering की असली बुनियाद दिखाता है। कभी न कभी यहां apply करना चाहूंगा। compiler bug सोचने से ज्यादा आम हैं (पहले gcc में हर साल कुछ न कुछ मिल जाते थे), लेकिन लेख की तरह ऐसे rare cases अक्सर बड़े scale पर ही सामने आते हैं। ज्यादातर लोग उस scale तक पहुंचते ही नहीं
    • आज apply क्यों नहीं करते, यह जानने की जिज्ञासा है
  • stack pointer को हमेशा atomically adjust किया जाना चाहिए, इस पर जोर दिया गया
    • लगता है preemption लिखने वालों ने x86 को आधार मानकर code बनाया, जहां instruction के भीतर constant आ सकता है और काम atomic हो जाता है; ARM porting के दौरान high level पर automatic splitting हुई और यह bug बन गया। किसी एक की गलती नहीं, लेकिन नतीजा अच्छा नहीं निकला
    • मेरे दिमाग में भी यही बात तुरंत आई थी
  • यह पूरी तरह समझ नहीं आया कि machine thread दो instructions के बीच कैसे रुक गया। bare metal पर क्या ऐसा संभव है, इस पर भी सवाल है
    • go GC notification के लिए interrupt इस्तेमाल करता है
    • signals
  • “यह बहुत मजेदार समस्या थी” वाली बात पर, यह तो तय है कि ऐसी बुनियादी समस्या को सुलझाना राहत देने वाला रहा होगा, लेकिन जब तक यह अनसुलझी रही होगी तब तक बिल्कुल भी मजेदार नहीं रही होगी। ऐसे bug पूरी मानसिक ऊर्जा खा जाते हैं। standard library या compiler में समस्या होगी, यह कोई सोचता ही नहीं, इसलिए developer बार-बार सिर्फ अपने code पर शक करता रहता है। मैंने भी एक बार standard library bug ढूंढा था, और SDK side में दिक्कत है यह सबसे आखिर में शक हुआ। नतीजा यह कि समय पूरी तरह गलत जगह खर्च हो जाता है। और जब इस मामले की तरह race condition भी हो, तो reproduce करना कठिन होता है; हर बार लगता है bug गायब हो गया, फिर वह वापस उभर आता है
    • यह comment अपने जैसे अनुभव जोड़ते हुए भी, लेखक को जो आनंद मिला उस पर अनावश्यक आपत्ति उठाकर थोड़ा असर कम कर देता है। हर व्यक्ति के लिए “मजेदार” का अर्थ अलग हो सकता है
    • कुछ लोग ऐसी बेहद अनोखी debugging से खुशी महसूस करते हैं, जिससे दूसरे लोग परेशान हो जाएं। जो किसी के लिए frustration है, वही किसी और के लिए आनंद हो सकता है
    • शायद लेखक का मतलब “funny” नहीं बल्कि “satisfying” था। मैंने भी deadline के दबाव में Ubuntu GCC ARM toolchain में sscanf bug पकड़ा था; उस समय मजा नहीं आया था, लेकिन problem को ठीक-ठीक isolate करने और regression test लिख देने के बाद बहुत संतोष मिला था
    • गहरी खामी को ठीक कर लेने पर जब समाधान मिल जाता है, तो जबरदस्त राहत मिलती है। compiler या CPU से जुड़े bug सुलझाते समय मैंने भी कई बार सबसे ज्यादा आनंद महसूस किया है
    • managed language में अगर Unsafe जैसी कोई चीज़ बिल्कुल इस्तेमाल किए बिना segfault आ जाए, तो मैं इसे इस संकेत की तरह लेता हूं कि संभव है समस्या मेरे code में न हो