2 पॉइंट द्वारा GN⁺ 2024-03-27 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Oxide के नेटवर्क स्विच firmware ने power sequencing बदलाव की testing के बाद boot नहीं किया, और कारण Hubris kernel के IPC memory lending check में एक bug था, जो नए memory layout तरीके से टकरा गया था
  • Hubris एक embedded operating system है जो MPU से tasks को isolate करता है, और IPC में किसी दूसरे task को memory lend करते समय kernel यह जांचता है कि वह memory सच में accessible region में है या नहीं
  • हाल में आया task packing कुछ firmware images में 30% RAM वापस दिला पाया, लेकिन पुराना check यह मानकर चलता था कि lend की गई memory एक ही MPU region के अंदर है, इसलिए वह fail हो गया
  • sequencer task ने I2C driver को 0x801bffd address वाली memory lend करने की कोशिश की, synthetic memory fault से मर गया, और humility tasks में 115 restarts और mem fault... in syscall status बचा रह गया
  • fix में check algorithm को इस तरह बदला गया कि adjacent multiple MPU regions के पार lending allow हो, और failure मिलने से kernel bug fix तक करीब 3 घंटे लगे

चालू न होने वाला नेटवर्क स्विच

  • Oxide के Arjen Roodselaar ने network switch firmware में power sequencing और clock setting बदलाव test करते हुए, मामूली दिखने वाले change के बाद switch के चालू न होने की समस्या देखी
  • firmware का कुछ हिस्सा queries का जवाब दे रहा था, लेकिन power supply sequencer संभालने वाला अहम हिस्सा रुका हुआ लग रहा था
  • power sequencing error hardware को सच में नुकसान पहुंचा सकता है, इसलिए पहले यह確認 करना जरूरी था कि switch मर गया है या बस respond नहीं कर रहा

Hubris और सीमित memory

  • Hubris keyboard internal controller जैसे deep embedded systems के लिए operating system है, और इसे Oxide Rack के बड़े processors को start करने के लिए जरूरी काम संभालने हेतु बनाया गया
  • Hubris-based firmware अलग-अलग compile किए गए कई programs यानी tasks से बना होता है
    • हर task अपने लिए जरूरी standard library code आदि खुद रखता है
    • tasks को hardware MPU से isolate किया जाता है ताकि वे एक-दूसरे को crash न कर सकें या memory corrupt न कर सकें
  • मुख्य तौर पर इस्तेमाल होने वाली ARM Cortex-M family की ARMv7-M में protected memory regions का size power of two होना चाहिए और उसी size के हिसाब से aligned होना चाहिए
    • उदाहरण के लिए, अगर 1024-byte region में 1 byte और चाहिए, तो वह 1025 bytes नहीं बल्कि 2048-byte region बन जाता है

task packing से बनी नई boundary

  • शुरुआती Hubris task RAM के लिए एक region और flash के लिए एक region इस्तेमाल करने का simple तरीका अपनाता था, लेकिन tasks के बीच unusable खाली जगह बनती थी और memory waste होती थी
  • Matt Keeter ने build system को बेहतर किया ताकि जहां संभव हो, कई power-of-two regions को combine करके tasks place किए जा सकें
    • hardware हर task के लिए अधिकतम 8 regions ही allow करता है
    • कुछ firmware images में 30% RAM reclaim हुई
    • सबसे छोटे devices, जो हर बार optimization मांगने जितने tight थे, उनमें spare space आ गई
  • इस बदलाव से task के flash और RAM के बीच unpredictable MPU region boundaries बन सकती थीं

humility tasks ने छोड़ा सुराग

  • Arjen ने Hubris debugger Humility से failed switch की जांच की, और power sequencing संभालने वाला service processor जिंदा और running था, इसलिए hardware issue की संभावना कम लगी
  • humility tasks output में sequencer task ने यह status दिखाया
mem fault (precise: 0x801bffd) in syscall (was: wait: reply from i2c_driver/gen0)
  • वही task 115 बार restart हो चुका था, और Hubris में लगभग हमेशा task crash के response में restart किया जाता है
  • status string का मतलब यह है
    • mem fault: memory handling rules का violation
    • precise: 0x801bffd: problem वाला specific address पता है
    • in syscall: task running नहीं था, बल्कि system call के बीच था
    • was: wait: reply from i2c_driver/gen0: I2C driver को भेजे message के response का इंतजार कर रहा था
  • gen0 का मतलब है कि i2c_driver अभी तक कभी crash नहीं हुआ था, और sequencer generation 115 पर था

