Skip to content

Seapp Contexts

从 PackageManager 生成 seinfo、seapp_contexts 构建与排序,追踪到 Zygote 进程域切换、App 数据目录 type 与 MLS categories。

基于android-17.0.0_r1
AndroidSELinuxseapp_contextsZygoteseinfoinstalld源码阅读

Seapp Contexts ​

本文面向已经读过 安全上下文、Type与Attribute、File Contexts 和 Property Contexts 的读者。前文解释了文件路径、属性名如何映射到安全 Context;本篇处理 App 特有的映射:同一个 UID、seinfo、包名和安装分区,怎样分别产生进程 domain 与数据目录 type,并附加多用户 MLS/MCS categories。

seapp_contexts 不是 PackageManager 的应用分类表,也不是 Zygote 的进程启动清单。它是一组有优先级的输入选择器和输出字段,由两个不同消费者使用:Zygote 子进程请求 SEAPP_DOMAIN,installd/libselinux 为 App data restorecon 请求 SEAPP_TYPE。把这两条链混在一起,会得到“进程域正确但数据目录标签错误”或“改了 type 却期待进程 domain 改变”之类错误判断。

本文以 Android 17 android-17.0.0_r1 为基线。读完后,读者应能从 PackageManager 生成的 seinfo 追到 AMS、Zygote、libselinux 的第一条匹配规则,再从 installd 追到 App data xattr;还能解释 target SDK、priv-app、instant app、SDK sandbox、PCC、多用户 category、分区来源和 enforcing/permissive 分支分别在哪里生效。

1. 两条输出链 ​

1.1 进程Domain ​

Zygote fork 出子进程后调用 selinux_android_setcontext()。libselinux 以 SEAPP_DOMAIN 查询规则,只接受包含 domain= 的匹配项,把当前 Context 的 type 字段替换为目标 domain,随后通过 setcon() 动态切换。

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

cpp
const char* se_info_ptr = se_info.has_value()
        ? se_info.value().c_str() : nullptr;

if (selinux_android_setcontext(uid, is_system_server,
                               se_info_ptr, nice_name_ptr) == -1) {
    fail_fn(CREATE_ERROR(
            "selinux_android_setcontext(%d, %d, \"%s\", \"%s\") failed",
            uid, is_system_server, se_info_ptr, nice_name_ptr));
}

这里的 nice_name_ptr 同时充当 seapp 的 pkgname 输入。普通 App 通常是包名或进程 nice name;若规则指定 name= 而调用方没有提供名字,该规则不会匹配。

1.2 数据目录Type ​

App data 的标签不能仅由 /data/user/... 路径正则决定,因为不同应用需要不同 type 和 categories。libselinux 先用 file contexts 得到基础 Context,再对 App data 路径调用 seapp_context_lookup(SEAPP_TYPE, ...),只消费包含 type= 的规则。

源码文件:external/selinux/libselinux/src/android/android_device.c

c
ctx = context_new(orig_ctx_str);
if (!ctx)
    goto err;

rc = seapp_context_lookup(SEAPP_TYPE,
                          info ? info->uid : uid,
                          false,
                          info ? info->seinfo : seinfo,
                          info ? info->name : pkgname,
                          ctx);
if (rc < 0)
    goto err;

ctx_str = context_str(ctx);
if (!ctx_str)
    goto err;

if (!strcmp(ctx_str, orig_ctx_str))
    goto out;

rc = security_check_context(ctx_str);
if (rc < 0)
    goto err;

freecon(*secontextp);
*secontextp = strdup(ctx_str);

SEAPP_TYPE 不改变 user、role 或已有基础 Context 的其他字段,只替换 type,并按规则设置 range。随后 restorecon 才把结果写入 security.selinux xattr。

1.3 共享输入 ​

两条链共享这些输入:

输入所有者作用
UIDAMS、installd 调用参数计算 Android user、appId 和特殊 UID 类别
isSystemServerZygote 调用参数只在进程 domain 查询中区分 system_server
seinfoPackageManager 生成并传递基础标签、priv/instant/SDK 等修饰符、target SDK、分区
package/name进程启动参数或 packages.list参与 name= 精确/前缀选择
当前 ContextZygote 当前进程或 file contexts 基础结果提供要被替换的 type/range 之外字段

2. 输入向量 ​

2.1 选择器全集 ​

平台文件头是 Android 17 最直接的格式契约。所有已指定 selector 必须同时匹配;未指定字段不是统一意义上的 false,部分布尔字段有默认 false,isEphemeralApp 和 isPrivApp 则另外记录“是否指定”。

源码文件:system/sepolicy/private/seapp_contexts

yaml
# Input selectors:
#       isSystemServer (boolean)
#       isEphemeralApp (boolean)
#       user (string)
#       seinfo (string)
#       name (string)
#       isPrivApp (boolean)
#       minTargetSdkVersion (unsigned integer)
#       fromRunAs (boolean)
#       isIsolatedComputeApp (boolean)
#       isSdkSandboxNext (boolean)
#       isSdkSandboxAudit (boolean)
#
# Outputs:
#       domain (string)
#       type (string)
#       levelFrom (string; one of none, all, app, or user)
#       level (string)

domain= 缺失的规则在 domain 查询中跳过;type= 缺失的规则在 data type 查询中跳过。同一输入 selector 可以理论上由不同规则分别提供 domain/type,但完全重复的 selector 会被 loader 和 checkseapp 拒绝,因此通常一条规则同时写两种输出。

2.2 UID分类 ​

libselinux 先把多用户 UID 拆成 userid 和 appid。小于 AID_APP_START 的固定 UID 通过 getpwuid(appid) 得到 system、radio 等用户名;普通 App、SDK sandbox、PCC component 和 isolated UID 映射为特殊 user 字符串。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
userid = uid / AID_USER_OFFSET;
appid = uid % AID_USER_OFFSET;

if (appid < AID_APP_START) {
    pwd = seapp_getpwuid(appid);
    if (!pwd)
        goto err;
    username = pwd->pw_name;
} else if (appid < AID_SDK_SANDBOX_PROCESS_START) {
    username = "_app";
    appid -= AID_APP_START;
} else if (appid < AID_PCC_COMPONENT_PROCESS_START) {
    username = "_sdksandbox";
    appid -= AID_SDK_SANDBOX_PROCESS_START;
} else if (appid <= AID_PCC_COMPONENT_PROCESS_END) {
    username = "_pcc_component";
} else {
    username = "_isolated";
    appid -= AID_ISOLATED_START;
}

if (appid >= CAT_MAPPING_MAX_ID || userid >= CAT_MAPPING_MAX_ID)
    goto err;

注意 category 计算使用的是归一化后的 appid。普通 App UID 10001 被转换为 appid 1;user 10 的同一个 App UID 是 1010001,仍得到 appid 1,但 userid 变为 10。

2.3 Seinfo编码 ​

