Skip to content

SharedUserSetting

追踪 SharedUserSetting 的 appId 所有权、成员聚合、签名加入、SELinux 域、权限状态、成员移除与 shared UID 迁移。

基于android-17.0.0_r1
AndroidPackageManagerServiceSharedUserSettingsharedUserIdSigningDetails源码阅读

SharedUserSetting ​

本文面向已经读过 PackageSetting 的读者。前一篇说明每个 PackageSetting 都保存自己的包名、路径与 per-user 状态,同时可以通过 mSharedUserAppId 关联 SharedUserSetting;本文继续追踪关联的另一端:Settings 为什么让 appId map 指向一个 group owner,而不是任意成员包,成员加入或移除后哪些聚合状态必须重算,签名轮换如何判断能否加入,所有成员为什么必须落入同一 SELinux domain,以及应用如何逐步退出 legacy shared UID。

shared UID 的学习难点不在“多个 APK 使用相同 Linux UID”这句话,而在 UID 级状态与包级状态的边界。成员包仍有独立 PackageSetting、Manifest、版本、安装路径与用户安装状态;SharedUserSetting 则拥有 appId、共享签名 lineage、UID flags、共享权限迁移状态、聚合进程声明和成员集合。Linux DAC、权限检查、包可见性、seInfo 和安装更新分别消费这组状态的不同切面。

本文只讨论 Android 17 framework 中已有 shared UID 的维护与迁移机制,不建议新应用采用 android:sharedUserId,也不把 UID 相同误写成“应用代码和全部数据自动合并”。数据目录仍按包名组织;相同 UID 带来的是内核身份和权限边界耦合,正因如此,签名、升级和退出流程都比普通包严格。

1. Group契约 ​

1.1 类型与字段 ​

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

java
/**
 * Settings data for a particular shared user ID we know about.
 */
public final class SharedUserSetting extends SettingBase implements SharedUserApi {
    final String name;

    int mAppId;

    /** @see SharedUserApi#getUidFlags() **/
    int uidFlags;
    int uidPrivateFlags;

    /** @see SharedUserApi#getSeInfoTargetSdkVersion() **/
    int seInfoTargetSdkVersion;

    private final WatchedArraySet<PackageSetting> mPackages;
    final WatchedArraySet<PackageSetting> mDisabledPackages;

    final PackageSignatures signatures = new PackageSignatures();
    Boolean signaturesChanged;

    final ArrayMap<String, ParsedProcess> processes;

SharedUserSetting 继承 SettingBase,所以它也有当前聚合 flags/privateFlags、Watchable 和 legacy permission state。类自身再增加六类 UID 级状态:

状态owner 含义是否直接等于成员字段
nameManifest sharedUserId 字符串否,是 group key
mAppId所有成员共享的 appId成员 PackageSetting appId 应相同
uidFlags/privateFlagsgroup 固有基础 flags不含动态成员 OR 结果
mPackages当前 active PackageSetting包级状态仍在成员中
mDisabledPackages被 data 更新遮蔽的 system PackageSetting用于回退/退出 shared UID
signaturesgroup 可接受的签名 lineage由成员签名验证/合并产生
processes同名 process 声明的聚合从可解析 active 成员重建
seInfoTargetSdkVersiongroup SELinux target SDK启动时取成员最低值

1.2 只读接口 ​

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

java
public interface SharedUserApi {
    @NonNull
    String getName();

    int getAppId();

    int getUidFlags();
    int getPrivateUidFlags();

    int getSeInfoTargetSdkVersion();

    @NonNull
    List<AndroidPackage> getPackages();

    @NonNull
    ArraySet<? extends PackageStateInternal> getPackageStates();

    @NonNull
    ArraySet<? extends PackageStateInternal> getDisabledPackageStates();

    @NonNull
    SigningDetails getSigningDetails();

    @NonNull
    ArrayMap<String, ParsedProcess> getProcesses();
}

接口同时提供 getPackages() 与 getPackageStates(),两者不能互换。前者只返回 pkg != null 的 AndroidPackage,用于需要 Manifest/进程/签名内容的消费者;后者返回所有 active PackageSetting,包括 APK 暂不可用但身份仍保留的成员。disabled system 原版本又通过独立集合暴露。

Computer snapshot 可以按 appId 返回 SharedUserApi 或成员 PackageState:

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

java
@Nullable
public SharedUserApi getSharedUserFromAppId(int sharedUserAppId) {
    return (SharedUserSetting) mSettings.getSettingLPr(sharedUserAppId);
}

@NonNull
public ArraySet<PackageStateInternal> getSharedUserPackages(int sharedUserAppId) {
    final ArraySet<PackageStateInternal> res = new ArraySet<>();
    final SharedUserSetting sharedUserSetting =
            (SharedUserSetting) mSettings.getSettingLPr(sharedUserAppId);
    if (sharedUserSetting != null) {
        final ArraySet<? extends PackageStateInternal> sharedUserPackages =
                sharedUserSetting.getPackageStates();
        for (PackageStateInternal ps : sharedUserPackages) {
            res.add(ps);
        }
    }
    return res;
}

2. AppId所有权 ​

2.1 Group创建 ​

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

java
SharedUserSetting getSharedUserLPw(String name, int pkgFlags, int pkgPrivateFlags,
        boolean create) throws PackageManagerException {
    SharedUserSetting s = mSharedUsers.get(name);
    if (s == null && create) {
        s = new SharedUserSetting(name, pkgFlags, pkgPrivateFlags);
        s.mAppId = mAppIds.acquireAndRegisterNewAppId(s);
        if (s.mAppId < 0) {
            throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE,
                    "Creating shared user " + name + " failed");
        }
        Log.i(PackageManagerService.TAG,
                "New shared user " + name + ": id=" + s.mAppId);
        mSharedUsers.put(name, s);
    }
    return s;
}