Hubris IPC और memory lending

  • Hubris tasks IPC messages से communicate करते हैं, और messages function call की तरह काम करते हैं
    • message भेजने वाला task रुक जाता है
    • receiving task को CPU control मिल जाता है
    • result वापस आने पर sending task फिर जागता है
  • IPC को Rust के ownership model से अच्छी तरह fit होने के लिए design किया गया है, ताकि task अपनी memory का कुछ हिस्सा IPC message के साथ दूसरे task को lend कर सके
  • I2C device से interact करने वाला task अपनी memory range I2C bus driver को lend करता है, और driver उस range को in-place पढ़ता या लिखता है
    • bus driver को अलग buffer pool रखने की जरूरत घटती है
    • data copy की संख्या घटती है
  • गलत implementation security hole बन सकती है, इसलिए Hubris kernel task को ऐसी memory lend करने से रोकता है जिसे वह सच में own या access नहीं कर सकता
    • server को error code मिलता है
    • client को fault मिलता है और वह हमेशा terminate होता है
    • इसे bug, corruption या exploit की संभावना दिखाने वाले access violation की तरह treat किया जाता है

synthetic fault और असली कारण

  • Hubris faults को real fault और synthetic fault में बांटता है
    • real fault hardware rule violation है, जैसे null pointer dereference या code region में write करना
    • synthetic fault Hubris द्वारा जोड़े गए software rules का violation है, जैसे IPC या memory lending
  • sequencer का fault IPC के जरिए I2C driver को memory lend करने की प्रक्रिया में आया synthetic fault था
  • समस्या वाला address 0x801bffd valid flash address था, लेकिन power-of-two boundary से 3 bytes नीचे था, इसलिए pattern अजीब दिखा
  • humility mem output दिखाता है कि sequencer task के दो flash regions 0x801c000 पर सटे हुए हैं
LOW         HIGH           SIZE ATTR   ID TASK
0x08018000 - 0x0801bfff   16kiB r-x--- 17 sequencer
0x0801c000 - 0x0801dfff    8kiB r-x--- 17 sequencer
  • दोनों regions एक ही task के थे, इसलिए normal program execution में hardware MPU बिना दिक्कत access allow कर सकता था, लेकिन kernel के IPC memory lending check की assumption अलग थी

पुरानी simplification जहां bug बन गई

  • पुराना kernel check सिर्फ यह देखता था कि lend की जाने वाली पूरी memory slice task के single region के अंदर पूरी तरह आती है या नहीं
self.region_table().iter().any(|region| {
    region.covers(slice)
        && region.attributes.contains(desired)
        && !region.attributes.intersects(forbidden)
})
  • यह code लिखे जाने के समय के design यानी हर task के लिए एक RAM region और एक flash region वाली assumption से मेल खाता था
  • task packing आने के बाद एक ही task की memory adjacent multiple MPU regions में बंट सकती थी, और पुरानी assumption अब सही नहीं रही
  • normal memory access पर असर नहीं पड़ा क्योंकि hardware MPU उसे directly check करता है; problem सिर्फ तब दिखी जब उस memory को IPC से lend करने की कोशिश हुई

दो features ने मिलकर बनाया failure

  • task packing opportunistic तरीके से काम करता है
    • हर task के लिए maximum 8 regions की limit है
    • hardware driver tasks memory-mapped registers के कारण कुछ regions पहले से use करते हैं
    • extra region slots बचे हों तभी ज्यादा smart placement की कोशिश होती है
  • नतीजा यह हुआ कि region boundaries ऐसी जगह बनती हैं जिनका task author के लिए अनुमान लगाना मुश्किल है
  • task A के छोटे size change से unrelated task B की MPU region boundary की position बदल सकती है
  • सिर्फ debugging code जोड़ने से भी placement decision और region boundary बदल सकती थी, जिससे crash गायब हो सकता था
  • Matt ने तुरंत build system में task packing बंद कर दिया ताकि Arjen working firmware image बना सके, और साथ-साथ kernel bug analysis और fix चलता रहा

