1 คะแนน โดย GN⁺ 2024-04-28 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Hubris เป็น OS ที่มีงาน (task) แยกขาดจากกันและสื่อสารกันผ่าน IPC โดย system call ตัวที่ 13 คือ REPLY_FAULT ทำให้เซิร์ฟเวอร์สามารถจบคำขอที่ผิดพลาดจากไคลเอนต์ด้วย fault แทนค่าข้อผิดพลาดได้
  • จากมุมมองไคลเอนต์ IPC ดูเหมือนการเรียกฟังก์ชัน แต่เพราะ task ถูก คอมไพล์แยกกัน คอมไพเลอร์จึงไม่สามารถกัน opcode ที่ผิดพลาด, ไบต์ที่ตีความไม่ได้, หรือ loaned memory ที่ไม่เหมาะสมได้ทั้งหมด
  • โปรแกรม Hubris ปกติแทบไม่พบข้อผิดพลาดเหล่านี้ ด้วยการตั้งค่าบิลด์และโค้ด Rust ที่สร้างขึ้นมา ดังนั้นหากบังคับให้ทุกการเรียกใช้ Result<T, IpcError> และ unwrap() จะทำให้ ขนาดโค้ด และค่าใช้จ่ายตอนรันไทม์เพิ่มขึ้น
  • เคอร์เนลจะฆ่า task ที่ละเมิดเงื่อนไขก่อนเรียก system call ทันทีโดยไม่มีรหัสข้อผิดพลาด และ REPLY_FAULT ขยาย นโยบาย fail-fast แบบเดียวกันไปถึงการตอบกลับของเซิร์ฟเวอร์
  • การออกแบบนี้ทำให้การใช้ API ผิดถูกเปิดเผยอย่างรวดเร็ว แต่ fuzz test หรือ task แบบ chaos ที่ส่ง IPC และ system call แบบสุ่มจะถูกรีสตาร์ตแทบจะทันที ทำให้ทดสอบได้ยาก

ตำแหน่งของ Hubris IPC และ REPLY_FAULT

  • Hubris มีเคอร์เนลขนาดเล็กที่เป็นอิสระจากแอปพลิเคชัน และวางโค้ดส่วนใหญ่ เช่น ไดรเวอร์, ลอจิกของแอปพลิเคชัน, และ network stack ไว้ใน task ที่แยกกัน ถูกคอมไพล์แยก และถูกกักกัน
  • การสื่อสารระหว่าง task ทำผ่าน system call สำหรับ IPC ที่เคอร์เนลเป็นผู้ทำงาน
    • RECV: ดึงข้อความรับที่มีลำดับความสำคัญสูงสุด หรือบล็อกจนกว่าจะมีข้อความเข้ามา
    • SEND: หยุดผู้เรียก แล้วส่งข้อความและสิทธิ์ควบคุมไปยัง task ผู้รับ จากนั้นรอจนกว่าจะได้รับคำตอบ
    • REPLY: ส่งคำตอบกลับไปยัง task ที่เคย SEND มา เพื่อให้รันต่อได้
  • ไคลเอนต์และเซิร์ฟเวอร์ใน Hubris ไม่ใช่ตัวตนตายตัว แต่เป็น บทบาทที่ task ทำอยู่
    • task ที่ใช้ SEND ทำหน้าที่เป็นไคลเอนต์
    • task ที่ใช้ RECV และ REPLY ทำหน้าที่เป็นเซิร์ฟเวอร์
    • task หนึ่งอาจเป็นเซิร์ฟเวอร์สำหรับ task หนึ่ง และเป็นไคลเอนต์สำหรับอีก task หนึ่งได้

