1 पॉइंट द्वारा GN⁺ 4 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • undefined behavior (UB) कोई compiler की दुर्भावनापूर्ण optimization नहीं है, बल्कि यह नियम है कि अगर code को मानक के अनुसार वैध माना जाए, तो असंभव execution path को handle करना ज़रूरी नहीं है
  • साधारण न दिखने वाले C/C++ code में सिर्फ double-free या boundary के बाहर access ही नहीं, बल्कि alignment, casting, initialization और type mismatch जैसे सूक्ष्म UB भी व्यापक रूप से छिपे होते हैं
  • unaligned int* या std::atomic<int>* access पर platform के हिसाब से SIGBUS, kernel fix-up, या सामान्य रूप से काम करता हुआ दिखने वाला परिणाम मिल सकता है, लेकिन standard के अनुसार यह पहले से ही UB है
  • isxdigit() को signed char देना, float को int में बदलना, या NULL और variadic arguments का गलत उपयोग करना जैसी आम code patterns भी आसानी से standard के बाहर चली जाती हैं
  • मौजूदा codebase को फेंका नहीं जा सकता, लेकिन LLM-आधारित UB detection और expert verification को मिलाकर बड़े पैमाने पर सुधार करना होगा; यह काम junior developers को सौंपने के लिए बहुत सूक्ष्म है

C/C++ में undefined behavior optimization की समस्या नहीं है

  • undefined behavior (UB) का मतलब यह नहीं कि compiler developer की गलती का “दुरुपयोग” करता है, बल्कि यह कि वह मान सकता है कि program standard के अनुसार valid है
  • इंसान को intent साफ़ दिखे, तब भी compiler stage या modules के बीच उस intent को व्यक्त करना मुश्किल हो सकता है
  • compiler पर यह बाध्यता नहीं है कि वह code generation में उन special cases को संभाले जो “हो ही नहीं सकते”, और hardware समेत execution path में intent से अलग परिणाम आ सकते हैं
  • optimization बंद करने से भी UB सुरक्षित नहीं हो जाता, और न ही इस बात की कोई गारंटी है कि वही behavior मौजूदा या भविष्य के compiler/architecture पर बना रहेगा

UB सिर्फ असामान्य code में नहीं होता

  • double-free, use-after-free, object boundary के बाहर access, और uninitialized memory access जाने-पहचाने UB हैं, लेकिन industry भर में ये बार-बार दोहराए जाते हैं
  • इससे भी अधिक सूक्ष्म और non-intuitive UB बहुत हैं, इसलिए साधारण दिखने वाला C/C++ code भी आसानी से standard के बाहर जा सकता है
  • C23 standard में “undefined” शब्द 283 बार आता है, और जिन मामलों में बात साफ़ न कही गई हो लेकिन behavior फिर भी defined न हो, उन्हें जोड़ें तो दायरा और बड़ा हो जाता है
  • साधारण से ऊपर के लगभग हर C/C++ code में कहीं न कहीं UB होता है, और इसे सिर्फ individual programmer की लापरवाही मानना कठिन है

unaligned object access

  • नीचे जैसे int* को dereference करने वाला function, pointer सही aligned न होने पर UB बन जाता है
    int foo(const int* p) {
       return *p;
    }
    
  • alignment का मतलब अक्सर sizeof(int) के गुणज वाले address से हो सकता है, लेकिन असली requirement platform और implementation के अनुसार बदल सकती है
  • Linux Alpha पर कुछ मामलों में kernel trap पकड़कर software के ज़रिए intended access की नकल कर सकता था, लेकिन दूसरे मामलों में program SIGBUS से crash हो सकता है
  • SPARC पर SIGBUS आता है, जबकि x86/amd64 पर यह अक्सर बिना समस्या के चलता दिख सकता है या atomic read जैसा लग सकता है
  • ARM, RISC-V और future architectures पर परिणाम को सामान्यीकृत नहीं किया जा सकता, और भविष्य की architecture में ऐसे special registers भी हो सकते हैं जो int* के निचले bits का उपयोग ही न करें
  • अगर compiler अलग load instruction इस्तेमाल करे, तो पहले kernel द्वारा fix-up किया जाने वाला access अब fix-up न हो सके
  • compiler पर यह बाध्यता नहीं है कि वह unaligned pointer के लिए भी काम करने वाला assembly बनाए; वह access अपने-आप में ही UB है

atomic type भी गलत alignment पर पहले से UB है

  • नीचे जैसे std::atomic<int>* पर store() या load() call करने पर भी, अगर object सही aligned नहीं है, तो behavior UB है
    void set_it(std::atomic<int>* p) {
            p->store(123);
    }
    int get_it(std::atomic<int>* p) {
            return p->load();
    }
    
  • “क्या unaligned object पर यह operation atomic है?” यह सवाल standard के नज़रिए से बनता ही नहीं
  • असली hardware पर atomicity समस्या हो सकती है, लेकिन standard के हिसाब से उससे पहले ही यह UB है
  • अगर जिसे atomic पढ़ा जा रहा है वह object page boundary पार कर रहा हो, तो स्थिति और जटिल होती है, लेकिन निष्कर्ष “ठीक है” नहीं बल्कि UB ही रहता है