动态 shared user 第一次在扫描中出现时,Settings 创建 group、为 group 分配 appId,并让 AppIdSettingMap 直接指向 SharedUserSetting。appId 不是先分给第一个 PackageSetting 再由其他成员借用;group 从一开始就是 UID owner。

固定系统 shared UID 则在 PMS 构造时按已知 appId 预注册,例如 android.uid.system、phone、nfc、bluetooth、shell、networkstack。读取 packages.xml 中的 <shared-user> 时,addSharedUserLPw() 遇到同名同 appId 就复用种子对象并恢复签名。

2.2 成员绑定 ​

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

java
void addPackageSettingLPw(PackageSetting p, SharedUserSetting sharedUser) {
    mPackages.put(p.getPackageName(), p);
    if (sharedUser != null) {
        SharedUserSetting existingSharedUserSetting = getSharedUserSettingLPr(p);
        if (existingSharedUserSetting != null
                && existingSharedUserSetting != sharedUser) {
            PackageManagerService.reportSettingsProblem(Log.ERROR,
                    "Package " + p.getPackageName() + " was user "
                            + existingSharedUserSetting + " but is now " + sharedUser
                            + "; I am not changing its files so it will probably fail!");
            existingSharedUserSetting.removePackage(p);
        } else if (p.getAppId() != 0 && p.getAppId() != sharedUser.mAppId) {
            PackageManagerService.reportSettingsProblem(Log.ERROR,
                    "Package " + p.getPackageName() + " was app id " + p.getAppId()
                            + " but is now user " + sharedUser
                            + " with app id " + sharedUser.mAppId
                            + "; I am not changing its files so it will probably fail!");
        }

        sharedUser.addPackage(p);
        p.setSharedUserAppId(sharedUser.mAppId);
        p.setAppId(sharedUser.mAppId);
    }

成员绑定要同时满足三条引用:SharedUserSetting 的 member set 包含 PackageSetting;PackageSetting 的 mSharedUserAppId 指回 group;PackageSetting mAppId 等于 group appId。只改其中一个会导致按包名、按 appId 和按 shared group 查询得出不同结果。

java
Object appIdPs = getSettingLPr(p.getAppId());
if (sharedUser == null) {
    if (appIdPs != null && appIdPs != p) {
        mAppIds.replaceSetting(p.getAppId(), p);
    }
} else {
    if (appIdPs != null && appIdPs != sharedUser) {
        mAppIds.replaceSetting(p.getAppId(), sharedUser);
    }
}

appId map 的 value 类型是 SettingBase:普通 appId 指向 PackageSetting,共享 appId 指向 SharedUserSetting。加入 group 后,即使 appId 之前临时指向成员 PackageSetting,也必须替换为 group owner。

2.3 用户维度 ​

相同 appId 不表示所有成员在所有用户都 installed。每个 PackageSetting 仍有独立 PackageUserStateImpl;user 10 可以只安装成员 A,成员 B 为 installed=false。Linux UID 对该用户仍存在,因为至少一个成员需要它。

SharedUserSetting 的 getNotInstalledUserIds() 对成员的未安装用户集合取交集:只有所有成员都未安装的 userId 才算 group 未安装。若任一成员在该用户安装,shared UID 仍应保留用户映射。

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

java
public int[] getNotInstalledUserIds() {
    int[] excludedUserIds = null;
    for (int i = 0; i < mPackages.size(); i++) {
        PackageSetting ps = mPackages.valueAt(i);
        final int[] userIds = ps.getNotInstalledUserIds();
        if (excludedUserIds == null) {
            excludedUserIds = userIds;
        } else {
            for (int userId : excludedUserIds) {
                if (!ArrayUtils.contains(userIds, userId)) {
                    excludedUserIds = ArrayUtils.removeInt(
                            excludedUserIds, userId);
                }
            }
        }
    }
    return excludedUserIds == null ? EmptyArray.INT : excludedUserIds;
}

3. 成员集合 ​

3.1 Active与disabled ​

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

java
private final WatchedArraySet<PackageSetting> mPackages;

// It is possible for a system app to leave shared user ID by an update.
// We need to keep track of the shadowed PackageSettings so that it is possible to uninstall
// the update and revert the system app back into the original shared user ID.
final WatchedArraySet<PackageSetting> mDisabledPackages;

active set 是当前参与 group 聚合的包;disabled set 保存 system image 中被 /data/app 更新遮蔽的原版本。尤其在系统 app 更新版本声明离开 shared UID 时,卸载更新后需要恢复原 system APK,它仍可能属于旧 shared group,所以 disabled membership 不能随 active 更新一起丢弃。

Settings 禁用 system package 时把 copy/原 setting 加入 mDisabledPackages,恢复 factory version 或永久删除 disabled setting 时再移除。isSingleUser() 同时检查两组集合:active 必须正好一个;disabled 最多一个,且存在时其 pkg 也必须声明 leaving shared user,才允许透明转换。

3.2 Watch传播 ​

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

java
private final Watcher mObserver = new Watcher() {
    @Override
    public void onChange(@Nullable Watchable what) {
        SharedUserSetting.this.onChanged();
    }
};

private void registerObservers() {
    mPackages.registerObserver(mObserver);
    mDisabledPackages.registerObserver(mObserver);
}

WatchedArraySet 在有 observer 时还会观察内部 PackageSetting。成员字段变化、成员增删都会向 SharedUserSetting 传播,再通过 SettingBase onChanged() 增加 PackageStateMutator sequence 和 snapshot pending version。SharedUser snapshot 因此不仅在集合结构变化时失效,成员 PackageSetting 改变时也失效。

WatchedArraySet.snapshot() 若元素实现 Snappable,会把元素替换成各自 snapshot;PackageSetting 正好实现 Snappable。因此 SharedUserSetting snapshot 中的 active/disabled member 是 PackageSetting snapshot,不只是不可修改的 set 外壳。

4. 聚合状态 ​

4.1 Flags聚合 ​

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

java
SharedUserSetting(String _name, int _pkgFlags, int _pkgPrivateFlags) {
    super(_pkgFlags, _pkgPrivateFlags);
    uidFlags = _pkgFlags;
    uidPrivateFlags = _pkgPrivateFlags;
    name = _name;
    // ... initialize sets, processes and snapshot ...
}

void addPackage(PackageSetting packageSetting) {
    if (mPackages.add(packageSetting)) {
        setFlags(this.getFlags() | packageSetting.getFlags());
        setPrivateFlags(this.getPrivateFlags()
                | packageSetting.getPrivateFlags());
        onChanged();
    }
    if (packageSetting.getPkg() != null) {
        addProcesses(packageSetting.getPkg().getProcesses());
    }
}

uidFlags/uidPrivateFlags 是 group 固有基础值,例如固定系统 shared UID 可以自带 system/privileged 属性;SettingBase 的 current flags 则是基础值 OR 所有 active 成员。只要任一成员 privileged,SharedUserSetting.isPrivileged() 就为 true。

移除成员不能简单清掉该成员 bits,因为其他成员可能也有相同 bit。代码从 base uidFlags 开始重新 OR 剩余成员:

java
boolean removePackage(PackageSetting packageSetting) {
    if (!mPackages.remove(packageSetting)) {
        return false;
    }
    if ((this.getFlags() & packageSetting.getFlags()) != 0) {
        int aggregatedFlags = uidFlags;
        for (int i = 0; i < mPackages.size(); i++) {
            aggregatedFlags |= mPackages.valueAt(i).getFlags();
        }
        setFlags(aggregatedFlags);
    }
    if ((this.getPrivateFlags() & packageSetting.getPrivateFlags()) != 0) {
        int aggregatedPrivateFlags = uidPrivateFlags;
        for (int i = 0; i < mPackages.size(); i++) {
            aggregatedPrivateFlags |= mPackages.valueAt(i).getPrivateFlags();
        }
        setPrivateFlags(aggregatedPrivateFlags);
    }
    updateProcesses();
    onChanged();
    return true;
}

聚合 flags 是 UID 级 effective policy,不应写回每个 PackageSetting。成员自己的 flags 仍表达包所属分区和 Manifest 特征;group flags 服务 seInfo、privilege 等需要看整个 UID 的消费者。

4.2 进程聚合 ​

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

java
void addProcesses(Map<String, ParsedProcess> newProcs) {
    if (newProcs != null) {
        for (String key : newProcs.keySet()) {
            ParsedProcess newProc = newProcs.get(key);
            ParsedProcess proc = processes.get(newProc.getName());
            if (proc == null) {
                proc = new ParsedProcessImpl(newProc);
                processes.put(newProc.getName(), proc);
            } else {
                ComponentMutateUtils.addStateFrom(proc, newProc);
            }
        }
        onChanged();
    }
}

public void updateProcesses() {
    processes.clear();
    for (int i = mPackages.size() - 1; i >= 0; i--) {
        final AndroidPackage pkg = mPackages.valueAt(i).getPkg();
        if (pkg != null) {
            addProcesses(pkg.getProcesses());
        }
    }
}

process key 是完整 process name。同名 process 来自多个成员时,不是保留多份,而是把状态合入 ParsedProcessImpl。实际 merge 会对 deniedPermissions 取并集、合并每包 application class name;GWP-ASan、MTE、zero-init、embedded-dex 等 scalar 值由后合入对象覆盖。成员 manifest 若对同名进程给出冲突 scalar 策略,结果依赖聚合顺序,因此共享进程声明应保持一致。

成员移除后 updateProcesses() 从剩余 active packages 全量重建,避免删除某成员时误删另一个成员贡献的同名 process state。

4.3 ABI协调 ​

shared UID 成员可能进入同一进程,ABI 不能各自任意选择。扫描用 PackageAbiHelper.getAdjustedAbiForSharedUser() 计算 group ABI,再把缺少显式 legacy ABI 的成员调整为同一值。

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

java
public static List<String> applyAdjustedAbiToSharedUser(
        SharedUserSetting sharedUserSetting,
        ParsedPackage scannedPackage, String adjustedAbi) {
    if (scannedPackage != null)  {
        scannedPackage.setPrimaryCpuAbi(adjustedAbi);
    }
    List<String> changedAbiCodePath = null;
    final WatchedArraySet<PackageSetting> sharedUserPackageSettings =
            sharedUserSetting.getPackageSettings();
    for (int i = 0; i < sharedUserPackageSettings.size(); i++) {
        PackageSetting ps = sharedUserPackageSettings.valueAt(i);
        if (scannedPackage == null
                || !scannedPackage.getPackageName().equals(ps.getPackageName())) {
            if (ps.getPrimaryCpuAbiLegacy() != null) {
                continue;
            }

            ps.setPrimaryCpuAbi(adjustedAbi);
            if (ps.getPkg() != null
                    && !TextUtils.equals(adjustedAbi,
                    AndroidPackageUtils.getRawPrimaryCpuAbi(ps.getPkg()))) {
                // record path whose effective ABI changed
            }
        }
    }
    return changedAbiCodePath;
}

已有 PackageSetting 若已保存 primary ABI,不在这里覆盖;空值成员使用 group adjustment。返回的 changed code path 可供后续 native library/dex 处理判断。ABI 细节属于包扫描专题,这里只说明 SharedUserSetting 是计算输入和成员遍历 owner。

5. 签名边界 ​

5.1 双重校验 ​

加入或更新 shared UID 成员要同时通过两层签名检查:先与该 package 自己之前保存的 SigningDetails 匹配,再与 SharedUserSetting 的 group SigningDetails 匹配。前者保护应用更新数据,后者保护 UID 边界。

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

java
if (sharedUserSetting != null
        && sharedUserSetting.getSigningDetails() != SigningDetails.UNKNOWN) {
    boolean match = canJoinSharedUserId(
            packageName, parsedSignatures, sharedUserSetting,
            pkgSetting.getSigningDetails().getSignatures() != null
                    ? SHARED_USER_ID_JOIN_TYPE_UPDATE
                    : SHARED_USER_ID_JOIN_TYPE_INSTALL);
    if (!match && compareCompat) {
        match = matchSignaturesCompat(
                packageName, sharedUserSetting.signatures, parsedSignatures);
    }
    if (!match && compareRecover) {
        match = matchSignaturesRecover(
                packageName,
                sharedUserSetting.signatures.mSigningDetails,
                parsedSignatures,
                SigningDetails.CertCapabilities.SHARED_USER_ID)
                || matchSignaturesRecover(
                packageName,
                parsedSignatures,
                sharedUserSetting.signatures.mSigningDetails,
                SigningDetails.CertCapabilities.SHARED_USER_ID);
    }
    if (!match) {
        throw new PackageManagerException(
                INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
                "Package " + packageName
                        + " has no signatures that match those in shared user "
                        + sharedUserSetting.name + "; ignoring!");
    }
    if (!parsedSignatures.hasCommonAncestor(
            sharedUserSetting.signatures.mSigningDetails)) {
        throw new PackageManagerException(
                INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
                "Package " + packageName + " has a signing lineage "
                        + "that diverges from the lineage of the sharedUserId");
    }
}

兼容/恢复比较只服务旧平台升级;正常路径依赖 signing lineage 与 SHARED_USER_ID capability。即使 capability 检查通过,lineage 没有共同祖先仍拒绝,防止两个独立证书树通过错误 capability 状态拼到同一 UID。

5.2 Join类型 ​

canJoinSharedUserId() 区分 install、update、system join。新安装最严格,因为 group 中可能还有用旧 key 签名且已被新 lineage 撤销信任的成员;更新已有成员可以逐步轮换;system package 在共同 lineage 下有额外容错。

java
boolean capabilityGranted =
        packageSigningDetails.checkCapability(
                sharedUserSigningDetails, SHARED_USER_ID)
                || sharedUserSigningDetails.checkCapability(
                packageSigningDetails, SHARED_USER_ID);

if (capabilityGranted
        && joinType != SHARED_USER_ID_JOIN_TYPE_INSTALL) {
    return true;
}

if (!capabilityGranted
        && sharedUserSigningDetails.hasAncestor(packageSigningDetails)) {
    return joinType == SHARED_USER_ID_JOIN_TYPE_SYSTEM;
}

if (!capabilityGranted
        && packageSigningDetails.hasAncestor(sharedUserSigningDetails)) {
    return joinType != SHARED_USER_ID_JOIN_TYPE_INSTALL;
}

if (!capabilityGranted) {
    return false;
}

新安装即使 capability granted,还要遍历现有成员:若新 package lineage 包含某成员 signer,却没有授予该 signer SHARED_USER_ID capability,就拒绝加入。这个 group-wide 检查防止新成员选择性信任 group aggregate、却不信任其中某个真实成员。

5.3 Lineage聚合 ​

Reconcile 成功后,SharedUserSetting 不只保存新 package 的当前证书,而是把 lineage 合并到 group SigningDetails。若新 lineage 引入 signer/capability 变化,还要用其他成员的 lineage 做 MERGE_RESTRICTED_CAPABILITY,确保任一成员撤销旧 signer 能力时,group aggregate 也反映该撤销。

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

java
SigningDetails sharedSigningDetails =
        sharedUserSetting.signatures.mSigningDetails;
SigningDetails mergedDetails = sharedSigningDetails.mergeLineageWith(
        signingDetails);
if (mergedDetails != sharedSigningDetails) {
    for (AndroidPackage androidPackage : sharedUserSetting.getPackages()) {
        if (androidPackage.getPackageName() != null
                && !androidPackage.getPackageName().equals(
                parsedPackage.getPackageName())) {
            mergedDetails = mergedDetails.mergeLineageWith(
                    androidPackage.getSigningDetails(),
                    MERGE_RESTRICTED_CAPABILITY);
        }
    }
    sharedUserSetting.signatures.mSigningDetails = mergedDetails;
}
if (sharedUserSetting.signaturesChanged == null) {
    sharedUserSetting.signaturesChanged = Boolean.FALSE;
}

signaturesChanged 是本次扫描 tri-state:null 表示该 group 还没有初始化签名变化判断;false 表示已检查未走 OTA 替换;true 表示 system package OTA 路径更新了 group signature。它不等同于永久持久化 flag。

5.4 System例外 ​

system package shared UID 签名在 OTA 中出现不兼容时,旧设备与新设备失败语义不同。首发 API level ≤29 的设备抛 ReconcileFailure,允许跳过问题包继续尝试启动;更高首发 API level 抛 IllegalStateException,把 system shared UID 签名不一致视为系统整体 fatal。源码注释明确说明新设备不再允许用历史 vendor 容错掩盖 shared UID 破坏。

还有一层 non-preload 防护:普通 /data app 即使与 system package 有 platform signing capability,也不能默认加入 system shared UID。启用 restrictNonpreloadsSystemShareduids 且非 debuggable build 时,SystemConfig allowlist 必须精确把该 package 映射到目标 shared UID,否则 reconcile 失败。

6. SELinux域 ​

6.1 最低targetSdk ​

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

java
/**
 * Determine the targetSdkVersion for a sharedUser and update pkg.applicationInfo.seInfo
 * to ensure that all apps within the sharedUser share an SELinux domain. Use the lowest
 * targetSdkVersion of all apps within the shared user, which corresponds to the least
 * restrictive selinux domain.
 */
public void fixSeInfoLocked() {
    if (mPackages == null || mPackages.size() == 0) {
        return;
    }
    for (int i = 0; i < mPackages.size(); i++) {
        PackageSetting ps = mPackages.valueAt(i);
        if (ps == null || ps.getPkg() == null) {
            continue;
        }
        if (ps.getPkg().getTargetSdkVersion() < seInfoTargetSdkVersion) {
            seInfoTargetSdkVersion = ps.getPkg().getTargetSdkVersion();
            onChanged();
        }
}

第一遍只计算 group target,不立即写成员。把“求最低值”和“应用 override”拆成两遍,避免前几个成员使用尚未收敛的中间 target;第二遍开始时所有成员看到同一个最终值。

java
for (int i = 0; i < mPackages.size(); i++) {
    PackageSetting ps = mPackages.valueAt(i);
    if (ps == null || ps.getPkg() == null) {
        continue;
    }
    final boolean isPrivileged = isPrivileged() | ps.isPrivileged();
    ps.getPkgState().setOverrideSeInfo(
            SELinuxMMAC.getSeInfo(ps, ps.getPkg(), isPrivileged,
                    seInfoTargetSdkVersion));
    onChanged();
}

所有 active 可解析成员使用 group 最低 targetSdk 计算 seInfo,保证相同 UID 不会因为成员 target SDK 不同落入不同 domain。group private flags 是成员 OR,因此任一成员 privileged 时 group isPrivileged() 为 true,所有成员按 shared privileged context 计算。

6.2 生效时机 ​

PMS 构造函数完成 system/data 扫描后,对全部 SharedUserSetting 调 fixSeInfoLocked()。seInfoTargetSdkVersion 构造时是 CUR_DEVELOPMENT,第一个成员加入时临时设为该成员 target;启动收敛后降到所有成员最低值。

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

java
// Apps which share a sharedUserId must be placed in the same selinux domain.
// These are later adjusted in PackageManagerService's constructor to be the lowest
// targetSdkVersion of all apps within the shared user.
// NOTE: As new packages are installed / updated, the shared user's seinfoTargetSdkVersion
// will NOT be modified until next boot, even if a lower targetSdkVersion is used. This
// ensures that all packages continue to run in the same selinux domain.
if ((sharedUser != null) && (sharedUser.getPackages().size() != 0)) {
    return sharedUser.getSeInfoTargetSdkVersion();
}

运行中安装一个更低 targetSdk 新成员时,group target 不立即降低,因为已有成员进程可能仍在旧 domain;立即只改新包会破坏同 UID 同 domain,不杀掉全部成员又无法安全切换。源码选择等下次 boot 一起重算。这是“安全一致性优先于即时采用更宽松 domain”的生命周期屏障。

7. 权限与可见性 ​

7.1 权限owner ​

SharedUserSetting 继承的 legacy permission state 以 group 为单位保存。Settings 生成 runtime-permissions state 时跳过有 sharedUser 的 PackageSetting,并按 shared user name 单独写权限:

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

java
for (int i = 0; i < packagesSize; i++) {
    String packageName = packageStates.keyAt(i);
    PackageStateInternal packageState = packageStates.valueAt(i);
    if (!packageState.hasSharedUser()) {
        List<RuntimePermissionsState.PermissionState> permissions =
                getPermissionsFromPermissionsState(
                        packageState.getLegacyPermissionState(), userId);
        packagePermissions.put(packageName, permissions);
    }
}

第一段明确排除 shared member 的 package-level entry;否则同一份 UID 权限会同时以成员包和 shared user 两种 key 写出,读取时无法判断哪个 owner 为准。

java
for (int i = 0; i < sharedUsersSize; i++) {
    String sharedUserName = sharedUsers.keyAt(i);
    SharedUserSetting sharedUserSetting = sharedUsers.valueAt(i);
    List<RuntimePermissionsState.PermissionState> permissions =
            getPermissionsFromPermissionsState(
                    sharedUserSetting.getLegacyPermissionState(), userId);
    sharedUserPermissions.put(sharedUserName, permissions);
}

当前权限决策由 PermissionManager/AccessChecking owner 执行,SharedUserSetting 中的 LegacyPermissionState 仍作为持久化迁移桥接。关键语义是权限状态按 UID group 存一份,而不是每个成员各写一份后运行时再求并集。

成员包声明的 requested permissions 仍各自来自 AndroidPackage;扫描/权限 reconcile 决定 group UID 最终 grant。共享 UID 会扩大权限耦合面,因此加入成员必须受签名与 system allowlist 约束。

7.2 包可见性 ​

AppsFilter 的缓存 key 以 appId/UID 为核心。移除一个 shared UID 成员时,代码先清掉该 package 的痕迹,再重新加入其 sibling members,恢复它们对外查询关系;随后重建 shared appId 的过滤 cache。

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

java
// After removing all traces of the package, if it's part of a shared user, re-add other
// shared user members to re-establish visibility between them and other packages.
final SharedUserApi sharedUserApi = setting.hasSharedUser()
        ? snapshot.getSharedUser(setting.getSharedUserAppId()) : null;
if (sharedUserApi != null) {
    final ArraySet<? extends PackageStateInternal> sharedUserPackages =
            sharedUserApi.getPackageStates();
    for (int i = sharedUserPackages.size() - 1; i >= 0; i--) {
        if (sharedUserPackages.valueAt(i) == setting) {
            continue;
        }
        addPackageInternal(sharedUserPackages.valueAt(i), settings);
    }
}

这说明 package visibility 不能只按被删除包局部减一:同 appId 成员的 queries/components 会共同影响 UID 可见性,成员变化需要以 group siblings 重新计算。

8. 持久化模型 ​

8.1 packages.xml ​

SharedUserSetting 在 packages.xml 中只写 name、appId 与 shared signing details:

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

java
for (final SharedUserSetting usr : mSharedUsers.values()) {
    serializer.startTag(null, "shared-user");
    serializer.attribute(null, ATTR_NAME, usr.name);
    serializer.attributeInt(null, "userId", usr.mAppId);
    usr.signatures.writeXml(serializer, "sigs", writtenSignatures);
    serializer.endTag(null, "shared-user");
}

成员列表不嵌套写入 shared-user tag。每个 <package>/<updated-package> 用 sharedUserId=<appId> 表示关联;读取 PackageSetting 时 shared user tag 可能尚未出现,所以先放入 mPendingPackages,根文档解析完成后再按 appId 关联。

uidFlags/private aggregate、process map、seInfo target 和 active/disabled member set都由固定种子、PackageSetting records 与本次扫描重建,不直接序列化。signaturesChanged 也是一次 boot/reconcile 状态。

8.2 读取关联 ​

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

java
for (int i = 0; i < mPendingPackages.size(); i++) {
    final PackageSetting p = mPendingPackages.get(i);
    final int sharedUserAppId = p.getSharedUserAppId();
    if (sharedUserAppId <= 0) {
        continue;
    }
    final Object idObj = getSettingLPr(sharedUserAppId);
    if (idObj instanceof SharedUserSetting) {
        final SharedUserSetting sharedUser = (SharedUserSetting) idObj;
        addPackageSettingLPw(p, sharedUser);
    } else if (idObj != null) {
        PackageManagerService.reportSettingsProblem(Log.ERROR,
                "shared uid is not a shared uid");
    } else {
        PackageManagerService.reportSettingsProblem(Log.ERROR,
                "shared uid is not defined");
    }
}
mPendingPackages.clear();

“appId 已有但指向普通 PackageSetting”和“appId 完全未定义”是不同损坏。前者说明 Settings identity map 类型冲突,后者说明 shared-user root record 缺失;两者都不能静默把 package 当普通 UID,否则数据目录 ownership 会改变。

disabled system PackageSetting 在全局读取末尾按 appId 重新加入 SharedUserSetting 的 mDisabledPackages。这样 data update 被卸载时,factory PackageSetting 仍能恢复旧 shared UID 关系。

8.3 权限文件 ​

Permission APEX runtime-permissions.xml 分别有 <package> 与 <shared-user> entry。SharedUser permissions key 是 shared user name,不是某个成员 package name;主文件损坏时 reserve copy 恢复逻辑已在 Settings 持久化专题展开。

9. 成员移除 ​

9.1 普通移除 ​

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

java
boolean removePackageAndAppIdLPw(String name) {
    final PackageSetting p = mPackages.remove(name);
    if (p != null) {
        removeInstallerPackageStatus(name);
        removePccIdLPw(p.getPccId());
        SharedUserSetting sharedUserSetting = getSharedUserSettingLPr(p);
        if (sharedUserSetting != null) {
            sharedUserSetting.removePackage(p);
            return checkAndPruneSharedUserLPw(
                    sharedUserSetting, false);
        } else {
            removeAppIdLPw(p.getAppId());
            return true;
        }
    }
    return false;
}

移除 shared member 不立即删除 appId;group 仍有 active 或 disabled member时,appId owner 必须保留。removePackage() 重算 flags/processes,权限 manager再根据剩余成员处理 UID grant与 shared-user conversion。

9.2 Group裁剪 ​

java
boolean checkAndPruneSharedUserLPw(
        SharedUserSetting s, boolean skipCheck) {
    if (skipCheck || (s.getPackageStates().isEmpty()
            && s.getDisabledPackageStates().isEmpty())) {
        if (mSharedUsers.remove(s.name) != null) {
            removeAppIdLPw(s.mAppId);
            return true;
        }
    }
    return false;
}

只有 active 与 disabled 两组都空,group 才能从 mSharedUsers 和 appId map 删除。忽略 disabled set 会在 updated system app 场景提前释放 appId,导致 factory version 回退时身份冲突。

InstallPackageHelper 在扫描结果显示包已从旧 shared user 移走时,先从旧 group remove,再 prune;若 prune 成功,把 removed appId 放入 PackageRemovedInfo,后续可发送 UID_REMOVED。状态顺序是“解除成员→确认 group 无引用→释放 appId→通知消费者”。

10. 退出shared UID ​

10.1 Manifest标记 ​

源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java

java
String str = nonConfigString(
        0, R.styleable.AndroidManifest_sharedUserId, sa);
if (TextUtils.isEmpty(str)) {
    return input.success(pkg);
}

boolean leaving = false;
if (PackageManager.ENABLE_SHARED_UID_MIGRATION) {
    int max = anInteger(
            0, R.styleable.AndroidManifest_sharedUserMaxSdkVersion, sa);
    leaving = (max != 0)
            && (max < Build.VERSION.RESOURCES_SDK_INT);
}

return input.success(pkg
        .setLeavingSharedUser(leaving)
        .setSharedUserId(str.intern())
        .setSharedUserLabelResourceId(
                resId(R.styleable.AndroidManifest_sharedUserLabel, sa)));

APK 仍保留 sharedUserId 字符串,同时用 sharedUserMaxSdkVersion 表示高于某 SDK 后希望退出。Parser 只有在 build 启用 shared UID migration 时设置 leaving bit;完全禁用时 max SDK 被忽略。

新安装或已经退出的包若 isLeavingSharedUser(),扫描直接忽略 manifest sharedUserId,给它普通独立 appId。已有 group member 更新时则先保持 group 关联,再根据策略决定是否透明转换,避免 appId 与数据 ownership 突变。

10.2 升级约束 ​

应用更新通常禁止改变 sharedUserId declaration:旧 group name 与新 name 不同会返回 INSTALL_FAILED_UID_CHANGED。例外是已归档且旧 PackageSetting 没有 pkg 的 unarchive 兼容路径。已经标记 leaving 的 package 也不允许后续版本取消 leaving 重新加入 group。

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

java
if (!oldSharedUid.equals(newSharedUid)) {
    if (!(oldSharedUid.equals("<nothing>") && ps.getPkg() == null
            && ps.isArchivedOnAnyUser(allUsers))) {
        throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
                "Package " + parsedPackage.getPackageName()
                        + " shared user changed from "
                        + oldSharedUid + " to " + newSharedUid);
    }
}

// APK should not re-join shared UID
if (oldPackageState.isLeavingSharedUser()
        && !parsedPackage.isLeavingSharedUser()) {
    throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
            "Package " + parsedPackage.getPackageName()
                    + " attempting to rejoin " + newSharedUid);
}

