- Hubris एक ऐसा OS है जिसमें अलग-थलग tasks IPC के जरिए communicate करते हैं, और इसका 13वां system call
REPLY_FAULTसर्वर को गलत client request को error value के बजाय fault के साथ खत्म करने देता है - client के नज़रिए से IPC function call जैसा दिखता है, लेकिन tasks अलग-अलग compile होते हैं, इसलिए गलत operation code, समझ में न आने वाले bytes, या अनुचित loaned memory को compiler पूरी तरह नहीं रोक पाता
- सामान्य Hubris program में build configuration और generated Rust code की वजह से ऐसी errors शायद ही मिलती हैं; इसलिए हर call पर
Result<T, IpcError>औरunwrap()को मजबूर करने से code size और runtime cost बढ़ती है - kernel system call की preconditions तोड़ने वाले task को error code के बिना तुरंत kill कर देता है, और
REPLY_FAULTइसी fail-fast policy को server response तक बढ़ाता है - यह design गलत API usage को जल्दी सामने लाता है, लेकिन random IPC और system calls भेजने वाले fuzz test या chaos tasks लगभग तुरंत restart हो जाते हैं, जिससे testing मुश्किल हो जाती है
Hubris IPC और REPLY_FAULT की स्थिति
- Hubris में एक छोटा application-independent kernel होता है, और drivers, application logic, network stack जैसे अधिकतर code को अलग compile किए गए isolated tasks में रखा जाता है
- tasks के बीच communication kernel द्वारा implement किए गए IPC system calls से होता है
RECV: सबसे high-priority receiving message लेता है या message आने तक block रहता हैSEND: caller को रोकता है, message और control receiving task को देता है, फिर response मिलने तक wait करता हैREPLY: पहलेSENDकर चुके task को response देकर उसे फिर से run करने देता है
- Hubris में client और server fixed identity नहीं, बल्कि task द्वारा निभाई जाने वाली roles हैं
SENDइस्तेमाल करने वाला task client role में होता हैRECVऔरREPLYइस्तेमाल करने वाला task server role में होता है- एक task किसी task के लिए server और किसी दूसरे task के लिए client हो सकता है
Task boundaries पर compiler से छूट जाने वाली errors
- सामान्य function call में compiler और linker types और call target की काफी हद तक guarantee देते हैं
- अगर कोई Rust function
Stringargument लेता है, तो caller द्वाराboolpass करना compiler रोक देता है pet_catcall करने के बजाय गलती सेfire_missilescall करने जैसी target confusion भी आमतौर पर नहीं होती
- अगर कोई Rust function
- Hubris IPC task boundaries पार करता है और हर task अलग program के रूप में compile होता है, इसलिए compiler पूरे IPC relation को सीधे verify नहीं कर पाता
- IPC server को मुख्य रूप से तीन तरह की errors मिल सकती हैं
- interface से मेल न खाने वाला operation code, जैसे दो operations वाले interface में “operation number 48” आ जाना
- expected message type के बजाय समझ में न आने वाले bytes का bunch आना, या message का बहुत छोटा या लंबा होना
- जरूरी loaned memory का न होना, या writable memory चाहिए लेकिन read-only memory मिलना
सामान्य programs पर error handling मजबूर न करने की वजह
- सामान्य Hubris programs ऐसे configured होते हैं कि ये IPC errors पैदा न हों
- task connections build system settings से configured होते हैं, इसलिए एक-दूसरे को confuse करना मुश्किल होता है
- client generated Rust code से IPC बनाता और भेजता है
- server भी अलग generated Rust code से result handle करता है
- अगर हर IPC operation को
Result<T, IpcError>return करवाया जाए, तो सामान्य program को ऐसी errors के लिएunwrap()डालना पड़ेगा जिनसे वह असल में मिल ही नहीं सकताunwrap()code size के लिहाज से भारी पड़ता है- runtime में भी ऐसी error check करने की cost आती है जो होगी ही नहीं
- generated code के अंदर
unwrap()याpanic!डालने से panic location centralize होकर code size impact कम हो सकता है, लेकिन runtime cost बनी रहती है - universal error code support करने के लिए हर operation को समान error encoding rules follow करने होंगे
- हर operation error return कर सकना चाहिए
- हर operation को उस error को उसी तरीके से encode करना होगा
- जो operations fail नहीं हो सकते, उन्हें भी fail-able form में represent करना होगा
- Hubris-based firmware में वास्तव में fail न हो सकने वाले operations लगातार मिले हैं, और GPIO pin configuration इसका एक उदाहरण है
Hubris kernel की aggressive fault policy
- कई operating systems system call preconditions टूटने पर भी error code return करते हैं या exception/signal handling का मौका देते हैं
- Unix में खुले नहीं हुए file descriptor को
closeकरने पर error code return होता है openको pathname के बजाय null pointer देने पर भी error code return होता है
- Unix में खुले नहीं हुए file descriptor को
- Hubris system call preconditions टूटते ही उस task को तुरंत destroy कर देता है
- task अब कोई instruction execute नहीं कर सकता
- task को खुद recover या resume करने का मौका नहीं मिलता
- application का supervisor task fault की notification पाता है, और आमतौर पर task को हटाकर restart करता है
- kernel द्वारा बनाया गया fault synthetic fault होता है
- यह null pointer dereference या division by zero जैसे CPU द्वारा बनाए जाने वाले hardware fault जैसा है
- hardware fault processor architecture rules के violation से आता है, और synthetic fault kernel rules के violation से
- उदाहरण के लिए
SENDcall में receiving task index application range से बाहर हो, या message pointer ऐसी memory की ओर point करे जिस पर access permission नहीं है, तो synthetic fault होता है - Hubris recoverable या resumable faults की अनुमति नहीं देता
- hardware fault हो या synthetic fault, fault पाने वाला task dead state में चला जाता है
- यह choice subtle failure modes से बचने और system reasoning को सरल बनाने के लिए है
Server द्वारा client को fault से respond करने का तरीका
REPLY_FAULTएक system call है जिसके जरिए server client को normal response के बजाय fault deliver करता है- सामान्य
REPLYflow इस प्रकार है- client
SENDइस्तेमाल करता है, तो kernel client task को receiving task के लिए “waiting to send” state में mark करता है - receiving task
RECVइस्तेमाल करता है, तो वह client “waiting for reply” state में चला जाता है - server
REPLYcall करता है, तो client runnable state में वापस आ जाता है
- client
REPLY_FAULT,REPLYजैसा है, लेकिन message deliver कर runnable state में लाने के बजाय fault deliver करके task को dead state में डाल देता है- server किसी भी arbitrary task को kill नहीं कर सकता
REPLY_FAULTकेवल उस task पर इस्तेमाल हो सकता है जिसे उस server नेRECVकिया है और अभी तकREPLYनहीं किया है- यह केवल उस client पर काम करता है जो किसी specific server के response का इंतज़ार कर रहा है
- Hubris
REPLY_FAULTका इस्तेमाल इन errors को handle करने में करता है- गलत operation code
- corrupt, truncated या meaningless message
- client द्वारा सही तरह की loaned memory न भेजना
Application errors और fail-fast experience
REPLY_FAULTको IPC format errors के अलावा application-specific errors के लिए भी इस्तेमाल किया जा सकता है- Hubris IP stack IP ports को tasks को statically assign करता है
- अगर कोई task किसी दूसरे task के IP port को छेड़ने की कोशिश करता है, तो IP stack उस task को fault देता है
- यह तरीका उन “theoretical” errors की handling घटाता है जो असल में नहीं होनी चाहिए, और गलत usage को development के दौरान जल्दी सामने लाता है
REPLY_FAULTउस model जैसा है जिसमें Rust function call precondition violation पर आमतौर परpanic!होता है; यानी यह server को client process पर cross-processpanic!करने का माध्यम देता है- client को इसके लिए code शामिल करने या cooperate करने की जरूरत नहीं होती
Security tendency और testing constraints
- Eliza Weissman ने Hubris को “malicious programs के प्रति aggressively hostile” बताया है
- exploitation attempts अक्सर पहले API errors या misuse के रूप में दिखते हैं, इसलिए misbehaving component की state मिटाने वाला system exploit करना अधिक कठिन हो सकता है
- यह hypothesis अभी test नहीं किया गया है
- Hubris exploit attempts में रुचि हो तो संपर्क करने का अनुरोध शामिल है
- देखा गया नुकसान यह है कि system पर fuzz test करना बहुत मुश्किल है
- random IPC और system calls generate करने वाला छोटा chaos task implement किया गया था, लेकिन वह लगभग कुछ भी करते ही तुरंत reset हो जाता है
- उपयोगी तरीके से काम करने के लिए उसे हर start पर observably बदलने वाले system uptime counter पर अपने decisions निर्भर करने होंगे
REPLY_FAULTserver को clients को randomly kill करके chaos मजबूर करने का तरीका भी देता है, लेकिन इस option का अभी पूरी तरह evaluation नहीं हुआ है- सामान्य Hubris task जानबूझकर गलत IPC messages dynamically generate नहीं करते, इसलिए वे आमतौर पर
REPLY_FAULTकी मौजूदगी से अनजान रहते हुए चल सकते हैं
1 टिप्पणियां
Hacker News की टिप्पणियां
REPLY_FAULT तब अच्छा लगता है जब सिस्टम छोटा और कसकर बंधा हो, और एप्लिकेशन भी मुख्य रूप से वही लोग लिख रहे हों जिन्होंने पूरा सिस्टम डिजाइन किया है
लेकिन एक एप्लिकेशन डेवलपर के नजरिए से, ऐसे IPC मॉडल में third-party code से जुड़ना काफी डरावना लगेगा जहां कोई दूसरी service कभी भी मेरे process को instant-death pill लौटा सकती है
मैं दूसरे एप्लिकेशन डेवलपर्स पर इतना भरोसा नहीं करता। दुनिया खराब drivers और managers के दबाव में काम कर रहे डेवलपर्स द्वारा बनाए गए background processes से भरी है, और अगर वे 8 बजे से पहले घर जा सकें तो शायद वे ढेर सारे default REPLY_FAULT डाल देंगे, भले ही वे inappropriate हों
सभी preconditions को आसानी से समझना भी संभव नहीं था, और वे device या OS version के हिसाब से बदल भी सकते थे
scope तो बढ़ता ही है, लेकिन मुझे नहीं लगता कि कोई ऐसे काम, जिन्हें host पर handle करना बेहतर है, उन्हें जबरन embedded controller के अंदर Hubris task में धकेलना चाहेगा
अगर server कहता है “वह client गलत है”, तो kernel उस client को kill कर देता है। मुख्य बात यह है कि दोनों एक-दूसरे को समझ नहीं पाए
और जब हम “process” कहते हैं, तो यह आपकी कल्पना से अलग हो सकता है। Hubris में threads सभी एक ही address space share करते हैं
क्या REPLY_FAULT chain होता है? उदाहरण के लिए, A, B को
SENDकरके wait कर रहा है, और B, C कोSENDकरके wait कर रहा है; अगर CREPLY_FAULTकरता है, तो क्या A भी B के साथ मर जाता है?अगर नहीं, तो कोई malicious task बस प्रयोगों को helper task को delegate कर देगा। उल्टा अगर ऐसा है, तो कुल मिलाकर यह काफी fragile लगता है, हालांकि मैं Hubris को ज्यादा अच्छी तरह नहीं जानता
इसके अलावा, अगर
SENDcyclic या mutual हो सकता है, तो कोई task गलती से खुद को भी मार सकता है। B → A → B जैसे case में यह REPLY_FAULT का इस्तेमाल न करने की incentive भी बन सकता हैserver client पर पलटवार कर सकता है, इसका कारण security नहीं बल्कि reliability है। मानकर चला जाता है कि errors intentional attack से नहीं, bugs से आते हैं, और kernel की extreme response डेवलपर को समस्या जितनी जल्दी हो सके खोजने में मदद करती है
बेशक, security से overlap वाले हिस्से हैं, और जब process कुछ ऐसा करने की कोशिश करता है जो उसे नहीं करना चाहिए, तब यह useful fallback defense हो सकता है
Hubris और इसका debugger Humility ऐसी technologies हैं जिनमें समय हो या कोई mission हो तो मैं गहराई से उतरना चाहूंगा। अफसोस, अभी यह संभव नहीं है
ऐसे सिस्टम में जहां एक ही team सारा code लिखती है, यह interesting है कि client ने अजीब तरह से देखा भर तो उसे orbit से उड़ा देने वाला तरीका iterative development की speed बढ़ा सकता है
algebraic effects पढ़ते-पढ़ते सो जाने के बाद सुबह यह लेख पढ़ना मजेदार लगा। थोड़ा twist करके देखें तो यह ऐसा kernel है जो server को ऐसे effects perform करने देता है जिन्हें client handle नहीं कर सकता
code reuse और composition शायद कहीं ज्यादा मुश्किल हो जाएंगे, लेकिन execution model कहीं ज्यादा simple हो जाता है। static embedded systems में यह निश्चित रूप से सही trade-off है। reuse चाहिए तो task को कभी भी vendor करके modify किया जा सकता है
बल्कि Unix में ignorable errors बहुत ज्यादा हैं, और व्यक्तिगत रूप से मुझे लगता है कि उनमें से काफी को fatal signal generate करना चाहिए था। इससे overall software quality काफी बेहतर होती
उदाहरण के लिए invalid file descriptor पर
close()call करना non-fatal error है, इसलिए अक्सर ignore कर दिया जाता है। लेकिन असल में यह खासकर multithreaded apps में बहुत खतरनाक है। ज्यादातर बार गलत file descriptor बंद करना harmless तरीके से fail होता है, लेकिन 1% मामलों में यह logging socket, database lock file, या unrelated IPC connection को बंद कर देता है। इसी तरह वह unstable software बनता है जिससे सब नफरत करते हैंErrand of Mercy का dialogue याद आता है: “तुम्हें पता चलेगा कि कई rules और regulations हैं। उन्हें post किया जाएगा। उनमें से सबसे छोटे का भी उल्लंघन मौत की सजा से दंडित होगा”
इसे HTTP के लिए April Fools' RFC बनाया जाना चाहिए
मैं HTTP 499 “Shame on you.” propose करता हूं। 499 पाने वाले client को, शायद केवल उन requests के लिए जो
Strict: trueजैसे किसी specific header से शुरू हुई हों, उस request को जारी करने वाले task को language-specific तरीके से terminate करना चाहिएइस context में दिखने वाले “ये क्या है… लेकिन असल में, ठीक है?” वाले balance को यह बिल्कुल perfect तरीके से पकड़ता है
बहुत मजे से पढ़ा, और यह single supervisor approach पुराने startup में हमारे applications को सब कुछ unwrap करने के लिए configure करने के तरीके जैसी है
इसने मेरी पसंदीदा posts में से एक https://medium.com/@mattklein123/crash-early-and-crash-often... की भी याद दिलाई
मुझे सच में उत्सुकता है कि क्या यह बहुत ज़्यादा आक्रामक है
Linux में सिर्फ़ socket के ज़रिए communicate कर रहे किसी दूसरे program को सीधे crash कराना संभव नहीं है, गलत data socket पर भेजने के मामले को छोड़कर
लेकिन उसे kill करना निश्चित रूप से संभव है। root के रूप में चल रहा कोई भी process किसी और को kill कर सकता है, और reboot करके पूरे system को भी down कर सकता है
यह थोड़ा ज़्यादा मुश्किल और कम आम है, लेकिन कम से कम containers में root privileges आम हैं। बेशक cgroup होते हैं, इसलिए यह और सीमित हो जाता है, पर मुख्य बात यही है
यह “receive करते समय उदार रहें, send करते समय conservative रहें” वाली आम समझ से भी थोड़ा अलग है। हालांकि वह बात शायद network systems से ज़्यादा जुड़ी हो सकती है
फिर भी शायद system को जो मिलता है उसे स्वीकार करने में उदार होना ही पड़ता है। वरना मौजूदा programs को तोड़े बिना API में हल्का बदलाव करने का तरीका नहीं बचता, है न?
मेरी समझ से यह runtime पर नए प्रकार के processes की अनुमति भी नहीं देता। चल सकने वाली सभी executable files compile time पर ही तय होनी चाहिए
“समस्या को ठीक करके task को resume करने का कोई तरीका नहीं है। यह subtle failure modes से बचने और system reasoning को सरल बनाने के लिए लिया गया सचेत निर्णय था” वाले हिस्से पर Einstein की मशहूर बात याद आती है: “जितना संभव हो उतना सरल, लेकिन उससे ज़्यादा सरल नहीं”
यह design उस दूसरे condition का उल्लंघन करता लगता है। मुझे ऐसे operating environment में रुचि नहीं है जो real world की अव्यवस्था को बिल्कुल सहन न कर सके, और commercially viable क्षेत्रों में भी समझ नहीं आता कि ऐसी चीज़ कौन अपनाएगा
तो आखिरकार init system पर वापस जाकर बार-बार retry करवाने की बात है? लेकिन कौन-सा mechanism हुए fault को समझकर बेहतर तरीके से फिर कोशिश कर सकता है?
खैर, beliefs की शुद्धता के लिए तालियां
दरअसल, जैसा Cliff ने blog में विस्तार से लिखा है, REPLY_FAULT शुरू में ऐसा feature था जिसके बारे में लगा था कि शायद यह बहुत aggressive हो, लेकिन system बनाने, deploy करने और सच कहें तो debug करने के अनुभव ने हमें भरोसा दिया कि यह हमारे system को मनमाने ढंग से खराब नहीं करेगा, बल्कि उसे ज़्यादा robust बनाएगा
यहां की सोच और वास्तविक रूप [0] और [1] में और देखा जा सकता है
[0] https://www.mattkeeter.com/blog/2024-03-25-packing/
[1] https://cliffle.com/blog/who-killed-the-network-switch/
hobby projects में भी मैंने देखा है कि जब I2C bus का एक protocol bit गड़बड़ा जाता है, तो वह अक्सर hang होकर पूरे system को down कर देती है, इसलिए मुझे यह design काफ़ी प्रेरक लगता है
मेरी समझ के मुताबिक यह पहले से ज्ञात error cases, यानी handled errors, की बात नहीं है, बल्कि protocol mismatch और उन चीज़ों की बात है जो कभी होनी ही नहीं चाहिए
दूसरे comments ने भी बताया है कि यह purpose-built OS है। जैसे Erlang से UI नहीं बनाएंगे, वैसे ही Hubris भी अपने कब्ज़े वाली जगह में अच्छी तरह fit दिखता है
वजह bug, attack, या damaged hardware में से कोई हो सकती है, और किसी भी case में आगे बढ़ना नहीं चाहिए। caller में गंभीर समस्या है और जारी रखने से सिर्फ़ ज़्यादा नुकसान होगा
यह Erlang/OTP की “let it crash” philosophy से थोड़ा मिलता-जुलता लगता है। Erlang काफ़ी mission-critical hardware में इस्तेमाल होता है और reliability के लिए जाना जाता है, इसलिए व्यवहार में यह इतना बड़ा दोष शायद न हो
इसे 0xide server rack के गहरे अंदर चलने के लिए लिखा गया है
“exploitation attempts अक्सर पहले API errors या misuse के रूप में सामने आते हैं, इसलिए किसी भी misbehavior पर गलत व्यवहार करने वाले component की state मिटा देने वाला system exploit करना ज़्यादा कठिन होना चाहिए” वाले हिस्से में, यहां application जो accept करती है उसकी थोड़ी ज़्यादा सख्त जांच की जा रही है
इसलिए security benefit है, लेकिन उस तरह का नहीं जैसा आप सोच रहे हैं। यह attacker की progress को नष्ट करके उसे पीछे नहीं धकेलता, बल्कि कुछ ऐसे गलत states को अब काम नहीं करने देता जिन्हें पहले ज़्यादा desired wrong state में जोड़कर आगे बढ़ाया जा सकता था
तब attacker उसे आज़माने के बजाय कहीं और देखेगा