सिर्फ pointer बनाना भी समस्या हो सकता है

  • unaligned pointer में dereference से पहले भी, किसी खास type के pointer में cast करना ही समस्या बन सकता है
    bool parse_packet(const uint8_t* bytes) {
            const int* magic_intp = (const int*)bytes;   // UB!
            int magic_raw = foo(magic_intp);  // Probably crashes on SPARC.
            int magic = ntohl(magic_raw); // this is fine, at least.
            […]
    }
    
  • यहाँ समस्या foo() call नहीं, बल्कि (const int*)bytes cast है
  • standard के अनुसार compiler int* के निचले bits को garbage collection या security tag bits जैसे अर्थ भी दे सकता है

isxdigit() को char देने की समस्या

  • नीचे का code सरल दिखता है, लेकिन जिन architectures पर char signed है, वहाँ input value 0–127 रेंज से बाहर हो तो यह UB हो सकता है
    bool bar(char ch) {
            return isxdigit(ch);
    }
    
  • isxdigit() यह जाँचने वाला function है कि character hexadecimal digit है या नहीं, और यह EOF भी argument के रूप में ले सकता है
  • C23 7.4p1 के अनुसार EOF एक int है, और इससे यह निष्कर्ष निकलता है कि वह ऐसा value है जिसे unsigned char के रूप में represent नहीं किया जा सकता
  • isxdigit() char नहीं बल्कि int लेता है, और char से int conversion संभव होने पर भी signed char की negative values समस्या पैदा करती हैं
  • C23 6.2.5 paragraph 20 के अनुसार char signed है या नहीं, यह implementation-defined है
  • नीचे जैसी implementation negative index के साथ अज्ञात memory पढ़ सकती है
    int isxdigit(int c) {
            if (c == EOF) {
                    return false;
            }
            return some_array[c];
    }
    
  • अगर वह memory I/O-mapped क्षेत्र हो, तो यह सिर्फ random value या crash नहीं बल्कि hardware action भी trigger कर सकती है
  • यह desktop OS application की तुलना में embedded system में अधिक संभव है, लेकिन user-space network drivers जैसी स्थितियों में सिर्फ user space होने से भी पर्याप्त सुरक्षा नहीं मिलती

float से int में cast करने की समस्या

  • seconds वाले float को milliseconds वाले int में बदलने का नीचे जैसा code आम है, लेकिन इसमें UB शामिल है
    int milliseconds(float seconds) {
            int tmp = (int)(seconds * 1000.0); /* WRONG */
            return tmp + 1; /* WRONG separately (signed overflow is UB) */
    }
    
  • C23 6.3.1.4 कहता है कि finite real floating value को integer type में बदलते समय, अगर उसका integral part उस integer type में represent नहीं किया जा सकता, तो behavior undefined है
  • non-finite values के लिए भी कोई स्पष्ट परिभाषा नहीं है, इसलिए वह भी UB है
  • float की INT_MAX से तुलना करना भी सीधा नहीं है
    • float को int में cast करने पर वही UB हो सकता है जिससे बचना चाह रहे थे
    • INT_MAX को float में cast करने पर यह निश्चित नहीं कि वह exactly represent होगा
    • अगर INT_MAX float में round होकर ऐसे value में बदल जाए जिसे int में represent नहीं किया जा सकता, तो comparison representative नहीं रह जाती
  • इसे सुरक्षित बनाने के लिए isfinite() check, INT_MIN + 1000, INT_MAX - 1000 जैसी margin comparison, और conversion के बाद addition से पहले extra checks चाहिए
    int milliseconds(float seconds) {
            const float ftmp = seconds * 1000.0f;
            if (!isfinite(ftmp)) {
                    return 0;
            }
            if ((float)(INT_MIN + 1000) > ftmp) {
                    return 0;
            }
            if ((float)(INT_MAX - 1000) < ftmp) {
                    return 0;
            }
            const int tmp = (int)ftmp;
            if (INT_MAX == tmp) {
                    return 0;
            }
            return tmp + 1;
    }
    
  • आप सिर्फ float को int में बदलना चाहते हैं, लेकिन सुरक्षित code बहुत लंबा हो जाता है

address 0 पर object और null pointer

  • OS kernel या embedded code में address 0 पर object रखने की ज़रूरत पड़ सकती है
  • लेकिन C standard के अनुरूप वास्तव में address 0 पर object रखने का कोई व्यावहारिक तरीका नहीं माना जा सकता
  • C 6.3.2.3 में pointer में convert होने वाला integer constant 0 और nullptr, “null pointer constant” हैं; यहाँ इन्हें NULL कहा जा सकता है
  • C यह निर्धारित नहीं करता कि वास्तविक NULL pointer machine address 0 की ओर इशारा करता है
  • C standard hardware नहीं बल्कि C abstract machine को संबोधित करता है, और NULL तथा 0 की तुलना में सिर्फ इतना guaranteed है कि वे equal होंगे
  • यह equality इसलिए भी हो सकती है कि integer 0 platform के native NULL value में convert हो, और वह value 0xffff भी हो सकती है
  • null pointer को dereference करना, उसका value कुछ भी हो, UB है; यह C 3.4.3 का canonical example है
  • इसलिए यह मानना सही नहीं कि memset(&ptr, 0, sizeof(ptr)); एक NULL pointer बना देगा
  • structure को zero-initialize करके उसके member pointers को NULL मान लेना, व्यवहार में भी कई programmers के लिए समस्या बन सकता है
  • इतिहास में non-zero NULL pointer का उपयोग करने वाली machines भी रही हैं

