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
/**
* 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 含义 | 是否直接等于成员字段 |
|---|---|---|
name | Manifest sharedUserId 字符串 | 否,是 group key |
mAppId | 所有成员共享的 appId | 成员 PackageSetting appId 应相同 |
uidFlags/privateFlags | group 固有基础 flags | 不含动态成员 OR 结果 |
mPackages | 当前 active PackageSetting | 包级状态仍在成员中 |
mDisabledPackages | 被 data 更新遮蔽的 system PackageSetting | 用于回退/退出 shared UID |
signatures | group 可接受的签名 lineage | 由成员签名验证/合并产生 |
processes | 同名 process 声明的聚合 | 从可解析 active 成员重建 |
seInfoTargetSdkVersion | group SELinux target SDK | 启动时取成员最低值 |
1.2 只读接口
源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/SharedUserApi.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
@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
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
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 查询得出不同结果。
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
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
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
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
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 剩余成员:
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
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
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
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 下有额外容错。
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
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
/**
* 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;第二遍开始时所有成员看到同一个最终值。
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
// 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
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 为准。
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
// 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
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
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
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裁剪
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
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
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
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
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
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 user | INSTALL_FAILED_UID_CHANGED | 包更新失败 |
| package 与 group signer 不兼容 | INSTALL_FAILED_SHARED_USER_INCOMPATIBLE | 安装/更新失败 |
| signing lineage 分叉 | shared-user incompatible | 禁止 group 合并 |
| 新非预装 platform-signed app 未在 allowlist | reconcile failure | 禁止加入 system shared UID |
| 新设备 system shared UID 签名不一致 | IllegalStateException | system 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
@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
@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漏更新,都可能在本次运行正常、重启或卸载更新后才暴露。