10.3 Strategy边界 ​

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

java
public static final int NEW_INSTALL_ONLY = 1;
public static final int BEST_EFFORT = 2;
public static final int TRANSITION_AT_BOOT = 3;
public static final int LIVE_TRANSITION = 4;

private static final int DEFAULT = NEW_INSTALL_ONLY;

public static int getCurrentStrategy() {
    if (!Build.IS_USERDEBUG) {
        return DEFAULT;
    }

    final int s = SystemProperties.getInt(PROPERTY_KEY, DEFAULT);
    // No transition strategies can be used
    if (s > BEST_EFFORT || s < NEW_INSTALL_ONLY) {
        return DEFAULT;
    }
    return s;
}

生产 build固定 NEW_INSTALL_ONLY;只有 userdebug 可通过 persist.debug.pm.shared_uid_migration_strategy=2 启用 BEST_EFFORT。TRANSITION_AT_BOOT 与 LIVE_TRANSITION 虽定义常量,当前 getter 明确拒绝大于 2,不能写成已启用功能。

BEST_EFFORT 要求 SharedUserSetting.isSingleUser():一个 active member,disabled 至多一个且也声明 leaving。转换只改变 Settings owner,不改变 appId:

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

java
void convertSharedUserSettingsLPw(SharedUserSetting sharedUser) {
    final PackageSetting ps =
            sharedUser.getPackageSettings().valueAt(0);
    mAppIds.replaceSetting(sharedUser.getAppId(), ps);

    // Unlink the SharedUserSetting
    ps.setSharedUserAppId(INVALID_UID);
    if (!sharedUser.getDisabledPackageSettings().isEmpty()) {
        final PackageSetting disabledPs =
                sharedUser.getDisabledPackageSettings().valueAt(0);
        disabledPs.setSharedUserAppId(INVALID_UID);
    }
    mSharedUsers.remove(sharedUser.getName());
}