PackageManager 提供的 seinfo 不是一个简单枚举。第一个冒号前是 base,如 default、platform、media;后续 token 编码 priv-app、instant app、isolated compute、SDK sandbox 模式、run-as、target SDK 和安装分区,并由 complete 截止。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
for (token = strtok_r(local_seinfo, ":", &saved_colon_ptr);
     token;
     token = strtok_r(NULL, ":", &saved_colon_ptr)) {
    if (first) {
        strncpy(info->base, token, SEINFO_BUFSIZ);
        first = false;
        continue;
    }
    if (!strcmp(token, "privapp")) {
        info->is |= IS_PRIV_APP;
        continue;
    }
    if (!strcmp(token, "ephemeralapp")) {
        info->is |= IS_EPHEMERAL_APP;
        continue;
    }
    if (!strcmp(token, "isolatedComputeApp")) {
        info->is |= IS_ISOLATED_COMPUTE_APP;
        continue;
    }
    if (!strcmp(token, "isSdkSandboxAudit")) {
        info->is |= IS_SDK_SANDBOX_AUDIT;
        continue;
    }
    if (!strcmp(token, "isSdkSandboxNext")) {
        info->is |= IS_SDK_SANDBOX_NEXT;
        continue;
    }
    if (!strcmp(token, "fromRunAs")) {
        info->is |= IS_FROM_RUN_AS;
        continue;
    }
    /* targetSdkVersion=、partition= 和 complete 分支省略。 */
}

未知 token 只记录 warning 并忽略;缺少 targetSdkVersion= 的值或 seinfo 长度达到 buffer 上限则返回错误。seapp_contexts 的 seinfo= selector 只比较 info.base,修饰符由各自 boolean/SDK/partition 逻辑使用。

2.4 包名匹配 ​

user= 与 name= 都允许末尾 * 前缀匹配,并且字符串比较不区分大小写。只有末尾星号有前缀语义;规则中的 com.google.android.gms.* 会匹配以该文本开头的名字,而 * 不提供通用正则表达式。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
if (cur->name.str) {
    if (!pkgname)
        continue;

    if (cur->name.is_prefix) {
        if (strncasecmp(pkgname, cur->name.str,
                        cur->name.len - 1))
            continue;
    } else {
        if (strcasecmp(pkgname, cur->name.str))
            continue;
    }
}

这里与 checkseapp neverallow 中的 PCRE 不是同一种语言:普通 rule 的 name= 只支持固定值/末尾星号,neverallow selector 才支持完整 PCRE。

3. Seinfo来源 ​

3.1 签名策略 ​

SELinuxMMAC 依次加载 platform、system_ext、product、vendor、odm 的 mac_permissions.xml。它将签名与可选包名 refinement 转换为 Policy,排序后保存到共享列表;任何 XML 解析失败都会使整个加载返回 false,不保留部分结果。

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
private static final List<Policy> sPolicies = new ArrayList<>();
private static boolean sPolicyRead;

static {
    sMacPermissions.add(new File(
            Environment.getRootDirectory(),
            "/etc/selinux/plat_mac_permissions.xml"));

    final File systemExt = new File(
            Environment.getSystemExtDirectory(),
            "/etc/selinux/system_ext_mac_permissions.xml");
    if (systemExt.exists()) sMacPermissions.add(systemExt);

    final File product = new File(
            Environment.getProductDirectory(),
            "/etc/selinux/product_mac_permissions.xml");
    if (product.exists()) sMacPermissions.add(product);

    final File vendor = new File(
            Environment.getVendorDirectory(),
            "/etc/selinux/vendor_mac_permissions.xml");
    if (vendor.exists()) sMacPermissions.add(vendor);

    final File odm = new File(
            Environment.getOdmDirectory(),
            "/etc/selinux/odm_mac_permissions.xml");
    if (odm.exists()) sMacPermissions.add(odm);
}

mac permissions 的 XML 语法由后续专题负责;本篇只追踪其产物 seinfo 怎样成为 seapp selector 输入。

3.2 修饰符拼接 ​

getSeInfo() 先按签名/包名选择 base,找不到时使用 default;随后附加 :privapp、:targetSdkVersion=N 和可选的 :partition=...。

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
String seInfo = null;
synchronized (sPolicies) {
    if (sPolicyRead) {
        for (Policy policy : sPolicies) {
            seInfo = policy.getMatchedSeInfo(pkg);
            if (seInfo != null) break;
        }
    }
}

if (seInfo == null) seInfo = DEFAULT_SEINFO;
if (isPrivileged) seInfo += ":privapp";

seInfo += ":targetSdkVersion=" + targetSdkVersion;

String partition = getPartition(packageState);
if (!partition.isEmpty()) {
    seInfo += ":partition=" + partition;
}
return seInfo;

isPrivileged 来自 PackageState 或 shared user。安装在 /system/app 并不自动等于 priv-app;selector 使用的是 PackageManager 计算后写入 seinfo 的 bit。

3.3 Shared UID ​

共享 UID 的所有包必须落在同一 SELinux domain。getTargetSdkVersionForSeInfo() 使用 shared user 保存的最低 target SDK,选择较旧、限制较少的 domain,并刻意不在安装/更新时立即降低该值,直到下次启动重新计算,避免同一 shared UID 的进程在运行期间改变分类。

源码文件:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java

java
if ((sharedUser != null) && (sharedUser.getPackages().size() != 0)) {
    return sharedUser.getSeInfoTargetSdkVersion();
}

final ApplicationInfo appInfo =
        AndroidPackageUtils.generateAppInfoWithoutState(pkg);
if (compatibility.isChangeEnabledAndReportInternal(
        SELINUX_LATEST_CHANGES, appInfo, false)) {
    return Math.max(Build.VERSION_CODES.CUR_DEVELOPMENT,
                    pkg.getTargetSdkVersion());
} else if (compatibility.isChangeEnabledAndReportInternal(
        SELINUX_R_CHANGES, appInfo, false)) {
    return Math.max(Build.VERSION_CODES.R,
                    pkg.getTargetSdkVersion());
}
return pkg.getTargetSdkVersion();

因此 minTargetSdkVersion= 匹配的输入不一定是 manifest 原始 target SDK;兼容性 change 和 shared UID 都能调整它。

3.4 User后缀 ​

每用户状态通过 seInfoUser 追加 instant-app 标志和终止 token。普通安装返回 :complete,instant app 返回 :ephemeralapp:complete。

源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/SELinuxUtil.java

java
public static String getSeinfoUser(PackageUserState userState) {
    if (userState.isInstantApp()) {
       return ":ephemeralapp:complete";
    }
    return ":complete";
}

complete 使 native parser 停止处理后续 selector。ProcessList 在 selinuxInputSelector() feature flag 开启时,会把 SDK sandbox 的附加 selector 放在 seInfoUser 之前,避免被 complete 截断。

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

java
String extraInfo = "";
if (app.isSdkSandbox) {
    if (getProcessListSettingsListener()
            .applySdkSandboxRestrictionsNext()) {
        extraInfo = APPLY_SDK_SANDBOX_NEXT_RESTRICTIONS;
    } else if (selinuxSdkSandboxAudit()
            && getProcessListSettingsListener()
                    .applySdkSandboxRestrictionsAudit()) {
        extraInfo = APPLY_SDK_SANDBOX_AUDIT_RESTRICTIONS;
    }
}

if (selinuxInputSelector()) {
    return app.info.seInfo + extraInfo
            + TextUtils.emptyIfNull(app.info.seInfoUser);
} else {
    return app.info.seInfo
            + TextUtils.emptyIfNull(app.info.seInfoUser)
            + extraInfo;
}