address 0 पर function होने की धारणा की समस्या

  • आधुनिक machines पर भले NULL address 0 की ओर इशारा करे और वहाँ सचमुच कोई object या function मौजूद हो, C 6.3.2.3 कहता है कि NULL किसी भी object या function के बराबर नहीं होता
  • इसलिए नीचे का code UB है
    void (*func_ptr)() = NULL;
    func_ptr();
    
  • C के नज़रिए से इसका मतलब है “वहाँ कोई function नहीं है”, और compiler के भीतर इस intent को व्यक्त करने का कोई तरीका शायद न हो
  • सिर्फ यह मान लेना कि वह all-zero bits वाले address पर call instruction emit करेगा, सही नहीं है
  • 16-bit x86 पर तो यह भी स्पष्ट नहीं कि “all zero” का मतलब 0000:0000 है या CS:0000

variadic arguments और type mismatch

  • execl() का आख़िरी argument pointer होना चाहिए, इसलिए NULL macro या integer 0 को सीधे pass करना UB हो सकता है
    execl("/bin/sh", "sh", "-c", "date", NULL);  /* WRONG */
    execl("/bin/sh", "sh", "-c", "date", 0);     /* WRONG */
    
  • सही रूप यह है कि उसे explicitly pointer type में cast किया जाए
    execl("/bin/sh", "sh", "-c", "date", (char*)NULL);
    
  • NULL macro integer 0 के रूप में interpret हो सकता है, और variadic arguments में ज़रूरी type information नहीं जाती
  • printf() में भी format specifier और actual argument type match न करें तो UB है
    uint64_t blah = 123;
    printf("%ld\n", blah);  /* WRONG */
    
  • uint64_t को print करने के लिए PRIu64 का उपयोग करना चाहिए
    uint64_t blah = 123;
    printf("%"PRIu64"\n", blah);
    
  • uid_t को print करने के लिए uintmax_t में cast करके PRIuMAX का उपयोग एक तरीका हो सकता है, लेकिन uid_t unsigned है या नहीं, यह भी निश्चित नहीं
  • सबसे खराब स्थिति में -1 की जगह कोई निरर्थक value print हो सकती है

0 से divide करना और security problem

  • 0 से divide करना UB है, यह बात व्यापक रूप से जानी जाती है, लेकिन जब denominator untrusted input से आता हो तो यह security issue बन जाता है
  • महत्वपूर्ण बात यह है कि यह सिर्फ runtime error नहीं, बल्कि input validation boundary पर होने वाला UB हो सकता है

UB नहीं, फिर भी integer promotion खतरनाक है

  • integer promotion के rules को code skim करते हुए लागू करना कठिन है, और ये intuition से अलग परिणाम दे सकते हैं
  • नीचे के code में overflowed का value 1 नहीं बल्कि 0 होगा
    unsigned char a = 0xff;
    unsigned char b = 1;
    unsigned char zero = 0;
    bool overflowed = (a + b) == zero;
    // overflowed is set to zero, not one.
    
  • नीचे के code में सभी variables unsigned जैसे दिखते हैं, फिर भी result 2147483648 (0x80000000) नहीं बल्कि 18446744071562067968 (ffffffff80000000) बनता है
    unsigned char a = 0x80;
    uint64_t b = a << 24;     // Bonus UB(?)
    
  • UB न भी हो, तब भी C/C++ के integer rules intuitive नहीं हैं और defects पैदा करना आसान है

LLM का उपयोग करके UB detection

  • आधुनिक LLM से arbitrary C code में UB ढूँढने को कहें तो वह लगभग हमेशा कुछ न कुछ समस्या पकड़ लेता है, और अधिकतर बार सही होता है
  • निजी code में UB खोजने के बाद यही तरीका mature और सख़्ती से लिखे गए OpenBSD code पर भी लागू किया गया
  • सबसे पहले दिमाग में आए tool find में कई समस्याएँ मिलीं
  • OpenBSD को out-of-bounds write के लिए patch और UB नहीं बल्कि logical bug के लिए patch भेजे गए
  • बचे हुए कई UB के लिए patch नहीं भेजे गए
    • अतीत में OpenBSD project bug reports को बहुत ग्रहणशील तरीके से नहीं लेता था, ऐसा अनुभव रहा था
    • यह भी लगा कि कुछ मामले व्यवहार में शायद ठीक हों
    • अगर OpenBSD को codebase से UB हटाना है, तो LLM और project के बीच individual patches भेजने से बड़ा project चाहिए होगा