kernel fix का तरीका

  • fix का core यह था कि memory access check algorithm बदला जाए ताकि lend की जाने वाली memory ठीक-ठीक adjacent multiple MPU regions के पार भी allow हो
  • नया algorithm region table को सिर्फ एक बार scan करने के लिए design किया गया
    • Hubris ऐसे operations expose करने से बचता है जिनकी time complexity task control कर सके
    • performance lend की गई memory के size पर नहीं, बल्कि fixed-size region table पर ही depend करनी चाहिए थी
    • region table का size 8 पर fixed है
  • इसके लिए build system बदला गया ताकि task regions address ascending order में sort हों
regions.sort_by_key(|i| region_table.get_index(*i).unwrap().1.base);
  • fix commit ने kernel को इस sorting property का इस्तेमाल करके सस्ता access check करने लायक बनाया
  • ज्यादा complex code को Hubris kernel core से अलग करके ज्यादा portable crate में ले जाया गया, और अहम corner cases के लिए unit tests जोड़े गए
  • नए code से task packing फिर enable किया जा सका, और task developers के लिए unpredictable crashes नहीं बचे

failure ज्यादा फैला क्यों नहीं

  • पूरी कहानी network switch के चालू न होने से शुरू हुई और करीब 3 घंटे में kernel bug fix तक पहुंच गई
  • fault isolation की वजह से 23 isolated tasks वाले switch firmware में सिर्फ sequencer बार-बार मर रहा था, और बाकी कई components चलते रहे
    • firmware update system
    • management/control interfaces के लिए IP network stack
    • echo protocol implementation से लेकर rack control plane interface तक की कई network services
    • sensors, fans और दूसरे system status monitoring के लिए I2C, SMBus, PMBus
    • front panel के 32 QSFP 100G transceiver drivers
  • Hubris IPC इस assumption पर design है कि दूसरे tasks fail हो सकते हैं, इसलिए idempotent चिह्नित operations transparently retry किए जा सकते हैं
  • पुराना memory access check bug correct program के access को रोकने वाला था; गलत या malicious access allow करने वाला नहीं, इसलिए security impact नहीं था
  • जिस क्षण sequencer और I2C driver लगभग memory share कर रहे थे, sequencer मर गया, लेकिन I2C driver corruption risk के बिना चलता रहा

debugging infrastructure और team operations

  • Humility, Hubris kernel के साथ विकसित हुआ debugger है, और Arjen कुछ मिनटों में crashed code location को line number level तक identify कर सके और service processor के independent snapshots share कर सके
  • Hubris crashed tasks के compressed coredumps RAM में record करता है और उन्हें network के जरिए retrieve किया जा सकता है
    • writable persistent storage न होने पर भी crash dump मिल सकता है
    • crashdump feature kernel में नहीं, बल्कि अलग task में है
  • ये processors customer workload data handle नहीं करते और केवल system management traffic process करते हैं, और crash reports automatically upload नहीं होतीं
  • Hubris kernel के architecture-independent हिस्से का आकार 1,789 lines code और 1,192 lines comments है, और ARMv6-M, ARMv7-M, ARMv8-M support में 1,075 lines code और 534 lines comments और जुड़ते हैं
  • kernel concepts और IPC simple हैं, इसलिए fault के IPC की ओर इशारा करने पर check करने की जगहें ज्यादा नहीं थीं

