2 पॉइंट द्वारा GN⁺ 2 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • अगर float का दशमलव हिस्सा हटाने के बाद मान target integer type की range से बाहर हो, तो undefined behavior (UB) होता है, और implicit conversion, function-style cast, तथा static_cast — सभी प्रभावित होते हैं
  • -Wall और -Wextra इसके लिए चेतावनी नहीं देते, और -Wconversion भी सिर्फ implicit conversion को detect करता है, इसलिए यह आसानी से छूट सकता है
  • Microsoft GSL का सुरक्षित narrowing conversion फ़ंक्शन gsl::narrow भी कुछ floating-point → integer input पर UB पैदा करता है, इसलिए वह documentation में बताए गए “represent न हो सकने वाले मान पर exception फेंकने” वाले व्यवहार को निभा नहीं पाता
  • x86 का CVTTSS2SI represent न हो सकने वाले मान को INT_MIN की तरह handle करता है, लेकिन AArch64 का FCVTZS saturated conversion करता है और NaN को 0 में बदल देता है, इसलिए hardware के अनुसार परिणाम अलग हो सकते हैं
  • सुरक्षित रूपांतरण के लिए cast से पहले range check करना चाहिए, और Clang·GCC के UBSan विकल्प -fsanitize=float-cast-overflow से इस समस्या को detect किया जा सकता है

रूपांतरण नियम और detection की सीमाएँ

  • C++ floating-point-से-integer conversion rules के अनुसार, दशमलव हिस्सा हटाने के बाद अगर मान target integer type में फिट नहीं होता, तो undefined behavior होता है
    • Target unsigned हो तब भी modular arithmetic लागू नहीं होती
    • int i0 = f, int(f), static_cast<int>(f) — तीनों कुछ input पर UB पैदा कर सकते हैं
  • सिर्फ सामान्य compiler warnings से इस समस्या को पूरी तरह पकड़ना मुश्किल है
    • -Wall और -Wextra तीनों तरह के conversion पर warning नहीं देते
    • -Wconversion सिर्फ implicit conversion पर warning देता है
  • भले ही मौजूदा processor और compiler पर program चलता रहे, परिणाम platform के अनुसार बदल सकते हैं
    • x86 का CVTTSS2SI represent न हो सकने वाले input को INT_MIN पर map करता है
    • AArch64 का FCVTZS saturation करता है और NaN को 0 पर map करता है
    • एक बार UB execute हो जाने पर, compiler किसी दूसरे conversion को लागू करते समय code के अचानक गलत काम करने का कारण बन सकता है

GSL का मामला और सुरक्षित उपाय

  • Microsoft Guidelines Support Library का gsl::narrow एक सुरक्षित narrowing conversion के रूप में पेश किया जाता है, जो target type में represent न हो सकने वाले मान पर exception फेंकता है
    • लेकिन वास्तविक floating-point → integer conversion कुछ input पर पहले ही UB execute कर देता है, इसलिए यह documentation से मेल नहीं खाता
    • GSL पक्ष का मानना था कि target platform पर hardware trap representation को नहीं छुआ जाता, इसलिए अंदरूनी UB हानिरहित है; यही तर्क code में भी दिखता है और समस्या सुधारी नहीं गई है
  • सही समाधान है cast से पहले range check करना
  • Clang और GCC के Undefined Behavior Sanitizer में -fsanitize=float-cast-overflow का उपयोग करके इस UB को detect किया जा सकता है
    • सभी C++ code को UBSan के साथ test करने की सिफारिश की जाती है

