3 पॉइंट द्वारा GN⁺ 2026-04-30 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • मेमोरी सुरक्षा में बड़ा सुधार होता है, लेकिन 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 से कम आकर्षक हो, तब भी अधिक ईमानदार रूप की ज़रूरत है

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

1 टिप्पणियां

 
GN⁺ 2026-04-30
Hacker News की राय
  • GNU Coreutils के मेंटेनर के रूप में मैंने यह लेख दिलचस्पी से पढ़ा, लेकिन जितना थोड़ा Rust मैंने इस्तेमाल किया है, उसमें std::fs के साथ TOCTOU race बनाना बहुत आसान लगा
    उम्मीद है कि openat जैसी API आखिरकार standard library में आएगी

    और पाथ की तुलना करने से पहले resolve करो वाले नियम से मैं सहमत नहीं हूँ
    आम तौर पर fstat कॉल करके st_dev और st_ino की तुलना करना बेहतर होता है, और लेख में यह बात कुछ हद तक शामिल भी थी

    एक कम चर्चा किया जाने वाला side effect performance cost है
    एक वास्तविक उदाहरण में बहुत गहरे directory path पर cp को 0.010 सेकंड लगे, जबकि uu_cp को 12.857 सेकंड लगे

    व्यवहार में लोग जानबूझकर ऐसे path कम ही बनाते हैं, लेकिन GNU software मनमानी सीमाओं से बचने की बहुत गंभीर कोशिश करता है
    https://www.gnu.org/prep/standards/standards.html#Semantics

    और लेख में कहा गया कि Rust rewrite में समान अवधि के दौरान memory safety bug शून्य थे, लेकिन यह सच नहीं है :)
    https://github.com/advisories/GHSA-w9vv-q986-vj7x

    • सही है, std::fs में lowest common denominator की समस्या है
      Rust 1.0 में कुछ न कुछ डालना था, और दुर्भाग्य से वही स्थिति लंबे समय तक जम गई

      मुझे लगता है uutils ऐसा अच्छा स्थान हो सकता है जहाँ कम गलती-प्रवण std::fs replacement API डिज़ाइन करके देखी जाए

    • दूसरी तरफ़ के नज़रिये को इतनी संक्षिप्तता से समझाने के लिए धन्यवाद

      मैं पूछना चाहता हूँ कि यहाँ से क्या सीखना चाहिए
      इंटरनेट पोस्ट के हिसाब से मैं इसे जानबूझकर थोड़ा आक्रामक ढंग से पूछ रहा हूँ, क्योंकि विरोधाभास होने पर फ़र्क और गलतियाँ ज़्यादा साफ़ दिखती हैं
      बेशक, आप पर अपना समय या मानसिक ऊर्जा खर्च करने की कोई बाध्यता नहीं है

      मुझे जिज्ञासा है कि speed, performance, race condition, और st_ino बार-बार साथ क्यों आते हैं
      latency, असली storage पर लिखना, atomicity, ACID, और सूचना-प्रेषण की सीमित गति — ये सब आखिरकार किसी एक जैसी मूल प्रकृति पर जाकर मिलते हुए लगते हैं
      accounting जैसी उच्च-विश्वसनीयता वाली प्रणालियाँ शायद अंततः ACID की ओर जाती हैं, और कम-विश्वसनीयता वाली प्रणालियाँ इतनी जल्दी भुला दी जाती हैं कि कभी-कभी लगता है जैसे कंप्यूटरों के फ़र्क उतने बड़े नहीं हैं

      यह भी जिज्ञासा है कि रोज़मर्रा के applications में throughput क्या सचमुच latency से ज़्यादा महत्वपूर्ण होता है

      और C, Unix-परिवार OS, तथा GNU coreutils के इतिहास के कारण inode number पर ध्यान देना समझ में आता है,
      लेकिन एक बहुत बुनियादी उदाहरण के तौर पर USB मेमोरी को फ़ाइल स्टोरेज के लिए बस ठीक से काम करने देना कैसा रहेगा, यह सोचता हूँ
      libc I/O buffering, fflush, kernel buffering, multicore, time-sharing, और कई applications के समानांतर चलने जैसी जटिलताओं से बचे बिना

    • मैं बिल्कुल शुरुआती हूँ, इसलिए सोच रहा था कि सीधे $(yes a/ | head -n $((32 * 1024)) | tr -d '\n') से cd क्यों नहीं किया गया और while loop की ज़रूरत क्यों पड़ी

      संपादन: समझ गया। वजह -bash: cd: a/a/a/....../a/a/: File name too long थी

    • पता नहीं आपने देखा या नहीं, लेकिन GNU utility जैसे wget को memory-safe C++ subset में ऑटोमेटिकली बदलने का एक डेमो है
      https://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget

      यह unsafe C elements को व्यवहार-समान safe C++ elements से लगभग 1:1 बदलता है, इसलिए rewrite की तुलना में नए bug और नए behavioral differences लाने की संभावना कम लगती है

      अगर मूल source code को थोड़ा सा साफ़ कर दिया जाए, तो conversion पूरी तरह automated हो सकता है, इसलिए build step में मूल C source से थोड़ा धीमा लेकिन memory-safe executable बनाया जा सकता है

    • यह शायद थोड़ा मूर्खतापूर्ण सवाल हो, लेकिन क्या GNU Coreutils पक्ष में अपनी तरफ़ से Rust rewrite पर विचार या कोई योजना चल रही है, यह जानने की उत्सुकता है

  • Rust शायद उन्हें आता था, लेकिन वे Unix API और उसकी semantics व traps से पर्याप्त रूप से परिचित नहीं थे
    उन गलतियों में से अधिकांश पुराने GNU coreutils या BSD, Solaris पृष्ठभूमि वाले डेवलपर की नज़र से काफ़ी शुरुआती स्तर की लगेंगी
    ऐसे मुद्दों में से बहुत से दशकों पहले ही सामने आ चुके थे और समझे जा चुके थे, और पुराने codebase में आज भी fixes की लंबी tail बची हुई है, लेकिन अब आम तौर पर केवल कम मात्रा में ही नई चीज़ें आती हैं

    • उस Canonical thread को पढ़कर मैं सचमुच दंग रह गया
      उसका सार कुछ ऐसा था: “Rust ज़्यादा सुरक्षित है, security सर्वोच्च प्राथमिकता है, इसलिए पूरे coreutils rewrite को deploy करना urgent है। कुछ टूट भी जाए तो कोई बात नहीं, बाद में ठीक कर लेंगे”

      मैं नहीं चाहता कि ऐसी सोच रखने वाले लोगों का लिखा code मेरी machine पर चले
      मैं भी Rust के पक्ष में हूँ, लेकिन Rust अधिक सुरक्षित है — यह बात बाकी सब बराबर होने पर ही सही है
      यहाँ बाकी सब बिल्कुल बराबर नहीं है

      rewrite में दशकों से मेंटेन किए गए code की तुलना में कहीं अधिक bugs और vulnerabilities होना लगभग तय है, इसलिए security वाला तर्क लंबी अवधि की transition strategy के लिए तो मायने रखता है, लेकिन जल्दबाज़ी में rollout के औचित्य के लिए नहीं

      deploy होने के बाद user impact को मामूली बताना, या यह कहना कि “ऐसे ही bugs सामने आते हैं”, “पुराने coreutils में भी proper tests नहीं थे”, बहुत गैर-जिम्मेदाराना रवैया है
      users प्रयोगशाला के चूहे नहीं हैं
      मेरा मानना है कि maintainers पर users के systems की reliability को नुकसान न पहुँचाने की नैतिक ज़िम्मेदारी होती है

    • उससे भी बुनियादी बात यह है कि Rust standard library शायद डेवलपर्स को गलत abstraction level पर एक साफ़-सुथरी API की तरफ़ धकेलती है
      जैसे handle-based file operations के बजाय path-based operations की तरफ़
      उम्मीद है कि मैं ग़लत हूँ

    • मेरे हिसाब से Rust का मुख्य बिंदु यह है कि सबसे बड़े और सबसे आसानी से फँसाने वाले traps के बारे में आपको जानबूझकर सोचना न पड़े

      लगता है इस लेख का केंद्रीय बिंदु भी यही है कि filesystem API को ऐसा ही काम करना चाहिए

    • किसी ने इसी तरह की अभिव्यक्ति में disassembler rage शब्द गढ़ा था
      मतलब, अगर आप काफ़ी क़रीब से देखें तो हर गलती शौकिया लगती है

      यह उस रवैये से निकला था जिसमें कोई सिर्फ disassembler देखकर, call stack में 100 frames नीचे किसी function के अंदर, high-level programmer को यह कहकर कोसता है कि उसने switch की जगह if क्यों लिखा

      अभी हम उनकी कुछ ग़लत चीज़ें देख रहे हैं, लेकिन आसपास की हज़ारों पंक्तियों का सही लिखा गया code लगभग नहीं देख रहे

    • ऐसे utilities में panic होना Rust के मानदंड से भी काफ़ी शौकिया गलती है
      अगर बात non-recoverable alloc error जैसी होती तो अलग बात थी, लेकिन expect और unwrap को तब तक सही ठहराना मुश्किल है जब तक आप बहुत सख़्ती से यह invariant सुनिश्चित न करें कि वह code path कभी चल ही नहीं सकता

  • code rewrite करते समय कठिनाइयों में से एक यह है कि मूल code वास्तविक production environment में ही सामने आने वाली समस्याओं का सामना करते हुए धीरे-धीरे बदलता गया होता है

    उस प्रक्रिया से मिली सीख code के भीतर चुपचाप समा जाती है, और यदि वह documented न हो, तो उसी स्तर तक पहुँचने से पहले करने वाला छिपा हुआ काम बहुत विशाल हो जाता है

    मूल लेख ठीक इसी तरह की सूची को अच्छी तरह दिखाता है

    फिर भी किसी को तुरंत शौकिया कहने से पहले यह भी देखना चाहिए कि यह software की सबसे software-जैसी घटनाओं में से एक है
    अगर ऐसा नहीं था कि coreutils में बहुत अच्छी technical documentation और उन मामलों को कवर करने वाले tests मौजूद थे और फिर भी उन्हें नज़रअंदाज़ किया गया, तो ऐसा होना लगभग अवश्यंभावी था

    • लेख का एक अच्छा उदाहरण chroot + NSS CVE है
      यह कि NSS dynamic है, और chroot के अंदर libraries को dlopen करता है, यह कहीं भी स्पष्ट रूप से लिखा नहीं होता

      यह लगभग उस तरह की बात है जो system administrators ने 25 साल से भी ज़्यादा समय तक ठोकरें खाकर सीखी है, और clean-room rewrite इसे अक्सर नए CVE के रूप में फिर से सीखता है
      वही code अगर LLM से port करवाएँ तो हालात मिलते-जुलते होंगे
      function signature पढ़ी जा सकती है, लेकिन वास्तव में ज़रूरी चीज़ उस code पर पड़े घाव और निशान हैं

    • अगर यह काम GPL से बचने के लिए मूल source पढ़े बिना किया जा रहा हो, तो यह और भी कठिन हो जाता है

      मेरी राय में अगर uutils GPL होता और coreutils original source से सीधे प्रेरणा ले सकता, तो नतीजा बहुत बेहतर होता

    • यह बात भी साफ़ कही जानी चाहिए कि ऐसी सीखों, या कम-से-कम जिन bugs और vulnerabilities से बचने की कोशिश की गई थी, उन्हें document न करना भी एक खराब practice है

      बेशक, शुरुआत से अच्छा code लिखकर जिन सभी bugs से implicitly बचा गया, उन सबको दस्तावेज़ में बदलना कठिन है,
      लेकिन भविष्य के पाठक के लिए यह लिख छोड़ना ज़्यादा महत्वपूर्ण है कि “यहाँ bar के बजाय foo इसलिए इस्तेमाल किया गया है, क्योंकि ABC condition में bar इस्तेमाल करने से XYZ के कारण खतरनाक baz बन जाता है”
      थोड़ा समय और documentation space ज़्यादा लगे तो भी वह बेहतर है

  • इस लेख में जिन चीज़ों की ओर इशारा किया गया है, उनमें से कई, खासकर GNU coreutils source से तुलना करने पर, सामान्य unit test या manual review में ही पकड़ी जानी चाहिए थीं
    coreutils rewrite एक भयानक विचार लगता है
    https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
    और लगता है कि इसे ऐसे गलत ढंग से आगे बढ़ाया गया जिसमें पुराने software का संचित ज्ञान पर्याप्त रूप से साथ नहीं लाया गया

    rewrite करना हो तो पिछले संस्करण को पूरी तरह समझना और उससे सीखना ज़रूरी है
    नहीं तो वही गलतियाँ दोहराई जाती हैं, और ईमानदारी से कहूँ तो यह काफ़ी शर्मनाक है

    साफ़ कर दूँ कि मुझे Rust पसंद है, मैं इसे कई projects में इस्तेमाल करता हूँ, और यह शानदार है
    लेकिन Rust खराब engineering से नहीं बचा सकता

    • दिलचस्प बात यह है कि uutils GNU coreutils test suite का उपयोग करता है

      जोड़ दूँ कि उन्होंने यह रुख भी स्पष्ट कर रखा है कि GPL source पढ़कर लिखे गए contributions स्वीकार नहीं किए जाएँगे

    • unity, upstart, snap बनाने वालों से ऐसी चीज़ की उम्मीद की जा सकती है

    • नए systems programmers के लिए शायद स्वागत-संदेश यही होना चाहिए
      Unix टूटा हुआ है, और अंततः आपको खुद बदसूरत और गैर-शैक्षिक workarounds लिखने पड़ेंगे, और empirical testing भी करनी पड़ेगी
      भरोसेमंद software और अच्छी software engineering अक्सर ऐसे ही चलती है

  • समझ नहीं आ रहा कि differential fuzzing ऐसे bugs क्यों नहीं पकड़ पाया

    https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz

  • किसी path पर एक syscall से जाँच करना, और फिर उसी path पर दोबारा syscall करके काम करना — यह पैटर्न हमेशा वही समस्या बुलाता है
    parent directory पर write permission रखने वाला attacker बीच में path component को symbolic link से बदल सकता है, और kernel दूसरी call पर path को शुरू से फिर resolve करेगा, जिससे privileged operation attacker द्वारा चुने गए target पर जा गिरेगा

    • असल में यह इससे भी बदतर है
      parent directory पर write permission रखने वाला attacker hard link से भी खेल सकता है
      भले ही वह केवल regular files को छू सके, तब भी वास्तव में ढंग की mitigation लगभग नहीं है
      उदाहरण के लिए https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml देखें
    • हम्म… शायद directory पर write lock लगाने का कोई तरीका हो, लेकिन timeout जैसी समस्याएँ जुड़ते ही यह जल्दी और जटिल हो जाएगा
  • कुछ bugs की root cause शायद यह है कि Unix API बहुत अपारदर्शी है

    उदाहरण के लिए, get_user_by_name नए root filesystem के भीतर shared library लोड करके user name resolve करे, और इस कारण chroot के अंदर files रख सकने वाला attacker uid 0 के साथ code execute करा दे — यह लगभग booby trap जैसा लगता है

    user data लेने वाला function अचानक shared libraries भी लोड करने लगे, तो यह concerns के मिश्रण वाला design लगता है
    user data lookup और library loading को function स्तर पर अलग होना चाहिए, या कम-से-कम नाम से ही यह व्यवहार स्पष्ट होना चाहिए

    • कुछ मामलों में ऐसा हो सकता है, लेकिन अगर आपने coreutils को शुरुआत से दोबारा लिखने का फैसला किया है, तो POSIX API को समझना शब्दशः मुख्य काम का हिस्सा है

      और अगर यह जाँचने वाला code कि path filesystem root को इंगित करता है या नहीं, file == Path::new("/") था, तो वह API की समस्या नहीं है
      जिसने ऐसा लिखा, वह शायद इस project में भाग लेने के योग्य ही नहीं था

    • उल्टा, functional safe language का उपयोग कभी-कभी यह भ्रम दे सकता है कि आप जिस data को संभाल रहे हैं वह भी stateless है
      लेकिन operating system में बहुत कुछ लगातार बदलता रहता है

      snapshot देने वाले filesystems आने तक आपको हर चीज़ बार-बार दोबारा जाँचनी पड़ती है

      आखिरकार ज़रूरत ऐसी API की है जो input मिलने पर या तो successful result दे या failure
      ऐसी API नहीं जो success, failure, और error — तीन में से एक दे

    • सही, musl libc ने ठीक ऐसा ही एक हिस्सा हटा दिया है

    • मेरी नज़र में मूल कारण Unix API की अपारदर्शिता से ज़्यादा यह है कि root का ऐसे directory में chroot करना जिस पर उसका नियंत्रण नहीं है, इस स्थिति के बारे में ठीक से सोचा ही नहीं गया

      जिस चीज़ पर भी आप chroot करते हैं, वह उस chroot के target के नियंत्रण में होती है, और अगर यह बात समझ में नहीं आती तो आपको chroot() इस्तेमाल नहीं करना चाहिए

      get_user_by_name भले trap जैसा लगे, लेकिन वास्तव में newroot/etc/passwd का उपयोग करने और newroot/usr/lib/x86_64-linux-gnu/libnss_compat.so, newroot/bin/sh जैसी चीज़ों का उपयोग करने में व्यवहारिक अंतर बहुत कम है

      इसलिए मुझे नहीं लगता कि /usr/sbin/chroot को शुरू से user ID lookup करने की ज़रूरत ही थी
      toybox chroot ऐसा नहीं करता
      अंततः bug यह नहीं था कि काम गलत तरीके से किया गया, बल्कि यह कि वह काम शुरू से किया ही गया

    • Unix और POSIX फ्रैक्टल की तरह हैं — जहाँ से काटो, वहीं traps निकलते हैं

  • चाहे मान भी लें कि Rust वाले लोगों ने Linux अनुभव के बिना coreutils दोबारा लिख दिया, फिर भी समझ नहीं आता कि Ubuntu ने उसे mainline में कैसे स्वीकार कर लिया

    • Ubuntu शायद लगभग हर release में सिस्टम के किसी न किसी आधारभूत हिस्से को ढीले-ढाले और अधूरे प्रयोग से बदलने की नीति पर चलता है

      मेरे हिसाब से असली मुद्दा “अरे, Rust code में bug था” नहीं, बल्कि यही है

    • मूल संस्करण GPL license पर है, और rewrite MIT license पर

  • अगर यह सही है कि “ये bugs वास्तव में deployed Rust code से आए थे, और लिखने वाले लोग भी जानते थे कि वे क्या कर रहे हैं”,
    तो यह जानने की उत्सुकता है कि क्या मूल utility में test harness नहीं था, और rewrite ने भी शुरुआत उसी से नहीं की

    edge cases बहुत हों, फिर भी OS और FS को कुछ हद तक abstract करके यह जाँचना संभव नहीं होना चाहिए कि rm .// वास्तव में उम्मीद के मुताबिक current directory को delete नहीं करता?

    यह गंदे coding practice या language criticism से ज़्यादा फिर वही पुराना रवैया लगता है कि systems programming में testing नहीं की जाती

    और अगर मूल utility में tests थे लेकिन फिर भी इतनी खामियाँ रह गईं, तो शायद original test suite ही बहुत अपर्याप्त थी

    • शायद हाँ

      लेकिन इस बात पर मुझे उतना भरोसा नहीं कि OS और FS को पर्याप्त रूप से abstract करके सब verify किया जा सकता है
      लोग मेरे जन्म से पहले से ऐसी कोशिशें कर रहे हैं, लेकिन लगता है अभी तक सफल नहीं हुए

      उदाहरण के लिए, परीक्षण के लिए / कितनी बार जोड़ना है, यह तय करना ही अस्पष्ट है

      और आगे बढ़ें तो मान लीजिए rm उस फ़ाइल को delete करने से मना कर दे जिसके पहले 9 bytes important हों,
      तो अगर वह string पहले से मालूम न हो, ऐसी behavior पकड़ने वाला test कैसे सोचा जाएगा, यह कठिन है
      और अगर वह जादुई शब्द dictionary में मौजूद string भी न हो, तो यह और कठिन हो जाता है

      मैंने लगभग किसी को गंभीरता से यह कहते नहीं सुना कि “systems programming में testing नहीं की जाती”
      लेकिन यह ज़रूर अक्सर सुना है कि testing वह भूमिका हमेशा नहीं निभाती जिसकी लोग उससे उम्मीद करते हैं

    • मेरी समझ के अनुसार uutils के development में मूल utilities के साथ व्यापक behavioral comparison testing शामिल थी, और यहाँ तक कि bugs को भी preserve करने की कोशिश की गई थी

    • यही एक कारण है कि Windows डिफ़ॉल्ट रूप से symlink को disable रखता है
      यह abstraction से हल निकालने के बजाय feature को ही लगभग हटा देने जैसा तरीका है

      Unix-परिवार में दशकों से बहुत सारा software symlink पर निर्भर रहा है, इसलिए वहाँ ऐसा करना संभव नहीं

      MacOS भी कुछ-कुछ इसी तरह की प्रतिक्रिया देता है
      उदाहरण के लिए chroot() bug डिफ़ॉल्ट settings में व्यवहारिक समस्या कम बनता है, क्योंकि MacOS chroot() को डिफ़ॉल्ट रूप से रोकता है
      इसे इस्तेमाल करने के लिए system integrity protection बंद करनी पड़ती है

      मूल समस्या POSIX API के नुकीले किनारों में है, और समाधान उसे abstract करना नहीं बल्कि लगभग हटा देना है

  • मेरा मानना है कि लोगों का प्रयोग करना और अनाड़ी तरीके से कोशिश करना ठीक है
    आखिर सीखना और बढ़ना अक्सर ऐसे ही होता है

    मेरी असली जिज्ञासा यह है कि Ubuntu की decision-making chain कैसे विफल हुई कि ऐसी चीज़ production तक पहुँच गई

    • कभी-कभी growth का मतलब सिर्फ लंबा हो जाना भी होता है