2 điểm bởi GN⁺ 2024-03-27 | 1 bình luận | Chia sẻ qua WhatsApp
  • Firmware switch mạng của Oxide không bật lên sau khi thử nghiệm thay đổi trình tự cấp nguồn, và nguyên nhân là một lỗi trong kiểm tra cho mượn bộ nhớ IPC của kernel Hubris xung đột với cách bố trí bộ nhớ mới
  • Hubris là hệ điều hành nhúng cô lập các task bằng MPU; trong IPC, khi cho task khác mượn bộ nhớ, kernel xác nhận rằng bộ nhớ đó thực sự nằm trong vùng có thể truy cập
  • Đóng gói task mới được đưa vào gần đây đã thu hồi 30% RAM trong một số image firmware, nhưng kiểm tra cũ giả định rằng bộ nhớ được cho mượn nằm trong một vùng MPU duy nhất nên bị lỗi
  • Task sequencer đã chết vì synthetic memory fault khi cố cho driver I2C mượn bộ nhớ chứa địa chỉ 0x801bffd, và humility tasks để lại trạng thái đã khởi động lại 115 lần cùng mem fault... in syscall
  • Cách sửa là thay đổi thuật toán kiểm tra để cho phép việc cho mượn băng qua nhiều vùng MPU liền kề; từ lúc phát hiện sự cố đến khi sửa lỗi kernel mất khoảng 3 giờ

Switch mạng không bật lên

  • Arjen Roodselaar của Oxide đang thử nghiệm thay đổi trình tự cấp nguồn và thiết lập clock trong firmware switch mạng thì gặp tình trạng switch không bật lên sau một thay đổi tưởng như nhỏ
  • Một phần firmware vẫn phản hồi truy vấn, nhưng phần quan trọng phụ trách bộ sequencer cấp nguồn dường như đã dừng
  • Lỗi trình tự cấp nguồn có thể thực sự làm hỏng phần cứng, nên trước hết cần xác nhận switch đã chết hay chỉ không phản hồi

Hubris và bộ nhớ bị giới hạn

  • Hubris là hệ điều hành dành cho các hệ thống nhúng sâu như bộ điều khiển bên trong bàn phím, được tạo ra để xử lý các tác vụ cần thiết nhằm khởi động các bộ xử lý lớn trong Oxide Rack
  • Firmware dựa trên Hubris gồm nhiều chương trình được biên dịch riêng gọi là task
    • Mỗi task tự mang theo mã thư viện chuẩn cần thiết, v.v.
    • Các task được thiết kế để cô lập với nhau bằng MPU phần cứng, để không thể làm crash nhau hoặc làm hỏng bộ nhớ của nhau
  • Trên dòng ARM Cortex-M ARMv7-M thường dùng, vùng bộ nhớ được bảo vệ phải có kích thước là lũy thừa của 2 và phải được căn chỉnh theo kích thước đó
    • Ví dụ nếu một vùng 1024 byte cần thêm 1 byte, nó sẽ trở thành vùng 2048 byte chứ không phải 1025 byte

Ranh giới mới do đóng gói task tạo ra

  • Hubris ban đầu dùng cách đơn giản: một vùng cho RAM của task và một vùng cho flash, nhưng giữa các task xuất hiện khoảng trống không dùng được, gây lãng phí bộ nhớ
  • Matt Keeter cải tiến hệ thống build để khi có thể, nó bố trí task bằng cách kết hợp nhiều vùng power-of-two
    • Phần cứng chỉ cho phép tối đa 8 vùng cho mỗi task
    • Trong một số image firmware, việc này thu hồi được 30% RAM
    • Các thiết bị nhỏ nhất, vốn chật chội đến mức lần nào cũng cần tối ưu, đã có được khoảng trống dự phòng
  • Thay đổi này khiến các ranh giới vùng MPU khó dự đoán có thể xuất hiện ở giữa flash và RAM của task

Manh mối từ humility tasks

  • Arjen dùng Humility, debugger của Hubris, để điều tra switch bị lỗi; service processor phụ trách trình tự cấp nguồn vẫn còn sống và đang chạy, nên khả năng lỗi phần cứng có vẻ thấp
  • Trong output của humility tasks, task sequencer có trạng thái sau
