7 पॉइंट द्वारा GN⁺ 2025-07-25 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 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 टिप्पणियां

 
GN⁺ 2025-07-25
Hacker News राय
  • मेरी Dropbox टीम में यह हुआ था कि Go सर्वर में data structure पर synchronization के बिना write किया जाता था, और नए आने वाले engineers के लिए बार-बार segfault करवाना जैसे एक तरह का rite of passage था
    Swift में भी यही समस्या है, इसलिए मैंने एक बार ऐसा program लिखा था जो दिखाता था कि shared data structure को access करते समय Swift कितनी आसानी से segfault कर सकता है
    Go को Rust या Java की तरह memory-safe कहना थोड़ा बढ़ा-चढ़ाकर कहना है
  • Swift इस समस्या को हल करने की दिशा में बढ़ रहा है, लेकिन असली दुनिया में पहले से बहुत सारा unsafe code मौजूद है, इसलिए बदलाव बहुत धीमा और दर्दनाक है
  • मुझे जिज्ञासा है, आम तौर पर map जैसे बुनियादी structure thread-safe नहीं होते, इसलिए modify करते समय सावधान रहना चाहिए — यह बात Go spec में भी साफ़ लिखी है
    Dropbox में जो समस्या हुई, उसके बारे में विस्तार से सुनना चाहूँगा
  • मैं यह रेखांकित करना चाहूँगा कि यहाँ “Rust या Java के अर्थ में memory safety” कोई सख्त technical term की परिभाषा नहीं है
    memory safety, PLT(programming language theory) की अवधारणा से ज़्यादा software security का शब्द है
    अंततः Go programmers भी इस फर्क को अच्छी तरह जानते हैं, इसलिए Go का बुनियादी premise कुछ ऐसा है: “sharing के ज़रिए communicate मत करो, communication के ज़रिए share करो”
    बेशक, वास्तविक दुनिया में यह concept पूरी तरह लागू नहीं हो पाया, और अब सब समझते हैं कि modern Go में sharing बहुत होती है और synchronization की ज़रूरत पड़ती है
  • संदर्भ के लिए, मैं यह पूछना चाहूँगा कि Go में memory-unsafe mutated cases कितने हैं, या वास्तव में Go program के memory-safe न होने की संभावना कितनी है
  • Java भी Rust वाले अर्थ में memory-safe नहीं है
  • यह issue कई बार Rust के soundness hole वाले मुद्दों की तरह बार-बार सामने आता है; यह बेकार की समस्या बिल्कुल नहीं है, लेकिन संयोग से इससे टकराने की संभावना काफ़ी कम है
    सच कहूँ तो कई साल Go चलाने के बाद, मुझे याद नहीं कि ऐसा bug वास्तव में कभी सामने आया हो
    Uber ने Go code में होने वाले bugs पर विस्तार से लिखा है, और इस लेख में table के साथ दिखाया है कि यह समस्या वास्तव में कितनी बार होती है
    Go में concurrent map या slice access की ज़्यादातर समस्याएँ उसी slice पर होती हैं, और उनमें “torn read” होना ज़रूरी है, इसलिए व्यवहार में यह आम नहीं है
    फिर भी लोग ऐसी समस्याओं से अक्सर बच जाते हैं, शायद इसलिए कि वे आम तौर पर काफ़ी सावधान रहते हैं और concurrent access की स्थिति में variables को reassign करने के जोखिम को जानते हैं
    भाषा में atomics, channel, mutex जैसी चीज़ें हैं, इसलिए concurrent access में गलत usage व्यवहार में कम होता है, और race detector भी है, इसलिए ऐसी समस्या हो तो जल्दी पकड़ में आ जाती है
    performance hit होने पर भी torn read वाली समस्या मुझे बस ठीक कर देने लायक issue लगती है, और production Go code में यह कभी बड़ी समस्या नहीं रही
    संबंधित वीडियो
  • मुझे Go में data race bug पकड़ने में कई महीने लगे थे
    race detector ने भी कुछ नहीं पकड़ा, और किसी को समझ नहीं आ रहा था कि हो क्या रहा है
    आखिर में पता चला कि loop counter overflow हो रहा था, जिससे वही computation बहुत ज़्यादा बार दोहराई जा रही थी, और requests कभी-कभी 100ms की जगह 3 मिनट ले रही थीं
    production में perf से अप्रत्यक्ष रूप से समस्या का पता चला, और platform developer के रूप में मेरा debugging अनुभव टीम के बहुत काम आया
    Go race स्थितियों से इतना सामना होने के बाद, निजी तौर पर मेरी इच्छा है कि Rust हर जगह अपनाया जाए
  • Rust maintainers भी soundness hole को bug मानते हैं
    उदाहरण के लिए यह issue compiler के बड़े refactor की मांग करता है, इसलिए इसमें बहुत समय लग रहा है
  • Uber कहता है कि Go program, Java microservices की तुलना में “8 गुना अधिक concurrency expose” करते हैं — यहाँ concurrency को countable noun की तरह इस्तेमाल करने का मतलब क्या है, यह जानना चाहता हूँ
  • Zig भी memory-safe होने का दावा करता है, लेकिन उसमें Rust के Send/Sync types जैसी कोई अवधारणा नहीं है
    अभी तक concurrent Zig code कम है, इसलिए समस्या बहुत उभरकर नहीं आई, लेकिन आगे async feature ज़्यादा इस्तेमाल होने लगे तो कई समस्याएँ एक साथ फट सकती हैं
  • ReleaseSafe से build किया गया single-threaded Zig program भी, उदाहरण के लिए, local variable की lifetime खत्म हो चुके pointer को dereference करने पर, सभी optimization modes में memory corruption के जोखिम से मुक्त नहीं है
  • Zig का memory safety दावा लगभग मज़ाक जैसा है
    हाँ, C की तुलना में bugs कम होते हैं, लेकिन यह बात C++ के लिए भी सही है, और कोई C++ को memory-safe नहीं कहता
  • वास्तविक code में, जब तक वह जानबूझकर malicious तरीके से design न किया गया हो, मैंने data race के कारण vulnerable Go code नहीं देखा
    बेशक इसका मतलब यह नहीं कि जोखिम बिल्कुल शून्य है, लेकिन इससे यह संकेत मिलता है कि Go applications की security में यह शायद top-priority issue नहीं है
    दूसरी ओर, C/C++ code में 60~75% वास्तविक vulnerabilities memory safety समस्याओं से आती हैं
    मुझे लगता है memory safety भी एक spectrum है, और एक स्तर के बाद उसका marginal utility कम होने लगता है
  • मैंने वास्तव में data race के कारण vulnerable Go code देखा है
  • मुझे लगता है maintenance का दर्द CVE से कहीं बड़ा है
    exploit न हो सकने वाला bug भी आखिर bug ही है, और उसे ठीक करना पड़ता है
    शुरुआती development की तुलना में maintenance में कहीं ज़्यादा समय लगता है, इसलिए अगर maintenance कम की जा सके, तो शुरुआती launch में देरी भी क़ीमती सौदा हो सकती है
  • memory safety महत्वपूर्ण इसलिए है क्योंकि ज़्यादातर C program CVE memory safety bugs से आते हैं
    जबकि Go में thread safety CVE का मुख्य कारण नहीं है
    सिद्धांत में इसमें दम है, लेकिन व्यवहार में यह बहुत उभरकर नहीं आता
  • असल सवाल यह है कि thread के भीतर क्या किया जा सकता है
    जब memory share की जाती है, तो अगर data structure बिगड़ जाए तो दूसरे thread में unsafe या गलत behavior हो सकता है
    उदाहरण के लिए, अगर एक thread vector का size बदल रहा हो और उसी समय दूसरा thread उसे access करे, तो sequential execution में safe दिखने वाला काम भी concurrency में खतरनाक हो जाता है
    Go भी इससे अछूता नहीं है
  • C की typical memory safety समस्या अक्सर RCE(remote code execution) तक जा सकती है
    जबकि thread safety issue अगर segfault पर खत्म हो, तो हो सकता है कि वह सिर्फ DoS(denial of service) हमला ही बने
    race condition ज़्यादा ताकतवर attack में बदल सकती है, लेकिन उसे trigger करना कहीं ज़्यादा कठिन है
  • CVE ज़्यादा गंभीर हो सकते हैं, लेकिन threading bug से होने वाला data corruption/crash भी आखिर ऐसा bug है जिसे किसी को triage, analyze और fix करना ही पड़ता है
  • दुखद सच्चाई यह है कि threads इस्तेमाल करने वाली ज़्यादातर भाषाएँ global variables और shared memory तक unlimited access को default में देती हैं
    यही data corruption और races का बड़ा कारण है
    कई स्थितियों में process-based model, thread-based model से बेहतर concurrency model हो सकता है, लेकिन उसकी कमी यह है कि वह बहुत heavy है
    अगर हर thread को ज़रूरी data message passing के ज़रिए देना ही default होता, तो मुझे लगता है ऐसी ज़्यादातर समस्याएँ गायब हो जातीं
    खैर, platforms हमें global variables और shared memory इस्तेमाल करने की आज़ादी देते हैं, तो बस हमें खुद उसे न इस्तेमाल करना होगा
  • Rust एक प्रमुख modern भाषा है जो thread safety को type system में embed कर सकती है
    Rust का मूल लक्ष्य memory-safe systems language बनना नहीं था, बल्कि thread-safe systems language बनना था, और memory safety उसका स्वाभाविक परिणाम बनकर आई
    Rust में structured concurrency को thread::scope जैसी चीज़ों से इस्तेमाल किया जा सकता है, इसलिए thread-based काम बहुत सुविधाजनक हो जाता है
  • message passing, memory sharing की तुलना में logical problems(race condition/deadlock आदि) और बढ़ा सकता है, इसलिए यह कोई सर्व-समाधान नहीं है
  • Go में direct memory sharing की बजाय goroutine के बीच communication(channel आदि) पर ज़्यादा ज़ोर दिया जाता है
    यह दस्तावेज़ देखें
  • goroutine के बीच channel से object पास करने पर भी Go में sendable type, ownership, read-only reference जैसी अवधारणाएँ नहीं हैं, इसलिए इसे सुरक्षित तरीके से इस्तेमाल करना आसान नहीं है
    वास्तविक उदाहरण:
    func processData(lines <-chan []byte) {
     for line := range lines {
      fmt.Printf("processing line: %v\n", line)
     }
    }
    
    func main() {
     lines := make(chan []byte)
     go processData(lines)
    
     var buf bytes.Buffer
     for range 3 {
      buf.WriteString("mock data, assume this got read into the buffer from a file or something")
      lines <- buf.Bytes()
      buf.Reset()
     }
    }
    
    ऊपर के 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 के लिए सही तरीके से इस्तेमाल करना और कठिन अनुभव बन सकता है
  • वास्तविक golang programs में "sharing के ज़रिए communication" वाला pattern भारी मात्रा में logical problems पैदा करता है, और अंत में memory sharing आम हो जाती है
    यानी “safe” race या “safe” deadlock जैसी चीज़ें उल्टा और अधिक सामान्य हो जाती हैं
  • concurrency bugs की चर्चा अक्सर इस बात को नज़रअंदाज़ कर देती है कि ज़्यादातर apps में व्यवहारिक रूप से महत्वपूर्ण bugs DB के भीतर lock, transaction, transaction isolation आदि के गलत इस्तेमाल से पैदा होते हैं
    PL theory में Rust का race-freedom approach आकर्षक लग सकता है, लेकिन वास्तविक apps में महत्वपूर्ण data तो वैसे भी RDBMS में होता है, और उदाहरण के लिए अगर SELECT के साथ FOR UPDATE न लगाया जाए, तो race आसानी से हो सकती है
    Rust app अगर बिल्कुल unsafe न भी इस्तेमाल करे, तब भी DB के कारण race बनी रहती है
  • “memory safety” शब्द मूल रूप से एक जटिल अवधारणा को समझाने के लिए आया था, लेकिन समय के साथ उसका अर्थ फैल भी गया और सिमट भी गया
    Go की बनावट ऐसी है कि वह memory corruption bugs को लगभग होने नहीं देती — यह बात वास्तविक exploits की अनुपस्थिति से समझी जा सकती है
    अगर इस लेख की दलील मानें, तो ज़्यादातर high-level languages भी memory-safe नहीं रह जाएँगी, जबकि लेख में केवल Java को अपवाद की तरह रखा गया है
    Rust, Go से “ज़्यादा” safe हो सकता है, लेकिन “memory safety” कोई continuous spectrum नहीं बल्कि pass/fail अवधारणा है
    अगर आप दावा करते हैं कि कोई भाषा memory-unsafe है, तो आपको POC ज़रूर दिखाना चाहिए
  • अगर memory safety शब्द में मुख्य बात “type confusion” है, तो Go भी उससे मुक्त नहीं है
    लेख में दिया गया उदाहरण दिखाता है कि int को गलती से pointer मान लेने से memory corruption आसानी से हो सकती है
    demo में जानबूझकर 42 इस्तेमाल किया गया है ताकि segfault हो, लेकिन अगर असली address value इस्तेमाल की जाती, तो वास्तविक corruption होती
  • data race program को ऐसी स्थिति में पहुँचा सकती है जिसे language spec पहचानती ही नहीं(जैसे SIGSEGV से जबरन termination), इसलिए यह memory safety का उल्लंघन है
    इसीलिए जिस भाषा में data race संभव है, उसे memory-safe नहीं कहा जा सकता
  • जैसा कि लेख में उदाहरण दिया गया है, type confusion के ज़रिए fat pointer का torn read, या slice के torn read से out-of-bounds write भी संभव है
    ऐसे cases में उसे memory-safe कहना सही होगा या नहीं, इस पर संदेह है
  • शब्दों का विकसित होना और अर्थ बदलना गणित और भौतिकी में भी आम बात है
    ऐसी समस्या से बचने के लिए कई बार “Gaussian Curvature” या “Riemann Integrals” जैसे व्यक्तिनाम लगाए जाते हैं
    “जहाँ शुरुआती अर्थ संकरा रह गया, लेकिन व्यापक अर्थ में विस्तार हुआ” — ऐसा “Galois Group” जैसे उदाहरणों में भी हुआ है
    memory safety भी इससे अलग नहीं है
  • लेखक की परिभाषा के हिसाब से Java को memory-unsafe क्यों कहा जा रहा है, यह जानना चाहता हूँ
    कृपया कोई ठोस उदाहरण दें
  • Go स्वयं भी आधिकारिक तौर पर 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 भाषा कहना उचित नहीं है
  • memory safety की परिभाषा भी समय के साथ थोड़ी बदलती रही है
    जब Go बनाया गया था, तब “garbage collector हो तो memory safe” जैसी सोच मुख्यधारा में थी, और C/C++ की तुलना में वह निश्चित रूप से कहीं ज़्यादा safe है