这条条件路径说明 seinfo token 顺序属于协议的一部分,不是日志展示格式。

4. 规则装载 ​

4.1 分区路径 ​

libselinux 为五个分区各定义正式路径和根目录兼容路径。每个分区只选第一个存在的 alternative,同时记录该文件属于 platform 侧还是 vendor 侧。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
static const path_alts_t seapp_context_paths = { .paths = {
    {
        "/system/etc/selinux/plat_seapp_contexts",
        "/plat_seapp_contexts"
    },
    {
        "/system_ext/etc/selinux/system_ext_seapp_contexts",
        "/system_ext_seapp_contexts"
    },
    {
        "/product/etc/selinux/product_seapp_contexts",
        "/product_seapp_contexts"
    },
    {
        "/vendor/etc/selinux/vendor_seapp_contexts",
        "/vendor_seapp_contexts"
    },
    {
        "/odm/etc/selinux/odm_seapp_contexts",
        "/odm_seapp_contexts"
    }
}, .partitions = {
    "system", "system_ext", "product", "vendor", "odm"
}};

4.2 一次初始化 ​

默认加载由 pthread_once 保护。Zygote 在 fork 前显式调用初始化,普通 lookup 也会兜底调用一次;运行时只有显式 selinux_android_seapp_context_reload() 才重新读文件。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
static void seapp_context_init(void)
{
    selinux_android_seapp_context_reload();
}

static pthread_once_t seapp_once = PTHREAD_ONCE_INIT;

void selinux_android_seapp_context_init(void)
{
    __selinux_once(seapp_once, seapp_context_init);
}

int seapp_context_lookup(enum seapp_kind kind, uid_t uid,
                         bool isSystemServer, const char *seinfo,
                         const char *pkgname, context_t ctx)
{
    selinux_android_seapp_context_init();
    return seapp_context_lookup_internal(kind, uid, isSystemServer,
                                         seinfo, pkgname, ctx);
}

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

cpp
// security_getenforce is not allowed on app process. Initialize and cache
// the value before zygote forks.
gIsSecurityEnforced = security_getenforce();

selinux_android_seapp_context_init();

预加载避免每个 App 子进程首次读取 contexts 文件,也确保 fork 后受限 domain 不需要自行打开这些策略文件。

4.3 解析失败 ​

loader 先统计所有非空、非注释行,再为每行分配结构体。未知 key、重复字段、非法 bool、非法 target SDK、seinfo 中包含冒号、错误 levelFrom 或没有 name 对应专用 seinfo 的不安全配置都会释放全部已加载规则并返回失败。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
if (!cur->isPrivApp && cur->name.str &&
    (!cur->seinfo || !strcmp(cur->seinfo, "default"))) {
    selinux_log(SELINUX_ERROR,
        "%s: No specific seinfo value specified with name=\"%s\", "
        "on line %u: insecure configuration!\n",
        seapp_contexts_files[i], cur->name.str, lineno);
    free_seapp_context(cur);
    goto err;
}

cur->partition = seapp_contexts_partitions[i];
seapp_contexts[nspec++] = cur;

专门按包名给非 priv-app 分配 domain 时必须有非 default seinfo,这阻止任意使用同一包名的普通签名应用获得专用域。

5. 排序优先级 ​

5.1 Comparator ​

加载完成后,libselinux 对所有分区 entries 统一 qsort()。文件中的行号不是运行时匹配优先级;比较器依次考虑 system server、ephemeral 是否指定、user specificity、seinfo、name specificity、priv 是否指定、较高 target SDK、run-as 和 platform/vendor 来源。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
static int seapp_context_cmp(const void *A, const void *B)
{
    const struct seapp_context *s1 =
            *(const struct seapp_context *const *)A;
    const struct seapp_context *s2 =
            *(const struct seapp_context *const *)B;

    if (s1->isSystemServer != s2->isSystemServer)
        return s1->isSystemServer ? -1 : 1;

    if (s1->isEphemeralAppSet != s2->isEphemeralAppSet)
        return s1->isEphemeralAppSet ? -1 : 1;

    if (s1->user.str && !s2->user.str) return -1;
    if (!s1->user.str && s2->user.str) return 1;
    if (s1->user.str) {
        if (s1->user.is_prefix != s2->user.is_prefix)
            return s2->user.is_prefix ? -1 : 1;
        if (s1->user.is_prefix && s1->user.len != s2->user.len)
            return s1->user.len > s2->user.len ? -1 : 1;
    }

    /* seinfo、name、isPrivApp 的相同模式省略。 */

    if (s1->minTargetSdkVersion != s2->minTargetSdkVersion)
        return s1->minTargetSdkVersion > s2->minTargetSdkVersion ? -1 : 1;

    if (s1->fromRunAs != s2->fromRunAs)
        return s1->fromRunAs ? -1 : 1;

    bool p1 = is_platform(s1->partition);
    bool p2 = is_platform(s2->partition);
    if (p1 != p2) return p1 ? -1 : 1;
    return 0;
}

isIsolatedComputeApp、SDK sandbox next/audit 没有出现在 comparator 的独立层级中;它们默认 false 并在 lookup 阶段严格比较。若两条规则仅在这些 boolean 上不同,比较器认为优先级相同,但它们不是 duplicate,因为 duplicate 检查会继续比较这些字段。

5.2 固定与前缀 ​

固定 user/name 比带星号前缀优先;两个前缀之间更长者优先。例如 name=com.google.android.gms 在 name=com.google.android.gms.* 之前,后者又在没有 name selector 的 priv-app catch-all 之前。

5.3 平台边界 ​

system、system_ext、product 被视为 platform,vendor、odm 被视为 vendor。只有前面所有 selector priority 都相同时,platform entry 才优先。这不是“platform 永远覆盖 vendor”;更具体的 vendor selector 仍可能排在宽泛 platform rule 前,但预装分区校验和 coredomain 检查会限制其可用输出。

5.4 重复规则 ​

排序后 loader 在相同优先级的连续区间中比较完整 input selectors。user、seinfo、name、priv、ephemeral、isolated compute、SDK audit/next 相同即判为 duplicate,整个加载失败。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
bool dup =
    (!s1->user.str || !strcmp(s1->user.str, s2->user.str)) &&
    (!s1->seinfo || !strcmp(s1->seinfo, s2->seinfo)) &&
    (!s1->name.str || !strcmp(s1->name.str, s2->name.str)) &&
    (!s1->isPrivAppSet || s1->isPrivApp == s2->isPrivApp) &&
    (!s1->isEphemeralAppSet ||
        s1->isEphemeralApp == s2->isEphemeralApp) &&
    (s1->isIsolatedComputeApp == s2->isIsolatedComputeApp) &&
    (s1->isSdkSandboxAudit == s2->isSdkSandboxAudit) &&
    (s1->isSdkSandboxNext == s2->isSdkSandboxNext);

if (dup) {
    selinux_log(SELINUX_ERROR,
                "seapp_contexts: Duplicated entry\n");
    goto err_no_log;
}

6. 构建链路 ​

6.1 分区Module ​

Soong 为五个分区分别生成 seapp contexts。平台 module 只依赖 platform policy,确保它不依赖其他分区;system_ext/product/vendor/odm 使用完整 precompiled policy。后续分区还把前面分区的 seapp files 作为 neverallow_files 输入。