保持 appId 使现有 data directory ownership 与 UID 不变;appId map 从 group owner换成唯一 PackageSetting,active/disabled PackageSetting 清 shared link,最后删除 group name。它不是重新分配 UID,也不是 live chown 数据。

11. Snapshot边界 ​

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

java
private SharedUserSetting(SharedUserSetting orig) {
    super(orig);
    name = orig.name;
    mAppId = orig.mAppId;
    uidFlags = orig.uidFlags;
    uidPrivateFlags = orig.uidPrivateFlags;
    mPackages = orig.mPackagesSnapshot.snapshot();
    mPackagesSnapshot = new SnapshotCache.Sealed<>();
    mDisabledPackages = orig.mDisabledPackagesSnapshot.snapshot();
    mDisabledPackagesSnapshot = new SnapshotCache.Sealed<>();
    // A SigningDetails seems to consist solely of final attributes, so
    // it is safe to copy the reference.
    signatures.mSigningDetails = orig.signatures.mSigningDetails;
    signaturesChanged = orig.signaturesChanged;
    processes = new ArrayMap<>(orig.processes);
    mSnapshot = new SnapshotCache.Sealed<>();
}

member WatchedArraySet snapshot 会对 PackageSetting 元素调用 snapshot,因此 group snapshot 与成员状态形成同一只读版本。SigningDetails 按 immutable 假设共享引用;process map 复制 map,但 ParsedProcess values 并未再次 deep copy。SharedUserApi 是内部只读接口,调用者不应修改返回的 process map/value。