mem fault (precise: 0x801bffd) in syscall (was: wait: reply from i2c_driver/gen0)
  • Cùng task đó đã được khởi động lại 115 lần, và trong Hubris, task gần như luôn được khởi động lại để phản ứng với crash
  • Chuỗi trạng thái có nghĩa là:
    • mem fault: vi phạm quy tắc xử lý bộ nhớ
    • precise: 0x801bffd: biết được địa chỉ cụ thể gây vấn đề
    • in syscall: task không đang chạy, mà đang ở trong system call
    • was: wait: reply from i2c_driver/gen0: đang chờ phản hồi của thông điệp đã gửi tới driver I2C
  • gen0 nghĩa là i2c_driver chưa từng crash, còn sequencer ở generation 115

IPC của Hubris và cho mượn bộ nhớ

  • Các task Hubris giao tiếp bằng thông điệp IPC, và thông điệp hoạt động giống lời gọi hàm
    • Task gửi thông điệp bị dừng lại
    • Task nhận có quyền điều khiển CPU
    • Khi kết quả trả về, task gửi được đánh thức trở lại
  • IPC được thiết kế để khớp tốt với mô hình ownership của Rust, cho phép một task cho task khác mượn một phần bộ nhớ của mình kèm theo thông điệp IPC
  • Task tương tác với thiết bị I2C cho driver bus I2C mượn một vùng bộ nhớ của mình, và driver đọc hoặc ghi trực tiếp trên vùng đó
    • Giảm nhu cầu driver bus phải có pool buffer riêng
    • Giảm số lần sao chép dữ liệu
  • Nếu triển khai sai, đây có thể trở thành lỗ hổng bảo mật, nên kernel Hubris cấm task cho mượn bộ nhớ mà nó không thực sự sở hữu hoặc không thể truy cập
    • Server nhận mã lỗi
    • Client nhận fault và luôn bị kết thúc
    • Điều này được xem là vi phạm truy cập cho thấy có thể có bug, hỏng hóc hoặc khả năng bị khai thác

Synthetic fault và nguyên nhân thực sự

  • Hubris phân biệt fault thành real faultsynthetic fault
    • real fault là vi phạm quy tắc phần cứng như dereference null pointer hoặc ghi vào vùng mã
    • synthetic fault là vi phạm quy tắc phần mềm do Hubris bổ sung, như IPC hoặc cho mượn bộ nhớ
  • Fault của sequencer là synthetic fault xảy ra trong quá trình cho driver I2C mượn bộ nhớ qua IPC
  • Địa chỉ gây vấn đề 0x801bffd là địa chỉ flash hợp lệ, nhưng có mẫu lạ vì nó thấp hơn ranh giới lũy thừa của 2 đúng 3 byte
  • Output của humility mem cho thấy hai vùng flash thuộc task sequencer tiếp giáp nhau tại 0x801c000
LOW         HIGH           SIZE ATTR   ID TASK
0x08018000 - 0x0801bfff   16kiB r-x--- 17 sequencer
0x0801c000 - 0x0801dfff    8kiB r-x--- 17 sequencer
  • Vì hai vùng thuộc cùng một task, trong khi chương trình chạy bình thường MPU phần cứng có thể cho phép truy cập không vấn đề, nhưng kiểm tra cho mượn bộ nhớ IPC của kernel lại có giả định khác

Điểm mà sự đơn giản hóa cũ trở thành bug

  • Kiểm tra kernel cũ chỉ xác nhận xem toàn bộ slice bộ nhớ định cho mượn có nằm hoàn toàn trong một vùng duy nhất của task hay không
self.region_table().iter().any(|region| {
    region.covers(slice)
        && region.attributes.contains(desired)
        && !region.attributes.intersects(forbidden)
})
  • Mã này phù hợp với thiết kế tại thời điểm được viết: mỗi task có một vùng RAM và một vùng flash
  • Khi đóng gói task được đưa vào, bộ nhớ của cùng một task có thể bị chia thành nhiều vùng MPU liền kề, nên giả định cũ không còn đúng
  • Truy cập bộ nhớ thông thường không bị ảnh hưởng vì MPU phần cứng kiểm tra trực tiếp; vấn đề chỉ lộ ra khi cố cho mượn bộ nhớ đó qua IPC

Sự cố do hai tính năng cùng tạo ra

  • Đóng gói task hoạt động theo kiểu cơ hội
    • Có giới hạn tối đa 8 vùng cho mỗi task
    • Các task driver phần cứng đã dùng sẵn một số vùng vì các thanh ghi memory-mapped
    • Chỉ khi còn slot vùng trống mới thử cách bố trí thông minh hơn
  • Kết quả là ranh giới vùng xuất hiện ở những vị trí khó dự đoán với người viết task
  • Một thay đổi nhỏ về kích thước của task A có thể thay đổi vị trí ranh giới vùng MPU của task B không liên quan
  • Chỉ riêng việc thêm mã debug cũng có thể làm thay đổi quyết định bố trí và ranh giới vùng, khiến crash biến mất
  • Matt lập tức tắt đóng gói task trong hệ thống build để Arjen có thể tạo image firmware hoạt động, đồng thời tiến hành phân tích và sửa bug kernel