源码文件:system/sepolicy/contexts/Android.bp

make
se_policy_binary {
    name: "plat_precompiled_sepolicy",
    srcs: [":plat_sepolicy.cil"],
    installable: false,
}

seapp_contexts {
    name: "plat_seapp_contexts",
    defaults: ["contexts_flags_defaults"],
    srcs: [":seapp_contexts_files{.plat_private}"],
    sepolicy: ":plat_precompiled_sepolicy",
}

seapp_contexts {
    name: "vendor_seapp_contexts",
    defaults: ["contexts_flags_defaults"],
    srcs: [
        ":seapp_contexts_files{.plat_vendor}",
        ":seapp_contexts_files{.vendor}",
        ":seapp_contexts_files{.reqd_mask}",
    ],
    neverallow_files: [
        ":seapp_contexts_files{.plat_private}",
        ":seapp_contexts_files{.system_ext_private}",
        ":seapp_contexts_files{.product_private}",
    ],
    soc_specific: true,
    sepolicy: ":precompiled_sepolicy",
}

6.2 三步生成 ​

buildSeappContexts() 的三步是:对 neverallow sources 做 m4 并提取行首 neverallow;对当前分区 contexts 做通用 m4;调用 checkseapp 校验并输出过滤后的 contexts。

源码文件:system/sepolicy/build/soong/selinux_contexts.go

go
func (m *selinuxContextsModule) buildSeappContexts(
        ctx android.ModuleContext, inputs android.Paths) android.Path {
    neverallowFile := pathForModuleOut(ctx, "neverallow")
    ret := pathForModuleOut(ctx, "checkseapp", m.stem())

    flags := m.getBuildFlags(ctx)
    m4NeverallowFile := pathForModuleOut(ctx, "neverallow.m4out")
    rule := android.NewRuleBuilder(pctx, ctx)
    rule.Command().
        Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
        Flag("--fatal-warnings").
        FlagForEachArg("-D", ctx.DeviceConfig().SepolicyM4Defs()).
        Flags(flagsToM4Macros(flags)).
        Inputs(android.PathsForModuleSrc(
                ctx, m.seappProperties.Neverallow_files)).
        FlagWithOutput("> ", m4NeverallowFile)

    rule.Command().
        Text("( grep").Flag("-ihe").Text("'^neverallow'").
        Input(m4NeverallowFile).Text(">").Output(neverallowFile).
        Text("|| true )")

    builtCtx := m.buildGeneralContexts(ctx, inputs)

    checkCmd := rule.Command().BuiltTool("checkseapp").
        FlagWithInput("-p ", android.PathForModuleSrc(
                ctx, proptools.String(m.seappProperties.Sepolicy))).
        FlagWithOutput("-o ", ret).
        Input(builtCtx).
        Input(neverallowFile)

    if m.shouldCheckCoredomain(ctx) {
        checkCmd.Flag("-c")
    }
    rule.Build("seapp_contexts", "Building seapp_contexts: "+m.Name())
    return ret
}

checkseapp 输出不包含 neverallow 行。运行时 libselinux 只读取真实 rule;neverallow 只在构建期约束这些输出。

6.3 Checkseapp字段 ​

工具维护一张 input/output key 表。domain 必须是 policy 中存在的 type;type 还必须具有 app_data_file_type;固定 level 必须通过 policy 的 MLS 校验。

源码文件:system/sepolicy/tools/check_seapp.c

c
key_map rules[] = {
    { .name = "isSystemServer", .dir = dir_in,
      .fn_validate = validate_bool },
    { .name = "isEphemeralApp", .dir = dir_in,
      .fn_validate = validate_bool },
    { .name = "user", .dir = dir_in },
    { .name = "seinfo", .dir = dir_in },
    { .name = "name", .dir = dir_in },
    { .name = "isPrivApp", .dir = dir_in,
      .fn_validate = validate_bool },
    { .name = "minTargetSdkVersion", .dir = dir_in,
      .fn_validate = validate_uint },
    { .name = "fromRunAs", .dir = dir_in,
      .fn_validate = validate_bool },
    { .name = "isIsolatedComputeApp", .dir = dir_in,
      .fn_validate = validate_bool },
    { .name = "isSdkSandboxAudit", .dir = dir_in,
      .fn_validate = validate_bool },
    { .name = "isSdkSandboxNext", .dir = dir_in,
      .fn_validate = validate_bool },
    { .name = "domain", .dir = dir_out,
      .fn_validate = validate_domain },
    { .name = "type", .dir = dir_out,
      .fn_validate = validate_type },
    { .name = "levelFrom", .dir = dir_out,
      .fn_validate = validate_levelFrom },
    { .name = "level", .dir = dir_out,
      .fn_validate = validate_selinux_level },
};

6.4 Type约束 ​

源码文件:system/sepolicy/tools/check_seapp.c

c
static bool validate_type(char *value, const char *filename,
                          int lineno, char **errmsg) {
    type_datum_t *type_dat = find_type(pol.db, value, TYPE_TYPE);
    if (!type_dat) {
        *errmsg = "Expecting a valid SELinux type";
        return false;
    }

    type_datum_t *attrib_dat = find_type(
            pol.db, "app_data_file_type", TYPE_ATTRIB);
    if (!attrib_dat) return true;

    if (!type_has_attribute(pol.db, type_dat, attrib_dat)) {
        *errmsg = "Missing required attribute app_data_file_type";
        return false;
    }
    return true;
}

因此 type=system_data_file 即使是合法 file type,也不能用于 seapp App data 输出,除非它属于 app_data_file_type。

6.5 Vendor核心域 ​

对 vendor/odm module,产品配置要求检查时,Soong 给 checkseapp 增加 -c。工具发现输出 domain 带 coredomain attribute 时记录 violation,并明确要求把条目移到 system/system_ext/product 或移除该 attribute。

7. 匹配循环 ​

7.1 全字段AND ​

runtime 按排序后的数组遍历,每个已指定 selector 都是 continue gate。target SDK 的方向是“应用值大于等于规则 minimum”;boolean default false 字段必须与 seinfo bit 完全相等。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
for (i = 0; i < nspec; i++) {
    cur = seapp_contexts[i];

    if (cur->isSystemServer != isSystemServer)
        continue;

    if (cur->isEphemeralAppSet &&
        cur->isEphemeralApp !=
            ((info.is & IS_EPHEMERAL_APP) != 0))
        continue;

    /* user、seinfo、name 匹配省略。 */

    if (cur->isPrivAppSet &&
        cur->isPrivApp != ((info.is & IS_PRIV_APP) != 0))
        continue;

    if (cur->minTargetSdkVersion > info.targetSdkVersion)
        continue;

    if (cur->fromRunAs != ((info.is & IS_FROM_RUN_AS) != 0))
        continue;

    if (cur->isIsolatedComputeApp !=
        ((info.is & IS_ISOLATED_COMPUTE_APP) != 0))
        continue;

    if (cur->isSdkSandboxAudit !=
        ((info.is & IS_SDK_SANDBOX_AUDIT) != 0))
        continue;

    if (cur->isSdkSandboxNext !=
        ((info.is & IS_SDK_SANDBOX_NEXT) != 0))
        continue;

7.2 输出过滤 ​

同一遍历根据 kind 跳过缺失输出,匹配后只替换 domain 或 type,再处理 range。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
    if (kind == SEAPP_TYPE && !cur->type)
        continue;
    else if (kind == SEAPP_DOMAIN && !cur->domain)
        continue;

    if (kind == SEAPP_TYPE) {
        if (context_type_set(ctx, cur->type))
            goto oom;
    } else if (kind == SEAPP_DOMAIN) {
        if (context_type_set(ctx, cur->domain))
            goto oom;
    }

    if (cur->levelFrom != LEVELFROM_NONE) {
        int res = set_range_from_level(ctx, cur->levelFrom,
                                       userid, appid);
        if (res != 0) return res;
    } else if (cur->level) {
        if (context_range_set(ctx, cur->level))
            goto oom;
    }
    break;
}