ข้อผิดพลาดที่คอมไพเลอร์มองข้ามที่ขอบเขตของ task

  • ในการเรียกฟังก์ชันทั่วไป คอมไพเลอร์และลิงเกอร์รับประกัน type และเป้าหมายการเรียกได้ค่อนข้างมาก
    • หากฟังก์ชัน Rust รับอาร์กิวเมนต์ String คอมไพเลอร์จะกันไม่ให้ผู้เรียกส่ง bool เข้าไป
    • ความสับสนเรื่องเป้าหมาย เช่น ตั้งใจเรียก pet_cat แต่ไปเรียก fire_missiles โดยปกติก็ไม่เกิดขึ้น
  • Hubris IPC ข้ามขอบเขตของ task และแต่ละ task ถูกคอมไพล์เป็น โปรแกรมแยกต่างหาก ทำให้คอมไพเลอร์ตรวจสอบความสัมพันธ์ IPC ทั้งหมดโดยตรงไม่ได้
  • ข้อผิดพลาดที่เซิร์ฟเวอร์ IPC อาจเจอแบ่งได้เป็นสามประเภทใหญ่ ๆ
    • opcode ที่ไม่ตรงกับอินเทอร์เฟซ เช่น มี “operation number 48” เข้ามาในอินเทอร์เฟซที่มีแค่สอง operation
    • ได้ก้อนไบต์ที่ไม่ใช่ชนิดข้อความที่คาดไว้และตีความไม่ได้ หรือข้อความสั้นหรือยาวเกินไป
    • ไม่มี loaned memory ที่ต้องใช้ หรือเมื่อจำเป็นต้องใช้หน่วยความจำที่เขียนได้แต่ได้รับมาเป็นแบบอ่านอย่างเดียว

เหตุผลที่ไม่บังคับให้โปรแกรมปกติต้องจัดการข้อผิดพลาด

  • ในโปรแกรม Hubris ปกติ ระบบถูกจัดวางให้ข้อผิดพลาด IPC เหล่านี้ไม่เกิดขึ้น
    • การเชื่อมต่อระหว่าง task ถูกกำหนดผ่านการตั้งค่าของระบบบิลด์ ทำให้สับสนกันได้ยาก
    • ไคลเอนต์ประกอบและส่ง IPC ด้วยโค้ด Rust ที่สร้างขึ้นมา
    • เซิร์ฟเวอร์ก็ประมวลผลผลลัพธ์ด้วยโค้ด Rust ที่สร้างขึ้นมาแยกต่างหาก
  • หากทำให้ทุก operation ของ IPC คืนค่าเป็น Result<T, IpcError> โปรแกรมปกติจะต้องใส่ unwrap() สำหรับข้อผิดพลาดที่ในทางปฏิบัติไม่มีวันเจอ
    • unwrap() เป็นภาระด้าน ขนาดโค้ด อย่างมาก
    • ตอนรันไทม์ก็มีต้นทุนในการตรวจสอบข้อผิดพลาดที่จะไม่เกิดขึ้น
  • หากใส่ unwrap() หรือ panic! ไว้ในโค้ดที่สร้างขึ้นมา จะรวมตำแหน่ง panic ไว้ที่ศูนย์กลางและลดผลกระทบต่อขนาดโค้ดได้ แต่ต้นทุนตอนรันไทม์ยังคงอยู่
  • การรองรับรหัสข้อผิดพลาดแบบทั่วไปต้องให้ทุก operation ทำตามกฎการเข้ารหัสข้อผิดพลาดเดียวกัน
    • ทุก operation ต้องสามารถคืนข้อผิดพลาดได้
    • ทุก operation ต้องเข้ารหัสข้อผิดพลาดนั้นด้วยวิธีเดียวกัน
    • แม้ operation ที่ไม่มีทางล้มเหลวก็ต้องถูกแสดงในรูปแบบที่ล้มเหลวได้
  • ในเฟิร์มแวร์ที่ใช้ Hubris พบ operation ที่ล้มเหลวไม่ได้จริง ๆ อยู่เรื่อย ๆ และ การตั้งค่าขา GPIO ก็เป็นตัวอย่างหนึ่ง

