- Memory safety और thread safety ऐसे concepts नहीं हैं जिन्हें अलग किया जा सके; अगर thread safety नहीं है, तो असली memory safety हासिल नहीं की जा सकती
- Go जैसी thread-safe नहीं होने वाली languages में केवल thread issues की वजह से भी memory safety टूट सकती है
- Java जैसी कुछ languages concurrency memory model के जरिए data race को भी defined behavior के रूप में संभालती हैं, जिससे language-level safety सुनिश्चित होती है
- Go data race के प्रति संवेदनशील है, और memory safety के वास्तविक उल्लंघन के मामले मौजूद हैं
- वास्तव में सबसे महत्वपूर्ण property है Undefined Behavior(UB) का अभाव
Thread safety के बिना memory safety की गारंटी नहीं दी जा सकती
अवधारणा का भ्रम: memory safety vs thread safety
- हाल के समय में memory safety पर बहुत ध्यान दिया जा रहा है, लेकिन व्यवहार में इसका अर्थ क्या है, इसकी परिभाषा स्पष्ट नहीं है
- पारंपरिक रूप से memory safety उन languages के लिए इस्तेमाल होती है जो use-after-free या out-of-bounds memory access को रोकती हैं
- दूसरी ओर, thread safety का मतलब है ऐसा program जिसमें concurrency bugs न हों, और दोनों concepts को अक्सर अलग-अलग माना जाता है
- लेखक का तर्क है कि यह भेद व्यवहार में उपयोगी नहीं है, और हम वास्तव में Undefined Behavior(UB) की अनुपस्थिति चाहते हैं
Data race से memory safety का उल्लंघन: Go उदाहरण
- memory safety और thread safety को अलग-अलग मानने की समस्या दिखाने के लिए Go language का उदाहरण दिया गया है
- Go को memory-safe language माना जाता है, लेकिन नीचे जैसे program में केवल data race की वजह से भी memory error हो सकती है
globalVar를 반복적으로 다른 타입 값(Int, Ptr)으로 변경하면서 동시에 별도 고루틴에서 이를 읽어 메서드를 호출
- जब दो threads आपस में overlap होकर globalVar के अंदर के दो pointers (data, vtable) को अलग-अलग update करते हैं, तो बीच में read होने पर mixed state बन सकती है और गलत memory access हो सकता है
- नतीजतन program गलत address (जैसे 0x2a; hexadecimal 42) को refer करने की कोशिश करता है और error के साथ बंद हो जाता है
- यह घटना Go के interface, slice आदि में भी समान रूप से संभव है, क्योंकि कई fields को atomically update नहीं किया जाता
दूसरी languages में concurrency handling और memory safety
- Java जैसी दूसरी languages में भी data race हो सकती है, लेकिन वे defined concurrency memory model लागू करके यह सुनिश्चित करती हैं कि program language को ही न तोड़ दे
- उदाहरण: Java multi-threaded environment में भी runtime error (जैसे forced segmentation fault) में न गिरे, इसके लिए memory model को बहुत सावधानी से design करता है
- अधिकांश languages concurrency issues को नीचे दिए गए दो तरीकों में से एक से नियंत्रित करती हैं
- ऐसा memory model define करना जो सभी concurrent programs के लिए consistent behavior सुनिश्चित करे (हालांकि इससे compiler optimizations पर सीमा लगती है और implementation का बोझ बढ़ता है)
- Java, C#, OCaml, JavaScript, WebAssembly आदि
- मजबूत type system के जरिए अधिकांश data race को प्रतिबंधित करना, और कुछ exceptions को सुरक्षित ढंग से handle करना (Rust, Swift की strict concurrency)
- Go इनमें से किसी भी विकल्प का पालन नहीं करता
- यह केवल data race न होने की स्थिति में ही memory safety की गारंटी देता है
- data race detection tool मौजूद हैं, लेकिन वास्तविक programs में सभी परिस्थितियों को testing से verify करना सीमित है
- research results और field experience में memory safety violation के कई वास्तविक मामले रिपोर्ट हुए हैं
Go का memory model और documentation issues
- Go memory model के आधिकारिक documents कहते हैं कि अधिकांश races के नतीजे सीमित होते हैं, लेकिन यह साफ़ तौर पर नहीं बताते कि कुछ data races के परिणाम असीमित हो सकते हैं
- कभी-कभी यह दावा भी किया जाता है कि यह Java/JavaScript जैसा है, लेकिन concurrency safety सुनिश्चित करने के लिए उन दोनों languages ने Go की तुलना में कहीं अधिक प्रयास किए हैं
- documentation के कुछ detail sections में ही सीमित रूप से यह उल्लेख मिलता है कि कुछ data races पूरी तरह undefined behavior पैदा कर सकती हैं
निष्कर्ष: Undefined Behavior(UB) का अभाव ही असली लक्ष्य है
- व्यवहार में users वास्तव में जो property चाहते हैं, वह है program का language को ही न तोड़ना (UB का अभाव)
- memory safety के उल्लंघन से पैदा होने वाली विभिन्न security vulnerabilities इसलिए संभव होती हैं क्योंकि UB वास्तव में घटित हो चुका होता है
- एक बार UB होने के बाद उसके बाद का हर behavior अप्रत्याशित हो जाता है, और attacker इसका दुरुपयोग कर सकता है
- 'safe' और 'unsafe' languages के बीच मूलभूत अंतर UB होने की संभावना में है
- memory safety, thread safety, type safety जैसी श्रेणियों में बाँटने से अधिक महत्वपूर्ण है UB होता है या नहीं, यही सवाल
- वास्तविकता में safety भी एक spectrum है; Go, C से अधिक सुरक्षित है, लेकिन पूर्ण safety की गारंटी नहीं देता
- data के आधार पर Go में वास्तविक safety को 'prove' करना बहुत कठिन है, और यह समझना महत्वपूर्ण है कि हर language के design choices के क्या गैर-सहज परिणाम निकलते हैं
1 टिप्पणियां
Hacker News राय
Swift में भी यही समस्या है, इसलिए मैंने एक बार ऐसा program लिखा था जो दिखाता था कि shared data structure को access करते समय Swift कितनी आसानी से segfault कर सकता है
Go को Rust या Java की तरह memory-safe कहना थोड़ा बढ़ा-चढ़ाकर कहना है
mapजैसे बुनियादी structure thread-safe नहीं होते, इसलिए modify करते समय सावधान रहना चाहिए — यह बात Go spec में भी साफ़ लिखी हैDropbox में जो समस्या हुई, उसके बारे में विस्तार से सुनना चाहूँगा
memory safety, PLT(programming language theory) की अवधारणा से ज़्यादा software security का शब्द है
अंततः Go programmers भी इस फर्क को अच्छी तरह जानते हैं, इसलिए Go का बुनियादी premise कुछ ऐसा है: “sharing के ज़रिए communicate मत करो, communication के ज़रिए share करो”
बेशक, वास्तविक दुनिया में यह concept पूरी तरह लागू नहीं हो पाया, और अब सब समझते हैं कि modern Go में sharing बहुत होती है और synchronization की ज़रूरत पड़ती है
सच कहूँ तो कई साल Go चलाने के बाद, मुझे याद नहीं कि ऐसा bug वास्तव में कभी सामने आया हो
Uber ने Go code में होने वाले bugs पर विस्तार से लिखा है, और इस लेख में table के साथ दिखाया है कि यह समस्या वास्तव में कितनी बार होती है
Go में concurrent
mapयाsliceaccess की ज़्यादातर समस्याएँ उसी slice पर होती हैं, और उनमें “torn read” होना ज़रूरी है, इसलिए व्यवहार में यह आम नहीं हैफिर भी लोग ऐसी समस्याओं से अक्सर बच जाते हैं, शायद इसलिए कि वे आम तौर पर काफ़ी सावधान रहते हैं और concurrent access की स्थिति में variables को reassign करने के जोखिम को जानते हैं
भाषा में
atomics,channel,mutexजैसी चीज़ें हैं, इसलिए concurrent access में गलत usage व्यवहार में कम होता है, और race detector भी है, इसलिए ऐसी समस्या हो तो जल्दी पकड़ में आ जाती हैperformance hit होने पर भी torn read वाली समस्या मुझे बस ठीक कर देने लायक issue लगती है, और production Go code में यह कभी बड़ी समस्या नहीं रही
संबंधित वीडियो
race detector ने भी कुछ नहीं पकड़ा, और किसी को समझ नहीं आ रहा था कि हो क्या रहा है
आखिर में पता चला कि loop counter overflow हो रहा था, जिससे वही computation बहुत ज़्यादा बार दोहराई जा रही थी, और requests कभी-कभी 100ms की जगह 3 मिनट ले रही थीं
production में
perfसे अप्रत्यक्ष रूप से समस्या का पता चला, और platform developer के रूप में मेरा debugging अनुभव टीम के बहुत काम आयाGo race स्थितियों से इतना सामना होने के बाद, निजी तौर पर मेरी इच्छा है कि Rust हर जगह अपनाया जाए
उदाहरण के लिए यह issue compiler के बड़े refactor की मांग करता है, इसलिए इसमें बहुत समय लग रहा है
Send/Synctypes जैसी कोई अवधारणा नहीं हैअभी तक concurrent Zig code कम है, इसलिए समस्या बहुत उभरकर नहीं आई, लेकिन आगे async feature ज़्यादा इस्तेमाल होने लगे तो कई समस्याएँ एक साथ फट सकती हैं
ReleaseSafeसे build किया गया single-threaded Zig program भी, उदाहरण के लिए, local variable की lifetime खत्म हो चुके pointer को dereference करने पर, सभी optimization modes में memory corruption के जोखिम से मुक्त नहीं हैहाँ, C की तुलना में bugs कम होते हैं, लेकिन यह बात C++ के लिए भी सही है, और कोई C++ को memory-safe नहीं कहता
बेशक इसका मतलब यह नहीं कि जोखिम बिल्कुल शून्य है, लेकिन इससे यह संकेत मिलता है कि Go applications की security में यह शायद top-priority issue नहीं है
दूसरी ओर, C/C++ code में 60~75% वास्तविक vulnerabilities memory safety समस्याओं से आती हैं
मुझे लगता है memory safety भी एक spectrum है, और एक स्तर के बाद उसका marginal utility कम होने लगता है
exploit न हो सकने वाला bug भी आखिर bug ही है, और उसे ठीक करना पड़ता है
शुरुआती development की तुलना में maintenance में कहीं ज़्यादा समय लगता है, इसलिए अगर maintenance कम की जा सके, तो शुरुआती launch में देरी भी क़ीमती सौदा हो सकती है
जबकि Go में thread safety CVE का मुख्य कारण नहीं है
सिद्धांत में इसमें दम है, लेकिन व्यवहार में यह बहुत उभरकर नहीं आता
जब memory share की जाती है, तो अगर data structure बिगड़ जाए तो दूसरे thread में unsafe या गलत behavior हो सकता है
उदाहरण के लिए, अगर एक thread vector का size बदल रहा हो और उसी समय दूसरा thread उसे access करे, तो sequential execution में safe दिखने वाला काम भी concurrency में खतरनाक हो जाता है
Go भी इससे अछूता नहीं है
जबकि thread safety issue अगर segfault पर खत्म हो, तो हो सकता है कि वह सिर्फ DoS(denial of service) हमला ही बने
race condition ज़्यादा ताकतवर attack में बदल सकती है, लेकिन उसे trigger करना कहीं ज़्यादा कठिन है
यही data corruption और races का बड़ा कारण है
कई स्थितियों में process-based model, thread-based model से बेहतर concurrency model हो सकता है, लेकिन उसकी कमी यह है कि वह बहुत heavy है
अगर हर thread को ज़रूरी data message passing के ज़रिए देना ही default होता, तो मुझे लगता है ऐसी ज़्यादातर समस्याएँ गायब हो जातीं
खैर, platforms हमें global variables और shared memory इस्तेमाल करने की आज़ादी देते हैं, तो बस हमें खुद उसे न इस्तेमाल करना होगा
Rust का मूल लक्ष्य memory-safe systems language बनना नहीं था, बल्कि thread-safe systems language बनना था, और memory safety उसका स्वाभाविक परिणाम बनकर आई
Rust में structured concurrency को
thread::scopeजैसी चीज़ों से इस्तेमाल किया जा सकता है, इसलिए thread-based काम बहुत सुविधाजनक हो जाता हैchannelआदि) पर ज़्यादा ज़ोर दिया जाता हैयह दस्तावेज़ देखें
sendabletype, ownership, read-only reference जैसी अवधारणाएँ नहीं हैं, इसलिए इसे सुरक्षित तरीके से इस्तेमाल करना आसान नहीं हैवास्तविक उदाहरण: ऊपर के code में
buf.Bytes()internal memory को उसी रूप में reference करके भेजता है, औरReset()call के कारण backing memory फिर से reuse होती है, जिससेprocessData/mainदोनों एक ही memory को साथ में access करने लगते हैं और data race हो जाती हैRust में ऐसा code दो mutable references होने के कारण compile ही नहीं होगा, और ownership transfer या copy के लिए मजबूर करेगा
Go में यह भ्रमित करने वाला है;
bytes.Buffer.ReadBytes("\n")या.String()copy लौटाते हैं, इसलिए वे safe हैं, लेकिन.Bytes()ऊपर की तरह खतरनाक हैRust के channels इस समस्या को ownership/transfer की अवधारणा से मूल रूप से रोकते हैं, लेकिन Go में ऐसी safety rail नहीं है
नतीजतन यह mutex से धीमा पड़ सकता है, और Go beginners के लिए सही तरीके से इस्तेमाल करना और कठिन अनुभव बन सकता है
यानी “safe” race या “safe” deadlock जैसी चीज़ें उल्टा और अधिक सामान्य हो जाती हैं
PL theory में Rust का race-freedom approach आकर्षक लग सकता है, लेकिन वास्तविक apps में महत्वपूर्ण data तो वैसे भी RDBMS में होता है, और उदाहरण के लिए अगर
SELECTके साथFOR UPDATEन लगाया जाए, तो race आसानी से हो सकती हैRust app अगर बिल्कुल
unsafeन भी इस्तेमाल करे, तब भी DB के कारण race बनी रहती हैGo की बनावट ऐसी है कि वह memory corruption bugs को लगभग होने नहीं देती — यह बात वास्तविक exploits की अनुपस्थिति से समझी जा सकती है
अगर इस लेख की दलील मानें, तो ज़्यादातर high-level languages भी memory-safe नहीं रह जाएँगी, जबकि लेख में केवल Java को अपवाद की तरह रखा गया है
Rust, Go से “ज़्यादा” safe हो सकता है, लेकिन “memory safety” कोई continuous spectrum नहीं बल्कि pass/fail अवधारणा है
अगर आप दावा करते हैं कि कोई भाषा memory-unsafe है, तो आपको POC ज़रूर दिखाना चाहिए
लेख में दिया गया उदाहरण दिखाता है कि
intको गलती से pointer मान लेने से memory corruption आसानी से हो सकती हैdemo में जानबूझकर
42इस्तेमाल किया गया है ताकि segfault हो, लेकिन अगर असली address value इस्तेमाल की जाती, तो वास्तविक corruption होतीSIGSEGVसे जबरन termination), इसलिए यह memory safety का उल्लंघन हैइसीलिए जिस भाषा में data race संभव है, उसे memory-safe नहीं कहा जा सकता
ऐसे cases में उसे memory-safe कहना सही होगा या नहीं, इस पर संदेह है
ऐसी समस्या से बचने के लिए कई बार “Gaussian Curvature” या “Riemann Integrals” जैसे व्यक्तिनाम लगाए जाते हैं
“जहाँ शुरुआती अर्थ संकरा रह गया, लेकिन व्यापक अर्थ में विस्तार हुआ” — ऐसा “Galois Group” जैसे उदाहरणों में भी हुआ है
memory safety भी इससे अलग नहीं है
कृपया कोई ठोस उदाहरण दें
FAQ आदि में memory safety का उल्लेख या unions पर उत्तरों में यह संकेत मिलता है कि Go memory-safe है, लेकिन वास्तव में उसका मतलब क्या है, यह स्पष्ट नहीं है
Rob Pike की 2012 प्रस्तुति में "Not purely memory safe" कहा गया था, लेकिन
purelyका मतलब भी परिभाषित नहीं हैGo के race detector दस्तावेज़ों में भी
safeकी परिभाषा अस्पष्ट है(उदाहरण दस्तावेज़)बाहरी दुनिया में तो Go को “memory-safe programming language” कहकर और भी ज़ोर से प्रस्तुत किया जाता है
उदाहरण के लिए fly.io के security document या memorysafety.org के दस्तावेज़, जिसमें Go को memory safe वर्गीकृत किया गया है
लेकिन इन्हीं दस्तावेज़ों में “Out of Bounds Reads and Writes” को भी memory safety issue बताया गया है, और पोस्ट में बताई गई Go error उसी श्रेणी में आती है
कम-से-कम Go और उसके community को “memory safety” के सही अर्थ को स्पष्ट कर देना चाहिए
जब तक ऐसे cases मौजूद हैं, तब तक Go को बिना स्पष्टीकरण के memory-safe भाषा कहना उचित नहीं है
जब Go बनाया गया था, तब “garbage collector हो तो memory safe” जैसी सोच मुख्यधारा में थी, और C/C++ की तुलना में वह निश्चित रूप से कहीं ज़्यादा safe है