Cách sửa kernel

  • Cốt lõi của bản sửa là thay đổi thuật toán kiểm tra truy cập bộ nhớ để cho phép bộ nhớ định cho mượn băng qua nhiều vùng MPU liền kề chính xác
  • Thuật toán mới được thiết kế để chỉ quét bảng vùng một lần
    • Hubris cố tránh phơi bày các thao tác có độ phức tạp thời gian mà task có thể kiểm soát
    • Hiệu năng phải chỉ phụ thuộc vào bảng vùng có kích thước cố định, chứ không phụ thuộc vào kích thước bộ nhớ được cho mượn
    • Kích thước bảng vùng cố định là 8
  • Vì vậy hệ thống build được đổi để sắp xếp vùng của task theo thứ tự địa chỉ tăng dần
regions.sort_by_key(|i| region_table.get_index(*i).unwrap().1.base);
  • Commit sửa lỗi khiến kernel dùng thuộc tính sắp xếp này để kiểm tra truy cập rẻ hơn
  • Phần mã trở nên phức tạp hơn được tách khỏi lõi kernel Hubris và chuyển sang một crate dễ portable hơn; unit test cho các corner case quan trọng cũng được bổ sung
  • Với mã mới, đóng gói task có thể được bật lại, và không còn để lại các crash khó dự đoán cho lập trình viên task

Vì sao sự cố không lan rộng

  • Toàn bộ diễn tiến bắt đầu từ trạng thái switch mạng không bật lên và đi đến bản sửa bug kernel trong khoảng 3 giờ
  • Nhờ fault isolation, trong firmware switch gồm 23 task cô lập, chỉ sequencer chết lặp lại, còn nhiều thành phần khác vẫn tiếp tục hoạt động
    • Hệ thống cập nhật firmware
    • Network stack IP cho giao diện quản lý và điều khiển
    • Nhiều dịch vụ mạng từ triển khai giao thức echo đến giao diện control plane của rack
    • I2C, SMBus, PMBus để giám sát cảm biến, quạt và các trạng thái hệ thống khác
    • Driver cho 32 transceiver QSFP 100G ở mặt trước
  • IPC của Hubris được thiết kế với giả định rằng task khác có thể thất bại, nên các thao tác được đánh dấu là idempotent có thể được thử lại một cách trong suốt
  • Bug kiểm tra truy cập bộ nhớ cũ có dạng chặn truy cập của chương trình đúng, chứ không cho phép truy cập sai hoặc độc hại, nên không có ảnh hưởng bảo mật
  • sequencer chết đúng lúc nó và driver I2C về cơ bản đang chia sẻ bộ nhớ, nhưng driver I2C vẫn tiếp tục hoạt động mà không có nguy cơ bị hỏng

Hạ tầng debug và vận hành đội ngũ

  • Humility là debugger phát triển cùng kernel Hubris; chỉ trong vài phút Arjen đã có thể xác định vị trí mã crash tới mức số dòng và chia sẻ snapshot độc lập của service processor
  • Hubris ghi compressed coredump của task bị crash vào RAM, và có thể thu hồi qua mạng
    • Có thể lấy crash dump ngay cả khi không có lưu trữ bền vững có thể ghi
    • Tính năng crashdump nằm trong một task riêng, không phải trong kernel
  • Các processor này không xử lý dữ liệu workload của khách hàng, chỉ xử lý traffic quản trị hệ thống, và báo cáo crash không được tự động upload
  • Phần độc lập kiến trúc của kernel Hubris có 1.789 dòng mã và 1.192 dòng chú thích; hỗ trợ ARMv6-M, ARMv7-M, ARMv8-M thêm 1.075 dòng mã và 534 dòng chú thích
  • Vì khái niệm kernel và IPC đơn giản, khi fault chỉ về IPC thì không có nhiều nơi cần kiểm tra