นโยบาย fault เชิงรุกของเคอร์เนล Hubris

  • ระบบปฏิบัติการจำนวนมากแม้จะละเมิดเงื่อนไขก่อนเรียก system call ก็ยังคืนรหัสข้อผิดพลาดหรือให้โอกาสจัดการ exception/signal
    • ใน Unix หาก close file descriptor ที่ไม่ได้เปิด จะคืนรหัสข้อผิดพลาด
    • หากส่ง null pointer ให้ open แทน path name ก็จะคืนรหัสข้อผิดพลาด
  • Hubris จะทำลาย task นั้นทันทีเมื่อทำผิดเงื่อนไขก่อนเรียก system call
    • task จะไม่สามารถรันคำสั่งต่อไปได้อีก
    • ตัว task เองไม่มีโอกาสกู้คืนหรือทำงานต่อ
    • task supervisor ของแอปพลิเคชันจะได้รับแจ้ง fault และโดยปกติจะลบ task แล้วรีสตาร์ต
  • fault ที่เคอร์เนลสร้างขึ้นคือ synthetic fault
    • คล้ายกับ hardware fault ที่ CPU สร้างขึ้น เช่น dereference null pointer หรือหารด้วย 0
    • hardware fault เกิดจากการละเมิดกฎของสถาปัตยกรรมโปรเซสเซอร์ ส่วน synthetic fault เกิดจากการละเมิดกฎของเคอร์เนล
  • ตัวอย่างเช่น ในการเรียก SEND หากดัชนี task ผู้รับอยู่นอกขอบเขตของแอปพลิเคชัน หรือ pointer ของข้อความชี้ไปยังหน่วยความจำที่ไม่มีสิทธิ์เข้าถึง จะเกิด synthetic fault
  • Hubris ไม่อนุญาตให้มี fault ที่กู้คืนได้หรือทำต่อได้
    • ไม่ว่าจะเป็น hardware fault หรือ synthetic fault task ที่ได้รับ fault จะกลายเป็นสถานะตาย
    • การเลือกนี้มีไว้เพื่อหลีกเลี่ยงโหมดความล้มเหลวที่ละเอียดอ่อน และทำให้การให้เหตุผลเกี่ยวกับระบบง่ายขึ้น

วิธีที่เซิร์ฟเวอร์ตอบกลับไคลเอนต์ด้วย fault

  • REPLY_FAULT คือ system call ที่ให้เซิร์ฟเวอร์ ส่ง fault ไปยังไคลเอนต์แทนการตอบกลับปกติ
  • ลำดับการทำงานของ REPLY ทั่วไปเป็นดังนี้
    • เมื่อไคลเอนต์ใช้ SEND เคอร์เนลจะทำเครื่องหมาย task ไคลเอนต์เป็นสถานะ “waiting to send” ไปยัง task ผู้รับ
    • เมื่อ task ผู้รับใช้ RECV ไคลเอนต์นั้นจะกลายเป็นสถานะ “waiting for reply”
    • เมื่อเซิร์ฟเวอร์เรียก REPLY ไคลเอนต์จะกลับมาอยู่ในสถานะ runnable
  • REPLY_FAULT คล้ายกับ REPLY แต่แทนที่จะส่งข้อความและทำให้กลับมารันได้ จะ ส่ง fault และทำให้ task อยู่ในสถานะตาย
  • เซิร์ฟเวอร์ไม่สามารถฆ่า task ใดก็ได้ตามใจ
    • REPLY_FAULT ใช้ได้เฉพาะกับ task ที่เซิร์ฟเวอร์นั้น RECV มาแล้วและยังไม่ได้ REPLY
    • ใช้ได้เฉพาะกับไคลเอนต์ที่กำลังรอการตอบกลับจากเซิร์ฟเวอร์นั้น
  • Hubris ใช้ REPLY_FAULT สำหรับการจัดการข้อผิดพลาดต่อไปนี้
    • opcode ที่ผิดพลาด
    • ข้อความที่เสียหาย ถูกตัด หรือไร้ความหมาย
    • กรณีที่ไคลเอนต์ไม่ได้ส่ง loaned memory ชนิดที่ถูกต้องมาให้

ข้อผิดพลาดของแอปพลิเคชันและประสบการณ์แบบ fail-fast

  • REPLY_FAULT ใช้ได้ไม่เพียงกับข้อผิดพลาดของรูปแบบ IPC แต่ยังใช้กับ ข้อผิดพลาดเฉพาะแอปพลิเคชัน ได้ด้วย
  • Hubris IP stack จัดสรร IP port ให้ task แบบคงที่
    • หาก task หนึ่งพยายามยุ่งกับ IP port ของ task อื่น IP stack จะให้ fault กับ task นั้น
  • วิธีนี้ลดการจัดการข้อผิดพลาด “เชิงทฤษฎี” ที่ไม่ควรเกิดขึ้นจริง และเปิดเผยการใช้งานผิดอย่างรวดเร็วระหว่างการพัฒนา
  • REPLY_FAULT กลายเป็นวิธีให้เซิร์ฟเวอร์ทำให้เกิด panic! ข้ามโปรเซส ต่อโปรเซสไคลเอนต์ คล้ายกับโมเดลที่โดยทั่วไปจะเกิด panic! เมื่อมีการละเมิดเงื่อนไขก่อนเรียกฟังก์ชัน Rust
  • ไคลเอนต์ไม่จำเป็นต้องมีโค้ดสำหรับสิ่งนี้หรือให้ความร่วมมือใด ๆ

