1 पॉइंट द्वारा GN⁺ 2024-04-28 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 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 String argument लेता है, तो caller द्वारा bool pass करना compiler रोक देता है
    • pet_cat call करने के बजाय गलती से fire_missiles call करने जैसी target confusion भी आमतौर पर नहीं होती
  • 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 होता है
  • 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 से
  • उदाहरण के लिए SEND call में 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 करता है
  • सामान्य REPLY flow इस प्रकार है
    • client SEND इस्तेमाल करता है, तो kernel client task को receiving task के लिए “waiting to send” state में mark करता है
    • receiving task RECV इस्तेमाल करता है, तो वह client “waiting for reply” state में चला जाता है
    • server REPLY call करता है, तो client runnable state में वापस आ जाता है
  • 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-process panic! करने का माध्यम देता है
  • 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_FAULT server को clients को randomly kill करके chaos मजबूर करने का तरीका भी देता है, लेकिन इस option का अभी पूरी तरह evaluation नहीं हुआ है
  • सामान्य Hubris task जानबूझकर गलत IPC messages dynamically generate नहीं करते, इसलिए वे आमतौर पर REPLY_FAULT की मौजूदगी से अनजान रहते हुए चल सकते हैं

1 टिप्पणियां

 
GN⁺ 2024-04-28
Hacker News की टिप्पणियां
  • REPLY_FAULT तब अच्छा लगता है जब सिस्टम छोटा और कसकर बंधा हो, और एप्लिकेशन भी मुख्य रूप से वही लोग लिख रहे हों जिन्होंने पूरा सिस्टम डिजाइन किया है
    लेकिन एक एप्लिकेशन डेवलपर के नजरिए से, ऐसे IPC मॉडल में third-party code से जुड़ना काफी डरावना लगेगा जहां कोई दूसरी service कभी भी मेरे process को instant-death pill लौटा सकती है
    मैं दूसरे एप्लिकेशन डेवलपर्स पर इतना भरोसा नहीं करता। दुनिया खराब drivers और managers के दबाव में काम कर रहे डेवलपर्स द्वारा बनाए गए background processes से भरी है, और अगर वे 8 बजे से पहले घर जा सकें तो शायद वे ढेर सारे default REPLY_FAULT डाल देंगे, भले ही वे inappropriate हों

    • यह intended design लगता है, और Hubris जिस environment को target करता है वह ठीक इसी तरह का है
    • असल में Symbian में ऐसा हुआ था। IPC server client को panic करा सकता था, और जिन एप्लिकेशन डेवलपर्स के पास OS source code तक access नहीं था, उनके लिए यह काफी भयावह था
      सभी preconditions को आसानी से समझना भी संभव नहीं था, और वे device या OS version के हिसाब से बदल भी सकते थे
    • deviance को जल्दी मार देना सिस्टम को tight बनाए रखने का तरीका है। जिस scope के लिए इसे design किया गया है, वही शायद इसे छोटा बनाए रखेगा
      scope तो बढ़ता ही है, लेकिन मुझे नहीं लगता कि कोई ऐसे काम, जिन्हें host पर handle करना बेहतर है, उन्हें जबरन embedded controller के अंदर Hubris task में धकेलना चाहेगा
    • embedded environment में, ऐसी गलतफहमी चाहे जिसकी भी जिम्मेदारी हो, उसे होते ही resolve करना बेहतर लगता है
      अगर server कहता है “वह client गलत है”, तो kernel उस client को kill कर देता है। मुख्य बात यह है कि दोनों एक-दूसरे को समझ नहीं पाए
    • यहां service को OS interface माना जा सकता है। single kernel में गलत kernel call करने पर OS का उस process को मार देना भी reasonable है
      और जब हम “process” कहते हैं, तो यह आपकी कल्पना से अलग हो सकता है। Hubris में threads सभी एक ही address space share करते हैं
  • क्या REPLY_FAULT chain होता है? उदाहरण के लिए, A, B को SEND करके wait कर रहा है, और B, C को SEND करके wait कर रहा है; अगर C REPLY_FAULT करता है, तो क्या A भी B के साथ मर जाता है?
    अगर नहीं, तो कोई malicious task बस प्रयोगों को helper task को delegate कर देगा। उल्टा अगर ऐसा है, तो कुल मिलाकर यह काफी fragile लगता है, हालांकि मैं Hubris को ज्यादा अच्छी तरह नहीं जानता
    इसके अलावा, अगर SEND cyclic या mutual हो सकता है, तो कोई task गलती से खुद को भी मार सकता है। B → A → B जैसे case में यह REPLY_FAULT का इस्तेमाल न करने की incentive भी बन सकता है

    • Hubris लगता नहीं कि general-purpose operating system के रूप में design किया गया है। processes build time पर define होते हैं
      server client पर पलटवार कर सकता है, इसका कारण security नहीं बल्कि reliability है। मानकर चला जाता है कि errors intentional attack से नहीं, bugs से आते हैं, और kernel की extreme response डेवलपर को समस्या जितनी जल्दी हो सके खोजने में मदद करती है
      बेशक, security से overlap वाले हिस्से हैं, और जब process कुछ ऐसा करने की कोशिश करता है जो उसे नहीं करना चाहिए, तब यह useful fallback defense हो सकता है
    • अगर B fault हो जाए, तो A को शायद यह error मिलेगा कि server मर गया है, और उसे newly restarted server को वही message दोबारा भेजने का मौका मिलेगा। यह chain crash नहीं लगता
  • 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 किया जा सकता है

    • expected errors, जैसे file not found, और unexpected errors, जैसे invalid opcode, के बीच अच्छी सीमा खींची जाए तो सामान्य programs में भी reusability बहुत खराब नहीं होगी
      बल्कि 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 में हल्का बदलाव करने का तरीका नहीं बचता, है न?

    • Hubris कोई general-purpose OS नहीं है, बल्कि Oxide server rack के अंदर low-level processors पर चलता है
      मेरी समझ से यह 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 की शुद्धता के लिए तालियां

    • Hubris कोई academic experiment नहीं है। यह Oxide rack के सभी core elements—compute sled, switch, power shelf controller—के केंद्र में चलता है, और इसका design सबसे बढ़कर वास्तविक delivered utility पर आधारित है
      दरअसल, जैसा 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/
    • watchdog timer ऐसे process को खुशी-खुशी kill या restart कर देता है जो उसे नियमित रूप से poke नहीं करता
      hobby projects में भी मैंने देखा है कि जब I2C bus का एक protocol bit गड़बड़ा जाता है, तो वह अक्सर hang होकर पूरे system को down कर देती है, इसलिए मुझे यह design काफ़ी प्रेरक लगता है
      मेरी समझ के मुताबिक यह पहले से ज्ञात error cases, यानी handled errors, की बात नहीं है, बल्कि protocol mismatch और उन चीज़ों की बात है जो कभी होनी ही नहीं चाहिए
      दूसरे comments ने भी बताया है कि यह purpose-built OS है। जैसे Erlang से UI नहीं बनाएंगे, वैसे ही Hubris भी अपने कब्ज़े वाली जगह में अच्छी तरह fit दिखता है
    • मुझे लगता है यह idea उन समस्याओं पर लागू करने के लिए है जो स्पष्ट रूप से गलत program state का नतीजा हैं। इसलिए उनसे reasonable तरीके से recover नहीं किया जा सकता
      वजह bug, attack, या damaged hardware में से कोई हो सकती है, और किसी भी case में आगे बढ़ना नहीं चाहिए। caller में गंभीर समस्या है और जारी रखने से सिर्फ़ ज़्यादा नुकसान होगा
      यह Erlang/OTP की “let it crash” philosophy से थोड़ा मिलता-जुलता लगता है। Erlang काफ़ी mission-critical hardware में इस्तेमाल होता है और reliability के लिए जाना जाता है, इसलिए व्यवहार में यह इतना बड़ा दोष शायद न हो
    • यह runtime पर नए tasks जोड़ने को support न करने वाला 2000-line Rust embedded system kernel है
      इसे 0xide server rack के गहरे अंदर चलने के लिए लिखा गया है
  • “exploitation attempts अक्सर पहले API errors या misuse के रूप में सामने आते हैं, इसलिए किसी भी misbehavior पर गलत व्यवहार करने वाले component की state मिटा देने वाला system exploit करना ज़्यादा कठिन होना चाहिए” वाले हिस्से में, यहां application जो accept करती है उसकी थोड़ी ज़्यादा सख्त जांच की जा रही है
    इसलिए security benefit है, लेकिन उस तरह का नहीं जैसा आप सोच रहे हैं। यह attacker की progress को नष्ट करके उसे पीछे नहीं धकेलता, बल्कि कुछ ऐसे गलत states को अब काम नहीं करने देता जिन्हें पहले ज़्यादा desired wrong state में जोड़कर आगे बढ़ाया जा सकता था
    तब attacker उसे आज़माने के बजाय कहीं और देखेगा