C/C++ codebase से निपटने की व्यावहारिक दिशा

  • मौजूदा C/C++ codebase को फेंका नहीं जा सकता, लेकिन उसे मूल रूप से टूटी अवस्था में छोड़ देना भी विकल्प नहीं है
  • AI द्वारा किए गए low-quality बदलाव commit किए बिना, और human reviewers को overwhelm किए बिना, UB को बड़े पैमाने पर ठीक करना होगा
  • 2026 में LLM-आधारित UB supervision के बिना C या C++ लिखना SOX violation जैसा माना जा सकता है, और गैर-जिम्मेदाराना समझा जा सकता है
  • अगर OpenBSD developers भी 30 साल से अधिक समय में ये सभी समस्याएँ नहीं खोज पाए, तो दूसरे projects की संभावना और भी कम है
  • निजी projects में LLM से UB खोजवाना, ज़रूरत पड़ने पर उससे explanation लेना, उससे fix बनवाना, और फिर इंसान द्वारा result verify करना एक कामचलाऊ तरीका हो सकता है
  • लेकिन result verify करने के लिए expert चाहिए, और experts आमतौर पर दूसरे कामों में व्यस्त रहते हैं
  • यह काम ऊपर से cleanup task जैसा दिखता है, लेकिन पारंपरिक रूप से ऐसे काम करने वाले junior programmers को सौंपने के लिए यह बहुत सूक्ष्म है

संबंधित सामग्री