แนวโน้มด้านความปลอดภัยและข้อจำกัดในการทดสอบ

  • Eliza Weissman อธิบาย Hubris ว่า “เป็นศัตรูอย่างก้าวร้าวต่อโปรแกรมประสงค์ร้าย”
  • ความพยายามโจมตีมักปรากฏก่อนในรูปแบบข้อผิดพลาดของ API หรือการใช้งานผิด ดังนั้นระบบที่ล้างสถานะของคอมโพเนนต์ที่ทำตัวผิดอาจถูกโจมตีได้ยากขึ้น
    • สมมติฐานนี้ยังไม่ได้รับการทดสอบ
    • มีคำขอให้ติดต่อหากสนใจลอง exploit Hubris
  • ข้อเสียที่สังเกตได้คือระบบนี้ fuzz test ได้ยากมาก
    • มีการทำ task แบบ chaos ขนาดเล็กที่สร้าง IPC และ system call แบบสุ่ม แต่แทบไม่ว่าจะทำอะไร ก็ถูกรีเซ็ตทันที
    • หากต้องการให้ทำงานอย่างมีประโยชน์ มันต้องตัดสินใจโดยอิงกับ system uptime counter ที่เปลี่ยนแปลงให้สังเกตได้ทุกครั้งที่เริ่มต้น
  • REPLY_FAULT ยังให้วิธีที่เซิร์ฟเวอร์สุ่มฆ่าไคลเอนต์เพื่อบังคับให้เกิด chaos ได้ด้วย แต่ตัวเลือกนี้ยังไม่ได้รับการประเมินอย่างเต็มที่
  • โดยทั่วไป task ของ Hubris ไม่ได้ตั้งใจสร้างข้อความ IPC ที่ผิดแบบไดนามิก จึงมักรันได้โดยไม่ต้องรับรู้ถึงการมีอยู่ของ REPLY_FAULT

