1 पॉइंट द्वारा GN⁺ 4 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Rust और C/C++ के CVE नंबरों की सीधे तुलना करने पर, memory safety vulnerabilities को “library problem” मानने के मानदंडों का अंतर आसानी से छूट जाता है
  • C/C++ में गलत API call से UB या segfault हो जाए, तब भी इसे आम तौर पर user code के misuse के रूप में देखा जाता है, और ऐसी हर संभावना को CVE के रूप में दर्ज नहीं किया जाता
  • libcurl में curl_getenv(NULL) call बिना warning के build हो सकता है और runtime पर segfault दे सकता है, लेकिन इसे आम तौर पर curl vulnerability नहीं माना जाता
  • Rust में अगर user code में unsafe नहीं है, फिर भी सिर्फ safe API call से memory bug होता है, तो उसे library का soundness bug माना जाता है
  • इसलिए Rust के कुछ CVE, C/C++ की तुलना में अधिक सख्त मानदंडों पर दर्ज होते हैं, और सिर्फ raw CVE count से memory safety का आकलन करना मुश्किल है

CVE नंबरों की तुलना भ्रामक क्यों हो सकती है

  • CVE software security vulnerabilities को वर्गीकृत और रिपोर्ट करने वाला database है
  • Vulnerabilities साधारण program logic bug से भी आ सकती हैं, या ऐसे memory safety issues से भी जो exploit तक पहुंचना आसान बनाते हैं
  • Rust और C/C++ के CVE count की तुलना करते हुए यह दावा भी किया जाता है कि Rust “असल में memory-safe नहीं है” या “इसे अपनाने का कोई फायदा नहीं”
  • लेकिन memory safety से जुड़ी संभावित vulnerabilities को दोनों ecosystems जिस तरह संभालते हैं, उसमें बड़ा अंतर है

Rust में भी vulnerabilities संभव हैं

  • Rust programs में भी UB और memory safety bugs हो सकते हैं
  • ज़्यादातर मामलों में ऐसे issues के लिए unsafe keyword की ज़रूरत होती है
  • यह दावा गलत है कि Rust program कभी UB का सामना नहीं कर सकते
  • Memory safety से असंबंधित सामान्य vulnerabilities भी Rust में संभव हैं
    • जैसे admin dashboard के access permission check को छोड़ देना, यह किसी भी language में हो सकता है

C library का उदाहरण: curl_getenv(NULL)

  • curl एक व्यापक रूप से इस्तेमाल होने वाली और अच्छी तरह maintain की गई C-based networking library है
  • libcurl का curl_getenv कई operating systems पर environment variable की value लेने के लिए एक portable abstraction function है
  • नीचे दिया गया C program, curl_getenv को NULL pointer pass करता है
#include <curl/curl.h>
int main(void) {
  curl_getenv(NULL);
}
  • यह program gcc test.c -otest -lcurl -Wall -Wextra के साथ बिना warning के compile हो सकता है
  • इसे चलाने पर segfault हो सकता है, और इसे memory safety bug तथा संभावित vulnerability माना जा सकता है
  • लेकिन ऐसे उदाहरण को आम तौर पर curl vulnerability के रूप में report नहीं किया जाता

C/C++ में केवल misuse की संभावना के आधार पर CVE नहीं बनता

  • curl_getenv(NULL) जैसी समस्या को आम तौर पर API के गलत उपयोग के रूप में देखा जाता है
  • Defect की जगह भी library या API नहीं, बल्कि application code को माना जाता है
  • इस प्रथा के पीछे दो कारण हैं
    • C के सीमित type system में API contracts, invariants, preconditions और postconditions को सटीक रूप से व्यक्त करना कठिन है
    • हर संभावित misuse को document करना भी व्यावहारिक नहीं है
  • वास्तव में curl_getenv documentation यह नहीं कहता कि NULL के साथ call करना मना है और इससे segfault हो सकता है
  • C/C++ में गलती से UB trigger करना बहुत आसान है, इसलिए अगर हर संभावित vulnerability को CVE के रूप में report किया जाए, तो ज़्यादातर libraries पर बहुत बड़ी संख्या में CVE आ सकते हैं
  • इसलिए C/C++ में आम तौर पर “misuse हो सकने वाले API की मौजूदगी” नहीं, बल्कि किसी खास misuse case को आधार बनाकर CVE बनाया जाता है