levelFrom 优先于固定 level:只有 levelFrom=none 时才应用 level=。parser 的 enum 初始值就是 NONE,所以没有 levelFrom 的 rule 仍可写固定 level。

7.3 无匹配差异 ​

domain 查询没有匹配时,在 enforcing 模式返回错误,避免 App 保留 Zygote domain;permissive 模式记录错误但返回成功。type 查询没有匹配时没有这段 fatal 分支,Context 保持 file contexts 给出的基础 type。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
if (kind == SEAPP_DOMAIN && i == nspec) {
    selinux_log(SELINUX_ERROR,
        "%s: No match for app with uid %d, seinfo %s, name %s\n",
        __FUNCTION__, uid, seinfo, pkgname);

    if (security_getenforce() == 1)
        goto err;
}
return 0;

这个不对称是故障定位的重要边界:进程 domain 缺失会阻止 enforcing 设备启动该子进程;App data type 缺失可能表现为目录保留通用 app_data_file 基础标签,而不是立刻中止。

7.4 分区警告 ​

预装在 system/system_ext/product 的 App 若匹配到 vendor/odm seapp rule,runtime 当前只记录 warning,源码带有“修复违规后改为 error”的 TODO。构建期 coredomain 检查和 seapp neverallow 仍可能更早阻止危险配置。

8. MLS Categories ​

8.1 四种模式 ​

levelFrom 把归一化 appid 和 Android userid 编码到四个 category 区间:App 低 8 位进入 c0-c255,高 8 位进入 c256-c511;user 低 8 位进入 c512-c767,高 8 位进入 c768-c1023。

源码文件:external/selinux/libselinux/src/android/android_seapp.c

c
switch (levelFrom) {
case LEVELFROM_NONE:
    strncpy(level, "s0", sizeof level);
    break;
case LEVELFROM_APP:
    snprintf(level, sizeof level, "s0:c%u,c%u",
             appid & 0xff,
             256 + (appid >> 8 & 0xff));
    break;
case LEVELFROM_USER:
    snprintf(level, sizeof level, "s0:c%u,c%u",
             512 + (userid & 0xff),
             768 + (userid >> 8 & 0xff));
    break;
case LEVELFROM_ALL:
    snprintf(level, sizeof level, "s0:c%u,c%u,c%u,c%u",
             appid & 0xff,
             256 + (appid >> 8 & 0xff),
             512 + (userid & 0xff),
             768 + (userid >> 8 & 0xff));
    break;
default:
    return -1;
}
return context_range_set(ctx, level) ? -2 : 0;

8.2 计算示例 ​

普通 App UID 10045:userid=0,归一化 appid=45。

levelFrom结果
nones0
apps0:c45,c256
users0:c512,c768
alls0:c45,c256,c512,c768

同一 App 在 Android user 10 的 UID 为 1010045,app categories 不变,user categories 变为 c522,c768。这使同一包的不同用户数据目录可由 MLS/MCS 隔离。

8.3 支持边界 ​

平台文件明确限制:levelFrom=user 只支持 _app 或 _isolated;levelFrom=app/all 只支持 _app。当前规则对 SDK sandbox 和 PCC 使用 levelFrom=all,其 UID 分类由新范围扩展;阅读历史规则时不能把旧注释边界直接外推到 Android 17 的新增 UID 类型,应以当前 loader、测试和生成规则为准。

8.4 溢出边界 ​

CAT_MAPPING_MAX_ID 为 2^16。userid 或归一化 appid 超出可唯一编码范围时 lookup 返回错误,避免 category 碰撞。

9. 进程切换 ​

9.1 AMS输入 ​

AMS 在启动前要求 ApplicationInfo.seInfoUser 已生成,调用 updateSeInfo() 合并 per-package 与 per-user token,再把结果保存进 ProcessRecord 的 start params。异步启动时同一字符串随 pending start 继续传递。

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

java
if (TextUtils.isEmpty(app.info.seInfoUser)) {
    Slog.wtf(ActivityManagerService.TAG, "SELinux tag not defined",
            new IllegalStateException("SELinux tag not defined for "
                    + app.info.packageName + " (uid " + app.uid + ")"));
}

String seInfo = updateSeInfo(app);

return startProcessLocked(hostingRecord, "android.app.ActivityThread",
        app, uid, gids, runtimeFlags, zygotePolicyFlags,
        mountExternal, seInfo, requiredAbi, instructionSet,
        invokeWith, startUptime, startElapsedTime);

9.2 Zygote协议 ​

Process.start() 把 seInfo 传给 ZygoteProcess,Zygote command parser 通过 --seinfo= 保存到 ZygoteArguments.mSeInfo,fork 路径再传给 native specialize。

源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteArguments.java

java
} else if (arg.startsWith("--seinfo=")) {
    if (mSeInfoSpecified) {
        throw new IllegalArgumentException("Duplicate arg specified");
    }
    mSeInfoSpecified = true;
    mSeInfo = getAssignmentValue(arg);
}

源码文件:frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java

java
pid = Zygote.forkAndSpecialize(
        parsedArgs.mUid, parsedArgs.mGid, parsedArgs.mGids,
        parsedArgs.mRuntimeFlags, rlimits,
        parsedArgs.mMountExternal, parsedArgs.mSeInfo,
        parsedArgs.mNiceName, fdsToClose, fdsToIgnore,
        parsedArgs.mStartChildZygote,
        parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,
        parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,
        parsedArgs.mAllowlistedDataInfoList,
        parsedArgs.mBindMountAppDataDirs,
        parsedArgs.mBindMountAppStorageDirs,
        parsedArgs.mBindMountSyspropOverrides);

9.3 Setcon提交 ​

libselinux 从当前 Context 构造可修改对象,执行 domain lookup,检查生成的完整 Context 是否被 policy 接受;只有字符串实际变化才调用 selinux_android_setcon()。

源码文件:external/selinux/libselinux/src/android/android_device.c

c
rc = getcon(&orig_ctx_str);
if (rc)
    goto err;

ctx = context_new(orig_ctx_str);
if (!ctx)
    goto oom;

rc = seapp_context_lookup(SEAPP_DOMAIN, uid, isSystemServer,
                          seinfo, pkgname, ctx);
if (rc < 0)
    goto err;

ctx_str = context_str(ctx);
if (!ctx_str)
    goto oom;

rc = security_check_context(ctx_str);
if (rc < 0)
    goto err;

