Post

Understanding Qualcomm Linux Boot Flow (Part 1): Chain of Trust and Exception Levels

A walkthrough of the Qualcomm Linux boot flow — PBL, XBL, Qualcomm TEE, hypervisor, UEFI and the boot manager, plus the chain of trust anchored in eFuse and the EL0–EL3 privilege model.

Understanding Qualcomm Linux Boot Flow (Part 1): Chain of Trust and Exception Levels

Boot flow của SoC nhúng thuần (TI, NXP, Rockchip…) thường đơn giản, tuyến tính: Boot ROM → SPL → U-Boot → Kernel, mỗi bước một việc, dễ trace, dễ debug. Nhưng Qualcomm — cụ thể dòng Dragonwing (QCS6490) — lại có boot flow phức tạp hơn hẳn, dù cùng là ARM SoC chạy Linux.

QCS6490 (Dragonwing) là board dùng xuyên suốt bài — không phải để đối chiếu qua loa, mà để đào sâu vào cơ chế thật đang chạy trên hệ thống.

Board Qualcomm có thể chạy nhiều OS khác nhau (Android, Qualcomm Linux, Windows…), mỗi OS đi kèm boot path riêng. Bài này tập trung đúng vào Qualcomm Linux.

1. Qualcomm’s Origins — Not a Typical Chip Vendor

Khác với TI — semiconductor company thuần phục vụ industrial/automotive từ đầu — Qualcomm xuất thân từ modem/baseband cho di động, làm chip cho mobile trước rồi mới lấn dần sang embedded (nay gọi là Dragonwing). Vì sinh sau đẻ muộn trên nền một kiến trúc vốn xây cho mobile/carrier, boot flow của nó giữ lại nguyên “gen” bảo mật viễn thông (carrier yêu cầu chứng nhận bảo mật khắt khe cho baseband) — khác hẳn tư duy embedded tối giản.

graph TD
    Q[Qualcomm] --> S["Snapdragon<br/>Mobile"]
    Q --> D["Dragonwing<br/>Industrial / Embedded IoT"]
    Q --> F["Dragonfly<br/>Data center"]

2. Qualcomm Linux Boot Flow — Chain of Trust, Step by Step

Qualcomm Linux đi theo flow mô tả ở Hình 1 dưới đây — bài này dùng nó làm sườn chính. Lưu ý systemd-boot trong hình là lựa chọn riêng của Qualcomm Linux ở tầng boot manager, tầng này thay thế được và sẽ nói kỹ ở mục 2.6.

Đây không chỉ là danh sách tên component nối tiếp nhau — theo thiết kế, mỗi bước chuyển giao đều đi kèm authenticate: stage sau chỉ được chạy nếu stage trước verify được chữ ký của nó. Đây chính là chain of trust, cơ chế cốt lõi của secure boot.

Cold boot flow của Qualcomm Linux

Hình 1 — Cold boot flow của Qualcomm Linux, đặt trên trục Exception Level và hai vùng Secure / Non-secure.
Nguồn: Qualcomm Linux Boot Guide (80-70014-4, Rev. AN).

Where the Chain of Trust Is Anchored

Một chuỗi verify nối tiếp chỉ có nghĩa nếu nó neo được vào một điểm mà không ai verify nữa — điểm đó gọi là root of trust. Trên Qualcomm, nó gồm hai mảnh:

  • PBL — code nằm trong mask ROM, đúc cứng vào silicon lúc sản xuất. Không ai kiểm tra PBL, đơn giản vì không ai thay được nó.
  • Hash của root public key — nằm trong eFuse trên chip, không nằm trong code. PBL đọc fuse này để biết “chữ ký hợp lệ trông như thế nào” mà đi verify XBL.

Fuse cũng chính là công tắc bật/tắt của cả cơ chế: khi chưa blow, PBL không enforce chữ ký — đây là lý do board phát triển vẫn flash và boot được ảnh tự build; khi đã blow (thiết bị thương mại), chỉ ảnh ký đúng key mới chạy, và thao tác blow là một chiều, không đảo ngược được.

