• मेमोरी सुरक्षा में बड़ा सुधार होता है, लेकिन Rust प्रोडक्शन कोड में भी system boundary handling की समस्याएँ बनी रह सकती हैं और वे कमजोरी में बदल सकती हैं
  • एक ही path को कई syscall में दोबारा resolve करने वाला flow, create करने के बाद permissions बदलने का तरीका, और string-based path comparison जैसी चीज़ें TOCTOU और permission exposure जैसी समस्याएँ पैदा करना आसान बनाती हैं
  • Unix में path, environment variables, और stream data raw bytes के रूप में चलते हैं, इसलिए String-केंद्रित handling या from_utf8_lossy, unwrap, expect डेटा corruption या DoS तक ले जा सकते हैं
  • अगर errors को छोड़ दिया जाए, तो failure सफलता जैसा दिख सकता है, और GNU coreutils के साथ behavior differences भी shell scripts और privileged tools में तुरंत security समस्या बन सकते हैं
  • इस audit में buffer overflow, use-after-free, double-free जैसे memory-safety वर्ग के bugs नहीं मिले, और बचा हुआ मुख्य जोखिम Rust के भीतर से ज़्यादा बाहरी दुनिया से जुड़ी boundaries पर केंद्रित था

audit में सामने आई Rust की सीमाएँ

  • Canonical द्वारा सार्वजनिक किए गए uutils के 44 CVE दिखाते हैं कि Rust प्रोडक्शन कोड में भी borrow checker, clippy, और cargo audit से न पकड़े जाने वाली कमजोरियाँ बच सकती हैं
  • समस्याओं का केंद्र memory safety से ज़्यादा system boundary handling था
    • path और syscall के बीच time gap था
    • Unix byte data और UTF-8 strings में mismatch था
    • मूल tool के साथ behavior differences थे
    • error handling की कमी और panic! termination मौजूद थी
  • यह CVE सूची संक्षेप में दिखाती है कि Rust system code में safety कहाँ खत्म होती है

path को दो बार resolve करने से TOCTOU बनता है

  • अगर एक ही path को एक syscall में check किया जाए और अगले syscall में फिर उसी पर काम किया जाए, तो यह आसानी से TOCTOU कमजोरी में बदल सकता है
    • इन दो calls के बीच parent directory पर write permission रखने वाला attacker path component को symbolic link में बदल सकता है
    • दूसरी call में kernel path को फिर से शुरू से resolve करता है, जिससे privileged operation attacker द्वारा चुने गए target पर जा सकता है
  • Rust का std::fs API डिफ़ॉल्ट रूप से &Path-आधारित re-resolution पर टिका है, इसलिए ऐसी गलती करना आसान हो जाता है
    • fs::metadata, File::create, fs::remove_file, fs::set_permissions हर call पर path को फिर से resolve करते हैं
    • जिन privileged tools को local attackers से बचना होता है, उनके लिए यह default path खतरनाक हो जाता है
  • CVE-2026-35355 में file delete करने के बाद उसी path पर नया file बनाने वाला flow exploit किया गया
    • src/uu/install/src/install.rs में fs::remove_file(to)? के बाद File::create(to)? आता था
    • अगर delete और create के बीच to को /etc/shadow जैसे target की ओर इशारा करने वाले symbolic link में बदल दिया जाए, तो privileged process उस file को overwrite कर सकता है
  • fix में OpenOptions::create_new(true) का उपयोग कर सिर्फ नया file create करने के लिए बदला गया
    • documentation के अनुसार create_new target location पर existing file के साथ-साथ dangling symlink को भी स्वीकार नहीं करता
  • अगर एक ही path पर दो बार काम करना पड़े, तो उसे file descriptor पर pin करना ज़्यादा सुरक्षित है
    • नए file creation के अलावा, parent directory को एक बार खोलकर उसके handle के आधार पर relative path operations करना बेहतर है
    • एक ही path पर दो बार काम हो तो, जब तक उलटा साबित न हो, उसे TOCTOU मानना चाहिए