if (strcmp(ctx_str, orig_ctx_str)) {
    rc = selinux_android_setcon(ctx_str);
    if (rc < 0)
        goto err;
}

selinux_android_setcon() 在 setcon() 后重新初始化 system properties 映射,避免子进程继续保留旧 domain 打开的属性文件 mmap。

源码文件:external/selinux/libselinux/src/android/android_device.c

c
int selinux_android_setcon(const char *con)
{
    int ret = setcon(con);
    if (ret)
        return ret;

    /* 关闭旧属性映射,并按新 domain 的访问权限重新初始化。 */
    return __system_properties_init();
}

9.4 动态切换权限 ​

Zygote 的 setcon() 不是 exec transition。policy 必须允许 zygote 对自身 setcurrent,并允许到 appdomain、system_server_startup、webview_zygote、app_zygote 的 dyntransition;neverallow 阻止跳到其他 domain。

源码文件:system/sepolicy/private/zygote.te

text
allow zygote self:process setcurrent;
allow zygote system_server_startup:process dyntransition;
allow zygote appdomain:process dyntransition;
allow zygote webview_zygote:process dyntransition;
allow zygote app_zygote:process dyntransition;

neverallow zygote ~{
  appdomain
  system_server_startup
  webview_zygote
  app_zygote
}:process dyntransition;

9.5 系统服务阶段 ​

Android 17 的 seapp rule 先把 isSystemServer=true 映射到 system_server_startup。native specialize 后,Zygote 又显式 setcon("u:r:system_server:s0"),所以 system_server 存在启动准备域与最终运行域两个阶段。

源码文件:system/sepolicy/private/seapp_contexts

text
isSystemServer=true domain=system_server_startup

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

cpp
if (is_system_server) {
    env->CallStaticVoidMethod(gZygoteClass,
                              gCallPostForkSystemServerHooks,
                              runtime_flags);
    if (env->ExceptionCheck()) {
        fail_fn("Error calling post fork system server hooks.");
    }

    static const char* kSystemServerLabel = "u:r:system_server:s0";
    if (selinux_android_setcon(kSystemServerLabel) != 0) {
        fail_fn(CREATE_ERROR("selinux_android_setcon(%s)",
                             kSystemServerLabel));
    }
}

10. 数据目录 ​

10.1 Installd入口 ​

PackageManager 通过 installd AIDL 传入 packageName、userId、appId、seInfo 和 target SDK。installd 计算多用户 uid,创建 CE/DE 目录、cache 和 code_cache,然后调用 lazy restorecon。

源码文件:frameworks/native/cmds/installd/InstalldNativeService.cpp

cpp
static binder::Status createAppDataDirs(
        const std::string& path, int32_t uid, int32_t gid,
        int32_t previousUid, int32_t cacheGid,
        const std::string& seInfo, mode_t targetMode,
        long projectIdApp, long projectIdCache) {
    struct stat st{};
    bool parent_dir_exists = stat(path.c_str(), &st) == 0;

    if (prepare_app_dir(path, targetMode, uid, gid, projectIdApp) ||
        prepare_app_cache_dir(path, "cache", 02771,
                              uid, cacheGid, projectIdCache) ||
        prepare_app_cache_dir(path, "code_cache", 02771,
                              uid, cacheGid, projectIdCache)) {
        return error("Failed to prepare " + path);
    }

    if (restorecon_app_data_lazy(path, seInfo, uid,
                                 parent_dir_exists)) {
        return error("Failed to restorecon " + path);
    }
    /* 新建 cache/code_cache 的补充 restorecon 分支省略。 */
    return ok();
}

10.2 Seapp覆盖 ​

restorecon 先用 file contexts 查路径,发现属于 App data 后改用 package-aware lookup。这样目录结构仍由 file contexts 提供基础 Context,而具体应用 type/range 由 seapp 决定。

源码文件:external/selinux/libselinux/src/android/android_device.c

c
if (selabel_lookup(fc_sehandle, &secontext,
                   pathname, sb->st_mode) < 0)
    return 0;

if (lgetfilecon(pathname, &oldsecontext) < 0)
    goto err;

if (is_app_data_path(pathname)) {
    if (pkgdir_selabel_lookup(pathname, seinfo,
                              uid, &secontext) < 0)
        goto err;
}

if (strcmp(oldsecontext, secontext) != 0 && !nochange) {
    if (lsetfilecon(pathname, secontext) < 0)
        goto err;
}

10.3 包列表回退 ​

调用方未显式传 seinfo 时,libselinux 从 package list 哈希表按包名找到 appId 和 seinfo,再把路径中的 userid 合成为完整 UID。找不到 package 信息时返回错误,不会猜测 default。

源码文件:external/selinux/libselinux/src/android/android_device.c

c
if (!seinfo) {
    info = package_info_lookup(pkgname);
    if (!info) {
        selinux_log(SELINUX_WARNING,
            "Could not look up information for package %s, "
            "cannot restorecon %s.\n",
            pkgname, pathname);
        free(pkgname);
        return -1;
    }
    info->uid += userid_from_path * AID_USER_OFFSET;
}

10.4 Lazy恢复 ​

新目录递归 restorecon;已有目录先只标顶层并比较 before/after,只有标签变化或上次操作中断时才递归。seapp rule 更新后,顶层 Context 变化就是触发整个 App tree 修复的信号。

源码文件:frameworks/native/cmds/installd/InstalldNativeService.cpp

cpp
if (!existing) {
    if (selinux_android_restorecon_pkgdir(
            path.c_str(), seInfo.c_str(), uid,
            SELINUX_ANDROID_RESTORECON_RECURSE) < 0) {
        return -1;
    }
    return 0;
}

bool inProgress = getRestoreconInProgress(path);
std::string before, after;
if (!inProgress) {
    before = lgetfilecon(path);
    if (selinux_android_restorecon_pkgdir(
            path.c_str(), seInfo.c_str(), uid, 0) < 0) {
        return -1;
    }
    after = lgetfilecon(path);
}

if (inProgress || before != after) {
    /* 用 user.restorecon_in_progress 保护递归恢复;实现省略。 */
}

11. 当前规则 ​

11.1 System与特殊UID ​

Android 17 平台规则包括 system_server startup、固定 system UID、isolated、SDK sandbox 和 PCC。这里不是完整清单,而是用于说明 selector 组合的真实条目。

源码文件:system/sepolicy/private/seapp_contexts

text
isSystemServer=true domain=system_server_startup
user=system seinfo=platform domain=system_app type=system_app_data_file
user=_isolated domain=isolated_app levelFrom=user
user=_isolated isIsolatedComputeApp=true domain=isolated_compute_app levelFrom=user
user=_sdksandbox domain=sdk_sandbox_34 type=sdk_sandbox_data_file levelFrom=all
user=_sdksandbox isSdkSandboxNext=true domain=sdk_sandbox_next type=sdk_sandbox_data_file levelFrom=all
user=_sdksandbox isSdkSandboxAudit=true domain=sdk_sandbox_audit type=sdk_sandbox_data_file levelFrom=all
user=_pcc_component domain=pcc_component type=pcc_data_file levelFrom=all

11.2 平台与特权应用 ​

Android 17 正在按 target SDK 将 platform/priv-app 切换到新 domain。target 37 进入 platform_app/priv_app,更早版本进入带 _36 后缀的兼容 domain。