Một điểm nữa nên biết trước khi đi vào từng thành phần: cơ chế ký duyệt đổi chủ tại UEFI. Toàn bộ PBL, XBL, TEE, hypervisor — và cả bản thân UEFI — đều do secure boot kiểu Qualcomm ký duyệt, với key trong eFuse và format ký riêng của hãng. Chỉ từ boot manager trở lên mới thuộc về UEFI Secure Boot, chuẩn công nghiệp với key nằm trong các biến db/KEK/PK.

Một chi tiết dễ bỏ qua: chuỗi ký duyệt và chuỗi trao quyền không trùng nhau. XBL ký duyệt TEE, hypervisor và UEFI ngay từ đầu trong một lượt; chỉ sau đó quyền thực thi mới lần lượt đi qua từng cái.

graph LR
    subgraph Z1["① Verified by Qualcomm (key in eFuse)"]
        direction LR
        P[PBL] --> X[XBL] --> T["Qualcomm TEE"] --> H[Hypervisor] --> U[UEFI]
    end
    subgraph Z2["② Verified by UEFI Secure Boot"]
        direction LR
        S["Boot manager"] --> K["Linux kernel<br/>(+ initrd, cmdline, DT<br/>if bundled as UKI)"]
    end
    U --> S
    FUSE[("eFuse<br/>root key hash")] -.->|provides key| P
    VARS[("UEFI variables<br/>db / KEK / PK")] -.->|provides key| U

UEFI vì thế đóng vai kép: vừa là mắt xích cuối cùng được Qualcomm ký duyệt, vừa là người đi kiểm đầu tiên của chuẩn công nghiệp phía trên.

Và cần phân biệt hai chuyện rất dễ lẫn:

  • Trạng thái bật/tắt thì độc lập — fuse đã blow mà UEFI Secure Boot vẫn off là chuyện bình thường, và ngược lại.
  • Độ tin cậy thì không độc lập — UEFI Secure Boot chỉ đáng tin đến mức bản thân ảnh UEFI đáng tin. Nếu tầng dưới không enforce, kẻ tấn công thay luôn ảnh UEFI bằng bản mang key của họ, và UEFI Secure Boot vẫn sẽ báo “hợp lệ” cho mọi thứ chúng ký.

Còn ngoài kernel ra thì initrd, cmdline và device tree có được chữ ký phủ tới hay không lại tuỳ cách đóng gói — nói ở mục 2.6.

Bài này chỉ dừng ở mức đó — chi tiết về provisioning, ký ảnh và key hierarchy xin để dành cho một bài riêng.

Exception Levels and the Two Worlds

Ngoài chain of trust, còn một trục nữa nên nắm trước khi đi vào từng thành phần: ai được chạy ở mức đặc quyền nào.

Kiến trúc ARM 64-bit chia quyền thực thi thành 4 mức gọi là Exception Level (EL0–EL3), số càng lớn quyền càng cao. Song song với đó, hệ thống còn bị chia làm hai “thế giới” — Secure và Non-secure — và ranh giới này do phần cứng cưỡng chế, không phải phần mềm tự giác.

Boot Guide nói rõ ba điểm:

  • EL3 luôn ở chế độ Secure. Còn EL0, EL1, EL2 thì có thể ở Secure hoặc Non-secure tuỳ lúc.
  • XBL chạy phần security setup ở EL3 Secure, xong mới chuyển sang UEFI ở Non-secure EL1.
  • Linux kernel chạy ở Non-secure EL1 — tức là ở mức đặc quyền thấp hơn cả TEE lẫn hypervisor.

Nhìn lại Hình 1 ở đầu mục này sẽ thấy rõ: trục dọc bên trái chính là EL0–EL3, còn hai cột Non-secure/Secure là hai thế giới. Đọc theo trục đó thì PBL và XBL nằm ở EL3, Qualcomm TEE xuất hiện ở cả hai chỗ — EL3 với vai secure monitor và Secure EL1 — hypervisor ở EL2, còn UEFI, systemd-boot và Linux kernel đều ở EL1 phía Non-secure.