当前 copy constructor 还有一个必须按源码记录的缺口:它没有给 seInfoTargetSdkVersion 赋值。该字段在普通构造函数中初始化为 CUR_DEVELOPMENT,但 copy constructor 不调用普通构造函数,因此 snapshot 实例会保留 Java 默认值 0。SharedUserApi.getSeInfoTargetSdkVersion() 若在 snapshot 上被消费,不能假设它与 live group 相同;当前测试树也没有专门覆盖这一字段。这是 Android 17 当前实现与“copy constructor 创建完整只读快照”直觉不一致的地方。

getPackageStates() 当前返回 mPackages.untrackedStorage()。live SharedUserSetting 上这是底层 mutable ArraySet;snapshot 中 set 已 seal,但 untracked storage 绕过 wrapper 的 change tracking。该 API 依赖 system_server 内部只读纪律,不能把集合交给不受控消费者。

12. 失败矩阵 ​

条件处理影响范围
shared user appId 无法分配INSTALL_FAILED_INSUFFICIENT_STORAGE新 group 创建失败
已安装包切换到不同 shared userINSTALL_FAILED_UID_CHANGED包更新失败
package 与 group signer 不兼容INSTALL_FAILED_SHARED_USER_INCOMPATIBLE安装/更新失败
signing lineage 分叉shared-user incompatible禁止 group 合并
新非预装 platform-signed app 未在 allowlistreconcile failure禁止加入 system shared UID
新设备 system shared UID 签名不一致IllegalStateExceptionsystem startup fatal
移除一个成员但 group仍有成员保留 appId owner其他成员继续运行
active 空但 disabled 非空不 prune group保留 factory rollback 身份
新低 targetSdk 成员运行期加入seInfo target 不立即降低等下次 boot 一致切换
production build单成员 leaving不执行 BEST_EFFORT继续留在 group

