Skip to content

权限与安全模型:从 UID 沙箱到 SELinux 域 ​

本页导读:用户点下的「允许/拒绝」只是这座安全大厦的门铃。本页自上而下拆四层防线——UID 进程沙箱、seccomp 与命名空间、SELinux 域与 MCS、权限框架与 AppOps,并给出 Android 11~16 的收紧时间线。前置:架构篇 init/Zygote 与 Binder 页;进程模型。

一、多层防御总览 ​

防线拦什么代表机制/文件
UID 沙箱应用互相读写、冒充身份一 App 一 UID,zygote 降权 fork
seccomp危险/过时系统调用按 ABI 的内核过滤(bionic 装载)
命名空间文件系统视图(isolated 进程尤其严)mount ns / default ns(以 AOSP 为准)
SELinux即便拿到 UID 也碰不到的对象untrusted_app 域 + MCS 分级
权限框架能力授予的业务语义PermissionManagerService + PermissionController
AppOps「有权限也限制使用频次/状态」AppOpsManager op 表

四层不是冗余,是纵深:越界攻击要同时突破内核过滤、MAC 策略与权限记账,任何一层都是独立审计对象。

顺带澄清 targetSdk 的角色:它不解锁安全策略,只解锁行为兼容(比如旧弹窗样式);安全层的安全默认值几乎都按设备版本一刀切。

二、UID 沙箱:一切隔离的锚点 ​

  • 安装时分配 UID(多用户下 userId*100000 + appId 编码),所有进程由 Zygote fork 后降权到该 UID 再执行应用代码;adb shell id -u 进应用进程可见 u0_a123 风格身份。
  • android:isolatedProcess 进一步进 9999x 保留段,无数据目录、无网络默认,只留给渲染/媒体解码这类「拿代码执行权但不给数据权」的场景。
  • WebView 的渲染进程走 App Zygote:应用自己 ZygoteProcess fork 的子沙箱(共享预加载),这是「一个 App 里还有自己的 Zygote」的正解。
  • 同签名 + 历史 sharedUserId 的共 UID 玩法已停止新增(Android 10+ 安装器逐步拒绝),跨 App 共享请走 FileProvider/绑定服务。

沙箱的直接后果:应用代码碰不到别的 UID 的 /data、socket、进程信号——「互相拉起保活」在 UID 层面本就该难,能保活的年代是策略放水,不是模型允许。

bash
adb shell ps -A -o PID,USER,NAME | grep com.example
  # u0_a123  = userId 0 + appId 123;换个工作资料用户则是 u10_a123
  # 同 App 的所有进程(:remote 等)共享同一 UID——「拆进程提优先级」不存在的根据

三、seccomp 与命名空间:收窄内核表面积 ​

  • seccomp-bpf:进程启动时装载按 ABI 的 allowlist 过滤器,不在名单的 syscall 直接 SIGSYS。NDK 可调用面(restricted NDK API)就是它的用户态投影——新内核 API 逐个解禁、危险老调用永久拉黑。
  • 命名空间:isolated 进程拥有严格裁剪的 mount ns;普通应用进程运行在共享的 default mount ns(Android 12~16 的 ns 口径有演进,进程 mount ns 化以 AOSP release notes 为准)。
  • 装载点可以在 Zygote 的 fork-specialize 路径读到(native):

验证进程当前 seccomp 状态(比读策略文件更直接):

bash
adb shell cat /proc/$(pidof com.example | cut -d' ' -f1)/status | grep -i seccomp
  # Seccomp: 2  ← 2 = filter mode(已装载 bpf 过滤器)
cpp
// frameworks/base/core/jni/com_android_internal_os_Zygote.cpp( specialize 阶段骨架大意)
static void specializeAppProcess(...) {
    // 1. uid/gid/caps 降级、补充组
    // 2. seccomp filter 装载(按 targetSdk/ABI 选择策略版本)
    // 3. rlimits / scheduler policy
    // 4. 命名空间与 SELinux 上下文切换
    //    SELinux 按 seapp_contexts 计算目标域 + MCS 级别
}

版本注记:基于 Android 16 / API 36;Android 5 起 SELinux enforcing 全量、6 起 NDK seccomp 收紧、11 起 seccomp 策略与 targetSdk 绑定(旧目标应用更宽);Android 12~15 差异:进程 mount ns 实验推进(以 AOSP 为准)。