permissions को बाद में बदलने के बजाय create करते समय तय करना चाहिए

  • directory या file को default permissions के साथ बनाकर बाद में chmod करना भी छोटी exposure window बनाता है
    • अगर fs::create_dir(&path)? के बाद fs::set_permissions(&path, Permissions::from_mode(0o700))? लिखा जाए, तो उनके बीच path default permissions के साथ मौजूद रहता है
    • दूसरे users इस window के दौरान open() कर सकते हैं, और बाद में chmod हो जाने पर भी पहले से मिले file descriptors वापस नहीं लिए जा सकते
  • permissions को creation time पर ही साथ में set करना चाहिए
    • OpenOptions::mode() और DirBuilderExt::mode() का उपयोग कर object को इच्छित permissions के साथ पैदा करना चाहिए
    • kernel इस पर अतिरिक्त रूप से umask लागू करता है, इसलिए अगर उसका प्रभाव महत्वपूर्ण है तो umask को भी स्पष्ट रूप से संभालना चाहिए

path string comparison, filesystem identity नहीं है

  • chmod की शुरुआती --preserve-root check केवल string comparison करती थी
    • recursive && preserve_root && file == Path::new("/")
    • ऐसे input जो वास्तव में root की ओर इशारा करते हैं लेकिन string / नहीं हैं, जैसे /../, /./, /usr/.., या / की ओर इशारा करने वाला symbolic link, इस check को bypass कर सकते थे
  • fix में fs::canonicalize से path को असली absolute path में resolve कर फिर compare करने का तरीका अपनाया गया
    • fix PR
    • canonicalize .., ., और symbolic links को resolve कर वास्तविक path लौटाता है
  • --preserve-root के मामले में / का parent directory नहीं होता, इसलिए यह तरीका काम करता है
  • सामान्य रूप से दो arbitrary paths एक ही filesystem object हैं या नहीं, यह जाँचने के लिए string नहीं बल्कि (dev, inode) compare करना चाहिए
    • GNU coreutils भी यही तरीका अपनाता है
  • CVE-2026-35363 में rm ने . और .. को reject किया, लेकिन ./ और ./// को स्वीकार कर लिया, जिससे current directory delete की जा सकती थी
    • input form के फर्क को सिर्फ string स्तर पर संभालने से checks आसानी से चकमा खा जाते हैं

Unix boundaries पर strings से पहले bytes को प्राथमिकता देनी चाहिए

  • Rust के String और &str हमेशा UTF-8 होते हैं, लेकिन Unix के path, environment variables, arguments, और stream data raw bytes की दुनिया में रहते हैं
  • इस boundary को पार करते समय गलत चुनाव दो तरह के bugs पैदा करता है
    • from_utf8_lossy जैसे lossy conversion गलत bytes को U+FFFD में बदलकर चुपचाप डेटा corrupt कर देते हैं
    • unwrap या ? जैसे strict conversion input को reject कर सकते हैं या process बंद कर सकते हैं
  • comm का CVE-2026-35346 lossy conversion की वजह से output खराब होने का मामला था
    • src/uu/comm/src/comm.rs में input bytes ra, rb को String::from_utf8_lossy में बदलकर print! किया गया था
    • GNU comm binary files में भी bytes को ज्यों का त्यों copy करता है, लेकिन uutils invalid UTF-8 को U+FFFD में बदलकर output को corrupt कर देता था
    • fix में BufWriter और write_all के जरिए raw bytes को सीधे stdout पर लिखने का तरीका अपनाया गया
  • print! Display के जरिए UTF-8 round trip को मजबूर करता है, लेकिन Write::write_all ऐसा नहीं करता
  • Unix system code में स्थिति के अनुसार सही type का उपयोग करना चाहिए
    • file paths के लिए Path, PathBuf
    • environment variables के लिए OsString
    • stream contents के लिए Vec<u8> या &[u8]
  • formatting की सुविधा के लिए String के रास्ते जाने पर data corruption आसानी से घुस सकता है

