वे बग जिन्हें Rust भी नहीं पकड़ पाता
(corrode.dev)- मेमोरी सुरक्षा में बड़ा सुधार होता है, लेकिन 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::fsAPI डिफ़ॉल्ट रूप से&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_newtarget location पर existing file के साथ-साथ dangling symlink को भी स्वीकार नहीं करता
- documentation के अनुसार
- अगर एक ही 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))?लिखा जाए, तो उनके बीचpathdefault 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-rootcheck केवल 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-35346lossy conversion की वजह से output खराब होने का मामला थाsrc/uu/comm/src/comm.rsमें input bytesra,rbकोString::from_utf8_lossyमें बदलकरprint!किया गया था- GNU
commbinary 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 का उपयोग करना चाहिए
- 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-35348NUL-separated filenames की सूची में non-UTF-8 filename मिलने पर रुक जाता था- parser हर name byte sequence पर
std::str::from_utf8(bytes).expect(...)चला रहा था - GNU
sortkernel की तरह filenames को raw bytes के रूप में संभालता है, लेकिन uutils UTF-8 को मजबूर कर पहली non-UTF-8 path पर पूरा process रोक देता था
- parser हर name byte sequence पर
- untrusted input संभालने वाले code में
unwrap,expect, indexing, औरascast को संभावित CVE की तरह देखना चाहिए?,get,checked_*,try_fromका उपयोग करें और असली error को caller तक पहुँचने दें
- CI में पकड़ने के लिए clippy नियम भी सुझाए गए
unwrap_usedexpect_usedpanicindexing_slicingarithmetic_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 गलत मान सकती थी कि पूरा काम बिना समस्या के पूरा हुआ
- पहले कई files पर failure होने के बावजूद अगर आखिरी file सफल हो जाए, तो command
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 में बदल सकती है
- GNU
- 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-35368chrootमें 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
chrootuser resolution कोchrootसे पहले करता है- fix में भी यही क्रम अपनाया गया
- एक बार trust boundary पार हो जाने के बाद, हर library call attacker code execute करा सकती है
- static linking भी इस समस्या को नहीं रोकती
- क्योंकि
get_user_by_nameNSS से होकर 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 लगातार दिए हैं
pwddeep path buffer overflownumfmtout-of-bounds readunexpand --tabsheap buffer overflowod --strings -Nheap buffer के बाहर NUL writesortheap buffer से पहले 1-byte readsplit --line-bytesheap overwrite वाला CVE-2024-0684b2sum --checkmalformed input में unallocated memory readtail -fstack 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की जगहOsStrunwrapकी जगह?- ज़्यादा साफ़ दिखने वाले semantics की जगह मूल implementation के साथ bug-for-bug compatibility
- type system बहुत कुछ व्यक्त कर सकता है, लेकिन दो syscall के बीच बीतने वाला समय जैसी नियंत्रण से बाहर की शर्तों को पूरी तरह नहीं समेट सकता
- idiomatic Rust में code के types, names, और control flow को runtime environment की सच्चाई दिखानी चाहिए
- whiteboard पर सुंदर दिखने वाले code से कम आकर्षक हो, तब भी अधिक ईमानदार रूप की ज़रूरत है
संदर्भ सामग्री
- An update on rust-coreutils: audit result का प्रकाशन
- Patterns for Defensive Programming in Rust: साथ पढ़े जा सकने वाले defensive Rust patterns
- Pitfalls of Safe Rust: safe Rust में भी होने वाली आम गलतियाँ
- Sharp Edges In The Rust Standard Library:
stdके अप्रत्याशित behaviors - uutils/coreutils on GitHub: Rust में दोबारा implemented GNU coreutils
1 टिप्पणियां
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 मेमोरी को फ़ाइल स्टोरेज के लिए बस ठीक से काम करने देना कैसा रहेगा, यह सोचता हूँ
libcI/O buffering,fflush, kernel buffering, multicore, time-sharing, और कई applications के समानांतर चलने जैसी जटिलताओं से बचे बिनामैं बिल्कुल शुरुआती हूँ, इसलिए सोच रहा था कि सीधे
$(yes a/ | head -n $((32 * 1024)) | tr -d '\n')सेcdक्यों नहीं किया गया औरwhileloop की ज़रूरत क्यों पड़ीसंपादन: समझ गया। वजह
-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 पढ़े बिना किया जा रहा हो, तो यह और भी कठिन हो जाता है
मेरी राय में अगर
uutilsGPL होता और 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 से नहीं बचा सकता
दिलचस्प बात यह है कि
uutilsGNU 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 देखें
कुछ 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 bytesimportantहों,तो अगर वह 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 में व्यवहारिक समस्या कम बनता है, क्योंकि MacOSchroot()को डिफ़ॉल्ट रूप से रोकता हैइसे इस्तेमाल करने के लिए system integrity protection बंद करनी पड़ती है
मूल समस्या POSIX API के नुकीले किनारों में है, और समाधान उसे abstract करना नहीं बल्कि लगभग हटा देना है
मेरा मानना है कि लोगों का प्रयोग करना और अनाड़ी तरीके से कोशिश करना ठीक है
आखिर सीखना और बढ़ना अक्सर ऐसे ही होता है
मेरी असली जिज्ञासा यह है कि Ubuntu की decision-making chain कैसे विफल हुई कि ऐसी चीज़ production तक पहुँच गई