Rust में safe API की responsibility boundary अलग होती है

  • मान लें कि Rust में सिर्फ hyper::foo(None) जैसे safe call से program segfault कर जाए, तो यह hyper का CVE हो सकता है
  • अगर user program में unsafe block नहीं है, फिर भी memory bug होता है, तो संबंधित library में soundness bug होना ही चाहिए
  • Rust में अगर safe library API को किसी भी तरीके से इस्तेमाल करने पर memory bug हो सकता है, तो इसे user code नहीं बल्कि library bug माना जाता है
  • ऐसे API को unsound या soundness hole वाला कहा जाता है
  • भले ही अभी तक किसी वास्तविक program में समस्या न मिली हो, अगर सिर्फ safe API usage से memory bug हो सकता है, तो CVE बनाया जा सकता है

safe और unsafe responsibility को स्पष्ट करते हैं

  • Rust में “क्या इस function को memory safety के नज़रिए से सही तरह इस्तेमाल किया जा रहा है” इसका जवाब C/C++ की तुलना में अधिक स्पष्ट है
    • अगर called function unsafe के रूप में marked नहीं है, तो उसे safely इस्तेमाल किया जा सकना चाहिए
    • अगर called function unsafe है, तो call site पर unsafe block चाहिए, और code review तथा codebase में जोखिम वाले हिस्से स्पष्ट दिखते हैं
  • यही भेद Rust की memory safety को व्यवहारिक रूप से scalable बनाता है
  • अगर user code unsafe का उपयोग नहीं करता और compiler bug भी नहीं है, तो संभावित memory safety कारणों को user code की जिम्मेदारी मानना कठिन है
  • अगर library unsafe interface expose नहीं करती, तो user को उस library का ऐसा उपयोग नहीं कर पाना चाहिए जिससे memory bug हो
  • भले ही library internally unsafe का उपयोग करके bug पैदा करे, fix library के अंदर होगा और user फिर से memory bugs से सुरक्षित हो जाएगा

सिर्फ raw CVE count से memory safety की तुलना करना कठिन है

  • यही तर्क अगर C पर लागू किया जाए, तो curl_getenv को भी curl का CVE मानना चाहिए, लेकिन C में Rust जैसे safe और unsafe का स्पष्ट भेद नहीं है
  • व्यवहार में लगभग हर C code implicitly unsafe के करीब है, इसलिए Rust वाले मानदंडों को उसी रूप में लागू करना कठिन है
  • भले ही C/C++ library developers सुरक्षित और robust libraries बनाएं, उन्हें इस्तेमाल करने वाले असंख्य C programs API को गलत तरह संभालकर आसानी से memory safety issues पैदा कर सकते हैं
  • यह अंतर सिर्फ curl तक सीमित नहीं है, बल्कि लगभग सभी C/C++ libraries और दोनों भाषाओं की standard libraries पर लागू होता है
  • Rust और C/C++ के line of code per CVE जैसे raw numeric comparisons memory safety का आकलन करते समय गलत निष्कर्ष दे सकते हैं