四、SELinux:域 + 级别,越权也撞墙 ​

  • 域转换:Zygote fork 出应用进程时,按 system/sepolicy 的 seapp_contexts 规则从 zygote 域切到 untrusted_app(系统应用按签名/分区进 platform_app/priv_app/app_zygote 等)。
  • MLS/MCS 级别:s0:c512:c1024... 的类别由 userId 与 appId 哈希生成——同一 App 同用户同级别,跨用户互不可见;这就是「工作资料里的进程摸不到主资料文件」的底层实现。
  • neverallow 规则让「加白名单」在代码评审层面就不可能合入 AOSP(如 untrusted_app 禁访别的 App 数据目录)。
  • 调试视角:adb shell dmesg | grep avc(userdebug)看 denial;应用市场审核与厂商 ROM 的「额外权限」最终都要落在 sepolicy 上,拿不到域内白名单就是物理不可能。

seapp_contexts 的规则行长这样(理解格式即可读全局域分配):

text
  # system/sepolicy/public/seapp_contexts(示意行)
user=_app seinfo=... domain=untrusted_app levelFrom=all
  # 关键词:user/isolate 匹配进程身份 → domain 决定 SELinux 域 →
  # levelFrom 决定 MCS 类别如何从 appId/userId 推导
  • 全局只有 enforcing 一种真相:user 版 SELinux 不可关闭(早期 setenforce 0 后门早已封死),厂商想加能力必须写 sepolicy 并过 CTS。
bash
adb shell ps -Z | grep com.example      # 每行的第一个字段就是进程 SELinux 上下文
  # u:r:untrusted_app:s0:c192,c256,c257,c258,c259,c260,c261 ← 域 + MCS 级别

五、权限框架:安装时、运行时、一次性 ​

protection level 决定「谁来批」:

级别授予方式例
normal安装即得(manifest 声明)INTERNET、VIBRATE
dangerous运行时弹窗(用户可撤)CAMERA、LOCATION、READ_MEDIA_*
signature/privileged同签名/特权分区SIGNATURE 类系统能力
appop/特殊设置页/对话框单独开通SYSTEM_ALERT_WINDOW、MANAGE_EXTERNAL_STORAGE

运行时权限的链路要点:

  1. requestPermissions 发起 → 系统权限 UI(AOSP 为 PermissionController,Android 13+ 授权流程下沉为系统侧服务逻辑)→ 结果写 PermissionManagerService 台账 → 回调 onRequestPermissionsResult/ActivityResultContracts。
  2. 可撤回是常态:用户随时在设置页撤销;代码必须按「权限可能从有到无」运行(监听 onCallbackRequired/AppOps 变更,相机/定位服务要能中途降级)。

1.5 权限组的「一次授权全组」已死:Android 11 起同组权限逐项询问、逐项可撤,RECORD_AUDIO 不再因 CAMERA 顺手给——任何「组授权」假设的代码都要按单项重写。 3. 一次性权限(Android 11):相机/麦克风出现「仅这一次」选项——授予随会话存在,进程结束即撤回,无需应用配合。 4. 自动重置(Android 11):长期未使用的 App,其运行时权限被系统静默撤销并通知——「装了就永远有」不存在。

bash
adb shell pm grant com.example android.permission.CAMERA   # 测试三连
adb shell pm revoke com.example android.permission.CAMERA
adb shell pm reset-permissions com.example                 # 恢复初始(需 root)

请求-回调的完整时序(以 Activity Result API 口径):