1 टिप्पणियां

 
GN⁺ 4 시간 전
Hacker News टिप्पणियाँ
  • C में हैरान करने वाले और अजीब undefined behavior बहुत हैं, लेकिन यह लेख उसे अच्छी तरह दिखा नहीं पाता, बस सतह को हल्का-सा छूता है
    इससे भी अजीब उदाहरण volatile int x = 5; printf("%d in hex is 0x%x.\n", x, x); है। अगर x सिर्फ int हो तो ठीक है, लेकिन volatile होने पर यह undefined behavior बन जाता है। C standard में volatile access केवल पढ़ना भी side effect माना जाता है, और एक ही scalar object पर बिना क्रम वाले side effects undefined behavior होते हैं, जबकि function arguments की evaluation का क्रम आपस में अनिश्चित होता है
    आम तौर पर data race का मतलब होता है कि अलग-अलग threads एक ही object को साथ में access करें और उनमें से कम-से-कम एक write हो, लेकिन C में single thread के भीतर भी बिना write के data race जैसी स्थिति बन सकती है

    • लेखक के रूप में सहमत हूँ। इस लेख का उद्देश्य standard में undefined शब्द जहाँ-जहाँ 283 बार आता है, या omission की वजह से undefined होने वाले हर मामले को गिनाना नहीं है
      मुद्दा यह है कि इससे बचा नहीं जा सकता। कम-से-कम 1972 में C आने के बाद से इंसान इसे पूरी तरह टाल नहीं पाए हैं
      अगर 54 साल में यह सफल नहीं हुआ, तो “और मेहनत करो” या “गलती मत करो” कोई समाधान नहीं है। Mythos ने OpenBSD में जो एक exploitable flaw पाया, उसे OpenBSD developers के लिए काफ़ी अच्छी रेटिंग मिली थी, लेकिन सबसे सरल code पर tools चलाने से भी undefined behavior की भरमार निकली
      उदाहरण के लिए, find में waitpid(&status) के बाद waitpid() error check करने से पहले uninitialized automatic variable status को पढ़ना भी undefined behavior है, हालाँकि ऐसा architecture या compiler कल्पना करना मुश्किल है जहाँ यह exploitable हो
      जैसा लेख में लिखा है, उद्देश्य दुनिया के हर undefined behavior को गिनाना नहीं, बल्कि यह कहना है कि हर non-trivial C/C++ code में undefined behavior होता है
    • volatile एक type system hack है। इसका समाधान अधिक सिद्धांतपूर्ण होना चाहिए था, और आधुनिक भाषाओं को “C ने ऐसा किया, इसलिए यह अच्छा विचार है” कहकर इसकी नकल नहीं करनी चाहिए
      शुरुआती C compilers हमेशा values को memory में लिखते थे, इसलिए अगर pointer को memory-mapped I/O hardware पर सेट कर दिया जाए, तो x बदलने पर हर बार CPU instruction सचमुच memory write करता था और driver code काम करता था
      लेकिन optimization आने पर compiler ने सोचा कि वह सिर्फ x को बदल रहा है, इसलिए उसे register में ही रखा, और driver टूट गया
      C का volatile compiler से कहने का hack है: “वह optimization मत करो”, जबकि सही समाधान, यानी library स्तर पर memory-mapped I/O intrinsics देना, कहीं बड़ा काम होता
      intrinsics की ज़रूरत इसलिए है कि वे ठीक-ठीक बता सकें कि कौन-सा behavior संभव है और कौन-सा नहीं। कुछ targets पर 1-byte, 2-byte, 4-byte write अलग-अलग क्रियाएँ हैं और hardware भी इन्हें अलग मानता है। कुछ devices 4-byte RGBA write की अपेक्षा करते हैं; अगर आप 1-byte write चार बार भेजें तो वे भ्रमित हो सकते हैं या काम ही न करें। कुछ targets bit-level write भी support करते हैं। केवल volatile से यह जानने का कोई तरीका नहीं कि क्या होने वाला है और उसका अर्थ क्या है
    • undefined behavior और race में फर्क करना चाहिए। undefined behavior पर चर्चा में यह भेद अक्सर गायब रहता है
      C program को compile करने के बाद disassemble करें तो वह ऐसा assembly program बनता है जिसमें undefined behavior नहीं होता, क्योंकि assembly में undefined behavior की अवधारणा नहीं होती
      undefined behavior source program का गुण है, executable का नहीं। इसका मतलब है कि जिस भाषा में source लिखा गया है, उसकी specification उस program को कोई अर्थ नहीं देती। दूसरी तरफ compile हुए executable को machine specification अर्थ देती है
      race program के behavior का गुण है। इसलिए C program में undefined behavior होने की बात कही जा सकती है, लेकिन यह नहीं कहा जा सकता कि executable में वास्तव में कोई race है। हाँ, compiler undefined behavior वाले program को मनचाहे ढंग से compile कर सकता है, इसलिए race introduce भी कर सकता है, लेकिन यदि वह नए threads बनाए बिना compile करे तो race नहीं होगी
    • volatile का अर्थ ही यह है कि value किसी और चीज़ द्वारा बदल सकती है। अगर वह global variable है, तो वह चीज़ किसी दूसरे thread के अलावा interrupt या signal handler भी हो सकती है। अगर वह किसी खास address को पढ़ने वाला pointer है, तो value बदलने वाला hardware device register भी हो सकता है
      volatile variable की अवधारणा अपने-आप में समस्या नहीं है। अगर कोई भाषा interrupt routines और memory-mapped I/O को support करना चाहती है, तो compiler को यह बताने का कोई तरीका होना चाहिए कि एक ही hardware register को दो बार पढ़ना, memory की एक ही location को दो बार पढ़ने जैसा नहीं है
      असली समस्या यह है कि language features और constraints की परस्पर क्रिया को पर्याप्त रूप से व्यवस्थित नहीं किया गया। अगर आपने साफ़ कहा है कि “यह value कभी भी बदल सकती है”, तो उसी वजह से उसके कुछ उपयोगों को undefined behavior मानना मूर्खता है। volatile variables के लिए “unordered side effects” की परिभाषा में exception होना चाहिए था
    • लेख का मूल बिंदु यह है कि undefined behavior तक पहुँचने के लिए अजीब code लिखने की भी ज़रूरत नहीं पड़ती
      बहुत से लोग भ्रम में रहते हैं कि C और C++ “जो चाहो वह करने देते हैं, इसलिए बहुत flexible हैं।” वास्तव में लगभग हर शक्तिशाली और शानदार दिखने वाली technique undefined behavior का minefield है
  • unaligned pointer का undefined behavior और भी बुरा है। unaligned pointer केवल access करने पर ही नहीं, pointer अपने-आप में भी undefined behavior है
    इसलिए void* v को int* i में implicit cast करना, जैसे C में i=v या int* लेने वाले f(v) को call करना भी undefined behavior है, अगर result pointer int की alignment requirement पूरी नहीं करता
    यह C-स्तर की समस्या है, यह महत्वपूर्ण है। अगर किसी C program में undefined behavior है, तो वह program औपचारिक रूप से वैध नहीं है, बल्कि गलत program है। यह hardware की समस्या नहीं है, न ही crash या fault से जुड़ी बात
    void* से int* में cast आम तौर पर hardware code में कुछ नहीं करता, और type केवल C में होते हैं, इसलिए hardware उस cast पर crash भी नहीं करता। आपको लग सकता है कि अगर register में integer value है तो सब ठीक है, लेकिन मुद्दा यह नहीं कि hardware में pointer सचमुच integer है या नहीं; मुद्दा यह है कि unaligned pointer में cast करते ही C program परिभाषा के हिसाब से टूट चुका है

    • लेखक के रूप में, सही। लेख के “Actually, it was UB even before that” भाग में यही बात थी
      मैं यह भी बताना चाहता था कि undefined behavior hardware में नहीं होता और crash या fault से उसका सीधा संबंध नहीं है। साथ ही मैं उन लोगों को उदाहरण दिखाना चाहता था जो कहते हैं, “देखो, यह तो ठीक चल रहा है”, जबकि वास्तव में ऐसा नहीं है
    • यह तो ठीक और अपेक्षित बात है। अच्छा programmer जानता है कि pointer casting साफ़ तौर पर सावधानी वाला क्षेत्र है
    • क्या आप बता सकते हैं कि standard में कहाँ लिखा है कि unaligned pointer अपने-आप में undefined behavior है?
    • अगर मैं #pragma pack(push, 1) से struct बनाऊँ, तो क्या इसका मतलब है कि संयोग से aligned member न हो तो member pointer इस्तेमाल ही नहीं कर सकता?
    • C में undefined behavior की अवधारणा का मूल अर्थ यह था कि भले machine instructions architecture के अनुसार थोड़ा बदलें, compiler को code को hardware से map करने की आज़ादी मिले। एक ही C program चलने वाले architecture के आधार पर अलग behavior व्यक्त कर सकता था
      इस तरह का undefined behavior ठीक था, और hardware differences के कारण bug आना ज़्यादातर लोगों को बड़ी समस्या नहीं लगता
      लेकिन समय के साथ आक्रामक व्याख्याओं ने C को एक तरह की implicit design by contract भाषा में बदल दिया, और constraints अदृश्य हो गए। यह वैसी ही समस्या पैदा करता है जैसी RAII में implicit destructor calls के अदृश्य होने से होती है
      C में जब आप pointer dereference करते हैं, compiler function signature में implicit non-null constraint जोड़ देता है। अगर null हो सकने वाला pointer किसी function को दे दिया जाए, तो “कोई check या assertion नहीं है” जैसी error देने के बजाय compiler चुपचाप उस non-null constraint को propagate कर देता है। अगर वह साबित कर दे कि यह constraint झूठा है, तो function को unreachable mark कर देता है, और unreachable function call calling function को भी unreachable बना सकती है
  • C में undefined behavior सीखने के 5 चरण
    इनकार: “मुझे पता है मेरी machine पर signed overflow कैसे होता है”
    ग़ुस्सा: “यह compiler बेकार है! जो मैं कहता हूँ वह करता क्यों नहीं?”
    मोलभाव: “मैं C को ठीक करने के लिए wg14 को यह proposal भेजूँगा…”
    अवसाद: “क्या किसी C code पर भरोसा किया जा सकता है?”
    स्वीकृति: “बस undefined behavior मत लिखो”

    • “compiler से undefined चीज़ों को define करवा लो” वाला चरण कहाँ आता है?
      unaligned access के लिए packed structs का उपयोग करो। compiler जादू की तरह सही code बना देगा। असल में compiler हमेशा सही कर सकता था, बस करता नहीं था
      strict aliasing rules के लिए union type punning का उपयोग करो। जो compilers मायने रखते हैं, वे standard कुछ भी कहे, यह काम करता है—ऐसा document करते हैं। या -fno-strict-aliasing से इसे बंद कर दो। memory को जैसे चाहो वैसे reinterpret करो; कुछ sharp edges रहेंगी, लेकिन कम-से-कम compiler की ओर से नहीं
      overflow को -fwrapv से define कर लो। +, -, * को __builtin_*_overflow से बदलो तो explicit error checking भी मुफ़्त मिलती है। functional interface भी अच्छा है और efficient code भी बनता है
      असली स्वीकृति शायद “सामान्य लोग C standard की परवाह नहीं करते” के अधिक करीब है। standard ख़राब है; असली चीज़ compiler है। compilers में ऐसे बहुत उपयोगी features हैं जिनसे इन ज़्यादातर समस्याओं को bypass किया जा सकता है। लोग उनका उपयोग नहीं करते क्योंकि वे “portable”, “standard” C लिखना चाहते हैं; उसी सोच से बाहर निकलना असली स्वीकृति है
      मैंने इसी तर्क से freestanding C environment में Lisp interpreter बनाया और UBSan भी pass किया। शुरू में लगा था कि यह फट जाएगा, पर ऐसा नहीं हुआ; अगर मैं कर सकता हूँ, तो कोई भी कर सकता है
    • लेखक के रूप में, लेख का बिंदु यही है कि “बस undefined behavior मत लिखो” असंभव है
      जब तक इंसान code लिखते रहेंगे, वह अंतिम अवस्था नहीं हो सकती। कोई भी इंसान C/C++ में undefined behavior को पूरी तरह नहीं टाल सकता
    • “बस undefined behavior मत लिखो” ज़्यादा से ज़्यादा अभी भी मोलभाव चरण जैसा लगता है
    • मेरे जैसे embedded devices पर काम करो। किसी निश्चित CPU को target करके software लिखना सचमुच आरामदायक है
    • C में स्वीकृति शायद यह है: “मैं undefined behavior लिखूँगा, और किसी दिन बुरा कुछ होगा”
  • उदाहरण असली undefined behavior कम और ऐसे मामलों जैसे ज़्यादा लगते हैं जो input या परिस्थिति के अनुसार undefined behavior बन सकते हैं
    अगर इतना व्यापक लें, तो हर function call भी undefined behavior है क्योंकि वह stack space पार कर सकता है। सच तो यह है कि इस अर्थ में लगभग किसी भी भाषा के लिए ऐसा कहा जा सकता है
    C में ध्यान देने लायक वास्तविक खुरदरे हिस्से वैसे भी बहुत हैं; इस तरह की सनसनीखेज़ी खासकर शुरुआती लोगों का ध्यान भटका सकती है और उल्टा नुकसान कर सकती है

    • Ada 83 call stack overflow को undefined behavior नहीं मानता। reference manual में STORAGE_ERROR exception define है
      http://archive.adaic.com/standards/83lrm/html/lrm-11-01.html
      उसमें लिखा है कि “subprogram call के execution के दौरान storage पर्याप्त न हो” तो भी यह exception हो सकता है
    • यह बिल्कुल सही नहीं है
      सबसे पहले, stack space खत्म होने पर क्या होना चाहिए, यह define किया जा सकता है। और सभी programs को मनमाने आकार का stack नहीं चाहिए; कुछ programs को पहले से गणना की जा सकने वाली स्थिर मात्रा ही चाहिए। कुछ language implementations stack का उपयोग ही नहीं करतीं
      भाषा शेष stack space जाँचने के tools दे सकती है और उसके आधार पर guarantees भी कर सकती है। या stack खत्म होने पर चलने वाला handler install करने दे सकती है
    • input पर निर्भर undefined behavior भी exploit path बन सकता है
    • उदाहरण साफ़ तौर पर undefined behavior हैं। बस
      सही मानसिकता यह है कि undefined behavior शुरू होते ही आप language standard की सुरक्षा-सीमा से बाहर हैं। कुछ समय तक, शायद हमेशा के लिए, सब ठीक चलता दिख सकता है। लेकिन वास्तव में आप अनजाने में toolchain, compiler replacement या upgrade, architecture, runtime, libc version differences की मनमानी पर निर्भर हो जाते हैं
      अंततः आप रेत पर नींव खड़ी कर रहे होते हैं, और यही undefined behavior का ख़तरा है
    • यह लेख लगभग FUD की परिभाषा जैसा है
  • undefined behavior की समस्या यह नहीं कि किसी architecture पर crash हो सकता है
    असली समस्या यह है कि compiler मानकर चलता है कि ऐसा code कभी आएगा ही नहीं। फिर भी अगर आप undefined behavior वाला code लिखते हैं, तो compiler, खासकर optimizer, उसे normal path के अनुकूल किसी भी रूप में translate कर सकता है। वह “कुछ भी” कभी-कभी बड़े code blocks हटाने जितना अप्रत्याशित हो सकता है

    • इससे जुड़ा उदाहरण यह है कि हर function या तो terminate करे या side effects दे। मैंने खुद अभी तक यह नहीं झेला, लेकिन गलती से infinite loop या recursion लिख देने पर function ही हट जाए—ऐसा सहज कल्पना किया जा सकता है
      tail recursion जुड़ जाए तो bug debug build में infinite loop तक पहुँचे ही नहीं, और optimization level बढ़ाने पर ही सामने आए—ऐसा भी हो सकता है
    • crash तो undefined behavior के सबसे नरम परिणामों में से है। कम-से-कम वह जल्दी दिखाई देता है
      इससे भी बुरे मामलों में program चुपचाप garbage values के साथ चलता रह सकता है, hard disk format कर सकता है, या attacker को पूरे राज्य की चाबी थमा सकता है
    • सही, लेकिन यही undefined behavior का सबसे उपयोगी feature और उसके अस्तित्व का कारण भी है
      जो लोग कहते हैं कि इसे बस define कर दो या unspecified behavior बना दो, वे यह मूल बात चूक जाते हैं कि compiler program के बड़े हिस्से हटा सके—यही मुद्दा है
      अगर आप ऐसा code लिखते हैं जो कुछ inputs पर undefined behavior बनता है, तो उन inputs के लिए आपका आशय है कि program का कोई behavior होना ही नहीं चाहिए। आप चाहते हैं कि compiler उस path को optimization से मिटा दे, या defined बाकी मामलों के behavior में मदद करने वाला कोई transformation कर दे
      undefined behavior से ही पहुँचे जा सकने वाले log strings डालना, और फिर binary में उन strings का न बचना—यह काफ़ी संतोषजनक है
    • लेख में जहाँ कहा गया कि यह optimization की समस्या नहीं है, वह बात विशेष रूप से ध्यान खींचने वाली थी
      मैंने पहले एक analysis pass लिखा था जो transformation pipeline के आख़िरी में चलता है—इस धारणा के साथ, और वही उसकी correctness के लिए ज़रूरी था। मैंने सोचा कि अब और optimization नहीं होगी, इसलिए safe है, लेकिन अब मुझे उतना विश्वास नहीं रहा
    • वह bug नहीं, feature है
  • 20 साल से C इस्तेमाल कर रहा हूँ, लेकिन पिछले 6 महीनों में Hacker News पर जितनी undefined behavior की बातें देखीं, उतनी पहले कभी नहीं
    असली बातचीत में यह लगभग कभी नहीं आया। code लिखो, न चले तो debug करो, fix करो या workaround लगा दो। समझ नहीं आता कि C के undefined behavior का विषय बार-बार front page पर क्यों चढ़ जाता है

    • Hacker News अभी भी वास्तविक programming से ज़्यादा programming languages में रुचि रखने वाली तरफ़ झुका हुआ है। शायद Y Combinator की Lisp विरासत जैसी कोई बात भी हो
      computer science के एक छोटे लेकिन स्थायी वर्ग को नई programming language बनाना या इस्तेमाल करना दुनिया की सबसे रोचक चीज़ लगता है, और उनमें से कुछ लोग हमेशा ऐसा ही सोचते रहते हैं
      ऐसे लोगों का language design पक्ष में रुचि लेना स्वाभाविक है, और C का undefined behavior उसी दायरे में आता है। हालाँकि इसकी बहुत-सी बातें मूलतः पुराने CPU architectures को performance loss के बिना accommodate करने की कोशिश से आई थीं, और इसे “design choice” कहना उतना ही अटपटा है जितना पहिये का गोल होना
    • क्या कह रहे हो? 20 साल पहले भी मैं C और C++ इस्तेमाल करता था, और तब भी undefined behavior बातचीत और पाठ्यक्रम का बड़ा हिस्सा था
      GCC 3.2 के आसपास compiler ने optimization में undefined behavior का बहुत अधिक आक्रामक उपयोग शुरू किया, जिससे कुछ काफ़ी मशहूर “scandals” हुए, और इसी कारण कई लोग लंबे समय तक GCC 2.95 पर टिके रहे। GCC 3.2, 2002 में आया था
    • पुराने कंप्यूटर शानदार थे, आज के कंप्यूटर ख़तरनाक हो गए हैं
      हर कंपनी safety और exposure, यानी news में आने वाली घटनाओं, पर लगातार ज़ोर देती है, इसलिए “unsafe” के खिलाफ़ narrative हद से ज़्यादा बढ़ गया है
      नई दुनिया कुछ वैसी है जैसे शहर के वे लोग जिन्होंने कभी कच्ची प्रकृति नहीं देखी, lawn mower देखकर डर जाएँ। ब्लेड घूम रहे हैं? यह तो पागलपन है!
    • runtime environment पूरी तरह अलग architecture हो सकता है, इसलिए ऐसी details बहुत महत्वपूर्ण हैं
      अगर असली target किसी दूरस्थ संचार टॉवर पर लगा छोटा embedded system है, तो “मेरी machine पर चलता है” बेकार है। बेशक ज़्यादातर लोग ऐसा काम नहीं करते, और यहाँ developers का बड़ा हिस्सा शायद web developers है, लेकिन खुद अनुभव न होने पर भी यह रोचक चर्चा है। बल्कि ऐसे मामलों में शायद और भी ज़्यादा
    • अधिक सटीक रूप से, यह किसी काल्पनिक specification के लिए नहीं, बल्कि target platform के लिए लिखा जाता है। specification target platform मोटे तौर पर क्या करेगा, इसका अनुमान लगाने में मदद देती है, लेकिन वह अंतिम मानक नहीं है
      compiler में ऐसे bugs हो सकते हैं जहाँ standard के अनुसार चीज़ काम करनी चाहिए थी लेकिन नहीं करती, standard counterpart के बिना कई extensions होते हैं, और standard में undefined मानी गई चीज़ों के लिए भी implementation-specific अर्थ दिए जाते हैं
  • शुरुआत से मैं मोटे तौर पर सहमत हूँ, लेकिन उदाहरण अच्छे नहीं हैं और पूरा लेख LLM coding को बढ़ावा देने के लिए पैकेजिंग जैसा लगता है

    • सही। उदाहरण एक-एक करके या तो वे standard चीज़ें हैं जिनसे portable code लिखते समय बचा जाता है, या address 0 पर object access जैसी अनावश्यक बातें
      ऐसा लगता है जैसे कोई व्यक्ति मनमाना code लिखना चाहता हो और चाहता हो कि वह हर environment में बिल्कुल एक जैसा चले। अगर भाषा ऐसी बना दी जाए, तो ज़रूरत पड़ने पर platform-specific लिख पाने का लाभ ही खत्म हो जाएगा
    • किस अर्थ में अच्छे नहीं? अगर यह सच है तो यह काफ़ी गंभीर है
  • इस लेख का C++ code कुछ जगहों पर 10 साल से भी ज़्यादा समय से idiomatic नहीं रहा, और आज के समय में code smell माना जाएगा
    भाषा अपने शुरुआती रूप से काफ़ी बदलकर दूसरी तरह की भाषा बन चुकी है। जैसे ही raw pointers और direct pointer access बहुत मात्रा में दिखे, यह साफ़ हो गया कि लेख के कुछ हिस्सों को सावधानी से पढ़ना चाहिए
    एक और साफ़ समस्या यह दृष्टिकोण है कि C और C++ को लगभग एक ही भाषा मानकर मिला दिया गया है। आजकल ये दोनों भाषाएँ वास्तव में काफ़ी दूर जा चुकी हैं

    • मैं कहने वाला था कि code C++ नहीं, C है, लेकिन दोबारा देखा तो सच में atomic_int नहीं बल्कि std::atomic लिखा था
  • क्या C के undefined behavior को इस तरह समझना सही है?
    program P के पास inputs का एक set A है, जो undefined behavior नहीं पैदा करता, और उसका पूरक set B, जो पैदा करता है
    एक सही compiler P को executable P' में compile करता है। A के हर input के लिए P' को P की तरह ही behavior करना चाहिए
    लेकिन B के किसी भी input के लिए P' के behavior पर कोई requirement नहीं है

    • सहज रूप से, हाँ। program ऐसे compile होता है मानो B वाले inputs कभी आएँगे ही नहीं, और इसमें B inputs को detect करने की कोशिश करने वाले code को हटाना भी शामिल हो सकता है
    • यह अच्छा सारांश है
  • unaligned pointer की वजह से हुए undefined behavior का एक ठोस उदाहरण: https://pzemtsov.github.io/2016/11/06/bug-story-alignment-on...

    • यह खास तौर पर x86 पर हुआ मामला है, जिसे आम तौर पर लोग समस्या-रहित मान लेते हैं