源码文件:system/sepolicy/private/seapp_contexts

text
user=_app minTargetSdkVersion=37 seinfo=platform domain=platform_app type=app_data_file levelFrom=user
user=_app seinfo=platform domain=platform_app_36 type=app_data_file levelFrom=user
user=_app minTargetSdkVersion=37 isPrivApp=true domain=priv_app type=privapp_data_file levelFrom=user
user=_app isPrivApp=true domain=priv_app_36 type=privapp_data_file levelFrom=user

由于 comparator 让更高 minTargetSdkVersion 优先,target 37 先命中新 rule;target 36 跳过后落到兼容 rule。不能按旧版本记忆把所有 platform/priv app 都归入同一个 domain。

11.3 Untrusted分级 ​

源码文件:system/sepolicy/private/seapp_contexts

text
user=_app minTargetSdkVersion=37 domain=untrusted_app type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=34 domain=untrusted_app_34 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=32 domain=untrusted_app_32 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=30 domain=untrusted_app_30 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=29 domain=untrusted_app_29 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=28 domain=untrusted_app_27 type=app_data_file levelFrom=all
user=_app minTargetSdkVersion=26 domain=untrusted_app_27 type=app_data_file levelFrom=user
user=_app domain=untrusted_app_25 type=app_data_file levelFrom=user

最后一条是 catch-all。target 28 与 26 虽然 domain 相同,range 不同:前者 levelFrom=all,后者 levelFrom=user。只看进程 type 会漏掉 App category 隔离变化。

11.4 Run-as ​

run-as 通过 seinfo token fromRunAs 进入 runas_app,并在 target 28 分界选择 all/user range。runas policy 需要 setcurrent、到非 system app domain 的 dyntransition,以及读取 seapp contexts 的权限。

源码文件:system/sepolicy/private/seapp_contexts

text
user=_app minTargetSdkVersion=28 fromRunAs=true domain=runas_app levelFrom=all
user=_app fromRunAs=true domain=runas_app levelFrom=user

12. Neverallow ​

12.1 Seapp断言 ​

seapp 文件自己的 neverallow 支持 PCRE,检查 selector/output 组合。例如 system_server domain 只能由 system server 获得,system_app 只能用于 system UID,ephemeral app 必须进入 ephemeral_app。

源码文件:system/sepolicy/private/seapp_contexts

text
neverallow isSystemServer=false domain=system_server
neverallow isSystemServer=false domain=system_server_startup
neverallow user=((?!system).)* domain=system_app
neverallow user=((?!system).)* type=system_app_data_file
neverallow isEphemeralApp=true domain=((?!ephemeral_app).)*
neverallow user=_isolated isIsolatedComputeApp=true domain=((?!isolated_compute_app).)*

12.2 Regex匹配 ​

checkseapp 为 neverallow value 自动加 ^ 和 $,使用 PCRE2 DOTALL 编译。普通 rule 仍不支持这些正则。

源码文件:system/sepolicy/tools/check_seapp.c

c
size = strlen(km->data) + 3;
anchored = alloca(size);
sprintf(anchored, "^%s$", km->data);

km->regex.compiled = pcre2_compile(
        (PCRE2_SPTR)anchored,
        PCRE2_ZERO_TERMINATED,
        PCRE2_DOTALL,
        errcode, erroff, NULL);

12.3 Null选择器 ​

neverallow 使用 key="" 表示 rule 未指定该 selector。检查器遍历真实 rule,若该 key 不存在就把 null condition 计为匹配;这使断言能够禁止“没有 seinfo 的命名规则”。

源码文件:system/sepolicy/tools/check_seapp.c

c
bool is_null_check = !strcmp(nrule->data, "\"\"");

for (i = 0; i < rm->length; i++) {
    rule = &(rm->m[i]);
    if (!strcmp(rule->name, nrule->name)) {
        is_null_check = false;
        if (match_regex(nrule, rule)) cnt++;
    }
}
if (is_null_check) cnt++;

13. 失败路径 ​

13.1 构建失败 ​

现象首查位置含义
unknown/duplicate keycheck_seapp.c::rule_map_new字段名错误或同一行重复字段
duplicate input selectorrule_add / runtime duplicate check两条规则无法确定唯一输出
invalid domainvalidate_domaindomain 不存在于 compiled policy
missing app_data_file_typevalidate_typedata type 不是允许的 App data 类型
neverallow violationrule_map_validateselector/output 组合违反结构约束
vendor coredomainvalidate_domain with -cvendor/odm 试图分配平台核心域

13.2 启动失败 ​

domain lookup 没有匹配、seinfo 无法解析、category 超界、生成 Context 非法或 setcon 被 dyntransition 拒绝,都会让 native specialize 调用 fail_fn。AMS 捕获进程启动 RuntimeException 后清理 pending bookkeeping,并 force-stop package。

源码文件:frameworks/base/services/core/java/com/android/server/am/ProcessList.java

java
} catch (RuntimeException e) {
    Slog.e(ActivityManagerService.TAG,
            "Failure starting process " + app.processName, e);

    mService.forceStopPackageLocked(
            app.info.packageName,
            UserHandle.getAppId(app.uid),
            false, false, true, false, false, false,
            app.userId, "start failure");
    return false;
}

13.3 数据恢复失败 ​

App data 路径解析失败、packages.list 找不到包、type lookup 失败、生成 Context 非法或 xattr 写入失败,都会使 installd 返回 Failed to restorecon。这不会通过修改进程 domain 自动恢复;需要检查 data type 和 range 输出。

13.4 分区不一致 ​

system 预装 App 命中 vendor policy 当前会产生 warning。若同时涉及 coredomain,构建期 -c 可能直接失败。排查时应同时记录 App 安装分区与命中 rule 的来源分区。

14. 测试证据 ​

14.1 双输出测试 ​

libselinux 单元测试用同一条 rule 分别查询 SEAPP_DOMAIN 与 SEAPP_TYPE。UID 10001 被归类为 _app、appid 1、userid 0,levelFrom=user 生成 c512,c768。

源码文件:external/selinux/libselinux/src/android/android_unittest.cpp

cpp
int ret = LoadSeAppContexts(
    "user=_app seinfo=platform domain=platform_app "
    "type=app_data_file levelFrom=user\n");
EXPECT_EQ(ret, 0);

context_t ctx = context_new("u:r:unknown");
ret = seapp_context_lookup_internal(
        SEAPP_DOMAIN, 10001, false,
        "platform", "com.android.test1", ctx);
EXPECT_EQ(ret, 0);
EXPECT_STREQ(context_str(ctx),
             "u:r:platform_app:s0:c512,c768");
context_free(ctx);

ctx = context_new("u:r:unknown_data_file");
ret = seapp_context_lookup_internal(
        SEAPP_TYPE, 10001, false,
        "platform", "com.android.test1", ctx);
EXPECT_EQ(ret, 0);
EXPECT_STREQ(context_str(ctx),
             "u:r:app_data_file:s0:c512,c768");

它证明同一 selector 的两种 kind 只替换各自输出,并共享 range;没有覆盖 Zygote setcon 或磁盘 xattr。

14.2 Boolean默认值 ​