shared UID 把多个包的失败域绑定到一个 UID。普通包签名错误只影响单包;system shared UID lineage 不一致可能阻止整机继续启动,因为放行其中一半会产生同 UID 不同安全身份。

13. 测试证据 ​

13.1 AppId owner ​

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

java
@Test
public void testAddPackageSetting() throws PackageManagerException {
    final Settings settings = makeSettings();
    final SharedUserSetting sus1 = new SharedUserSetting(
            "TestUser", 0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/);
    sus1.mAppId = 10001;
    final PackageSetting ps1 = createPackageSetting("com.foo");
    ps1.setAppId(10001);
    assertTrue(settings.registerAppIdLPw(ps1, false));

    settings.addPackageSettingLPw(ps1, sus1);

    assertSame(sus1, settings.getSharedUserSettingLPr(ps1));
}

输入先让 appId 10001 指向普通 PackageSetting,再把同一 package绑定到 SharedUserSetting;断言按 PackageSetting 的 shared appId 反查得到 group。结合 addPackageSettingLPw() 源码可确认 appId map owner被替换为 SharedUserSetting。测试没有断言成员 flags/process 聚合。

testCreateNewSetting03() 构造 appId=10064 的 SharedUserSetting,再创建 shared member;断言 PackageSetting appId 为 10064、包字段保持正常,并且 user 0 installed=true、stopped=false。它验证 shared member 初始身份来自 group,而不是新分配 appId。