```text
requestPermissions(permArray, n)
 └ PermissionManagerService 记录「等待结果」
    └ 拉起 PermissionController UI(可能连弹多个)
       └ 用户选择 → 台账更新(grant/revoke + flags)
          └ onRequestPermissionsResult / ActivityResult 回调
             └ 进程若在弹窗期间被回收:结果丢,重开后重新发起——别把 UI 状态押在弹窗上

## 六、AppOps 与特殊权限:有资格 ≠ 随便用

AppOps 给「运行时行为」记账:同一个 CAMERA 权限,op 状态可以是 `MODE_ALLOWED / MODE_IGNORED / MODE_ERRORED / MODE_DEFAULT`——IGNORED 表现为 API 静默失败(相机黑帧),ERRORED 直接抛 SecurityException,这是「权限给了却像没给」类工单的底层原因。

- 查看:`adb shell appops query`(或 `dumpsys appops`);改:`adb shell appops set <pkg> SYSTEM_ALERT_WINDOW allow` 之类——特殊权限(悬浮窗、安装未知应用、所有文件访问、精确闹钟)在实现上都是 appop + 专属设置页。
- 敏感 op 还有**使用计数与审计**(`AppOpsManager.startOperation/getHistoricalOps`),隐私仪表盘的数据源就是它——滥用某能力会被平台看到,这是 Play 政策执法的技术依据。

```bash
adb shell dumpsys appops | sed -n '1,40p'     # 全部 op 的当前模式(输出较长)
adb shell appops get --user 0 com.example COARSE_LOCATION
  # 常见审计字段:时间窗、持续时间、拒绝次数(版本字段名以 AOSP 为准)
  • 工程注意:定位的「精确/大致」(Android 12)、「仅使用期间」(Android 10 起 while-in-use 语义)都同时横跨权限与 appop 两层,排查要两头看。
kotlin
// 入口三连查:权限台账 / AppOps 实际状态 / 硬件在位
fun cameraUsable(ctx: Context): Boolean =
    ctx.checkSelfPermission(Manifest.permission.CAMERA) == PERMISSION_GRANTED
        && (ctx.getSystemService(AppOpsManager::class.java)
                .unsafeCheckOpNoThrow(AppOpsManager.OPSTR_CAMERA,
                        Process.myUid(), ctx.packageName)
            == AppOpsManager.MODE_ALLOWED)
        && ctx.packageManager.hasSystemFeature(FEATURE_CAMERA_ANY)

七、Android 11 → 16 收紧年表 ​

text
API 30  一次性权限;权限自动重置;后台位置弹窗独立化;软件键盘类收紧
API 31  精确/大致定位拆分;BLUETOOTH_SCAN/CONNECT/ADVERTISE 新运行时权限
API 32  (小幅,无用户面重点)
API 33  POST_NOTIFICATIONS 成为运行时权限;NEARBY_WIFI_DEVICES;READ_MEDIA_* 取代存储三件套
API 34  部分照片/视频访问(READ_MEDIA_VISUAL_USER_PROPERTIES + 用户选择);FGS 类型与权限强绑定;Health Connect 数据类走向标准化
API 35  Health Connect 以 android.permission.health.* 体系接管健康数据
API 34+ 后台位置继续加压:while-in-use 未授予则 FGS 定位类直接崩
API 36  延续「使用期间」与可见性绑定(如 while-in-use 类 FGS 的退后台受限),新能力以行为变更文档为准

一句话史观:Android 安全模型的十年是把「授予」从一次性事件改造成持续谈判——每一次使用都在弹窗、设置页、审计表与系统限流里重新确认。

八、常见误区问答 ​

Q1:targetSdk 不升是不是还能躲过新权限? 躲得过弹窗,躲不过平台底线:部分安全策略(seccomp 版本、FGS 限制、敏感路径拦截)按设备版本生效,与 target 无关;且商店上架门槛绑定 target 晋级。

Q2:弹窗拒绝了两次会不会永久拉黑? 系统会在连续拒绝后把选项变灰/改为「去设置」,逻辑由 PermissionController 裁决(各厂商 UI 有差异);应用侧正确姿势:shouldShowRequestPermissionRationale 判时机、永久拒绝引导设置页,别循环弹窗骚扰。

Q3:查 checkSelfPermission 返回 GRANTED 就万事大吉? 它只看权限台账;AppOps 可能 IGNORED、FGS 可能缺类型、用户可能刚撤销。关键功能入口要做「权限 + op + 硬件可用性」三连查。

Q4:厂商 ROM 的「自启动/后台弹出」开关算哪层? 都不算——它是厂商在权限框架之外私加的策略层,没有公开 API 与稳定语义;测试矩阵要单独覆盖,但架构决策不能依赖它。

本页小结:安全是四层纵深——UID 沙箱定进程边界,seccomp/命名空间收内核面,SELinux 域+MCS 封 MAC 越界,权限框架与 AppOps 管「能力语义」;运行时时代的核心心智是授予可撤回、使用可审计、后台必受限,Android 11~16 的每一项新弹窗都站在这条主线上。

参考链接 ​

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处