1 bình luận

 
GN⁺ 2024-03-27
Ý kiến trên Hacker News
  • Hubris thật sự rất tốt. Tôi đã đọc mã kernel khoảng 30 phút, và nó khác xa kiểu mã C tôi từng thấy trước đây, đầy macro ifdef, chuộng tên biến hai ký tự và thiếu chú thích; nó được viết rất rõ ràng, mạch lạc
    Cũng hợp để đọc trước khi ngủ, và tôi khuyên nên lướt qua một lần: https://github.com/oxidecomputer/hubris/blob/b44e677fb39cde8...

    • Tôi thấy khá khó chịu vì một phần lớn văn hóa C có vẻ có thể tóm gọn là “ngại học cách gõ với tốc độ vừa phải”
      Dung lượng đĩa của mã nguồn đã không còn là vấn đề lớn từ 40 năm trước, vậy mà người ta vẫn keo kiệt với tên biến
    • AI có thể xóa bỏ tập quán này. Nếu đưa mã C cũ kỹ, thô ráp vào AI, đột nhiên mọi biến có thể trở nên gọn gàng và được đặt tên theo cách người dùng thích
      Đó là vì AI đã học chính xác sở thích và thói quen của một lập trình viên cụ thể
  • Bài viết hay, nhưng vị trí chú thích dưới đây thì hơi tiếc
    Chú thích phía trên regions.sort_by_key(|i| region_table.get_index(*i).unwrap().1.base); rằng “phải sắp xếp theo địa chỉ tăng dần, và kernel dùng thuộc tính này để kiểm tra truy cập với chi phí thấp” giống một bất biến của trường mà mọi người viết phải tuân thủ và mọi phía đọc đều có thể tận dụng, hơn là chi tiết của riêng hàm này
    Vì vậy có vẻ hợp lý hơn nếu đưa vào chuỗi tài liệu của TaskDesc::regions: https://github.com/oxidecomputer/hubris/commit/b44e677fb39cd...

    • Dù vậy, có chú thích ngay cạnh đoạn mã sắp xếp cũng tốt. Nếu không, bản thân thao tác sắp xếp đó có thể trông khá bất ngờ
      Có lẽ cách tốt nhất là tạo một phương thức khởi tạo cho TaskDesc để nó sắp xếp các vùng, qua đó ép buộc bất biến. Có thể thấy mã đang ngày càng phức tạp theo thời gian, nên giờ bỏ chút thời gian để bọc độ phức tạp vào trong các phương thức có vẻ đáng giá
  • Đây là một trong những tin tuyển dụng hay nhất tôi từng thấy. Cách nó tự nhiên chuyển sang chuyện văn hóa rồi cuối cùng thêm câu “nhân tiện, chúng tôi đang tuyển” rất hay
    Đây thật sự là một bài phân tích hậu sự cố xuất sắc, và ngay cả tôi, một lập trình viên tầng ứng dụng, cũng có thể theo dõi được. Đúng lúc tôi đang đọc Rust in Action nên cũng được chuẩn bị tốt hơn cho kiểu nội dung này
    Nhìn thấy người chú thích nhiều trong mã cũng luôn là điều thú vị. Lập trình văn chương có hiệu quả

    • Tiếc là chuyện này chỉ áp dụng ở Mỹ
  • Có vẻ có thể xem phần trước ở đây

    1. https://hachyderm.io/@mjk/112157472314396711
    2. https://www.mattkeeter.com/blog/2024-03-25-packing/
  • Phần “sự tích hợp chặt chẽ, phi phân cấp của đội ngũ” khiến tôi chú ý. Bản thân nó không phải là tính năng của Hubris, nhưng thật khó tách Hubris khỏi đội đã tạo ra nó, và mô tả rằng đội ngũ kỹ thuật của Oxide hầu như không có silo nội bộ thật ấn tượng
    Tôi muốn nghe thêm vì sao họ xây dựng một văn hóa khuyến khích cởi mở, tò mò, giao tiếp và hạn chế tính phòng thủ, xây đế chế, gatekeeping, cũng như cụ thể họ đã triển khai nó như thế nào. Tôi cũng tò mò liệu khi nuôi dưỡng kiểu văn hóa này trong một tổ chức có nhược điểm gì không
    Có nơi chọn hệ thống phân cấp nghiêm ngặt hơn, và sơ đồ tổ chức có thể cần được quyết định một cách chiến lược, nên tôi chưa cảm nhận rõ được các đánh đổi

    • Bản thân các giá trị được nêu thì khó đánh giá, nhưng nhìn chung nhược điểm của tổ chức không có cấu trúc được định nghĩa mạnh là dù sao một dạng cấu trúc quyền lực nào đó vẫn sẽ xuất hiện
      Nếu cấu trúc đó không được nêu rõ, nó kém công khai hơn, không phải thứ được chủ ý lựa chọn, và đặc biệt khó hiểu hơn với những người không giỏi tương tác xã hội. Vì tính chất bóng tối đó, nó có thể cho phép nhiều hành vi bệnh hoạn hơn, và ngay cả khi không trở nên quá tệ, nó cũng có thể khiến việc phối hợp khó hơn nhiều
      Tôi đã gặp chuyện này ở nhiều công ty. Một công ty tư vấn lớn có cấu trúc quyền lực chính thức, nhưng trên thực tế nó không được tuân thủ tốt; cách tham gia dự án cũng giống việc thân thiết với người làm sales/quản lý hơn là qua các kênh chính thức. Nếu bạn có thể xây dựng tốt mạng lưới xã hội cần thiết thì ổn, còn không thì nó vận hành không tốt
      Một ví dụ tương tự là “The Tyranny of Structurelessness”. Đó là bài nói của một nhà nữ quyền từng chứng kiến điều tương tự xảy ra trong các tổ chức xem hệ thống cấp bậc là gia trưởng và từ chối nó; cũng có thảo luận tương tự về Valve, nơi cấu trúc nội bộ không rõ ràng. Các dự án mã nguồn mở cũng có thể gặp cùng vấn đề, và tôi cho rằng một số xung đột trong Rust cũng bắt nguồn từ vấn đề tương tự
      Tuy vậy, cấu trúc quyền lực rõ ràng không nhất thiết phải mang tính phân cấp. Tổ chức kinh doanh truyền thống là phân cấp, nhưng cấu trúc của Oxide có thể vừa rõ ràng vừa phi phân cấp. Kiểu này thường hoạt động tốt hơn khi quy mô nhỏ, và công ty tư vấn nói trên là ví dụ lớn nhất mà tôi biết trong số các công ty vận hành gần như tự do, nhưng dù vậy vẫn có một số bệ đỡ nhất định
      Đây không phải là lưỡng phân mà là một phổ. Ngay cả cấu trúc quyền lực cứng nhắc nhất trên giấy tờ cũng có một cấu trúc ngầm phức tạp hơn bên dưới, và đó là bản chất của các nhóm người
      Tôi không tin rằng cấu trúc rõ ràng luôn tốt hơn cấu trúc ngầm. Tôi chỉ đang nói về những nhược điểm quan sát được ở các tổ chức kém rõ ràng hơn, còn cấu trúc quyền lực rõ ràng hơn cũng có vấn đề riêng. Liên quan đến chuyện này còn có “seeing like a state” hay vấn đề tính dễ đọc/khả kiến
  • Đây là một bài viết tuyệt vời, cho thấy sâu sắc quá trình debug một vấn đề phức tạp. Việc phần còn lại của hệ thống vẫn duy trì ổn định cho thấy rõ chất lượng kỹ thuật của đội Oxide
    Cá nhân tôi cũng được truyền cảm hứng khá nhiều và định thử áp dụng các kỹ thuật tương tự vào công việc hằng ngày

  • Nếu đối xử phần cứng đó như TLB được nạp bằng phần mềm, họ cũng có thể hỗ trợ hơn 8 vùng

    • Có lẽ họ (a) muốn hiệu năng soft real-time, và (b) không muốn đưa vào một yếu tố cốt lõi có thể cản trở khả năng debug hoặc độ tin cậy
      Tôi sẽ tuyệt đối không làm vậy trừ khi đó là lựa chọn cuối cùng. Phân trang ảo rất bừa bộn, và tôi không muốn để lại nghi ngờ
    • Tôi biết TLB là translation lookaside buffer, nhưng tò mò “soft fill” ở đây nghĩa là gì
  • Những gì Oxide làm thật sự đáng kinh ngạc

    • Sau Tailscale, giờ đến Oxide trở thành cái tên được yêu mến với những dự án mà 99% mọi người không cần đến
  • Tôi thích mọi thứ người Oxide làm, và đây cũng là một trong số đó

  • Họ đặt tên hệ điều hành là Hubris à? À, chuyện đó thì… tôi không biết nói sao nữa

    • Bạn sẽ vui khi biết trình debug của nó tên là “humility”: https://github.com/oxidecomputer/humility
    • Chính xác là Brian Cantrill đã đặt tên hệ điều hành là hubris
      Người tỉnh táo nào thời nay lại viết một hệ điều hành mới chứ? Câu trả lời là người muốn giải quyết vấn đề mà mọi hệ điều hành đều làm ngơ: các bộ điều khiển trên bo mạch chủ và card mở rộng, những thứ hệ điều hành không kiểm soát và cũng không thể kiểm soát
    • Trông khá hợp với thương hiệu đấy