13.2 切换拒绝 ​

同文件 testUpdatePackageSetting03() 创建普通 PackageSetting与另一个 SharedUserSetting,调用 Settings.updatePackageSetting() 尝试从无 group 切换到该 shared user;测试要求抛 PackageManagerException。输入、动作直接覆盖 existingSharedUserSetting != sharedUser 分支,证明已安装包不能通过普通 update 任意进入 shared UID。

该测试只断言有异常,没有核对错误码;INSTALL_FAILED_UID_CHANGED 的精确结果仍由源码 throw 分支提供。

13.3 签名lineage ​

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

java
@Test
public void testReadXmlWithSigningLineage() throws Exception {
    verifyReadXmlReturnsExpectedSignaturesAndLineage(
            "xml/two-signers-in-lineage.xml", 3,
            FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE);
    verifyReadXmlReturnsExpectedSignaturesAndLineage(
            "xml/three-signers-in-lineage.xml", 3,
            FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE,
            THIRD_EXPECTED_SIGNATURE);
}

fixture 来自真实 apksigner lineage 与设备 packages.xml,cert flags 包含 INSTALLED_DATA、SHARED_USER_ID、PERMISSION、ROLLBACK capability。测试断言过去 signer与能力能从 XML 恢复,为 canJoinSharedUserId() 的 lineage/capability 输入提供反向证据;它没有直接执行 join decision,因此不能单独证明 install/update/system 三种分支。