हर panic सेवा बाधित करने तक ले जा सकता है

  • CLI में unwrap, expect, slice indexing, unchecked arithmetic, और from_utf8 ऐसी जगहें बन सकती हैं जहाँ attacker input नियंत्रित कर सके तो वे DoS points बन जाती हैं
    • panic! stack को unwind करता है और process को रोक देता है
    • अगर यह cron job, CI pipeline, या shell script में चल रहा हो तो पूरा काम रुक सकता है
    • बार-बार चलने वाले environment में crash loop बनकर पूरे system को ठप भी कर सकता है
  • sort --files0-from का CVE-2026-35348 NUL-separated filenames की सूची में non-UTF-8 filename मिलने पर रुक जाता था
    • parser हर name byte sequence पर std::str::from_utf8(bytes).expect(...) चला रहा था
    • GNU sort kernel की तरह filenames को raw bytes के रूप में संभालता है, लेकिन uutils UTF-8 को मजबूर कर पहली non-UTF-8 path पर पूरा process रोक देता था
  • untrusted input संभालने वाले code में unwrap, expect, indexing, और as cast को संभावित CVE की तरह देखना चाहिए
    • ?, get, checked_*, try_from का उपयोग करें और असली error को caller तक पहुँचने दें
  • CI में पकड़ने के लिए clippy नियम भी सुझाए गए
    • unwrap_used
    • expect_used
    • panic
    • indexing_slicing
    • arithmetic_side_effects
  • test code में ये warnings बहुत ज़्यादा सख्त हो सकती हैं, इसलिए cfg(test) दायरे में इन्हें सीमित करना उचित है

अगर errors को छोड़ दिया जाए, तो failure सफलता जैसा दिख सकता है

  • कुछ CVE ऐसे flow से आए जहाँ errors को ignore किया गया या error information खो गई
  • chmod -R और chown -R पूरी operation में सिर्फ आखिरी file का exit code लौटाते थे
    • पहले कई files पर failure होने के बावजूद अगर आखिरी file सफल हो जाए, तो command 0 के साथ खत्म हो सकती थी
    • script गलत मान सकती थी कि पूरा काम बिना समस्या के पूरा हुआ
  • dd ने /dev/null पर GNU behavior की नकल करने के लिए set_len() के result पर Result::ok() बुलाया था
    • इरादा कुछ सीमित स्थितियों में error को छोड़ने का था, लेकिन वही code सामान्य files पर भी लागू हो गया
    • disk भर जाने पर भी आधी-अधूरी लिखी destination file चुपचाप बची रह सकती थी
  • .ok(), .unwrap_or_default(), या let _ = से Result को छोड़ देने पर महत्वपूर्ण failure cause गायब हो जाती है
  • भले ही पहली failure पर तुरंत न रुकें, फिर भी सबसे गंभीर error code याद रखकर उसी के साथ exit करना चाहिए
  • अगर Result को छोड़ना ही पड़े, तो code में यह कारण छोड़ना चाहिए कि उस failure को सुरक्षित रूप से क्यों ignore किया जा सकता है

मूल tool के साथ सटीक compatibility भी एक security feature है

  • कई CVE इसलिए नहीं हुए कि code ने कोई खतरनाक operation किया, बल्कि इसलिए हुए कि उसका behavior GNU से अलग था
    • वास्तविक shell scripts मूल GNU behavior पर निर्भर होती हैं, इसलिए semantic difference security समस्या में बदल सकता है
  • kill -1 का CVE-2026-35369 इसका प्रतिनिधि उदाहरण है
    • GNU -1 को signal 1 मानता है और PID माँगता है
    • uutils ने इसे PID -1 पर default signal भेजने के रूप में समझा
    • Linux में PID -1 का मतलब दिखाई देने वाली सभी processes होता है, इसलिए साधारण typo पूरे system kill में बदल सकती है
  • reimplementation tools में bug-for-bug compatibility exit codes, error messages, edge cases, और option semantics तक फैला हुआ एक safety mechanism बन जाता है
  • जहाँ भी GNU से अलग behavior होगा, वहाँ shell scripts के गलत निर्णय लेने की संभावना बढ़ती है
  • uutils अब CI में upstream GNU coreutils test suite भी साथ चलाता है
    • इस तरह के फर्क रोकने के लिए यह उचित स्तर की रक्षा लगती है

