5 min read

SELinux Multi-SoC 策略架構:從踩坑到設計契約

Table of Contents

你以為 SELinux 最難的是寫 .te 規則?做單一 SoC 的時候是。但一旦你要同時養 AML、RTK、MTK 三家晶片,每家有自己的 BSP、自己的 HAL、自己的 sysfs 節點,難的就不再是「寫什麼」,而是「這條規則該放哪個目錄」。

這篇整理的是 multi-SoC sepolicy 踩過的坑:靜默失效的 build flag、跨 repo 漏審的 companion CL、Treble 隔離邊界導致的 type 不可見。


基礎:三層保護模型

進架構之前先對齊基礎。SELinux 要保護一個節點(檔案、socket、property),三層缺一不可:

做什麼在哪裡少了會怎樣
Type 宣告定義一個新安全類型type sysfs_hdmi, file_type, sysfs_type;build fail:unknown type
標籤綁定把路徑綁到 typefile_contexts / property_contexts節點留在預設 label,規則不生效
授權規則允許某 domain 操作該 type.teallowavc: denied
# 1. Type 宣告
type sysfs_hdmi, file_type, sysfs_type;

# 2. 標籤綁定 (file_contexts)
/sys/class/hdmi/hdmi0/hpd_state  u:object_r:sysfs_hdmi:s0

# 3. 授權規則 (.te)
allow system_server sysfs_hdmi:file { open read };

少任何一層的後果不對稱:少 type 宣告會 build fail,少另外兩層則是 runtime 靜默失敗。後者更難察覺,debug 成本也更高。記住這個「三層完整性」,後面所有設計決策都繞著它轉。


架構:分層契約設計

分目錄不是架構

第一直覺通常是按 SoC 切平行目錄:

sepolicy/
├── soc/aml/
├── soc/rtk/
└── soc/mtk/

目錄分了,然後呢?最關鍵的幾個問題一個都沒回答:

  • sysfs_hdmisoc/aml/ 宣告,soc/rtk/ 也要用同名 type,誰負責宣告?
  • 多家 SoC 都有 HDMI 但 sysfs 路徑不同,共用的 allow 規則要每家重抄一份?
  • ifeq ($(DEVICE_SOC_AML), true) 這個 flag 沒人設 setter,整個目錄會怎樣?

該按的不是 SoC,是「可見性契約」:

sepolicy/
├── common/              # Tier 1: 跨 SoC 共用
│   ├── public/          #   匯出給所有 vendor .te 引用的 type
│   ├── *.te             #   共用 domain 的 allow
│   └── file_contexts    #   共用節點標籤

├── soc/                 # Tier 2: SoC 專屬
│   ├── aml/
│   ├── mtk/
│   └── rtk/

└── product/             # Tier 3: 產品/記憶體變體覆寫
    └── (PRODUCT_PRIVATE_SEPOLICY_DIRS)

Type 往上提,allow 往下沉

放哪為什麼
任何 SoC 可能引用的 type → common/public/vendor .te 只看得到 public type(Treble 隔離),放這才跨 SoC 可引用
SoC 專屬的 domain + allow → soc/<soc>/隔離,不污染別家
共用 domain 的 allow → common/別讓每家 SoC 各抄一份

這裡有個 Treble 的硬約束:vendor 分區的 .te 只能引用標記為 public 的 type。type 宣告放在 product-private,vendor 側 .te 一引用就 build fail —— 不是 runtime,是直接編不過。所以會被跨 SoC 引用的 type,一律丟進 common/public/,可見性問題就不存在了。


實戰踩坑

靜默失效的 build flag

最致命的不是 avc: denied,是整個 SoC 的 sepolicy 目錄無聲無息消失。

# ❌ 危險:多個獨立 boolean flag
ifeq ($(DEVICE_SOC_AML), true)
  BOARD_SEPOLICY_DIRS += vendor/<platform>/sepolicy/soc/aml
endif

DEVICE_SOC_AML 只要沒有任何 makefile 設定它,ifeq 永遠是 false。build 不報錯,只是靜默跳過整個目錄。你的 .te 一條都沒載入,build 照過,問題拖到 runtime 才爆,而且爆出來是一堆 avc: denied —— 你會以為規則寫錯,事實是規則根本沒載入。

換成單一 enum 變數拼路徑:

# device/<vendor>/<soc>/soc.mk
TARGET_SOC_FAMILY := aml   # 每個 SoC 的 device makefile 必須設

# 共用的 sepolicy.mk
BOARD_SEPOLICY_DIRS += vendor/<platform>/sepolicy/common
BOARD_SEPOLICY_DIRS += vendor/<platform>/sepolicy/soc/$(TARGET_SOC_FAMILY)

$(TARGET_SOC_FAMILY) 直接拼路徑比 ifeq 安全:變數沒設,路徑就變成 soc/,build 階段就因為目錄不存在而報錯,不會拖到 runtime。再補一道 assert 把失敗提到最前面:

ifeq ($(TARGET_SOC_FAMILY),)
  $(error TARGET_SOC_FAMILY not set - check device soc.mk)
endif

底層哲學跟處理記憶體變體一樣:變體是配置,不是分支。同一套 makefile、同一棵 sepolicy tree,靠變數驅動走不同路徑,而不是 fork 多份目錄各養各的。

跨 repo 的三層完整性

有人提 CL 刪掉某條 SELinux 規則,reviewer 看那個 CL 本身合理就 +2 了。但那個 type 的 file_contexts 綁在另一個 repo,需要一個 companion CL 一起移除 —— 沒人注意到,merge 完 build 炸。

這不是 reviewer 失職,是流程有洞。指望每個人都記得「看到刪 SELinux 規則要跑去另一個 repo 檢查」,本來就不該成為流程依賴。

設計上要做的是讓三層盡量收斂:

# ✅ AML 專屬節點,三層都在 soc/aml/
soc/aml/
├── sysfs_hdmi.te          # type 宣告 + allow
└── file_contexts          # 標籤綁定

# ❌ 三層散在不同位置
common/public/sysfs_hdmi.te     # type 宣告
soc/aml/file_contexts           # 標籤綁定
soc/aml/system_server.te        # allow

跨 repo / 跨 tier 拆三層技術上合法,build system 會把所有 BOARD_SEPOLICY_DIRS 合併編譯。但合法不等於好維護。目標是讓 reviewer 看一個 CL 就能判斷三層完不完整。所以 SoC 專屬的節點,type、file_contexts、allow 三層都壓在對應的 soc/<soc>/ 裡;只有真正跨 SoC 共用的才往上提到 common/public/

init_daemon_domain() 缺 file_contexts

init_daemon_domain(my_service) 宣告一個由 init 啟動的 service domain 時,SELinux 要知道「哪個 binary 對應這個 domain」,這資訊來自 file_contexts

# .te
init_daemon_domain(my_service)

# file_contexts(沒有的話 domain transition 不會發生)
/system/bin/my_service_binary  u:object_r:my_service_exec:s0

寫了 init_daemon_domain() 卻忘了在 file_contexts 標 binary 會怎樣?Binary 會以 init 的 domain 跑 —— 拿到 init 的全部權限。這是個實打實的權限放大問題,但 build 不會吭一聲。

這種事不該靠人 review 抓。AOSP 自帶 sepolicy_testscheckfc,能檢測 file_contexts 引用不存在的 type、binary 沒標籤這類問題。接進 CI 當 gating check,讓機器擋。

get_prop() 的冗餘

手動展開 macro 已經包含的權限:

# ❌ 冗餘:get_prop() 已經含 open/read/getattr
allow my_domain my_prop:file { open read getattr };
get_prop(my_domain, my_prop)

# ✅ macro 已經處理了
get_prop(my_domain, my_prop)

get_prop() 展開就是 allow ... file { open read getattr map },手動再寫一遍不會壞,但 policy 會膨脹,而且會誤導 reviewer 以為「這裡有特殊需求」—— 其實沒有。這類冗餘丟給 lint 掃就好。


延伸:產品變體的 Tier 3

平台有記憶體變體(2GB / 4GB STB)的話,偶爾會有 sepolicy 差異 —— 比如某個記憶體優化 service 只在 2GB 機型存在。

PRODUCT_PRIVATE_SEPOLICY_DIRS += \
    vendor/<platform>/sepolicy/product/$(MEMORY_VARIANT)

讓它跟 soong_config、manifest overlay 對齊,「變體即配置」這套哲學從 manifest、build flag 一路貫穿到 sepolicy。


總結

決策原則反模式
Type 可見性跨 SoC 引用的 type → common/public/public type 放 product-private,vendor .te 編不過
SoC 隔離專屬三層收斂在 soc/<soc>/三層散在不同 repo / tier,companion CL 漏審
Build flag單一 enum 變數 + 路徑拼接 + assert多個 boolean flag,無 setter 時靜默失效
完整性檢查checkfc / sepolicy_tests 進 CI靠 reviewer 人腦檢查
產品變體Tier 3 走 PRODUCT_PRIVATE_SEPOLICY_DIRSfork sepolicy 目錄各養各的
Macro信任展開,不手動重複get_prop() 旁邊再補 allow ... open read

multi-SoC sepolicy 到後面不是技術問題,是治理問題:誰定義契約、誰驗證契約。目錄結構只是契約的外殼,真正要釘死的是三件事 —— type 宣告的可見性邊界有沒有定義清楚、三層完整性能不能被機器驗證、build flag 失效會不會在早期就被攔下來。這三件做好,剩下的就是按表操課。


備註:TARGET_SOC_FAMILY 是示意用的變數名,AOSP 沒有這個標準變數。實作時請依各平台慣例選用對應的 SoC 識別變數。