Go के ARM64 कंपाइलर में बग कैसे मिला
(blog.cloudflare.com)- 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 इस प्रकार था:
- दो ADD निर्देशों के बीच asynchronous preemption होता है
- GC या किसी अन्य कारण से stack unwinding routine चलती है
- असामान्य stack pointer स्थिति खोजी जाती है और गलत function address की व्याख्या होती है
- 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 टिप्पणियां
Hacker News राय