trust boundary पार करने से पहले resolve करना चाहिए

  • CVE-2026-35368 chroot में local root code execution का मामला था
  • समस्या का pattern यह था कि chroot(new_root)? के बाद attacker-नियंत्रित नई root के भीतर user name resolve किया गया
    • get_user_by_name(name)? नई root filesystem की shared libraries पढ़कर user name resolve करने लगा
    • अगर attacker chroot के भीतर files रख दे, तो यह uid 0 code execution तक जा सकता है
  • GNU chroot user resolution को chroot से पहले करता है
    • fix में भी यही क्रम अपनाया गया
  • एक बार trust boundary पार हो जाने के बाद, हर library call attacker code execute करा सकती है
  • static linking भी इस समस्या को नहीं रोकती
    • क्योंकि get_user_by_name NSS से होकर runtime पर libnss_* modules को dlopen करता है

वे bugs जिन्हें Rust ने वास्तव में रोका

  • इस audit में कुछ bug classes का न मिलना भी महत्वपूर्ण है
    • buffer overflow नहीं था
    • use-after-free नहीं था
    • double-free नहीं था
    • shared mutable state का data race नहीं था
    • null-pointer dereference नहीं था
    • uninitialized memory read नहीं था
  • tool में bug होने पर भी audit में ऐसा कुछ नहीं मिला जिसे arbitrary memory read जैसे रूप में exploit किया जा सके
  • GNU coreutils ने पिछले कुछ वर्षों में ऐसे memory-safety वर्ग के CVE लगातार दिए हैं
    • pwd deep path buffer overflow
    • numfmt out-of-bounds read
    • unexpand --tabs heap buffer overflow
    • od --strings -N heap buffer के बाहर NUL write
    • sort heap buffer से पहले 1-byte read
    • split --line-bytes heap overwrite वाला CVE-2024-0684
    • b2sum --check malformed input में unallocated memory read
    • tail -f stack buffer overrun
  • उसी अवधि की तुलना में Rust reimplementation ने इस श्रेणी के bugs को 0 मामलों पर बनाए रखा
    • हालांकि यह भी जोड़ा गया कि audit ने memory-safety bugs की अनुपस्थिति साबित नहीं की, बस उन्हें पाया नहीं
  • बाकी समस्याएँ Rust के भीतर से ज़्यादा बाहरी दुनिया से जुड़ी boundaries पर पैदा होती हैं
    • path
    • bytes और strings
    • syscall
    • time gap और filesystem state changes

सही Rust, idiomatic Rust भी है

  • idiomatic Rust सिर्फ ऐसा code नहीं है जो borrow checker से पास हो जाए और clippy को शांत रखे
  • correctness भी idiomatic होने का हिस्सा होना चाहिए
    • क्योंकि वास्तविक दुनिया में टिकने वाले code के रूप community experience के जरिए स्थिर हुए हैं
  • मज़बूत systems को वास्तविक दुनिया की गड़बड़ी को छिपाने के बजाय उसे वैसा का वैसा reflect करना चाहिए
    • path की जगह file descriptor
    • String की जगह OsStr
    • unwrap की जगह ?
    • ज़्यादा साफ़ दिखने वाले semantics की जगह मूल implementation के साथ bug-for-bug compatibility
  • type system बहुत कुछ व्यक्त कर सकता है, लेकिन दो syscall के बीच बीतने वाला समय जैसी नियंत्रण से बाहर की शर्तों को पूरी तरह नहीं समेट सकता
  • idiomatic Rust में code के types, names, और control flow को runtime environment की सच्चाई दिखानी चाहिए
    • whiteboard पर सुंदर दिखने वाले code से कम आकर्षक हो, तब भी अधिक ईमानदार रूप की ज़रूरत है

संदर्भ सामग्री

अभी कोई टिप्पणी नहीं है.

अभी कोई टिप्पणी नहीं है.