测试为 priv/ephemeral 指定 false rule、特殊 boolean 的显式 false rule和 catch-all。isEphemeralApp 的“已指定 false”比 isPrivApp=false 优先;而默认 false 的 isolated/SDK/run-as 显式写 false 会与 catch-all 构成 duplicate,loader 返回 -1。

源码文件:external/selinux/libselinux/src/android/android_unittest.cpp

cpp
int ret = LoadSeAppContexts(
    "user=_app isPrivApp=false domain=noprivapp "
    "type=x levelFrom=user\n"
    "user=_app isEphemeralApp=false domain=noephemeralapp "
    "type=x levelFrom=user\n"
    "user=_app domain=catchall_app type=x levelFrom=user\n");
EXPECT_EQ(ret, 0);

ExpectContextForSeInfo(
    "default:partition=system:complete",
    "u:r:noephemeralapp:s0:c512,c768");

/* isIsolatedComputeApp=false + catch-all 被断言为 duplicate。 */

这个测试解释了为何 comparator 对 isEphemeralAppSet 有独立优先级,而部分其他 boolean 没有 set-state 字段。

14.3 Seinfo解析 ​

源码文件:external/selinux/libselinux/src/android/android_unittest.cpp

cpp
string seinfo =
    "default:privapp:targetSdkVersion=10000:partition=system:complete";
int ret = parse_seinfo(seinfo.c_str(), &info);

EXPECT_EQ(ret, 0);
EXPECT_STREQ(info.base, "default");
EXPECT_EQ(info.targetSdkVersion, 10000);
EXPECT_EQ(info.is, IS_PRIV_APP);
EXPECT_EQ(info.isPreinstalledApp, true);
EXPECT_STREQ(info.partition, "system");

seinfo = "default:targetSdkVersion:complete";
EXPECT_EQ(parse_seinfo(seinfo.c_str(), &info), -1);

它覆盖正常 token 与缺少 = 值的错误输入;不证明 PackageManager 拼接顺序。

14.4 PM测试 ​

SELinuxMMACTest mock PackageState 和 PlatformCompat,断言 compat opt-in 会抬高 target SDK token,预装分区会追加 partition,非预装 App 不追加。

源码文件:frameworks/base/services/tests/servicestests/src/com/android/server/pm/SELinuxMMACTest.java

java
assertThat(SELinuxMMAC.getSeInfo(
        packageState, packageState.getAndroidPackage(),
        null, mMockCompatibility),
        is("default:targetSdkVersion=" + LATEST_OPT_IN_VERSION));

assertThat(SELinuxMMAC.getSeInfo(
        systemPackage, systemPackage.getAndroidPackage(),
        null, mMockCompatibility),
        containsString(":partition=system"));

15. 验证路径 ​

15.1 构建检查 ​

bash
# 初始化 Android 构建环境并选择实际产品。
source build/envsetup.sh
lunch PRODUCT-userdebug

# 构建五个分区的 seapp 产物;每个目标都会运行 checkseapp。
m plat_seapp_contexts system_ext_seapp_contexts \
  product_seapp_contexts vendor_seapp_contexts odm_seapp_contexts

若失败,先读 checkseapp 的文件/行号、invalid type、neverallow 或 coredomain 消息。不能用调整文本行顺序修复 runtime precedence,因为 loader 会重新排序。

15.2 单元测试 ​

bash
# libselinux 测试覆盖 seapp loader、lookup、boolean 和 seinfo parser。
atest libselinux_test

# PackageManager 测试覆盖 target SDK compat 与安装分区 token。
atest SELinuxMMACTest

测试目标需要出现在所选产品/测试配置中。第一组不启动真实 App,第二组不执行 native lookup;二者结合仍不能替代设备闭环。

15.3 设备观察 ​

bash
# 查 PackageManager 保存的 seinfo、uid、targetSdk 与安装路径。
adb shell dumpsys package PACKAGE_NAME | \
  rg 'userId=|seinfo=|targetSdk=|codePath='

# 查进程 domain 与完整 MLS range。
adb shell ps -AZ | rg 'PACKAGE_NAME|PROCESS_NAME'

# 查 CE/DE App data 顶层 type 与 range。
adb shell ls -Zd /data/user/USER_ID/PACKAGE_NAME
adb shell ls -Zd /data/user_de/USER_ID/PACKAGE_NAME

dumpsys package 字段随版本和权限变化;输出缺少 seinfo 时应改查 PackageManager debug 输出或 packages.list,而不是推断为 default。ps -AZ 只能观察已启动进程,ls -Zd 需要足够权限。

15.4 Category实验 ​

bash
# 用与 set_range_from_level 相同的位运算复算 UID 1010045。
uid=1010045
userid=$((uid / 100000))
appid=$((uid % 100000 - 10000))

printf 's0:c%d,c%d,c%d,c%d\n' \
  "$((appid & 255))" \
  "$((256 + ((appid >> 8) & 255)))" \
  "$((512 + (userid & 255)))" \
  "$((768 + ((userid >> 8) & 255)))"

预期输出是 s0:c45,c256,c522,c768。这只复算 category 公式,不证明某条 rule 实际选择了 levelFrom=all。

16. 源码导航 ​

问题首选源码关键符号
平台 selector 与当前规则system/sepolicy/private/seapp_contexts文件头契约、真实 entries、neverallow
分区 module 如何生成system/sepolicy/contexts/Android.bpplat/vendor/odm_seapp_contexts
M4 与 checkseapp 如何串联system/sepolicy/build/soong/selinux_contexts.gobuildSeappContexts
输出 type/domain 如何校验system/sepolicy/tools/check_seapp.cvalidate_type、validate_domain
runtime 如何读五个分区external/selinux/libselinux/src/android/android_seapp.cseapp_context_paths、reload
优先级如何计算同上seapp_context_cmp
seinfo 怎样解析同上parse_seinfo
UID 怎样分类同上seapp_context_lookup_internal
MLS categories 怎样计算同上set_range_from_level
Zygote 怎样切换 domainexternal/selinux/libselinux/src/android/android_device.cselinux_android_setcontext
Java 怎样传递 seinfoframeworks/base/services/core/java/com/android/server/am/ProcessList.javaupdateSeInfo、startProcess
seinfo base 从哪里来frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.javareadInstallPolicy、getSeInfo
App data 怎样选择 typeexternal/selinux/libselinux/src/android/android_device.cpkgdir_selabel_lookup
Installd 何时提交标签frameworks/native/cmds/installd/InstalldNativeService.cppcreateAppDataDirs、lazy restorecon
Zygote 动态切换权限system/sepolicy/private/zygote.tesetcurrent、dyntransition

给定一个 target 34、普通签名、非 priv、安装在 userdata 的 App,完整复述应包括:PackageManager 生成 default:targetSdkVersion=34 并追加 per-user complete;AMS 把它和 UID、包名传给 Zygote;libselinux 将 UID 分类为 _app,按排序命中 minTargetSdkVersion=34 的 untrusted_app_34 rule,计算 levelFrom=all 后 setcon;installd 为 CE/DE 目录再次以 SEAPP_TYPE 命中同一 rule,把基础 file Context 改为 app_data_file 和相同 categories。任何一侧缺少匹配,都必须按各自的 domain/type 失败语义处理,不能用另一侧“看起来正确”代替验证。