1 टिप्पणियां

 
GN⁺ 4 시간 전
Lobste.rs की राय
  • यह शायद एक भोला सवाल हो, लेकिन अगर C/C++ की बहुत-सी समस्याएँ undefined behavior से आती हैं, तो इन्हें बस define क्यों नहीं कर दिया जाता, यह जानने की जिज्ञासा है

    • मुझे लगता है कि standard में कुछ behavior undefined होने के कम-से-कम तीन कारण हैं
      पहला, कुछ चीज़ें ऐतिहासिक विरासत हैं जिनकी अब किसी को परवाह नहीं है, इसलिए उन्हें “बस define” किया जा सकता है, और जैसा @fanf ने कहा, इस पर काम चल रहा है। उदाहरण के लिए, C में ऐसा source file जिसमें एक unterminated string literal हो, वास्तव में undefined behavior है
      दूसरा, कुछ चीज़ें define तो की जा सकती हैं, लेकिन उनकी performance cost होती है। इसका प्रमुख उदाहरण signed integer overflow है; अगर इसे बस wrapping की तरह define कर दिया जाए, तो यह अब undefined behavior नहीं रहेगा, लेकिन compiler फिर इस धारणा पर आधारित optimizations नहीं कर पाएगा कि “यह कभी नहीं होगा।” Committee में compiler पक्ष के लोग बहुत हैं और उनमें benchmark को लेकर एक तरह का जुनून है, इसलिए यह आसानी से बदलता नहीं दिखता। फिर भी बदलाव बिल्कुल नहीं हो रहे, ऐसा नहीं है; उदाहरण के लिए, P2723 यह प्रस्ताव देता है कि C++ में सभी uninitialized local variables को implicit 0 initialization मिले
      तीसरा, कुछ चीज़ों को व्यवहारिक रूप से define करना कठिन है। इसका अच्छा उदाहरण use-after-free है। जब तक Fil-C जैसी भारी runtime capability system सभी पर थोपी न जाए, या Rust-style lifetime annotations पूरी भाषा में न जोड़ दिए जाएँ, तब तक use-after-free में दिख सकने वाले behavior की सीमा कैसे तय की जाए, यह अस्पष्ट है। यह लिखा जा सकता है कि “use-after-free होने पर उस समय उस जगह मौजूद memory को छुआ जाएगा या segfault/abort होगा,” लेकिन इससे किसी का भला नहीं होता। यह फिर भी खतरनाक रहेगा और CVE भी वैसे ही बनेंगे, और उसके बाद program क्या कर सकता है और क्या नहीं, इस पर कोई सार्थक बात नहीं कही जा सकेगी, इसलिए यह बस किसी और नाम से undefined behavior ही होगा
      दुर्भाग्य से तीसरी श्रेणी का प्रभाव बहुत भारी है, इसलिए कुछ चीज़ों को “अब बस define” कर देना अच्छा तो है, पर इससे पूरी स्थिति में बड़ा बदलाव नहीं आता
    • इस revision round में C committee भाषा के undefined behavior को कम कर रही है। https://open-std.org/jtc1/sc22/wg14/www/wg14_document_log.htm पर “slaying earthly demons” दस्तावेज़ देख सकते हैं
      जहाँ तक मुझे पता है, library का ज़्यादातर हिस्सा अभी नहीं लिया गया है, लेकिन size argument लेने वाले functions को null pointer के प्रति reasonable behavior देने के लिए बदला गया है। यह उस language change से जुड़ा था जो null pointer में 0 जोड़ने की अनुमति देता है। इसी तरह और भी कई functions सुधारे जा सकते हैं, लेकिन getenv() वाला बदलाव शायद POSIX के साथ समन्वय करके करना बेहतर होगा
    • सबसे आम दोहराई जाने वाली व्याख्या यह है कि कुछ behavior को undefined रखना ज़रूरी है ताकि वे optimizations संभव हों जो वरना allowed नहीं होते। लेकिन मुझे यह ज़्यादातर self-justification जैसा लगता है
      ऐसे performance gains लगभग पूरी तरह मामूली किनारों पर होते हैं और ज़्यादा से ज़्यादा बहुत छोटे होते हैं। अगर कोई function हो जो rm -rf / call करता है लेकिन वास्तव में कभी call नहीं होता, और आप undefined behavior वाला function pointer call बना दें, तो compiler तकनीकी रूप से ऐसा code generate करने के लिए भी स्वतंत्र है जो disk मिटाने वाले उस function को हमेशा call करे। अंत में यह बस ख़राब spec design और विरासत का मामला है
    • कुछ undefined behavior समय के साथ define किए गए हैं, लेकिन बहुत-सी चीज़ें optimization की वजह से बनी रहनी पड़ती हैं। एक प्रसिद्ध उदाहरण है for (int ii = 0; ii < something; ii++), जो इस बात पर निर्भर करता है कि signed integer overflow undefined है, इसलिए something == INT_MAX की संभावना को नज़रअंदाज़ किया जा सकता है, और इससे कई loop transformations संभव होते हैं
      Rust में इसी तरह की functionality safe functions और unsafe functions में बँटी हुई है। Safe functions थोड़े धीमे हो सकते हैं, और unsafe functions अगर ग़लत इस्तेमाल हों तो undefined behavior की अनुमति देते हैं। i32::wrapping_add() और i32::unchecked_add() देख सकते हैं
      अगर C में कुछ functions को unsafe के रूप में mark करने और कुछ specific scope में unsafe functions के उपयोग की अनुमति देने वाला notation जोड़ दिया जाए, तो safe variants को define करना शुरू किया जा सकता है। लेकिन एक बिंदु पर C को बदलने की मेहनत, और उससे भी ज़्यादा C को नियंत्रित करने वाले लोगों की सोच बदलने की मेहनत, लक्ष्य की तुलना में अनुपयुक्त हो जाती है; तब बेहतर है कि ऐसा language चुना जाए जो लक्ष्य से ज़्यादा मेल खाता हो
    • एक उदाहरण है जो दिखाता है कि यह इतना कठिन क्यों है
      C में अगर heap object की ओर इशारा करने वाला pointer free को देने के बाद भी उस object को access किया जाए, तो वह undefined behavior है। CHERIoT में इसे trap के रूप में define किया गया है, लेकिन यह इसलिए संभव है क्योंकि हमने ऐसा hardware बनाया है जो इसे संभव बनाता है। Standard को तरह-तरह के hardware को support करना होता है, इसलिए सवाल यह है कि इसे किस रूप में define किया जाए
      मोटे तौर पर दो तरीके हैं। एक यह कि deallocation टाल दिया जाए, और कहा जाए कि object तब तक नष्ट नहीं होगा जब तक उसकी ओर इशारा करने वाले सभी pointers ग़ायब न हो जाएँ। इसके लिए garbage collector जैसी किसी चीज़ की ज़रूरत पड़ेगी, और C के बहुत-से उपयोगों में इसका overhead असहनीय होगा। दूसरा तरीका यह है कि ऐसा type system define किया जाए जो object की ओर इशारा करने वाले सभी pointers की स्थिति जान सके और उन्हें invalidate कर सके। Rust ने दूसरा तरीका चुना है, इसलिए Rust में tree न होने वाले data structures को implement करने के लिए unsafe या standard library की वे सुविधाएँ चाहिए जो unsafe का उपयोग करती हैं। ऐसी चीज़ें language design stage में डाली जा सकती हैं, लेकिन बाद में जोड़ना लगभग असंभव होता है
      Bounds errors भी इसी तरह के हैं। CHERI systems में object या subobject की bounds pointer का intrinsic हिस्सा होती हैं, इसलिए bounds के बाहर access trap बन जाता है। दूसरी platforms पर pointer बस address रखने वाला एक word होता है। Arithmetic करने के बाद उसे मूल object से फिर map करने का कोई तरीका नहीं रहता, इसलिए सवाल उठता है कि bounds आएँगी कहाँ से। AddressSanitizer जैसे tools bounds को अलग structure में store करते हैं और pointer arithmetic पर checks की माँग करते हैं, लेकिन memory और performance overhead इतना अधिक है कि production environment में ASan चालू किए हुए C की तुलना में Java इस्तेमाल करना कहीं बेहतर है, और शायद code भी ज़्यादा तेज़ी से लिखा जा सकता है
  • मुझे लगा था कि null pointer dereference well-defined behavior है

    • https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf के पेज 4, PDF के हिसाब से पेज 18 पर यह लिखा है
      1. Terms, definitions, and symbols

      3.5.3 undefined behavior

      Example: an example of undefined behavior is the behavior on null pointer dereference

    • CPU instruction set के नज़रिए से यह सही हो सकता है, लेकिन programming target वह नहीं बल्कि C abstract machine है, और C abstract machine इसे undefined behavior कहती है
  • इस लेख में एक बात खटकती है
    SEGFAULT panic की तरह एक denial-of-service attack है
    दोनों एक ही तरह की error category में आते हैं, और आम तौर पर memory safety के साथ जो बातें जोड़ी जाती हैं, वे stack smashing, data corruption, code corruption जैसी चीज़ें हैं। Rust में ये चीज़ें बहुत, बहुत ज़्यादा कठिन हैं, और कुछ हद तक C में भी इन्हें कठिन बनाया जा सकता है
    कुल मिलाकर यह लेख ज़्यादातर इस बात जैसा लगा कि C का type system बहुत ख़राब है। C++ में ऐसी गलतियों को रोका जा सकता है, और C में भी GCC के nonnull attribute का इस्तेमाल करके किसी function को NULL देना compiler error तक बढ़ाया जा सकता है
    व्यक्तिगत रूप से मुझे लगा कि out-of-bounds access इससे बेहतर और अधिक प्रतिनिधि उदाहरण होता

    • “SEGFAULT panic की तरह denial-of-service attack है” यह बात सही नहीं है
      Panic program में built-in safety check है, यह reliably होता है और इसका behavior साफ़ तौर पर define किया गया है
      Segfault वह है जिसे operating system ने ग़लत memory operation के रूप में पकड़ा है, और यह केवल उन addresses पर होता है जो program के virtual memory map में मौजूद pages के बाहर हों। इसलिए बहुत-से segfault bugs को किसी न किसी रूप में arbitrary code execution में बदला जा सकता है
      सामान्य स्थिति में दोनों का परिणाम एक जैसा दिख सकता है, लेकिन बुनियादी रूप से ये अलग चीज़ें हैं