1 टिप्पणियां

 
GN⁺ 2 시간 전
Lobste.rs की राय
  • C और C++ के सूक्ष्म undefined behavior के कई मामलों के बारे में पता था, फिर भी यह मामला चौंकाने वाला था
    Rust ने भी LLVM IR के वही float→int conversion नियम विरासत में लिए थे, इसलिए कुछ समय तक वहाँ भी undefined behavior था, और 2020 में जाकर इसे ठीक किया गया ताकि अधिक जटिल IR बनाया जाए। performance में बड़ा फर्क होने के कारण, high-performance loops के लिए जहाँ यह पता हो कि value finite है और target type की range में है, unchecked float→int conversion भी दिया गया है
    C++ Core Guidelines Library का इसे हल्के में लेना बेतुका है। LLVM ने conversion से मिली जानकारी का उपयोग करके bounds checks हटा दिए, और उसके परिणामस्वरूप ऐसा मामला हुआ जिसमें bounds check होने के बावजूद array bounds से बाहर चला गया। अगर memory safety और undefined behavior से बचने को गंभीरता से लिया जाए, तो इसे अनदेखा नहीं किया जा सकता, और Herb Sutter का “benign undefined behavior” कहना भी निराशाजनक है

  • पता था कि C++ में undefined behavior बहुत है, लेकिन यह खास तौर पर चौंकाता है। सोच रहा हूँ कि क्या conversion को जितना संभव हो उतना तेज़ बनाने की कोशिश में, और क्योंकि अलग-अलग architectures पर इस extreme case को अलग तरह से संभालने वाले instructions हैं, इसे undefined behavior छोड़ा गया। signed integer overflow की तरह यही इस नियम का मुख्य कारण लगता है

    • अगर यही कारण है, तो यह undefined behavior नहीं बल्कि implementation-defined behavior होना चाहिए। 0 से division कुछ architectures पर trap कर सकता है, इसलिए उसका undefined behavior होना समझ में आता है
      undefined behavior को use-after-free जैसी चीज़ों तक सीमित होना चाहिए, जहाँ C abstract machine के बाहर के effects के कारण same platform पर भी consistent result की गारंटी नहीं दी जा सकती, या कुछ targets पर trap हो सकता है
      standard-compliant implementation अपने स्तर पर undefined behavior को define कर सकती है, और GCC कुछ मदों में ऐसा करता भी है। अगर बिना performance cost के stable semantics की गारंटी दी जा सकती है, तो target-specific float→int instruction के behavior को ही define किया जा सकता है, लेकिन दूसरी implementations में यह फिर भी undefined behavior रहेगा
    • शायद ऐसा ही है। PowerPC की fctiw series saturating conversion करती है, NaN को INT_MIN में बदलती है और FPSCR flags भी set करती है। 64-bit Power ISA की fctid भी बड़े integers के लिए यही करती है, लेकिन इनमें से कोई भी AArch64 या x86 के behavior से मेल नहीं खाता
  • ऐसे मामलों में इसे implementation-defined behavior या unspecified value की तरह संभालना ज़्यादा सही है। सिर्फ infinity को int में convert करने की वजह से पूरा program C++ standard के नियंत्रण से बाहर चला जाए, यह समझ से परे है
    C++26 ने कुछ अव्यावहारिक undefined behavior हटा दिए हैं, और यह भी स्पष्ट रूप से हटाए जाने योग्य उम्मीदवार है। प्रयोग से लगता है कि GCC और Clang इस undefined behavior का optimization में उपयोग नहीं करते, इसलिए वास्तविक असर सीमित दिखता है

    • counterexample है: https://godbolt.org/z/bqnoMMrz8
    • लिंक किए गए issue की comment के अनुसार Clang वास्तव में इस नियम का optimization में उपयोग करता है
  • यह एक और अप्रिय उदाहरण है कि specification के पास इस operation को undefined behavior घोषित करने का कोई कारण नहीं है

  • IEEE 754 को नापसंद करने की एक और वजह मिल गई। अगर दूसरी libraries या performance के कारण मजबूरी न हो, तो floating-point की जगह pure integers, बड़े integer numerator-denominator वाले rationals, और fixed-point decimals का जितना हो सके उतना उपयोग करता हूँ

    • यह मामला IEEE 754 से असंबंधित है और पूरी तरह C++ की गलती है
  • अगर C या C++ इस्तेमाल किया है, तो float32 और int32, यहाँ तक कि int64, के बीच का अंतर चौंकाने वाला नहीं होना चाहिए। बड़े exponent वाला float32, int64 से कहीं बड़े integer values दिखा सकता है
    जब ऐसे types हों जो एक-दूसरे के superset नहीं हैं और जिनकी representational capability अलग है, तो language syntax से अलग भी float→int conversion को safe मानने का कोई कारण नहीं है

    • आप मूल बात चूक रहे हैं। floating-point के हर value को preserve न कर पाना और undefined behavior एक ही बात नहीं हैं
      uint32_t भी uint64_t के सभी values represent नहीं कर सकता, लेकिन conversion semantics truncation के रूप में define हैं। यहाँ मूल समस्या यह है कि कुछ input पर compiler कुछ भी कर सकता है
    • representational range अलग होने का मतलब यह नहीं कि safe conversion define नहीं किया जा सकता। Rust float→int conversion को स्पष्ट रूप से define करता है, और Java 26 language specification section 5.1.3 में conversion procedure को विस्तार से बताती है
      low-level language होने पर CVTTSS2SI या FCVTZS जैसे assembly instructions से इसे map करना भी संभव है। low-level language का assembly से निकट होना, और दूसरे “benign” undefined behavior पर भी बहुत विवाद होना, इस तथ्य को और चौंकाने वाला बनाता है कि यह conversion undefined behavior की अनुमति देता है
    • hardware के अनुसार implementation-specific garbage value मिलना, या hardware/tooling का trap करके program रोक देना, बहुत चौंकाने वाली बात नहीं है
      लेकिन unrelated code का अजीब तरह से miscompile हो जाना, hard drive को format कर देना, या “नाक से दानव निकल आना” तक अनुमति होना चौंकाता है
      double-free या array bounds से बाहर write में कुछ भी हो सकता है—इस तरह का undefined behavior समझ में आता है, लेकिन C और C++ वहाँ भी इसका अति-उपयोग करते हैं जहाँ implementation-defined garbage value या program abort जैसे अधिक सख्त नियम रखे जा सकते थे। Rust कुछ integer overflow को debug mode में trap करता है और release mode में unspecified value लौटाता है, लेकिन उसे बे-सिर-पैर के दूसरे code को तोड़ने नहीं देता
      C++ को Java या Rust बनने की ज़रूरत नहीं है, लेकिन अनावश्यक undefined behavior को कम करने से यह निश्चित रूप से बेहतर भाषा बनेगी
    • यह मेरे लिए नई जानकारी थी। मैं समझता था कि non-representable floating-point को integer में convert करने पर बस result integer में कोई गलत value आ जाएगी, यह नहीं कि पूरा program हमेशा के लिए दूषित होकर C++ standard के नियंत्रण से बाहर चला जाएगा