1 टिप्पणियां

 
GN⁺ 2024-03-27
Hacker News की टिप्पणियाँ
  • Hubris वाकई बहुत बढ़िया है। मैंने लगभग 30 मिनट kernel code पढ़ा, और यह उन C codebases से बिल्कुल अलग लगा जिन्हें मैंने पहले देखा है—ifdef macro का ढेर, दो-अक्षर वाले variable names, और comments की कमी—इन सबसे दूर, यह बहुत साफ़ और स्पष्ट तरीके से लिखा गया है
    सोने से पहले पढ़ने के लिए भी अच्छा है, एक बार ज़रूर देखिए: https://github.com/oxidecomputer/hubris/blob/b44e677fb39cde8...

    • C culture का बड़ा हिस्सा मुझे इस बात पर सिमटा हुआ लगता है कि “ठीक-ठाक speed से typing करना सीखने में आलस”
      source code का disk space 40 साल से कोई बड़ी समस्या नहीं रहा, फिर भी variable names को लेकर लोग अभी भी कंजूसी करते हैं
    • AI शायद इस परंपरा को खत्म कर दे। अगर पुराने, खुरदरे C code को AI में डालो, तो अचानक सारे variables साफ-सुथरे हो सकते हैं और user की पसंद के हिसाब से नाम दिए जा सकते हैं
      क्योंकि AI ने किसी खास coder की पसंद और आदतों को ठीक से सीख लिया होगा
  • लेख अच्छा है, लेकिन नीचे वाली comment की जगह थोड़ी खटकती है
    regions.sort_by_key(|i| region_table.get_index(*i).unwrap().1.base); के ऊपर लिखा comment—“इसे address के बढ़ते क्रम में sort करना ज़रूरी है और kernel इसी property का इस्तेमाल करके access checks सस्ते में करता है”—इस function की implementation detail कम और ऐसा field invariant ज़्यादा है जिसे हर writer को मानना चाहिए और हर reader इस्तेमाल कर सकता है
    इसलिए इसे TaskDesc::regions की docstring में डालना ज़्यादा सही लगता है: https://github.com/oxidecomputer/hubris/commit/b44e677fb39cd...

    • फिर भी sort code के पास comment होना अच्छा है। नहीं तो वह sort खुद थोड़ा अप्रत्याशित लग सकता है
      शायद सबसे अच्छा तरीका यह होगा कि TaskDesc में एक constructor method बनाई जाए जो regions को sort करे, ताकि invariant enforce हो सके। code को समय के साथ और जटिल होते देख रहे हैं, इसलिए अब complexity को methods के अंदर समेटने में थोड़ा समय लगाना काफ़ी सार्थक लगता है
  • यह अब तक देखी गई सबसे बेहतरीन job postings में से एक है। culture की बातों में सहज रूप से जाना और अंत में “वैसे, हम hiring कर रहे हैं” जैसा flow बहुत अच्छा है
    यह सचमुच शानदार postmortem है, और मैं application-layer developer होते हुए भी इसे समझ पाया। वैसे भी मैं अभी Rust in Action पढ़ रहा हूँ, इसलिए इस तरह की चीज़ों के लिए थोड़ा तैयार था
    code पर ढेर सारे comments लिखने वाले लोगों को देखना हमेशा अच्छा लगता है। literate programming काम करती है

    • अफ़सोस, यह सिर्फ़ अमेरिका के लिए लागू है
  • लगता है पिछला भाग यहाँ मिल सकता है

    1. https://hachyderm.io/@mjk/112157472314396711
    2. https://www.mattkeeter.com/blog/2024-03-25-packing/
  • “टीम का घना non-hierarchical integration” वाला हिस्सा ध्यान खींचता है। यह Hubris की feature नहीं है, लेकिन Hubris और उसे बनाने वाली टीम को अलग करना मुश्किल है, और Oxide engineering team में लगभग कोई internal silo नहीं है—यह बात प्रभावशाली लगी
    मैं और सुनना चाहूँगा कि openness, curiosity, communication को बढ़ावा देने और defensiveness, empire-building, gatekeeping को रोकने वाली culture उन्होंने क्यों बनाई और उसे ठोस रूप में कैसे लागू किया। यह भी जानना चाहूँगा कि संगठन में ऐसी culture विकसित करने के नुकसान क्या हो सकते हैं
    कुछ जगहें ज़्यादा सख्त hierarchical system चुनती हैं, और org chart शायद रणनीतिक रूप से तय करना पड़ता है, इसलिए trade-off साफ़ समझ नहीं आता

    • घोषित values का मूल्यांकन करना अपने आप में कठिन है, लेकिन सामान्य तौर पर strongly defined structure न रखने वाले संगठनों की दिक्कत यह होती है कि फिर भी किसी न किसी तरह की power structure बन जाती है
      अगर वह structure explicitly defined न हो, तो वह कम पारदर्शी होती है, जानबूझकर चुनी हुई नहीं होती, और खासकर उन लोगों के लिए उसे समझना और मुश्किल होता है जो social interaction में बहुत सहज नहीं हैं। इसलिए उसकी यह छाया-जैसी प्रकृति अधिक pathological behavior को जगह दे सकती है, और बहुत बुरी न भी हो तो coordination को काफ़ी मुश्किल बना सकती है
      मैंने यह कई कंपनियों में देखा है। एक बड़ी consulting company में formal power structure तो थी, लेकिन व्यवहार में उसका ठीक से पालन नहीं होता था, और projects में जाने का तरीका official channels से ज़्यादा sales/management वालों से दोस्ती रखने जैसा था। अगर आप ज़रूरी social network बना सकते थे तो ठीक था, नहीं तो यह अच्छा नहीं चलता था
      इसी तरह का एक उदाहरण “The Tyranny of Structurelessness” है। यह एक feminist का व्याख्यान है जिसने उन संगठनों में वही चीज़ देखी जो hierarchy को patriarchy मानकर ठुकराते थे, और Valve जैसी internal structure में भी इस पर मिलती-जुलती चर्चा है जहाँ ढाँचा स्पष्ट नहीं है। open source projects भी इसी तरह की समस्या झेल सकते हैं, और Rust समुदाय के कुछ विवाद भी मुझे इसी तरह की समस्या से निकले लगते हैं
      इसका मतलब यह नहीं कि explicit power structure ज़रूर hierarchical ही हो। पारंपरिक business organizations hierarchical होती हैं, लेकिन Oxide की structure explicit होते हुए भी non-hierarchical हो सकती है। इस तरह की व्यवस्था आमतौर पर छोटे scale पर बेहतर चलती है, और ऊपर जिस consulting company का ज़िक्र किया, वह relatively free-form चलने वाली कंपनियों में मेरे लिए सबसे बड़ा उदाहरण था, लेकिन वहाँ भी कुछ हद तक सहारा देने वाला आधार मौजूद था
      यह binary नहीं बल्कि एक spectrum है। कागज़ पर सबसे rigid power structure के नीचे भी अधिक जटिल implicit structures होती हैं, और यही इंसानी समूहों की प्रकृति है
      मैं यह नहीं मानता कि explicit structure हमेशा implicit structure से बेहतर होती है। मैं सिर्फ़ कम explicit संगठनों में दिखने वाली दिक्कतों की बात कर रहा हूँ, और ज़्यादा explicit power structures की अपनी समस्याएँ होती हैं। इस संदर्भ में “seeing like a state” और legibility की समस्या भी है
  • यह जटिल समस्याओं को debug करने की प्रक्रिया को गहराई से दिखाने वाला शानदार लेख है। बाकी system का स्थिर बने रहना Oxide team की engineering quality को अच्छी तरह दिखाता है
    व्यक्तिगत रूप से भी यह काफ़ी प्रेरक लगा, और मैं ऐसे ही techniques को अपने रोज़मर्रा के काम में अपनाने की सोच रहा हूँ

  • अगर उस hardware को software-filled TLB की तरह treat करें, तो 8 से ज़्यादा regions भी support किए जा सकते हैं

    • शायद वे (a) soft real-time performance चाहते थे, और (b) ऐसा कोई core mechanism नहीं डालना चाहते थे जो debuggability या reliability में बाधा डाल सके
      आख़िरी विकल्प न हो तो वे शायद यह कभी न करते। virtual paging गंदी चीज़ है, और वे किसी तरह का संदेह नहीं छोड़ना चाहते होंगे
    • मुझे पता है कि TLB का मतलब translation lookaside buffer है, लेकिन यहाँ “soft fill” से क्या मतलब है?
  • Oxide जो कर रही है, वह सचमुच चौंकाने वाला है

    • Tailscale के बाद अब Oxide भी उन projects की प्यारी कंपनी बन गई है जिनकी 99% लोगों को ज़रूरत नहीं होती
  • Oxide के लोग जो भी करते हैं, मुझे पसंद आता है, और यह भी उनमें से एक है

  • OS का नाम Hubris रखा? अरे, वह तो… क्या कहूँ

    • यह जानकर खुशी होगी कि debugger का नाम “humility” है: https://github.com/oxidecomputer/humility
    • ठीक-ठीक कहें तो Brian Cantrill ने OS का नाम hubris रखा
      आजकल कौन समझदार व्यक्ति नया operating system इस्तेमाल करेगा? जवाब है: वही लोग जो उस समस्या को हल करना चाहते हैं जिसे हर OS नज़रअंदाज़ करता है—motherboard और expansion cards के controllers की समस्या, जिन्हें OS नियंत्रित नहीं करता और कर भी नहीं सकता
    • ब्रांड के हिसाब से तो यह काफ़ी फिट बैठता है