1 ความคิดเห็น

 
GN⁺ 2024-04-28
ความคิดเห็นบน Hacker News
  • REPLY_FAULT ดูดีเมื่อระบบมีขนาดเล็กและแน่นกระชับ และแอปพลิเคชันก็เขียนโดยคนกลุ่มเดียวกับที่ออกแบบระบบทั้งหมดเป็นหลัก
    แต่จากมุมมองของนักพัฒนาแอปพลิเคชัน การเชื่อมต่อกับโค้ดของบุคคลที่สามผ่านโมเดล IPC ที่บริการอื่นสามารถส่งยาพิษฆ่าทันทีคืนมายังกระบวนการของฉันได้ทุกเมื่อนั้นน่าจะค่อนข้างน่ากลัว
    ผมไม่ได้เชื่อใจนักพัฒนาแอปพลิเคชันคนอื่นถึงขนาดนั้น โลกนี้เต็มไปด้วยคนขับรถแย่ ๆ และโปรเซสเบื้องหลังที่สร้างโดยนักพัฒนาซึ่งถูกผู้จัดการกดดัน และถ้าแค่ได้เลิกงานก่อนสองทุ่ม พวกเขาก็อาจใส่ REPLY_FAULT ค่าเริ่มต้นที่อาจไม่เหมาะสมไว้เต็มไปหมด

    • นั่นดูเหมือนจะเป็นการออกแบบโดยตั้งใจ และสภาพแวดล้อมที่ Hubris เล็งไว้ก็คือแนว ๆ นั้น
    • เรื่องแบบนี้เคยเกิดขึ้นจริงใน Symbian เซิร์ฟเวอร์ IPC สามารถทำให้ไคลเอนต์ panic ได้ และสำหรับนักพัฒนาแอปพลิเคชันที่เข้าถึงซอร์สโค้ดของ OS ไม่ได้ มันค่อนข้างเลวร้าย
      เงื่อนไขล่วงหน้าทั้งหมดก็ไม่ใช่ว่าจะเข้าใจได้ง่าย และยังอาจแตกต่างกันไปตามอุปกรณ์หรือเวอร์ชันของ OS ด้วย
    • วิธีฆ่าพฤติกรรมที่เบี่ยงเบนอย่างรวดเร็วเป็นวิธีรักษาระบบให้แน่นกระชับ ขอบเขตที่ถูกออกแบบไว้เองก็น่าจะช่วยให้มันเล็กอยู่แล้ว
      ขอบเขตย่อมขยายออกไปเสมอ แต่คงไม่มีใครอยากยัดงานที่ควรจัดการบนโฮสต์มากกว่าเข้าไปเป็นทาสก์ Hubris ในคอนโทรลเลอร์ฝังตัวโดยไม่จำเป็น
    • ในสภาพแวดล้อมแบบฝังตัว ความเข้าใจผิดแบบนี้ไม่ว่าจะเป็นความรับผิดชอบของใคร ก็ดูเหมือนควรแก้ทันทีที่เกิดขึ้น
      ถ้าเซิร์ฟเวอร์บอกว่า “ไคลเอนต์ตัวนั้นผิด” เคอร์เนลก็ฆ่าไคลเอนต์นั้น ประเด็นสำคัญคือทั้งสองฝ่ายไม่เข้าใจกัน
    • บริการในที่นี้มองได้ว่าเป็น อินเทอร์เฟซของ OS ในเคอร์เนลเดี่ยว ถ้าเรียกเคอร์เนลผิด OS จะฆ่าโปรเซสนั้นก็สมเหตุสมผล
      อีกอย่าง คำว่า “โปรเซส” ที่พูดถึงอาจต่างจากสิ่งที่คุณนึกถึง ใน Hubris เธรดทั้งหมดแชร์ address space เดียวกัน
  • REPLY_FAULT จะลามเป็นลูกโซ่ไหม? เช่น A SEND ไปหา B แล้วรอ, B SEND ไปหา C แล้วรอ ถ้า C ทำ REPLY_FAULT ผมสงสัยว่า A จะตายพร้อมกับ B ด้วยหรือเปล่า
    ถ้าไม่เป็นเช่นนั้น ทาสก์ประสงค์ร้ายก็แค่มอบหมายการทดลองให้ทาสก์ผู้ช่วยทำแทนก็จบ ในทางกลับกัน ถ้าเป็นเช่นนั้น โดยรวมก็ดูเปราะบางพอสมควร แม้ผมจะไม่ได้รู้จัก Hubris ดีไปกว่านี้ก็ตาม
    แถมถ้า SEND สามารถเป็นแบบวนรอบหรือส่งกลับกันไปมาได้ ทาสก์ก็อาจเผลอฆ่าตัวเองได้ กรณีอย่าง B → A → B อาจกลายเป็นแรงจูงใจให้ไม่ใช้ REPLY_FAULT

    • Hubris ดูไม่ได้ถูกออกแบบมาเป็น ระบบปฏิบัติการเอนกประสงค์ โปรเซสถูกกำหนดตอน build
      เหตุผลที่เซิร์ฟเวอร์สามารถยิงกลับใส่ไคลเอนต์ได้ไม่ใช่เรื่องความปลอดภัย แต่เป็นเรื่องความน่าเชื่อถือ กล่าวคือมองว่าข้อผิดพลาดมาจากบั๊ก ไม่ใช่การโจมตีโดยเจตนา และปฏิกิริยาสุดโต่งของเคอร์เนลช่วยให้นักพัฒนาหาปัญหาได้เร็วที่สุด
      แน่นอนว่ามีส่วนที่ทับซ้อนกับความปลอดภัย และอาจเป็นแนวป้องกันสำรองที่มีประโยชน์เมื่อโปรเซสพยายามทำสิ่งที่ไม่ควรทำ
    • ถ้า B fault ผมคิดว่า A จะได้รับข้อผิดพลาดว่าเซิร์ฟเวอร์ตายแล้ว และมีโอกาสส่งข้อความเดิมอีกครั้งไปยังเซิร์ฟเวอร์ที่รีสตาร์ตขึ้นมาใหม่ ไม่น่าจะเป็น การล่มแบบลูกโซ่
  • Hubris กับดีบักเกอร์ Humility เป็นเทคโนโลยีที่ถ้ามีเวลาหรือมีภารกิจต้องทำ ก็อยากขุดลึกลงไปจริง ๆ น่าเสียดายที่ตอนนี้ทำไม่ได้

  • ในระบบที่ทีมเดียวเขียนโค้ดทั้งหมด น่าสนใจที่วิธีระเบิดไคลเอนต์ออกจากวงโคจรเพียงเพราะมันมองมาแปลก ๆ อาจช่วยเพิ่มความเร็วในการพัฒนาแบบวนซ้ำได้
    หลังจากอ่านเรื่อง algebraic effects แล้วเผลอหลับไป พอตอนเช้าได้มาอ่านบทความนี้ก็สนุกดี ถ้ามองแบบบิดมุมนิดหนึ่ง นี่คือเคอร์เนลที่ทำให้เซิร์ฟเวอร์สามารถทำ effect ที่ไคลเอนต์รับมือไม่ได้
    การใช้โค้ดซ้ำและการประกอบเข้าด้วยกันน่าจะยากขึ้นมาก แต่โมเดลการรันกลับเรียบง่ายขึ้นมาก ในระบบฝังตัวแบบสแตติก นี่ชัดเจนว่าเป็นการแลกเปลี่ยนที่เหมาะสม ถ้าต้องการใช้ซ้ำ ก็ vendor ทาสก์มาปรับแก้ได้ทุกเมื่อ

    • ถ้าแบ่งให้ดีระหว่างข้อผิดพลาดที่คาดได้ เช่น ไฟล์ไม่พบ กับข้อผิดพลาดที่คาดไม่ถึงอย่าง opcode ที่ไม่ถูกต้อง ผมไม่คิดว่าความสามารถในการใช้ซ้ำในโปรแกรมทั่วไปจะเสียหายมากนัก
      กลับกัน Unix มีข้อผิดพลาดที่เพิกเฉยได้มากเกินไป และโดยส่วนตัวผมคิดว่าหลายอย่างในนั้นควรทำให้เกิด สัญญาณร้ายแรง ด้วยซ้ำ ถ้าเป็นแบบนั้น คุณภาพซอฟต์แวร์โดยรวมคงดีขึ้นไม่น้อย
      ตัวอย่างเช่น การเรียก close() กับ file descriptor ที่ไม่ถูกต้องเป็นข้อผิดพลาดที่ไม่ร้ายแรง จึงมักถูกเพิกเฉย แต่ในความเป็นจริงมันอันตรายมาก โดยเฉพาะในแอปหลายเธรด ส่วนใหญ่การปิด file descriptor ที่ผิดจะล้มเหลวแบบ harmless แต่ 1% อาจปิด logging socket หรือไฟล์ล็อกฐานข้อมูล หรือการเชื่อมต่อ IPC ที่ไม่เกี่ยวข้องไปเลย แล้วก็กลายเป็นซอฟต์แวร์ไม่นิ่งที่ทุกคนเกลียด
  • นึกถึงประโยคจาก Errand of Mercy ที่ว่า “คุณจะได้รู้ว่ามีกฎและข้อบังคับต่าง ๆ อยู่ กฎเหล่านั้นจะถูกประกาศไว้ การละเมิดแม้แต่ข้อที่เล็กที่สุดมีโทษถึงตาย”

  • ควรเอาเรื่องนี้ไปทำเป็น RFC วันเมษาหน้าโง่สำหรับ HTTP
    ขอเสนอ HTTP 499 “Shame on you.” ไคลเอนต์ที่ได้รับ 499 ควรยุติทาสก์ที่ออกคำขอนั้นด้วยวิธีเฉพาะของแต่ละภาษา อาจจำกัดเฉพาะคำขอที่เริ่มด้วยเฮดเดอร์บางอย่างอย่าง Strict: true
    มันจับสมดุลแบบ “อะไรกันเนี่ย… แต่เอาจริง ๆ ก็โอเคนะ?” ที่เห็นในบริบทนี้ได้อย่างสมบูรณ์แบบ

  • อ่านแล้วสนุกมาก และวิธีใช้ supervisor เดี่ยวแบบนี้คล้ายกับวิธีที่สตาร์ทอัปเก่าของผมจัดแอปพลิเคชันให้ unwrap ทุกอย่าง
    ยังทำให้นึกถึงบทความหนึ่งที่ผมชอบคือ https://medium.com/@mattklein123/crash-early-and-crash-often...

  • สงสัยจริง ๆ ว่านี่ก้าวร้าวเกินไปหรือเปล่า
    บน Linux เป็นไปไม่ได้ที่จะทำให้โปรแกรมอื่นที่กำลังสื่อสารกันผ่านแค่ซ็อกเก็ตล่มโดยตรง ยกเว้นกรณีส่งข้อมูลผิดรูปแบบไปยังซ็อกเก็ต
    แต่การฆ่ามันนั้นทำได้แน่นอน อะไรก็ตามที่รันด้วย root สามารถฆ่าอย่างอื่นได้ และยังรีบูตเพื่อทำให้ทั้งระบบลงได้ด้วย
    ยากขึ้นและไม่พบบ่อยเท่าไร แต่ในคอนเทนเนอร์อย่างน้อยสิทธิ์ root ก็เป็นเรื่องปกติ แน่นอนว่ามี cgroup อยู่เลยถูกจำกัดมากขึ้น แต่ประเด็นคือแบบนั้น
    มันก็แตกต่างจากภูมิปัญญาทั่วไปที่ว่า “จงใจกว้างกับสิ่งที่รับเข้า และระมัดระวังกับสิ่งที่ส่งออก” อยู่เล็กน้อย เพียงแต่นั่นอาจเป็นเรื่องที่ผูกกับระบบเครือข่ายมากกว่า
    ถึงอย่างนั้น บางทีระบบก็คงต้องใจกว้างกับสิ่งที่รับเข้าอย่างเลี่ยงไม่ได้ ไม่อย่างนั้นจะมีวิธีปรับ API เล็กน้อยโดยไม่ทำให้โปรแกรมเดิมพังได้อย่างไร?

    • Hubris ไม่ใช่ OS อเนกประสงค์ แต่ทำงานบนโปรเซสเซอร์ระดับต่ำใน แร็กเซิร์ฟเวอร์ Oxide
      เท่าที่รู้ มันไม่อนุญาตให้มีโปรเซสชนิดใหม่ใน runtime ด้วย ไฟล์รันได้ทั้งหมดที่เป็นไปได้ต้องถูกกำหนดไว้ตั้งแต่ตอนคอมไพล์
  • สำหรับส่วนที่ว่า “ไม่มีวิธีแก้ปัญหาแล้วกลับไปทำงานต่อ นี่เป็นการเลือกโดยตั้งใจเพื่อหลีกเลี่ยง failure mode ที่ละเอียดอ่อนและทำให้การให้เหตุผลเกี่ยวกับระบบง่ายขึ้น” ทำให้นึกถึงคำพูดดังของไอน์สไตน์ว่า “ให้เรียบง่ายที่สุดเท่าที่เป็นไปได้ แต่ไม่เรียบง่ายไปกว่านั้น”
    ดีไซน์นี้ดูเหมือนจะละเมิดเงื่อนไขส่วนหลัง ผมไม่สนใจสภาพแวดล้อมการทำงานที่ทนต่อความยุ่งเหยิงของโลกจริงไม่ได้เลย และก็ไม่ค่อยรู้ว่ามีพื้นที่เชิงพาณิชย์ที่ใช้ได้จริงตรงไหนจะยอมรับเรื่องแบบนั้น
    สุดท้ายคือให้ย้อนกลับไปที่ระบบ init แล้วลองใหม่ไปเรื่อย ๆ อย่างนั้นหรือ? แล้วจะเข้าใจ fault ที่เกิดขึ้นด้วยกลไกแบบไหนเพื่อจะลองใหม่ด้วยวิธีที่ดีกว่าได้?
    อย่างไรก็ดี ขอปรบมือให้กับความบริสุทธิ์ของความเชื่อ

    • Hubris ไม่ใช่การทดลองเชิงวิชาการ มันทำงานอยู่ที่แกนกลางขององค์ประกอบสำคัญทั้งหมดของ แร็ก Oxide ได้แก่ compute sled, สวิตช์ และคอนโทรลเลอร์ของ power shelf และดีไซน์ของมันตั้งอยู่บนประโยชน์ใช้สอยที่ส่งมอบได้จริงเหนือสิ่งอื่นใด
      ตามที่ Cliff เขียนไว้อย่างละเอียดในบล็อก จริง ๆ แล้ว REPLY_FAULT เป็นฟีเจอร์ที่ตอนแรกคิดว่าอาจก้าวร้าวเกินไป แต่ประสบการณ์จากการสร้างและ deploy ระบบ รวมถึงพูดตรง ๆ คือการดีบัก ทำให้เรามั่นใจว่ามันจะไม่ได้ทำให้ระบบของเราพังแบบเอาแน่เอานอนไม่ได้ แต่จะทำให้แข็งแกร่งขึ้น
      สามารถดูแนวคิดและสภาพจริงของเรื่องนี้เพิ่มเติมได้ที่ [0] และ [1]
      [0] https://www.mattkeeter.com/blog/2024-03-25-packing/
      [1] https://cliffle.com/blog/who-killed-the-network-switch/
    • watchdog timer พร้อมจะฆ่าหรือรีสตาร์ตโปรเซสที่ไม่คอยสะกิดมันเป็นระยะ ๆ
      แม้แต่ในโปรเจกต์งานอดิเรก ผมก็เคยเห็นบัส I2C ค้างบ่อยเมื่อบิตของโปรโตคอลสักบิตเพี้ยน แล้วทำให้ทั้งระบบลงไปเลย ดังนั้นจึงคิดว่าดีไซน์นี้ค่อนข้างให้แรงบันดาลใจ
      ตามที่เข้าใจ นี่เป็นเรื่องของกรณีข้อผิดพลาดที่รู้อยู่แล้ว กล่าวคือไม่ใช่ข้อผิดพลาดที่ถูกจัดการ แต่เป็นความไม่ตรงกันของโปรโตคอลและสิ่งที่ไม่ควรเกิดขึ้นเด็ดขาด
      ความเห็นอื่น ๆ ก็ชี้ไว้แล้วว่าเป็น OS ที่ทำมาเฉพาะวัตถุประสงค์ เหมือนกับที่เราคงไม่ทำ UI ด้วย Erlang, Hubris ก็ดูเหมาะกับพื้นที่ที่มันครอบครองอยู่ดี
    • ผมมองว่านี่เป็นแนวคิดที่ตั้งใจนำไปใช้กับปัญหาที่เห็นได้ชัดว่าเป็นผลจาก สถานะของโปรแกรมที่ผิดพลาด จึงไม่สามารถกู้คืนได้อย่างสมเหตุสมผล
      สาเหตุอาจเป็นบั๊ก การโจมตี หรือฮาร์ดแวร์เสียหาย และไม่ว่ากรณีไหนก็ไม่ควรเดินหน้าต่อ ผู้เรียกมีปัญหาร้ายแรง และถ้ายังทำต่อก็มีแต่จะก่อความเสียหายมากขึ้น
      ฟังดูคล้ายปรัชญา “let it crash” ของ Erlang/OTP อยู่บ้าง Erlang ถูกใช้ในฮาร์ดแวร์ mission-critical จำนวนไม่น้อยและขึ้นชื่อเรื่องความน่าเชื่อถือ ดังนั้นในทางปฏิบัติอาจไม่ใช่ข้อเสียร้ายแรงขนาดนั้น
    • นี่คือ เคอร์เนลระบบฝังตัวที่เขียนด้วย Rust ขนาด 2,000 บรรทัด ซึ่งไม่รองรับการเพิ่ม task ใหม่ใน runtime
      มันถูกเขียนมาให้รันอยู่ลึก ๆ ภายในแร็กเซิร์ฟเวอร์ 0xide
  • ในส่วนที่ว่า “ความพยายาม exploit มักปรากฏก่อนในรูปของข้อผิดพลาด API หรือการใช้งานผิด ดังนั้นระบบที่ล้างสถานะของคอมโพเนนต์ที่ทำงานผิดเมื่อมีพฤติกรรมผิดปกติใด ๆ ควรถูก exploit ได้ยากขึ้น” ตรงนี้เหมือนเป็นการตรวจสิ่งที่แอปพลิเคชันยอมรับให้เข้มงวดขึ้นเล็กน้อย
    ดังนั้นจึงมีข้อดีด้านความปลอดภัย แต่ไม่ใช่แบบที่คิดกัน ไม่ใช่การทำลายความคืบหน้าของผู้โจมตีเพื่อผลักให้ถอยกลับ แต่เป็นการทำให้สถานะผิดพลาดบางอย่างที่เมื่อก่อนเคยนำไปต่อยอดเป็นสถานะผิดพลาดที่ต้องการมากกว่า ไม่สามารถใช้ได้อีกต่อไป
    เมื่อนั้นผู้โจมตีก็จะไปหาเป้าหมายอื่นแทนที่จะลองวิธีนั้น