当前源码树未提供直接覆盖 canJoinSharedUserId() 全决策矩阵或 SharedUidMigration BEST_EFFORT conversion 的专用测试。相关结论必须结合方法控制流、Settings tests 和真实安装/升级集成测试,不能把 PackageSignatures XML test冒充端到端 shared UID 安装验证。

13.4 权限round-trip ​

Permission APEX 的 RuntimePermissionsPersistenceTest.testWriteRead() 构造同时含 package permission 与 sharedUser permission map 的 RuntimePermissionsState,写入后逐项比较 version、fingerprint、grant、flags 和 sharedUserPermissions。它验证 shared user权限作为独立 map entry持久化,而不是复制到成员 package;不覆盖 PermissionManager 如何计算最终 grant。

14. 源码推演 ​

给定“卸载 shared UID 的一个成员后 appId 没有释放”,应检查 SharedUserSetting 的 active 与 disabled sets。只要任一集合非空,checkAndPruneSharedUserLPw() 就必须保留 group;同时 removePackage() 要重算 flags/processes,AppsFilter 要按 siblings 重建可见性。

给定“新成员签名是 group 旧 signer 的后代,却仍不能新安装”,应沿 canJoinSharedUserId() 区分 update 与 install。新安装不仅要求 lineage/capability,还会遍历现有成员,确认新 lineage没有撤销任何成员 signer 的 SHARED_USER_ID capability;更新已有成员允许逐步轮换。

给定“新安装成员 targetSdk 更低,但 seInfoTargetSdkVersion 没立即变化”,应查看 SELinuxMMAC 注释:运行期保持现有 group domain,等下次 boot 对全部成员一起取最低 target重新计算。只更新一个成员会造成同 UID 不同 domain,源码有意推迟。

给定“应用设置 sharedUserMaxSdkVersion 后仍在 production 设备保留 shared UID”,应区分 NEW_INSTALL_ONLY 与 BEST_EFFORT。新安装可忽略 sharedUserId;已安装 group只有 userdebug strategy=2 且 single-user 条件满足时才透明转换,生产默认不会执行该转换。

最后,修改 SharedUserSetting 代码时不能只运行单包测试。成员集合变化会同时影响 appId owner、权限、seInfo、ABI、process、AppsFilter、packages.xml 与 system package rollback;任何一个聚合 owner漏更新,都可能在本次运行正常、重启或卸载更新后才暴露。