你以為 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 |
| 標籤綁定 | 把路徑綁到 type | file_contexts / property_contexts | 節點留在預設 label,規則不生效 |
| 授權規則 | 允許某 domain 操作該 type | .te 的 allow | avc: 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_hdmi在soc/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_tests 和 checkfc,能檢測 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_DIRS | fork sepolicy 目錄各養各的 |
| Macro | 信任展開,不手動重複 | get_prop() 旁邊再補 allow ... open read |
multi-SoC sepolicy 到後面不是技術問題,是治理問題:誰定義契約、誰驗證契約。目錄結構只是契約的外殼,真正要釘死的是三件事 —— type 宣告的可見性邊界有沒有定義清楚、三層完整性能不能被機器驗證、build flag 失效會不會在早期就被攔下來。這三件做好,剩下的就是按表操課。
備註:
TARGET_SOC_FAMILY是示意用的變數名,AOSP 沒有這個標準變數。實作時請依各平台慣例選用對應的 SoC 識別變數。