Đây là chỗ giải thích được mấy điều mà các mục sau sẽ nhắc tới:

  • Vì sao Linux không chạm được vào TEE — TEE nằm bên Secure World, Linux nằm bên Non-secure. Không phải Linux “được lập trình để không đụng vào”, mà là phần cứng chặn.
  • Vì sao hypervisor lại là một lớp cô lập — nó ngồi ở EL2, cao hơn EL1 nơi Linux chạy. Cái gì ở trên thì kiểm soát được cái ở dưới.
  • Vì sao phải có SMC — muốn đi từ Non-secure sang Secure thì phải qua một cửa duy nhất có kiểm soát, chứ không gọi hàm trực tiếp được.

Components in the Chain

2.1. PBL (Primary Boot Loader)

Đoạn code đầu tiên chạy khi cấp nguồn — nằm sẵn trong silicon lúc sản xuất chip (mask ROM), không bao giờ thay đổi được. Lúc này DDR RAM chưa hoạt động, nên PBL chỉ chạy được trong một vùng SRAM nhỏ ngay trên chip. Việc nó làm rất tối giản: tìm đúng thiết bị lưu trữ chứa bước tiếp theo, đọc vào SRAM, verify chữ ký (nếu fuse đã blow, như nói ở mục trên), rồi nhảy vào chạy.

Vì nằm trong mask ROM và chưa có console ở giai đoạn này, những gì biết về PBL dừng ở mức vai trò trong chain of trust — không có gì để trace sâu hơn từ phía người dùng board.

2.2. XBL (eXtensible Boot Loader)

Bước kế tiếp, được PBL nạp lên từ storage — khác PBL, cái này flash lại được, không nằm cứng trong silicon. Việc nặng nhất nó làm là khởi tạo DDR RAM thật (mỗi board dùng loại RAM khác nhau nên phải cấu hình runtime), cộng với PMIC và các phần cứng cơ bản khác. Sau khi có RAM thật để dùng, nó mới đủ khả năng load và verify nhiều thành phần tiếp theo (TEE, hypervisor, UEFI).

Vì sao phải tách hẳn ra một stage riêng thay vì để PBL làm luôn? Vì tham số DDR thay đổi theo từng board, trong khi PBL đúc cứng trong mask ROM — không sửa được. Thứ gì thay đổi theo board thì bắt buộc phải nằm trong thành phần flash lại được. Nói cách khác, XBL tồn tại không phải vì ai đó chọn thiết kế như vậy, mà là hệ quả cưỡng bức của việc PBL không đổi được. Cùng logic đó cũng giải thích vì sao dữ liệu cấu hình thường được tách khỏi code: một binary XBL phục vụ được nhiều board, chỉ khác nhau ở phần cấu hình đi kèm.

2.3. Qualcomm TEE (Trusted Execution Environment)

Khác PBL hay XBL — vốn là bước “đi qua rồi thôi” — TEE là môi trường thực thi tồn tại song song với Linux suốt thời gian máy chạy, trong một “thế giới” riêng (secure world, đối lập với normal world nơi Linux chạy) được cô lập ở tầng phần cứng, không phải sandbox phần mềm.

Lý do tồn tại: có những việc quá nhạy cảm để giao cho Linux (thao tác key, verify chữ ký, DRM…), vì Linux bề mặt tấn công quá lớn. Linux không đọc/ghi trực tiếp được vào tài nguyên của TEE, muốn nhờ việc thì phải gọi qua SMC (Secure Monitor Call). Tài liệu cũ gọi nó là “TrustZone” hoặc “QSEE”.

Bên trong TEE là closed-source, từ phía Linux gần như không quan sát được gì ngoài các SMC call — nên phần này chỉ dừng ở mức vai trò.

2.4. Qualcomm Hypervisor

Hypervisor — lớp phần mềm nằm dưới Linux, quyết định ai được cấp CPU core nào, vùng RAM nào. Điểm khiến người quen SoC nhúng thấy lạ: thiết bị chỉ chạy đúng 1 Linux, không có “nhiều máy ảo” nào cần quản lý — vậy nó ở đó làm gì?

Qualcomm dùng hypervisor không phải để chạy nhiều OS cùng lúc (dù nó có khả năng đó), mà như một lớp cô lập phần cứng bổ sung: Linux bị nhốt trong một “virtual machine” do hypervisor quản lý, không chạm trực tiếp được vào tài nguyên dùng chung của chip dù có bug hay bị khai thác. Vì vai trò này gắn cố định vào kiến trúc chứ không phải tính năng optional, hypervisor luôn xuất hiện trong chain — kể cả trên thiết bị đơn giản nhất.

Cũng closed-source như TEE, nên phần này dừng ở mức hiểu vai trò kiến trúc.

2.5. UEFI (Unified Extensible Firmware Interface)

Một chuẩn công nghiệp mở, do UEFI Forum định nghĩa — hiểu đơn giản là bản kế nhiệm hiện đại của BIOS trên PC ngày xưa. Bất kỳ hãng nào cũng có thể implement UEFI cho chip của mình, miễn tuân theo spec chung. Qualcomm không tự viết từ đầu, mà implement dựa trên TianoCore EDK2 — reference implementation mã nguồn mở của UEFI, cũng chính là nền tảng nhiều hãng khác dùng (kể cả BIOS trên PC thường).

Vấn đề UEFI giải quyết: XBL (bước trước đó) đã có DRAM, đã có hardware cơ bản chạy được, nhưng nó vẫn là code low-level, viết riêng cho từng chip. Nếu để mọi bootloader phía sau phải “nói chuyện” trực tiếp với hardware theo kiểu Qualcomm riêng, thì phần mềm phía trên (bootloader, OS) sẽ không portable được sang chip khác. UEFI đứng ra làm lớp trừu tượng: cung cấp driver chuẩn cho storage/console, một tập API chuẩn (boot services, runtime services) cho phần mềm phía trên gọi, và quan trọng nhất — quy ước chuẩn để tìm và nạp bootloader kế tiếp từ một phân vùng đặc biệt gọi là ESP (EFI System Partition), đọc theo filesystem chuẩn (thường FAT32), không cần biết gì về cách Qualcomm tổ chức storage bên dưới.

Một đặc điểm quan trọng của UEFI: nó chia dịch vụ của mình làm hai loại. Boot services chỉ sống trong giai đoạn boot — driver, cấp phát bộ nhớ, truy cập file. Runtime services thì sống tiếp cả khi hệ điều hành đã chạy — biến EFI, đồng hồ thời gian thực, lệnh reset. Ranh giới giữa hai loại là lời gọi ExitBootServices(): kernel gọi nó để tuyên bố “tôi tự lo được rồi”, firmware thu hồi toàn bộ boot services, và từ giây phút đó Linux làm chủ phần cứng. Đây là lý do khi Linux đã chạy từ lâu, bạn vẫn đọc được biến EFI qua /sys/firmware/efi/ — phần đó thuộc runtime services, chưa bị thu hồi.

Còn vì sao nền tảng này lại chọn UEFI? Quay lại xuất thân ở mục 1: nó vốn được xây cho mobile, và dòng chip cùng gốc còn phải chạy được Windows on ARM — mà Windows thì bắt buộc firmware phải là UEFI. Lựa chọn này không xuất phát từ nhu cầu của thế giới nhúng, nó là di sản mang sang.

Nói cách khác: UEFI là ranh giới nơi mọi thứ “Qualcomm-specific” (PBL, XBL) kết thúc, và mọi thứ “chuẩn công nghiệp” (UEFI, rồi tới boot manager) bắt đầu.

2.6. Boot Manager

Sau khi UEFI đã sẵn sàng, nó cần biết nạp cái gì tiếp theo — đây là việc của boot manager. Điểm quan trọng: thành phần này không do hãng chip quy định, mà do image hệ điều hành quyết định. Cùng một phần cứng, image này lên systemd-boot, image kia lên GRUB, trong khi toàn bộ chain bên dưới không đổi một byte.

Các lựa chọn phổ biến ở tầng này:

Boot managerĐặc điểmPhù hợp khi
GRUB2Nặng và đầy đủ: menu tương tác, ngôn ngữ scripting riêng, đọc được nhiều filesystem, chain-load được hệ điều hành khácMáy đa dụng, cài nhiều hệ điều hành, cần sửa tham số boot ngay lúc boot hoặc cần menu cứu hộ
systemd-bootRất nhẹ, tối giản có chủ đích: không scripting, không theme, không network boot, chỉ đọc entry khai báo sẵnThiết bị chạy một hệ điều hành cố định, ưu tiên bề mặt tấn công nhỏ, hợp với chain mà mọi thứ đều phải ký
U-BootTrung bình, mạnh về thao tác phần cứng: nạp ảnh qua mạng, thao tác eMMC/SD, sửa biến môi trường trực tiếpGiai đoạn bring-up board, hoặc nền tảng không có sẵn UEFI
(không dùng gì cả)Nhẹ nhất: kernel Linux tự nó đã là một EFI application hợp lệ — UEFI gọi thẳng, bỏ qua hẳn tầng nàyThiết bị chuyên dụng boot cố định một ảnh, cần boot nhanh và gọn

Đánh đổi xuyên suốt bảng này là độ nặng lấy đường lui: càng đầy đủ thì càng có chỗ cứu khi kernel mới hỏng (chọn entry cũ, sửa tham số ngay tại menu), càng tối giản thì càng ít thứ để tấn công nhưng cũng càng ít lối thoát khi sự cố.

Dòng cuối bảng đáng để ý nhất: boot manager là tầng có thể bỏ hẳn. Điều đó cho thấy nó không thuộc kiến trúc chip mà là lựa chọn của người dựng hệ điều hành — bằng chứng cụ thể cho luận điểm “UEFI là ranh giới” ở mục trên.

Boot Loader Specification xuất hiện ở hai dòng đầu bảng là một chuẩn chung, không thuộc riêng boot manager nào (GRUB cũng đọc được), quy định cách mô tả một mục boot trên ESP. Nó có 2 dạng:

  • Type 1 — entry dạng text đơn giản trong thư mục loader/entries/, mỗi file mô tả một cặp kernel + initrd + tham số boot.
  • Type 2 (UKI — Unified Kernel Image) — gộp thẳng kernel + initrd + cmdline thành một file EFI executable duy nhất, đặt trong /EFI/Linux/. UEFI có thể chạy thẳng file này như một chương trình bình thường.

Theo Boot Guide thì Qualcomm Linux dùng systemd-boot, nhưng như bảng trên cho thấy, đó là lựa chọn của bản phân phối chứ không phải ràng buộc của phần cứng.

Điểm đáng chú ý: toàn bộ chain of trust (TEE, hypervisor, UEFI) được dựng xong trước khi bất kỳ thành phần nào chạy được — khác với SoC nhúng thuần, nơi lớp bảo mật (nếu có) chỉ là một stage riêng chen giữa SPL và U-Boot.

Comparison with Plain Embedded SoCs

Vẫn là flow của Qualcomm Linux — bên trong khung là thành phần của Qualcomm, còn nhãn khung là thành phần tương ứng trong chuỗi boot nhúng thuần nhắc ở đầu bài:

graph LR
    subgraph T1["~Boot ROM"]
        direction LR
        Q1[PBL]
    end
    subgraph T2["~SPL"]
        direction LR
        Q2[XBL]
    end
    Q3["Qualcomm TEE"]
    Q4["Hypervisor"]
    subgraph T4["~U-Boot"]
        direction LR
        Q5[UEFI] --> Q6["Boot manager"]
    end
    Q1 --> Q2
    Q2 --> Q3
    Q3 --> Q4
    Q4 --> Q5
    Q6 --> Q7[Kernel]

Hai chỗ lệch — chính là hai node nằm ngoài khung:

  • Qualcomm TEE và Hypervisor — chuỗi nhúng thuần không có hai tầng này. Chúng chỉ xuất hiện khi chip dùng lớp bảo mật riêng, và ngay cả khi có thì hypervisor thường vẫn là tuỳ chọn — còn bên Qualcomm cả hai đều là mắt xích cố định, dù thiết bị chẳng ảo hoá gì.
  • UEFI + boot manager — bên nhúng thuần, U-Boot ôm cả hai vai: vừa là bootloader cuối, vừa chọn thứ để boot. Bên Qualcomm hai việc này tách đôi.

Nói cách khác, độ phức tạp của Qualcomm không đến từ việc có thêm bước lạ, mà từ việc các bước quen thuộc bị tách nhỏ và siết chặt hơn.

This post is licensed under CC BY 4.0 by the author.