From 3a44ec16579f19b9afab130b114c7c3d93bdeeec Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 11 Aug 2026 18:16:23 +0000 Subject: [PATCH 01/10] update ActivityThreadHooks.onBind2() comment --- core/java/android/app/ActivityThreadHooks.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/java/android/app/ActivityThreadHooks.java b/core/java/android/app/ActivityThreadHooks.java index b98b29ce2f1c0..a3f4e51ee6748 100644 --- a/core/java/android/app/ActivityThreadHooks.java +++ b/core/java/android/app/ActivityThreadHooks.java @@ -59,8 +59,8 @@ static Bundle onBind(ActivityThread.AppBindData appBindData) { return args; } - // called after ActivityThread instrumentation is inited, which happens before execution of any - // of app's code + // called after ActivityThread instrumentation is inited, which happens before Application class + // init but after app-overridable AppComponentFactory init // ActivityThread.handleBindApplication static void onBind2(Context appContext, Bundle appBindArgs) { ActivityThreadHooks.appContext = appContext; From 5ccae3f9b5412ce0d3399dc466f60dbc9add628c Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Wed, 12 Aug 2026 12:18:33 +0000 Subject: [PATCH 02/10] fixup! notify the user about notable system journal entries Adjust SystemJournalNotif to support PackageIdOwnershipChecks notifications which are added by the next commit. --- .../android/server/ext/DropBoxMonitor.java | 2 +- .../server/ext/SystemErrorNotification.java | 50 +++++++++++++++---- .../server/ext/SystemJournalNotif.java | 39 +++++++++++---- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/services/core/java/com/android/server/ext/DropBoxMonitor.java b/services/core/java/com/android/server/ext/DropBoxMonitor.java index 04742b15b9243..0aacfb1f31a19 100644 --- a/services/core/java/com/android/server/ext/DropBoxMonitor.java +++ b/services/core/java/com/android/server/ext/DropBoxMonitor.java @@ -301,7 +301,7 @@ void handleFsckEntry(DropBoxManager.Entry e) { i.putExtra(LogViewerApp.EXTRA_ERROR_TYPE, "fsck_error"); i.putExtra(LogViewerApp.EXTRA_SHOW_REPORT_BUTTON, true); - SystemJournalNotif.show(context, e.getTimeMillis(), context.getString(R.string.fsck_error_notif_title), i); + SystemJournalNotif.show(e.getTimeMillis(), context.getString(R.string.fsck_error_notif_title), i); } } diff --git a/services/core/java/com/android/server/ext/SystemErrorNotification.java b/services/core/java/com/android/server/ext/SystemErrorNotification.java index 9c6e29202feaf..2dd4d339fd5d1 100644 --- a/services/core/java/com/android/server/ext/SystemErrorNotification.java +++ b/services/core/java/com/android/server/ext/SystemErrorNotification.java @@ -2,40 +2,68 @@ import android.annotation.CurrentTimeMillisLong; import android.annotation.Nullable; +import android.annotation.StringRes; import android.content.Context; import android.content.Intent; import android.ext.LogViewerApp; import android.os.Binder; import android.util.Slog; +import java.util.function.Function; + public class SystemErrorNotification { static final String TAG = SystemErrorNotification.class.getSimpleName(); public @CurrentTimeMillisLong long when = System.currentTimeMillis(); public final String type; - public final String title; - public final String message; + public final Function textSupplier; public boolean showReportButton = true; + public static class Text { + public final CharSequence title; + public final String message; + + public Text(CharSequence title, String message) { + this.title = title; + this.message = message; + } + } + public SystemErrorNotification(String type, String message) { this(type, type, message); } public SystemErrorNotification(String type, String title, String message) { + this(type, ctx -> new Text(title, message)); + } + + public SystemErrorNotification(String type, @StringRes int title, String message) { + this(type, ctx -> new Text(ctx.getString(title), message)); + } + + public SystemErrorNotification(String type, Function textSupplier) { this.type = type; - this.title = title; - this.message = message; + this.textSupplier = textSupplier; + } + + // kept for compatibility with the previous callers, context is intentionally ignored + public void show(@Nullable Context context) { + show(); } - public void show(@Nullable Context ctx) { - var i = LogViewerApp.createBaseErrorReportIntent(message); - i.putExtra(Intent.EXTRA_TITLE, title); - i.putExtra(LogViewerApp.EXTRA_ERROR_TYPE, type); - i.putExtra(LogViewerApp.EXTRA_SHOW_REPORT_BUTTON, showReportButton); - Slog.e(TAG, type + ", title: " + title + ", message: " + message); + public void show() { + Function notifDataSupplier = ctx -> { + Text text = this.textSupplier.apply(ctx); + var intent = LogViewerApp.createBaseErrorReportIntent(text.message); + intent.putExtra(Intent.EXTRA_TITLE, text.title); + intent.putExtra(LogViewerApp.EXTRA_ERROR_TYPE, type); + intent.putExtra(LogViewerApp.EXTRA_SHOW_REPORT_BUTTON, showReportButton); + Slog.e(TAG, type + ", title: " + text.title + ", message: " + text.message); + return new SystemJournalNotif.NotificationData(text.title, intent); + }; final long identity = Binder.clearCallingIdentity(); try { - SystemJournalNotif.show(ctx, when, title, i); + SystemJournalNotif.show(when, notifDataSupplier); } finally { Binder.restoreCallingIdentity(identity); } diff --git a/services/core/java/com/android/server/ext/SystemJournalNotif.java b/services/core/java/com/android/server/ext/SystemJournalNotif.java index 591b8361ce5c7..7f22e451a8e83 100644 --- a/services/core/java/com/android/server/ext/SystemJournalNotif.java +++ b/services/core/java/com/android/server/ext/SystemJournalNotif.java @@ -20,6 +20,7 @@ import com.android.internal.os.BackgroundThread; import java.util.ArrayList; +import java.util.function.Function; public class SystemJournalNotif { @@ -34,29 +35,45 @@ static void showCrash(Context ctx, String progName, String errorReport, i.putExtra(LogViewerApp.EXTRA_TEXT_TOMBSTONE_LAST_MODIFIED_TIME, textTombstoneFileSpec.second.longValue()); } - show(ctx, crashTimestamp, ctx.getString(R.string.process_crash_notif_title, progName), i); + show(crashTimestamp, ctx.getString(R.string.process_crash_notif_title, progName), i); } - // If ctx is null then default system context will be used as a fallback - static void show(@Nullable Context ctx, @CurrentTimeMillisLong long when, String notifTitle, Intent mainIntent) { + public static class NotificationData { + public final CharSequence title; + public final Intent mainIntent; + + public NotificationData(CharSequence title, Intent mainIntent) { + this.title = title; + this.mainIntent = mainIntent; + } + } + + static void show(@CurrentTimeMillisLong long when, Function notifDataSupplier) { synchronized (pendingActions) { if (!isSystemServerInited) { // NotificationManagerService isn't ready yet, delay this notification until after // system_server init completion - pendingActions.add(() -> showInner(ctx, when, notifTitle, mainIntent)); + pendingActions.add(() -> { + Context ctx = ActivityThread.currentActivityThread().getSystemContext(); + showInner(ctx, when, notifDataSupplier); + }); return; } } - showInner(ctx, when, notifTitle, mainIntent); + Context ctx = ActivityThread.currentActivityThread().getSystemContext(); + showInner(ctx, when, notifDataSupplier); } - private static void showInner(@Nullable Context ctx, @CurrentTimeMillisLong long when, String notifTitle, Intent mainIntent) { - if (ctx == null) { - ctx = ActivityThread.currentActivityThread().getSystemContext(); - } + static void show(@CurrentTimeMillisLong long when, String notifTitle, Intent mainIntent) { + show(when, ctx -> new NotificationData(notifTitle, mainIntent)); + } + + private static void showInner(Context ctx, @CurrentTimeMillisLong long when, + Function notifDataSupplier) { + NotificationData notifData = notifDataSupplier.apply(ctx); var b = new Notification.Builder(ctx, SystemNotificationChannels.SYSTEM_JOURNAL); b.setSmallIcon(R.drawable.ic_error); - b.setContentTitle(notifTitle); + b.setContentTitle(notifData.title); b.setContentText(ctx.getText(R.string.notif_text_tap_to_see_details)); b.setAutoCancel(true); b.setWhen(when); @@ -64,7 +81,7 @@ private static void showInner(@Nullable Context ctx, @CurrentTimeMillisLong long UserHandle user = UserHandle.of(ActivityManager.getCurrentUser()); - var pi = PendingIntent.getActivityAsUser(ctx, 0, mainIntent, + var pi = PendingIntent.getActivityAsUser(ctx, 0, notifData.mainIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT, null, user); b.setContentIntent(pi); From 03348adc6108f37f8ce168ea393d0195e0c8b25a Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 11 Aug 2026 18:16:23 +0000 Subject: [PATCH 03/10] add custom handling of stock OS packages adevtool parses all stock OS APKs and builds an ApkParserConfig protobuf, which is included in the OS at /product/etc/apk-parser-config.pb. ApkParserConfig is checked at runtime by system_server for the following purposes: - Ownership enforcement of system-wide stock OS package IDs: package names, permission names, permission group names, content provider authorities. These IDs may be used only if the signing certificate matches. Stock OS packages and native binaries don't always perform these checks themselves, since on stock OS all of these IDs are already taken and can't be reused. Therefore, it's not safe in the general case to include a subset of stock OS packages without these checks. - Blocking installation of a subset of stock OS packages. - Isolation of specific stock OS packages from GmsCore and Play Store, or from all user-installed apps. --- .../src/android/os/PackageParsingPerfTest.kt | 1 + .../internal/pm/parsing/PackageParser2.java | 17 +- .../pm/parsing/PackageParserConfig.java | 180 ++++++++++++++++++ .../internal/pm/parsing/pkg/PackageImpl.java | 58 +++++- .../pm/pkg/parsing/ParsingPackage.java | 10 + .../pm/pkg/parsing/ParsingPackageUtils.java | 41 +++- .../android/server/pm/pkg/AndroidPackage.java | 6 + core/res/res/values/public-ext.xml | 2 + core/res/res/values/string_ext.xml | 3 + proto/src/apk_parser_config.proto | 39 ++++ .../server/pm/PackageManagerService.java | 5 +- .../android/server/pm/ext/PackageExtInit.java | 41 +++- .../android/server/pm/ext/PackageHooks.java | 30 ++- .../pm/ext/PackageIdOwnershipChecks.java | 100 ++++++++++ .../java/com/android/server/SystemServer.java | 2 + .../server/pm/parsing/ParsingUtils.java | 1 + 16 files changed, 511 insertions(+), 25 deletions(-) create mode 100644 core/java/com/android/internal/pm/parsing/PackageParserConfig.java create mode 100644 proto/src/apk_parser_config.proto create mode 100644 services/core/java/com/android/server/pm/ext/PackageIdOwnershipChecks.java diff --git a/apct-tests/perftests/packagemanager/src/android/os/PackageParsingPerfTest.kt b/apct-tests/perftests/packagemanager/src/android/os/PackageParsingPerfTest.kt index 48f2c1554c30a..7b3fe7fa73f3a 100644 --- a/apct-tests/perftests/packagemanager/src/android/os/PackageParsingPerfTest.kt +++ b/apct-tests/perftests/packagemanager/src/android/os/PackageParsingPerfTest.kt @@ -205,6 +205,7 @@ public class PackageParsingPerfTest { override fun hasFeature(feature: String) = true override fun startParsingPackage( + flags: Int, packageName: String, baseApkPath: String, path: String, diff --git a/core/java/com/android/internal/pm/parsing/PackageParser2.java b/core/java/com/android/internal/pm/parsing/PackageParser2.java index 1f4ab65874fb5..6d30b3e7c4af4 100644 --- a/core/java/com/android/internal/pm/parsing/PackageParser2.java +++ b/core/java/com/android/internal/pm/parsing/PackageParser2.java @@ -188,14 +188,23 @@ public void close() { public abstract static class Callback implements ParsingPackageUtils.Callback { + private static volatile boolean idOwnershipChecksEnabled; + + public static void enableIdOwnershipChecks() { + idOwnershipChecksEnabled = true; + } + @Override - public final ParsingPackage startParsingPackage(@NonNull String packageName, + public final ParsingPackage startParsingPackage(int flags, @NonNull String packageName, @NonNull String baseCodePath, @NonNull String codePath, @NonNull TypedArray manifestArray, boolean isCoreApp) { - var res = PackageImpl.forParsing(packageName, baseCodePath, codePath, manifestArray, + var pkg = PackageImpl.forParsing(packageName, baseCodePath, codePath, manifestArray, isCoreApp, Callback.this); - res.initPackageParsingHooks(); - return res; + if ((flags & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR) == 0 && idOwnershipChecksEnabled) { + pkg.enableIdOwnershipChecks(); + } + pkg.initPackageParsingHooks(); + return pkg; } /** diff --git a/core/java/com/android/internal/pm/parsing/PackageParserConfig.java b/core/java/com/android/internal/pm/parsing/PackageParserConfig.java new file mode 100644 index 0000000000000..421fbc1e94555 --- /dev/null +++ b/core/java/com/android/internal/pm/parsing/PackageParserConfig.java @@ -0,0 +1,180 @@ +package com.android.internal.pm.parsing; + +import android.annotation.Nullable; +import android.os.Build; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.pm.parsing.nano.ApkParserConfig; +import com.android.internal.pm.parsing.nano.ApcPackageConfig; +import com.android.internal.pm.parsing.nano.PackageInstallRequirements; +import com.android.internal.pm.pkg.component.ParsedPermission; +import com.android.internal.pm.pkg.component.ParsedPermissionGroup; +import com.android.internal.pm.pkg.component.ParsedProvider; +import com.android.internal.pm.pkg.parsing.ParsingPackage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; + +public class PackageParserConfig { + public static final String TAG = "PackageParserConfig"; + + private ApkParserConfig config; + private HashSet nonInstallablePackages; + + private static volatile PackageParserConfig instance; + + public static PackageParserConfig get() { + PackageParserConfig res = instance; + if (res == null) { + throw new RuntimeException("PackageParserConfig was not initialized"); + } + return res; + } + + public static void init() { + Log.d(TAG, "init"); + + ApkParserConfig res; + try { + byte[] protobuf = Files.readAllBytes(Path.of("/product/etc/apk-parser-config.pb")); + res = ApkParserConfig.parseFrom(protobuf); + } catch (IOException e) { + if (Build.IS_DEBUGGABLE || Build.IS_EMULATOR) { + if (!Build.IS_EMULATOR) { + Slog.e(TAG, "", e); + } + res = new ApkParserConfig(); + } else { + // apk-parser-config.pb is a trusted part of the OS + throw new SecurityException(e); + } + } + if (res.permissionOwners == null) { + res.permissionOwners = Map.of(); + } + if (res.permissionGroupOwners == null) { + res.permissionGroupOwners = Map.of(); + } + if (res.contentProviderAuthorityOwners == null) { + res.contentProviderAuthorityOwners = Map.of(); + } + if (res.parsingConfigs == null) { + res.parsingConfigs = Map.of(); + } + if (res.installablePackages == null) { + res.installablePackages = Map.of(); + } + + var ppc = new PackageParserConfig(); + ppc.config = res; + ppc.nonInstallablePackages = new HashSet<>(Arrays.asList(res.nonInstallablePackages)); + + HashSet packages = new HashSet<>(ppc.nonInstallablePackages); + for (String pkg : res.installablePackages.keySet()) { + packages.add(pkg); + } + for (String pkg : res.permissionOwners.values()) { + if (!packages.contains(pkg)) { + throw new SecurityException(pkg); + } + } + for (String pkg : res.permissionGroupOwners.values()) { + if (!packages.contains(pkg)) { + throw new SecurityException(pkg); + } + } + for (String pkg : res.contentProviderAuthorityOwners.values()) { + if (!packages.contains(pkg)) { + throw new SecurityException(pkg); + } + } + for (String pkg : res.parsingConfigs.keySet()) { + if (!packages.contains(pkg)) { + throw new SecurityException(pkg); + } + } + + instance = ppc; + } + + public boolean isInstallationBlocked(String pkgName) { + return nonInstallablePackages.contains(pkgName); + } + + public void checkPermissionOwnership(ParsingPackage pkg, ParsedPermission permission) { + Map ownershipMap = config.permissionOwners; + String name = permission.getName(); + { + String ownerPkgName = ownershipMap.get(name); + if (ownerPkgName != null) { + String pkgName = pkg.getPackageName(); + if (!ownerPkgName.equals(pkgName)) { + pkg.recordIdOwnershipViolation("permission " + name + " is owned by " + ownerPkgName); + } + } + } + if (permission.isTree()) { + String prefix = name + '.'; + for (String permName : ownershipMap.keySet()) { + if (!permName.startsWith(prefix)) { + continue; + } + String ownerPkgName = ownershipMap.get(permName); + String pkgName = pkg.getPackageName(); + if (!ownerPkgName.equals(pkgName)) { + pkg.recordIdOwnershipViolation("permission-tree " + name + " conflicts with " + permName + " which is owned by " + ownerPkgName); + } + } + } + } + + public void checkPermissionGroupOwnership(ParsingPackage pkg, ParsedPermissionGroup permissionGroup) { + Map ownershipMap = config.permissionGroupOwners; + String name = permissionGroup.getName(); + String ownerPkgName = ownershipMap.get(name); + if (ownerPkgName != null) { + String pkgName = pkg.getPackageName(); + if (!ownerPkgName.equals(pkgName)) { + pkg.recordIdOwnershipViolation("permission-group " + name + " is owned by " + ownerPkgName); + } + } + } + + public void checkContentProviderAuthorityOwnership(ParsingPackage pkg, ParsedProvider provider) { + String authority = provider.getAuthority(); + if (authority == null) { + return; + } + Map ownershipMap = config.contentProviderAuthorityOwners; + String pkgName = pkg.getPackageName(); + + for (String auth : authority.split(";")) { + String authOwnerPkgName = ownershipMap.get(auth); + if (authOwnerPkgName == null) { + continue; + } + if (!authOwnerPkgName.equals(pkgName)) { + pkg.recordIdOwnershipViolation("provider " + provider.getName() + " has authority " + auth + " which is owned by " + authOwnerPkgName); + } + } + } + + @Nullable + public ApcPackageConfig getApcPackageConfig(String pkgName) { + return config.parsingConfigs.get(pkgName); + } + + @Nullable + public PackageInstallRequirements getPackageInstallRequirements(String pkgName) { + return config.installablePackages.get(pkgName); + } + + public static boolean hasApcFlag(ApcPackageConfig config, int flag) { + return (config.flags & (1L << flag)) != 0; + } +} diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java index 08eeaa36fa817..1ca5b9801a695 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java @@ -24,6 +24,7 @@ import android.annotation.LongDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityThread; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; @@ -45,6 +46,7 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Log; import android.util.Pair; import android.util.SparseArray; import android.util.SparseIntArray; @@ -52,6 +54,8 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.pm.parsing.AppInfoUtils; +import com.android.internal.pm.parsing.PackageParserConfig; +import com.android.internal.pm.parsing.nano.ApcPackageConfig; import com.android.internal.pm.pkg.AndroidPackageSplitImpl; import com.android.internal.pm.pkg.SEInfoUtil; import com.android.internal.pm.pkg.component.ComponentMutateUtils; @@ -103,7 +107,6 @@ import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.function.Function; @@ -610,14 +613,27 @@ public ParsingPackage addOverlayable(String overlayableName, String actorName) { return this; } + @Override + public boolean areIdOwnershipChecksEnabled() { + return idOwnershipChecksEnabled; + } + @Override public PackageImpl addPermission(ParsedPermission permission) { + if (areIdOwnershipChecksEnabled()) { + PackageParserConfig.get().checkPermissionOwnership(this, permission); + } + this.permissions = CollectionUtils.add(this.permissions, permission); return this; } @Override public PackageImpl addPermissionGroup(ParsedPermissionGroup permissionGroup) { + if (areIdOwnershipChecksEnabled()) { + PackageParserConfig.get().checkPermissionGroupOwnership(this, permissionGroup); + } + this.permissionGroups = CollectionUtils.add(this.permissionGroups, permissionGroup); return this; } @@ -651,9 +667,14 @@ public PackageImpl addProtectedBroadcast(String protectedBroadcast) { @Override public PackageImpl addProvider(ParsedProvider parsedProvider) { if (getPackageParsingHooks().shouldSkipProvider(parsedProvider)) { + Log.d("PackageParsingHooks", "skipped provider " + parsedProvider.getAuthority() + " in " + getPackageName()); return this; } + if (areIdOwnershipChecksEnabled()) { + PackageParserConfig.get().checkContentProviderAuthorityOwnership(this, parsedProvider); + } + this.providers = CollectionUtils.add(this.providers, parsedProvider); addMimeGroupsFromComponent(parsedProvider); return this; @@ -4066,13 +4087,25 @@ private static class Booleans2 { } private PackageParsingHooks packageParsingHooks = PackageParsingHooks.DEFAULT; + @Nullable + private ApcPackageConfig apcPackageConfig; public static Function packageParsingHooksSupplier; @Override public void initPackageParsingHooks() { var supplier = packageParsingHooksSupplier; - packageParsingHooks = supplier != null ? supplier.apply(getPackageName()) : PackageParsingHooks.DEFAULT; + String pkgName = getPackageName(); + packageParsingHooks = supplier != null ? supplier.apply(pkgName) : PackageParsingHooks.DEFAULT; + if (ActivityThread.isSystem()) { + apcPackageConfig = PackageParserConfig.get().getApcPackageConfig(pkgName); + } + } + + @Nullable + @Override + public ApcPackageConfig getApcPackageConfig() { + return apcPackageConfig; } @Override @@ -4080,6 +4113,27 @@ public PackageParsingHooks getPackageParsingHooks() { return packageParsingHooks; } + private boolean idOwnershipChecksEnabled; + private final ArrayList idOwnershipViolations = new ArrayList<>(); + + public void enableIdOwnershipChecks() { + idOwnershipChecksEnabled = true; + } + + @Override + public void recordIdOwnershipViolation(String text) { + synchronized (idOwnershipViolations) { + idOwnershipViolations.add(text); + } + } + + @Override + public String[] getIdOwnershipViolations() { + synchronized (idOwnershipViolations) { + return idOwnershipViolations.toArray(String[]::new); + } + } + private PackageExtIface ext = PackageExtDefault.INSTANCE; @Override diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java index 3beb7174220a3..5696a3a2bfcd1 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java @@ -31,6 +31,7 @@ import android.util.SparseArray; import android.util.SparseIntArray; +import com.android.internal.pm.parsing.nano.ApcPackageConfig; import com.android.internal.pm.parsing.pkg.PackageExtIface; import com.android.internal.pm.parsing.pkg.ParsedPackage; import com.android.internal.pm.pkg.component.ParsedActivity; @@ -636,10 +637,19 @@ ParsingPackage setResetEnabledSettingsOnAppDataCleared( void initPackageParsingHooks(); + @Nullable + ApcPackageConfig getApcPackageConfig(); + default PackageParsingHooks getPackageParsingHooks() { return PackageParsingHooks.DEFAULT; } + boolean areIdOwnershipChecksEnabled(); + + void recordIdOwnershipViolation(String text); + + String[] getIdOwnershipViolations(); + void setPackageExt(@Nullable PackageExtIface ext); boolean isDeclaredHavingCode(); diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index 699072c44a873..fc6d4dd21152c 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -84,6 +84,7 @@ import android.util.ArraySet; import android.util.AttributeSet; import android.util.DisplayMetrics; +import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -327,6 +328,13 @@ public ParsingPackageUtils(String[] separateProcesses, DisplayMetrics displayMet mCallback = callback; } + @Nullable + public static PackageIdOwnershipChecksIface packageIdOwnershipChecks; + + public interface PackageIdOwnershipChecksIface { + ParseResult run(ParseInput input, ParsingPackage pkg); + } + /** * Parse the package at the given location. Automatically detects if the package is a monolithic * style (single APK file) or cluster style (directory of APKs). @@ -343,11 +351,22 @@ public ParsingPackageUtils(String[] separateProcesses, DisplayMetrics displayMet * do so. */ public ParseResult parsePackage(ParseInput input, File packageFile, int flags) { + ParseResult result; if (packageFile.isDirectory()) { - return parseClusterPackage(input, packageFile, flags); + result = parseClusterPackage_(input, packageFile, flags); } else { - return parseMonolithicPackage(input, packageFile, flags); + result = parseMonolithicPackage_(input, packageFile, flags); } + if ((flags & PARSE_IS_SYSTEM_DIR) == 0 && result.isSuccess()) { + PackageIdOwnershipChecksIface check = packageIdOwnershipChecks; + if (check != null) { + ParseResult override = check.run(input, result.getResult()); + if (override != null) { + return override; + } + } + } + return result; } /** @@ -359,7 +378,7 @@ public ParseResult parsePackage(ParseInput input, File packageFi * Note that this does not perform signature verification; that must be done separately * in {@link #getSigningDetails(ParseInput, ParsedPackage, boolean)}. */ - private ParseResult parseClusterPackage(ParseInput input, File packageDir, + private ParseResult parseClusterPackage_(ParseInput input, File packageDir, int flags) { int liteParseFlags = 0; if ((flags & PARSE_APK_IN_APEX) != 0) { @@ -434,7 +453,7 @@ private ParseResult parseClusterPackage(ParseInput input, File p * Note that this does not perform signature verification; that must be done separately * in {@link #getSigningDetails(ParseInput, ParsedPackage, boolean)}. */ - private ParseResult parseMonolithicPackage(ParseInput input, File apkFile, + private ParseResult parseMonolithicPackage_(ParseInput input, File apkFile, int flags) { // The signature parsing will be done later in method parseBaseApk. int liteParseFlags = flags & ~PARSE_COLLECT_CERTIFICATES; @@ -477,7 +496,7 @@ public ParseResult parsePackageFromPackageLite(ParseInput input, final String pkgName = lite.getPackageName(); final TypedArray manifestArray = null; - final ParsingPackage pkg = mCallback.startParsingPackage(pkgName, + final ParsingPackage pkg = mCallback.startParsingPackage(flags, pkgName, lite.getBaseApkPath(), lite.getPath(), manifestArray, lite.isCoreApp()); final int targetSdk = lite.getTargetSdk(); @@ -777,7 +796,7 @@ private ParseResult parseBaseApk(ParseInput input, String apkPat try { final boolean isCoreApp = parser.getAttributeBooleanValue(null /*namespace*/, "coreApp", false); - final ParsingPackage pkg = mCallback.startParsingPackage( + final ParsingPackage pkg = mCallback.startParsingPackage(flags, pkgName, apkPath, codePath, manifestArray, isCoreApp); final ParseResult result = parseBaseApkTags(input, pkg, manifestArray, res, parser, flags, @@ -1582,7 +1601,13 @@ private ParseResult parseUsesPermission(ParseInput input, if (!found) { var p = new ParsedUsesPermissionImpl(name, usesPermissionFlags, purposeStringResource, purposes, generalPurposes); - if (!pkg.getPackageParsingHooks().shouldSkipUsesPermission(p)) { + var config = pkg.getApcPackageConfig(); + if (config != null && ArrayUtils.contains(config.usesPermissionsToIgnore, name)) { + // ArrayUtils.contains is O(n), but this array is expected to be small + Log.d(TAG, "PackageParserConfig: " + pkg.getPackageName() + ": skipped uses-permission " + name); + } else if (pkg.getPackageParsingHooks().shouldSkipUsesPermission(p)) { + Log.d(TAG, "PackageParsingHooks: " + pkg.getPackageName() + ": skipped uses-permission " + name); + } else { pkg.addUsesPermission(p); } } @@ -3756,7 +3781,7 @@ public static ArrayMap> readKeySetMapping(@NonNull P public interface Callback { boolean hasFeature(String feature); - ParsingPackage startParsingPackage(@NonNull String packageName, + ParsingPackage startParsingPackage(int flags, @NonNull String packageName, @NonNull String baseApkPath, @NonNull String path, @NonNull TypedArray manifestArray, boolean isCoreApp); diff --git a/core/java/com/android/server/pm/pkg/AndroidPackage.java b/core/java/com/android/server/pm/pkg/AndroidPackage.java index c80f34502b8f6..1f2332170e58e 100644 --- a/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -48,6 +48,7 @@ import android.util.SparseIntArray; import com.android.internal.R; +import com.android.internal.pm.parsing.nano.ApcPackageConfig; import com.android.internal.pm.parsing.pkg.PackageExtIface; import com.android.internal.pm.pkg.component.ParsedActivity; import com.android.internal.pm.pkg.component.ParsedAllowComponentAccessPolicy; @@ -1560,6 +1561,11 @@ public interface AndroidPackage { @Immutable.Ignore PackageExtIface ext(); + /** @hide */ + @Immutable.Ignore + @Nullable + ApcPackageConfig getApcPackageConfig(); + /** * Returns true if this application has any component that should run in * the Private Compute Core sandbox. diff --git a/core/res/res/values/public-ext.xml b/core/res/res/values/public-ext.xml index f53b1ec17b23e..356a221c8b5b6 100644 --- a/core/res/res/values/public-ext.xml +++ b/core/res/res/values/public-ext.xml @@ -135,5 +135,7 @@ + + diff --git a/core/res/res/values/string_ext.xml b/core/res/res/values/string_ext.xml index 12fbc7f26e3b6..4c8e3bd33456d 100644 --- a/core/res/res/values/string_ext.xml +++ b/core/res/res/values/string_ext.xml @@ -53,4 +53,7 @@ USB-C port security feature has malfunctioned + %1$s was blocked from claiming a reserved name + + diff --git a/proto/src/apk_parser_config.proto b/proto/src/apk_parser_config.proto new file mode 100644 index 0000000000000..5a5d8e4eca9d8 --- /dev/null +++ b/proto/src/apk_parser_config.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +package com.android.internal.pm.parsing; +option java_multiple_files = true; + +message ApcPackageConfig { // Apc is short for "APK parser config" + enum Flag { + // enables bidirectional AppsFilter isolation from GmsCore and Finsky (Play Store) + FLAG_ISOLATE_FROM_GMSCORE_AND_FINSKY = 0; + // enables bidirectional AppsFilter isolation from all user apps + FLAG_ISOLATE_FROM_USER_APPS = 1; + // makes adevtool include priv-app packages as regular unprivileged packages + FLAG_INCLUDE_AS_UNTRUSTED_APP = 2; + } + + repeated string uses_permissions_to_ignore = 1; + int64 flags = 2; +} + +message PackageInstallRequirements { + string cert_sha256 = 1; + int64 min_version = 2; +} + +message ApkParserConfig { + // permission name -> package name + map permission_owners = 1; + // permission group name -> package name + map permission_group_owners = 2; + // content provider authority -> package name + map content_provider_authority_owners = 3; + // packages from stock OS that are not allowed to be installed + repeated string non_installable_packages = 4; + // map keys are package names + map parsing_configs = 5; + // packages from stock OS that can be installed if requirements are met; map keys are package names + map installable_packages = 6; +} + diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 201d99aa9e9af..8cb7a6aebec03 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -234,6 +234,7 @@ import com.android.server.pm.Settings.VersionInfo; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.DynamicCodeLogger; +import com.android.server.pm.ext.PackageIdOwnershipChecks; import com.android.server.pm.local.PackageManagerLocalImpl; import com.android.server.pm.parsing.PackageCacher; import com.android.server.pm.parsing.PackageInfoUtils; @@ -243,7 +244,6 @@ import com.android.server.pm.permission.LegacyPermissionSettings; import com.android.server.pm.permission.PermissionManagerService; import com.android.server.pm.permission.PermissionManagerServiceInternal; -import com.android.server.pm.permission.SpecialRuntimePermUtils; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.ArchiveState; import com.android.server.pm.pkg.PackageStateInternal; @@ -2358,10 +2358,13 @@ public Set getInstallConstraintsAllowlist() { .getBoolean(R.bool.config_stopSystemPackagesByDefault); final int[] userIds = mUserManager.getUserIds(); + com.android.internal.pm.parsing.PackageParserConfig.init(); PackageParser2 packageParser = mInjector.getScanningCachingPackageParser(); mOverlayConfig = mInitAppsHelper.initSystemApps(packageParser, packageSettings, userIds, startTime); + PackageParser2.Callback.enableIdOwnershipChecks(); mInitAppsHelper.initNonSystemApps(packageParser, userIds, startTime); + PackageIdOwnershipChecks.setInitialPackageScanCompleted(); packageParser.close(); mRequiredVerifierPackages = getRequiredButNotReallyRequiredVerifiersLPr(computer); diff --git a/services/core/java/com/android/server/pm/ext/PackageExtInit.java b/services/core/java/com/android/server/pm/ext/PackageExtInit.java index 9eeff7579f48c..77f660cc374e1 100644 --- a/services/core/java/com/android/server/pm/ext/PackageExtInit.java +++ b/services/core/java/com/android/server/pm/ext/PackageExtInit.java @@ -9,6 +9,8 @@ import android.os.Bundle; import android.util.Slog; +import com.android.internal.pm.parsing.PackageParserConfig; +import com.android.internal.pm.parsing.nano.PackageInstallRequirements; import com.android.internal.pm.parsing.pkg.PackageImpl; import com.android.internal.pm.pkg.parsing.ParsingPackage; import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; @@ -43,8 +45,29 @@ public ParseResult getSigningDetailsParseResult() { public void run() { int packageId = getPackageId(); + String pkgName = pkg.getPackageName(); if (packageId != UNKNOWN) { - Slog.d(TAG, "set packageId of " + pkg.getPackageName() + " to " + packageId); + Slog.d(TAG, "set packageId of " + pkgName + " to " + packageId); + } + + if (pkg.areIdOwnershipChecksEnabled()) { + PackageInstallRequirements pir = PackageParserConfig.get().getPackageInstallRequirements(pkgName); + if (pir != null) { + long pkgVersion = pkg.getLongVersionCode(); + if (pkgVersion < pir.minVersion) { + pkg.recordIdOwnershipViolation("package version (" + pkgVersion + + ") is lower than the minimum allowed version (" + pir.minVersion + ")"); + } + // hasCertificateSha256() intentionally checks past certificates in addition to the + // current certificate + if (!hasCertificateSha256(pir.certSha256)) { + pkg.recordIdOwnershipViolation("package is not signed with the " + + "expected certificate (" + pir.certSha256 + ")"); + } + } + if (PackageParserConfig.get().isInstallationBlocked(pkgName)) { + pkg.recordIdOwnershipViolation("package name " + pkgName + " is reserved"); + } } var ext = new PackageExt(packageId, getExtFlags()); @@ -123,7 +146,14 @@ private int validate(int packageId, long minVersionCode, String... validCertific " pkgVersion: " + pkg.getLongVersionCode()); return PackageId.UNKNOWN; } + if (hasCertificateSha256(validCertificatesSha256)) { + return packageId; + } + Slog.w(TAG, pkg.getPackageName() + " isn't signed with any expected certificate"); + return PackageId.UNKNOWN; + } + private boolean hasCertificateSha256(String... validCertificatesSha256) { SigningDetails signingDetails = pkg.getSigningDetails(); if (signingDetails == SigningDetails.UNKNOWN) { @@ -134,7 +164,7 @@ private int validate(int packageId, long minVersionCode, String... validCertific Slog.e(TAG, "unable to parse SigningDetails for " + parsingPackage.getPackageName() + "; code " + result.getErrorCode() + "; msg " + result.getErrorMessage(), result.getException()); - return PackageId.UNKNOWN; + return false; } signingDetails = result.getResult(); @@ -143,12 +173,9 @@ private int validate(int packageId, long minVersionCode, String... validCertific for (String certSha256String : validCertificatesSha256) { byte[] validCertSha256 = HexEncoding.decode(certSha256String); if (signingDetails.hasSha256Certificate(validCertSha256)) { - return packageId; + return true; } } - - Slog.d(TAG, "SigningDetails of " + pkg.getPackageName() + " don't contain any of known certificates"); - - return PackageId.UNKNOWN; + return false; } } diff --git a/services/core/java/com/android/server/pm/ext/PackageHooks.java b/services/core/java/com/android/server/pm/ext/PackageHooks.java index 5f42cfad02603..f51450bb1dc32 100644 --- a/services/core/java/com/android/server/pm/ext/PackageHooks.java +++ b/services/core/java/com/android/server/pm/ext/PackageHooks.java @@ -3,12 +3,15 @@ import android.annotation.Nullable; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; +import android.ext.PackageId; import android.util.ArraySet; +import com.android.internal.pm.parsing.nano.ApcPackageConfig; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageState; import com.android.server.pm.pkg.PackageStateInternal; -import com.android.server.pm.pkg.PackageUserStateInternal; + +import static com.android.internal.pm.parsing.PackageParserConfig.hasApcFlag; public class PackageHooks { static final PackageHooks DEFAULT = new PackageHooks(); @@ -75,8 +78,29 @@ private static boolean shouldBlockPackageVisibilityInner( boolean isSelfToOther) { AndroidPackage pkg = pkgSetting.getPkg(); if (pkg != null) { - return PackageExt.get(pkg).hooks() - .shouldBlockPackageVisibility(pkgUserId, otherPkgSetting, isSelfToOther); + if (PackageExt.get(pkg).hooks() + .shouldBlockPackageVisibility(pkgUserId, otherPkgSetting, isSelfToOther)) { + return true; + } + + ApcPackageConfig config = pkg.getApcPackageConfig(); + if (config != null) { + if (hasApcFlag(config, ApcPackageConfig.FLAG_ISOLATE_FROM_USER_APPS)) { + if (isUserInstalledPkg(otherPkgSetting)) { + return true; + } + } + if (hasApcFlag(config, ApcPackageConfig.FLAG_ISOLATE_FROM_GMSCORE_AND_FINSKY)) { + String otherPkgName = otherPkgSetting.getPackageName(); + // Finsky is the internal name of the Play Store + switch (otherPkgName) { + case PackageId.GSF_NAME: + case PackageId.GMS_CORE_NAME: + case PackageId.PLAY_STORE_NAME: + return true; + } + } + } } return false; diff --git a/services/core/java/com/android/server/pm/ext/PackageIdOwnershipChecks.java b/services/core/java/com/android/server/pm/ext/PackageIdOwnershipChecks.java new file mode 100644 index 0000000000000..f49380e2955e3 --- /dev/null +++ b/services/core/java/com/android/server/pm/ext/PackageIdOwnershipChecks.java @@ -0,0 +1,100 @@ +package com.android.server.pm.ext; + +import android.annotation.Nullable; +import android.app.ActivityThread; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.parsing.result.ParseInput; +import android.content.pm.parsing.result.ParseResult; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.os.BackgroundThread; +import com.android.internal.pm.parsing.PackageInfoCommonUtils; +import com.android.internal.pm.parsing.pkg.AndroidPackageInternal; +import com.android.internal.pm.pkg.parsing.ParsingPackage; +import com.android.server.ext.SystemErrorNotification; + +public class PackageIdOwnershipChecks { + private static final String TAG = "PackageIdOwnershipChecks"; + + private static volatile boolean initialPackageScanCompleted; + + public static void setInitialPackageScanCompleted() { + initialPackageScanCompleted = true; + } + + @Nullable + public static ParseResult maybeOverridePackageParserResult(ParseInput input, ParsingPackage pkg) { + String[] violations = pkg.getIdOwnershipViolations(); + if (violations.length == 0) { + return null; + } + String pkgName = pkg.getPackageName(); + for (String text : violations) { + Slog.w(TAG, "ID ownership violation for " + pkgName + ": " + text); + } + + if (!initialPackageScanCompleted) { + // Keep packages that were installed before introduction of PackageIdOwnershipChecks. + // + // TODO: notify the user about such packages + Slog.w(TAG, "initial package scan is in progress, keeping " + pkgName); + return null; + } + + if (android.os.Flags.isDevBuild()) { + if (SystemProperties.getBoolean("persist.disable_package_id_ownership_checks", false)) { + return null; + } + } + + String msg; + if (violations.length == 1) { + msg = "Package " + pkgName + " violates an ID ownership requirement: " + violations[0]; + } else { + msg = "Package " + pkgName + + " violates the following ID ownership requirements:\n• " + + String.join("\n• ", violations); + } + if (android.os.Flags.isDevBuild()) { + msg += "\n\nTo disable this check, run 'setprop persist.disable_package_id_ownership_checks 1'"; + } + + final int maxSize = 20_000; + final String finalMessage = msg.length() > maxSize ? msg.substring(0, maxSize) : msg; + + String appLabel = null; + try { + Context ctx = ActivityThread.currentActivityThread().getSystemContext(); + // the package will be discarded after this methods returns, modifying it is safe + AndroidPackageInternal pkgFinal = pkg.hideAsParsed().hideAsFinal(); + PackageInfo pkgInfo = PackageInfoCommonUtils.generate(pkgFinal, 0L, UserHandle.USER_SYSTEM); + appLabel = pkgInfo.applicationInfo.loadSafeLabel(ctx.getPackageManager(), PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, + TextUtils.SAFE_STRING_FLAG_TRIM | TextUtils.SAFE_STRING_FLAG_SINGLE_LINE).toString(); + } catch (Exception|OutOfMemoryError|StackOverflowError e) { + Slog.e(TAG, "", e); + // don't crash system_server, loading an app label isn't that important + } + if (appLabel == null) { + appLabel = pkgName; + } + final String finalAppLabel = appLabel; + + BackgroundThread.getHandler().post(() -> { + var notif = new SystemErrorNotification("attempt to claim a reserved name", ctx -> { + String title = ctx.getString(com.android.internal.R.string.pkg_id_violation_blocked, + finalAppLabel); + return new SystemErrorNotification.Text(title, finalMessage); + }); + notif.showReportButton = false; + notif.show(); + }); + + return input.error(PackageManager.INSTALL_FAILED_INVALID_APK, finalMessage); + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index d34efa7e49ffb..70800a86328e8 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -242,6 +242,7 @@ import com.android.server.pm.ext.GmsCompatPkgParsingHooks; import com.android.server.pm.ext.PackageExtInit; import com.android.server.pm.ext.PackageHooksRegistry; +import com.android.server.pm.ext.PackageIdOwnershipChecks; import com.android.server.pm.permission.PermissionMigrationHelper; import com.android.server.pm.permission.PermissionMigrationHelperImpl; import com.android.server.pm.verify.domain.DomainVerificationService; @@ -736,6 +737,7 @@ public static void main(String[] args) { PackageImpl.packageParsingHooksSupplier = PackageHooksRegistry::getParsingHooks; ParsingPackageUtils.packageExtInitSupplier = PackageExtInit::new; ParsingPackageUtils.gmsCompatClientServiceSupplier = GmsCompatPkgParsingHooks::maybeCreateClientService; + ParsingPackageUtils.packageIdOwnershipChecks = PackageIdOwnershipChecks::maybeOverridePackageParserResult; } public SystemServer() { diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/ParsingUtils.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/ParsingUtils.java index a9eac95dfd871..7ff2405d7e12d 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/ParsingUtils.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/ParsingUtils.java @@ -59,6 +59,7 @@ public boolean hasFeature(String feature) { @Override public ParsingPackage startParsingPackage( + int flags, @NonNull String packageName, @NonNull String baseApkPath, @NonNull String path, From 7c5fa9640259faba5d0957ed71b9d0af2cffe32b Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 11 Aug 2026 18:16:23 +0000 Subject: [PATCH 04/10] remove package hooks that are now handled by PackageParserConfig --- .../server/pm/ext/EuiccGoogleHooks.java | 49 --------------- .../pm/ext/EuiccGoogleParsingHooks.java | 13 ++++ .../server/pm/ext/EuiccSupportPixelHooks.java | 25 -------- .../server/pm/ext/PackageHooksRegistry.java | 7 +-- .../pm/ext/PixelCameraServicesHooks.java | 59 ------------------- 5 files changed, 14 insertions(+), 139 deletions(-) delete mode 100644 services/core/java/com/android/server/pm/ext/EuiccGoogleHooks.java create mode 100644 services/core/java/com/android/server/pm/ext/EuiccGoogleParsingHooks.java delete mode 100644 services/core/java/com/android/server/pm/ext/EuiccSupportPixelHooks.java delete mode 100644 services/core/java/com/android/server/pm/ext/PixelCameraServicesHooks.java diff --git a/services/core/java/com/android/server/pm/ext/EuiccGoogleHooks.java b/services/core/java/com/android/server/pm/ext/EuiccGoogleHooks.java deleted file mode 100644 index a9a3e2b127aa0..0000000000000 --- a/services/core/java/com/android/server/pm/ext/EuiccGoogleHooks.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.android.server.pm.ext; - -import android.Manifest; -import android.content.pm.PackageManager; -import android.ext.PackageId; - -import com.android.internal.pm.pkg.component.ParsedUsesPermission; -import com.android.internal.pm.pkg.parsing.PackageParsingHooks; -import com.android.server.pm.pkg.AndroidPackage; -import com.android.server.pm.pkg.PackageStateInternal; - -class EuiccGoogleHooks extends PackageHooks { - - static class ParsingHooks extends PackageParsingHooks { - - @Override - public int overrideDefaultPackageEnabledState() { - return PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - } - - @Override - public boolean shouldSkipUsesPermission(ParsedUsesPermission p) { - switch (p.getName()) { - // Carrier apps aren't shipped on GrapheneOS, these permissions are needed only to - // install/enable them - case Manifest.permission.INSTALL_EXISTING_PACKAGES: - case Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE: - return true; - } - - return false; - } - } - - @Override - public boolean shouldBlockPackageVisibility(int userId, PackageStateInternal otherPkg) { - // EuiccGoogle is a privileged app, block it from interacting with unprivileged - // parts of sandboxed Google Play - switch (otherPkg.getPackageName()) { - case PackageId.GSF_NAME: - case PackageId.GMS_CORE_NAME: - case PackageId.PLAY_STORE_NAME: - return true; - } - - // Some third-party carrier apps need to interact with EuiccGoogle for eSIM activation - return false; - } -} diff --git a/services/core/java/com/android/server/pm/ext/EuiccGoogleParsingHooks.java b/services/core/java/com/android/server/pm/ext/EuiccGoogleParsingHooks.java new file mode 100644 index 0000000000000..784ad14b709ab --- /dev/null +++ b/services/core/java/com/android/server/pm/ext/EuiccGoogleParsingHooks.java @@ -0,0 +1,13 @@ +package com.android.server.pm.ext; + +import android.content.pm.PackageManager; + +import com.android.internal.pm.pkg.parsing.PackageParsingHooks; + +class EuiccGoogleParsingHooks extends PackageParsingHooks { + + @Override + public int overrideDefaultPackageEnabledState() { + return PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + } +} diff --git a/services/core/java/com/android/server/pm/ext/EuiccSupportPixelHooks.java b/services/core/java/com/android/server/pm/ext/EuiccSupportPixelHooks.java deleted file mode 100644 index 6f44dd2ef1226..0000000000000 --- a/services/core/java/com/android/server/pm/ext/EuiccSupportPixelHooks.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.android.server.pm.ext; - -import android.Manifest; - -import com.android.internal.pm.pkg.parsing.PackageParsingHooks; -import com.android.server.pm.pkg.PackageStateInternal; -import com.android.internal.pm.pkg.component.ParsedUsesPermission; - -class EuiccSupportPixelHooks extends PackageHooks { - - static class ParsingHooks extends PackageParsingHooks { - @Override - public boolean shouldSkipUsesPermission(ParsedUsesPermission p) { - // EuiccSupportPixel uses INTERNET perm only as part of its dev mode - return Manifest.permission.INTERNET.equals(p.getName()); - } - } - - @Override - public boolean shouldBlockPackageVisibility(int userId, PackageStateInternal otherPkg) { - // EuiccSupportPixel is a privileged package, block it from interacting with unprivileged - // parts of GMS - return isUserInstalledPkg(otherPkg); - } -} diff --git a/services/core/java/com/android/server/pm/ext/PackageHooksRegistry.java b/services/core/java/com/android/server/pm/ext/PackageHooksRegistry.java index 2f37f07576543..c3ba7396fcacf 100644 --- a/services/core/java/com/android/server/pm/ext/PackageHooksRegistry.java +++ b/services/core/java/com/android/server/pm/ext/PackageHooksRegistry.java @@ -14,9 +14,7 @@ public static PackageParsingHooks getParsingHooks(String pkgName) { return switch (pkgName) { case PackageId.GSF_NAME -> new GsfParsingHooks(); - case PackageId.EUICC_SUPPORT_PIXEL_NAME -> new EuiccSupportPixelHooks.ParsingHooks(); - case PackageId.G_EUICC_LPA_NAME -> new EuiccGoogleHooks.ParsingHooks(); - case PackageId.PIXEL_CAMERA_SERVICES_NAME -> new PixelCameraServicesHooks.ParsingHooks(); + case PackageId.G_EUICC_LPA_NAME -> new EuiccGoogleParsingHooks(); case PackageId.PIXEL_HEALTH_NAME -> new PixelHealthHooks.ParsingHooks(); default -> PackageParsingHooks.DEFAULT; }; @@ -24,11 +22,8 @@ public static PackageParsingHooks getParsingHooks(String pkgName) { public static PackageHooks getHooks(int packageId) { return switch (packageId) { - case PackageId.EUICC_SUPPORT_PIXEL -> new EuiccSupportPixelHooks(); case PackageId.G_CARRIER_SETTINGS -> new GCarrierSettingsHooks(); - case PackageId.G_EUICC_LPA -> new EuiccGoogleHooks(); case PackageId.ANDROID_AUTO -> new AndroidAutoHooks(); - case PackageId.PIXEL_CAMERA_SERVICES -> new PixelCameraServicesHooks(); case PackageId.PIXEL_HEALTH -> new PixelHealthHooks(); case PackageId.GMS_CORE -> new GmsCoreHooks(); default -> PackageHooks.DEFAULT; diff --git a/services/core/java/com/android/server/pm/ext/PixelCameraServicesHooks.java b/services/core/java/com/android/server/pm/ext/PixelCameraServicesHooks.java deleted file mode 100644 index 8d4b5fb7b0295..0000000000000 --- a/services/core/java/com/android/server/pm/ext/PixelCameraServicesHooks.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.android.server.pm.ext; - -import android.Manifest; -import android.ext.PackageId; - -import com.android.internal.pm.pkg.component.ParsedUsesPermission; -import com.android.internal.pm.pkg.parsing.PackageParsingHooks; -import com.android.server.pm.pkg.PackageStateInternal; - -public class PixelCameraServicesHooks extends PackageHooks { - - static class ParsingHooks extends PackageParsingHooks { - @Override - public boolean shouldSkipUsesPermission(ParsedUsesPermission p) { - switch (p.getName()) { - // Pixel Camera Services currently doesn't use any of these permissions. - // [1] states that PCS (Pixel Camera Servicss) is planned "to be able act as a media - // app and use Exoplayer for playing recorded video files". This functionality is - // not implemented in the current release as of January 2024. - // - // [1] https://android.googlesource.com/device/google/gs-common/+/46d6a8821196ab313a5f28c009ce9b2548ea2569 - case Manifest.permission.BLUETOOTH_CONNECT: - case Manifest.permission.BLUETOOTH_SCAN: - case Manifest.permission.CAPTURE_AUDIO_OUTPUT: - case Manifest.permission.MODIFY_AUDIO_ROUTING: - // added in 14 QPR3: - case Manifest.permission.ACCESS_FINE_LOCATION: - case Manifest.permission.INTERACT_ACROSS_USERS: - // needed for the Connected Cameras feature that is currently unsupported on GrapheneOS: - // https://support.google.com/pixelphone/answer/15985851?hl=en - case Manifest.permission.MANAGE_USB: - case Manifest.permission.MODIFY_AUDIO_SETTINGS: - return true; - } - - return false; - } - } - - @Override - public boolean shouldBlockPackageVisibility(int userId, PackageStateInternal otherPkg) { - // Pixel Camera Services is a privileged app, block it from interacting with unprivileged - // parts of sandboxed Google Play. - // - // Pixel Camera Services currently uses GmsCore only for telemetry (perf data reporting), - // and doesn't use Play Store, at least directly. - // - // Also, Play Store would be unable to update Pixel Camera Services if it was allowed to see - // it due to the fs-verity requirements for system package updates. - switch (otherPkg.getPackageName()) { - case PackageId.GSF_NAME: - case PackageId.GMS_CORE_NAME: - case PackageId.PLAY_STORE_NAME: - return true; - } - - return false; - } -} From 84dc965babf84c67618f50fcd496698cb2994523 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Wed, 12 Aug 2026 07:03:24 +0000 Subject: [PATCH 05/10] optimize checks of APK certificate SHA-256 digests --- .../android/content/pm/PackageManager.java | 4 ++++ core/java/android/content/pm/Signature.java | 18 ++++++++++++++++++ .../android/content/pm/SigningDetails.java | 5 ++--- .../server/pm/parsing/PackageInfoUtils.java | 18 +++++++++++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 4360a8a45d117..c14c10844e306 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -974,6 +974,7 @@ default void onPermissionsChanged(int uid, @NonNull String persistentDeviceId) { MATCH_DIRECT_BOOT_AWARE, MATCH_DIRECT_BOOT_UNAWARE, GET_ATTRIBUTIONS_LONG, + CACHE_SIGNING_CERTIFICATE_DIGESTS, }) @Retention(RetentionPolicy.SOURCE) public @interface PackageInfoFlagsBits {} @@ -1467,6 +1468,9 @@ default void onPermissionsChanged(int uid, @NonNull String persistentDeviceId) { @FlaggedApi(android.security.Flags.FLAG_APP_LOCK_APIS) public static final long GET_APP_LOCK_INFO = 1L << 35; + /** @hide */ + public static final long CACHE_SIGNING_CERTIFICATE_DIGESTS = 1L << 55; + //------------------------------------------------------------------------- // End of GET_ and MATCH_ flags //------------------------------------------------------------------------- diff --git a/core/java/android/content/pm/Signature.java b/core/java/android/content/pm/Signature.java index 3e032872c5e22..e4bb45091bc76 100644 --- a/core/java/android/content/pm/Signature.java +++ b/core/java/android/content/pm/Signature.java @@ -216,6 +216,22 @@ public byte[] toByteArray() { return bytes; } + @Nullable + private volatile byte[] mSha256Digest; + + /** @hide */ + @NonNull + public byte[] getSha256Digest() { + byte[] cache = mSha256Digest; + if (cache != null) { + return cache; + } + byte[] result = android.util.PackageUtils.computeSha256DigestBytes(mSignature); + java.util.Objects.requireNonNull(result); + mSha256Digest = result; + return result; + } + /** * Returns the public key for this signature. * @@ -286,6 +302,7 @@ public int describeContents() { public void writeToParcel(Parcel dest, int parcelableFlags) { dest.writeByteArray(mSignature); + dest.writeByteArray(mSha256Digest); } public static final @android.annotation.NonNull Parcelable.Creator CREATOR @@ -307,6 +324,7 @@ public void writeToXmlAttributeBytesHex(@NonNull TypedXmlSerializer out, private Signature(Parcel source) { mSignature = source.createByteArray(); + mSha256Digest = source.createByteArray(); } /** diff --git a/core/java/android/content/pm/SigningDetails.java b/core/java/android/content/pm/SigningDetails.java index 6b0c1ddbd6add..7133a293b50f2 100644 --- a/core/java/android/content/pm/SigningDetails.java +++ b/core/java/android/content/pm/SigningDetails.java @@ -1021,8 +1021,7 @@ private boolean hasSha256CertificateInternal(byte[] sha256Certificate, int flags // check all past certs, except for the last one, which automatically gets all // capabilities, since it is the same as the current signature, and is checked below for (int i = 0; i < mPastSigningCertificates.length - 1; i++) { - byte[] digest = PackageUtils.computeSha256DigestBytes( - mPastSigningCertificates[i].toByteArray()); + byte[] digest = mPastSigningCertificates[i].getSha256Digest(); if (Arrays.equals(sha256Certificate, digest)) { if (flags == PAST_CERT_EXISTS || (flags & mPastSigningCertificates[i].getFlags()) == flags) { @@ -1034,7 +1033,7 @@ private boolean hasSha256CertificateInternal(byte[] sha256Certificate, int flags // not in previous certs signing history, just check the current signer if (mSignatures.length == 1) { - byte[] digest = PackageUtils.computeSha256DigestBytes(mSignatures[0].toByteArray()); + byte[] digest = mSignatures[0].getSha256Digest(); return Arrays.equals(sha256Certificate, digest); } return false; diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index 5229ae998bb58..46e2409b10916 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -291,7 +291,23 @@ private static PackageInfo generateWithComponents(AndroidPackage pkg, int[] gids if ((flags & PackageManager.GET_SIGNING_CERTIFICATES) != 0) { if (signingDetails != SigningDetails.UNKNOWN) { // only return a valid SigningInfo if there is signing information to report - info.signingInfo = new SigningInfo(signingDetails); + var si = new SigningInfo(signingDetails); + info.signingInfo = si; + if ((flags & PackageManager.CACHE_SIGNING_CERTIFICATE_DIGESTS) != 0) { + Signature[] certs = si.getApkContentsSigners(); + if (certs != null) { + for (Signature cert : certs) { + // the cached value is kept during parceling + cert.getSha256Digest(); + } + } + Signature[] certHistory = si.getSigningCertificateHistory(); + if (certHistory != null) { + for (Signature cert : certHistory) { + cert.getSha256Digest(); + } + } + } } else { info.signingInfo = null; } From e0cdb4a2cb0ad27f6ca2b1d4819d0cb49539f5cd Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 11 Aug 2026 18:16:23 +0000 Subject: [PATCH 06/10] add support for using hasPlayStoreSourceStamp() for non-self packages --- .../android/content/pm/ApplicationInfo.java | 40 +++++++++---------- .../android/content/pm/PackageManager.java | 4 ++ .../internal/pm/parsing/pkg/PackageImpl.java | 24 +++++++++++ .../android/server/pm/pkg/AndroidPackage.java | 3 ++ .../server/pm/parsing/PackageInfoUtils.java | 8 ++++ 5 files changed, 58 insertions(+), 21 deletions(-) diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 57af3320ff795..3a445027efef3 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -2139,6 +2139,7 @@ public ApplicationInfo() { public ApplicationInfo(ApplicationInfo orig) { super(orig); ext = orig.ext; + playStoreSourceStampPresence = orig.playStoreSourceStampPresence; taskAffinity = orig.taskAffinity; permission = orig.permission; mKnownActivityEmbeddingCerts = orig.mKnownActivityEmbeddingCerts; @@ -2239,6 +2240,7 @@ public void writeToParcel(Parcel dest, int parcelableFlags) { final int preWriteSize = dest.dataSize(); super.writeToParcel(dest, parcelableFlags); ext.writeToParcel(dest, parcelableFlags); + dest.writeInt(playStoreSourceStampPresence); dest.writeString8(taskAffinity); dest.writeString8(permission); dest.writeString8(processName); @@ -2371,6 +2373,7 @@ public ApplicationInfo[] newArray(int size) { private ApplicationInfo(Parcel source) { super(source); ext = AppInfoExt.CREATOR.createFromParcel(source); + playStoreSourceStampPresence = source.readInt(); taskAffinity = source.readString8(); permission = source.readString8(); processName = source.readString8(); @@ -3222,31 +3225,26 @@ public void setExt(AppInfoExt ext) { return ext; } - private static volatile Boolean hasPlayStoreSourceStamp; + private int playStoreSourceStampPresence; + private static final int PLAY_STORE_SOURCE_STAMP_NOT_PRESENT = -1; + private static final int PLAY_STORE_SOURCE_STAMP_PRESENCE_UNKNOWN = 0; + private static final int PLAY_STORE_SOURCE_STAMP_PRESENT = 1; /** @hide */ - public boolean hasPlayStoreSourceStamp() { - Boolean cache = hasPlayStoreSourceStamp; - if (cache != null) { - return cache.booleanValue(); - } + public void setPlayStoreSourceStampPresent(boolean present) { + playStoreSourceStampPresence = present ? PLAY_STORE_SOURCE_STAMP_PRESENT : PLAY_STORE_SOURCE_STAMP_NOT_PRESENT; + } - var apkPaths = new ArrayList(); - apkPaths.add(Objects.requireNonNull(sourceDir)); - String[] splits = splitSourceDirs; - if (splits != null) { - for (String splitPath : splits) { - apkPaths.add(Objects.requireNonNull(splitPath)); + /** @hide */ + public boolean hasPlayStoreSourceStamp() { + if (playStoreSourceStampPresence == PLAY_STORE_SOURCE_STAMP_PRESENCE_UNKNOWN) { + String msg = "playStoreSourceStampPresence is unknown; check whether PackageManager.GET_PLAY_STORE_SOURCE_STAMP_STATE flag is used"; + if (android.os.Flags.isDevBuild()) { + throw new RuntimeException(msg); + } else { + Log.e(TAG, msg); } } - byte[] playStoreSourceStampCertDigest = java.util.HexFormat.of().parseHex( - "3257d599a49d2c961a471ca9843f59d341a405884583fc087df4237b733bbd6d"); - boolean result = android.util.apk.SourceStampVerifier - .verify(apkPaths, /* requiredSourceStamp */ playStoreSourceStampCertDigest) - .isVerified(); - hasPlayStoreSourceStamp = Boolean.valueOf(result); - android.util.Log.d("PlayStoreSourceStampCheck", - "result for " + packageName + ": " + result); - return result; + return playStoreSourceStampPresence == PLAY_STORE_SOURCE_STAMP_PRESENT; } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index c14c10844e306..4b5b4e105eb33 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -975,6 +975,7 @@ default void onPermissionsChanged(int uid, @NonNull String persistentDeviceId) { MATCH_DIRECT_BOOT_UNAWARE, GET_ATTRIBUTIONS_LONG, CACHE_SIGNING_CERTIFICATE_DIGESTS, + GET_PLAY_STORE_SOURCE_STAMP_STATE, }) @Retention(RetentionPolicy.SOURCE) public @interface PackageInfoFlagsBits {} @@ -996,6 +997,7 @@ default void onPermissionsChanged(int uid, @NonNull String persistentDeviceId) { MATCH_APEX, MATCH_ARCHIVED_PACKAGES, GET_APP_LOCK_INFO, + GET_PLAY_STORE_SOURCE_STAMP_STATE, }) @Retention(RetentionPolicy.SOURCE) public @interface ApplicationInfoFlagsBits {} @@ -1470,6 +1472,8 @@ default void onPermissionsChanged(int uid, @NonNull String persistentDeviceId) { /** @hide */ public static final long CACHE_SIGNING_CERTIFICATE_DIGESTS = 1L << 55; + /** @hide */ + public static final long GET_PLAY_STORE_SOURCE_STAMP_STATE = 1L << 56; //------------------------------------------------------------------------- // End of GET_ and MATCH_ flags diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java index 1ca5b9801a695..d8885a1ea3b7a 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java @@ -4147,6 +4147,30 @@ public PackageExtIface ext() { return ext; } + private volatile Boolean hasPlayStoreSourceStamp; + + @Override + public boolean hasPlayStoreSourceStamp() { + Boolean cache = hasPlayStoreSourceStamp; + if (cache != null) { + return cache.booleanValue(); + } + + var apkPaths = new ArrayList(); + apkPaths.add(getBaseApkPath()); + apkPaths.addAll(Arrays.asList(getSplitCodePaths())); + + byte[] playStoreSourceStampCertDigest = java.util.HexFormat.of().parseHex( + "3257d599a49d2c961a471ca9843f59d341a405884583fc087df4237b733bbd6d"); + boolean result = android.util.apk.SourceStampVerifier + .verify(apkPaths, /* requiredSourceStamp */ playStoreSourceStampCertDigest) + .isVerified(); + hasPlayStoreSourceStamp = Boolean.valueOf(result); + android.util.Slog.d("PlayStoreSourceStampCheck", + "result for " + packageName + ": " + result); + return result; + } + public long cachedCompatConfigVersionCode; public Object cachedCompatConfig; } diff --git a/core/java/com/android/server/pm/pkg/AndroidPackage.java b/core/java/com/android/server/pm/pkg/AndroidPackage.java index 1f2332170e58e..5ed38c7ca892c 100644 --- a/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -1557,6 +1557,9 @@ public interface AndroidPackage { */ int getIntentMatchingFlags(); + /** @hide */ + boolean hasPlayStoreSourceStamp(); + /** @hide */ @Immutable.Ignore PackageExtIface ext(); diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index 46e2409b10916..be244bdd0a615 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -436,6 +436,14 @@ public static Signature[] getDeprecatedSignatures(SigningDetails signingDetails, private static void updateApplicationInfo(ApplicationInfo ai, long flags, PackageUserState state, AndroidPackage pkg, int userId) { + if ((flags & PackageManager.GET_PLAY_STORE_SOURCE_STAMP_STATE) != 0) { + boolean res = false; + if (pkg != null) { + res = pkg.hasPlayStoreSourceStamp(); + } + ai.setPlayStoreSourceStampPresent(res); + } + if ((flags & PackageManager.GET_META_DATA) == 0) { ai.metaData = null; } From da04e54fae9d04dbd74d688fd9173e7fab3f2223 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Wed, 12 Aug 2026 06:54:32 +0000 Subject: [PATCH 07/10] fixup! bypass pairip for apps that are verified by the Play Store source stamp --- .../com/android/internal/app/PairipHooks.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/core/java/com/android/internal/app/PairipHooks.java b/core/java/com/android/internal/app/PairipHooks.java index 6ddd1259fcf35..9c22ad285816b 100644 --- a/core/java/com/android/internal/app/PairipHooks.java +++ b/core/java/com/android/internal/app/PairipHooks.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.ApplicationInfoFlags; import android.ext.PackageId; import android.os.RemoteException; import android.provider.Settings; @@ -55,18 +56,26 @@ private static boolean shouldBypass(Context context) { if (cache != null) { return cache.booleanValue(); } - - boolean res = context.getApplicationInfo().hasPlayStoreSourceStamp(); + PackageManager pm = context.getPackageManager(); + String selfPkgName = context.getPackageName(); + ApplicationInfo selfAppInfo; + try { + selfAppInfo = pm.getApplicationInfo(selfPkgName, + ApplicationInfoFlags.of(PackageManager.GET_PLAY_STORE_SOURCE_STAMP_STATE)); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException(e); + } + boolean res = selfAppInfo.hasPlayStoreSourceStamp(); if (res) { boolean installedFromPlayStore = false; String installerPkg; try { - installerPkg = AppGlobals.getPackageManager().getInstallerPackageName(context.getPackageName()); + // ApplicationPackageManager result might be spoofed, see InstallSourceSpoofingHooks + installerPkg = AppGlobals.getPackageManager().getInstallerPackageName(selfPkgName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } if (PackageId.PLAY_STORE_NAME.equals(installerPkg)) { - PackageManager pm = context.getPackageManager(); try { ApplicationInfo ai = pm.getApplicationInfo(PackageId.PLAY_STORE_NAME, 0); installedFromPlayStore = ai.ext().getPackageId() == PackageId.PLAY_STORE; From ad787d4ade637bf7af3c8fa2ec866ad6bf97baa9 Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 11 Aug 2026 18:16:23 +0000 Subject: [PATCH 08/10] spoof install source to Play Store for verified apps --- .../app/ApplicationPackageManager.java | 13 ++- .../app/InstallSourceSpoofingHooks.java | 90 +++++++++++++++++++ core/java/android/provider/Settings.java | 5 ++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 core/java/android/app/InstallSourceSpoofingHooks.java diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index b1f0fc042391b..38ab51ba6802b 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -2735,7 +2735,12 @@ public void setUpdateAvailable(String packageName, boolean updateAvailable) { @Override public String getInstallerPackageName(String packageName) { try { - return mPM.getInstallerPackageName(packageName); + String res = mPM.getInstallerPackageName(packageName); + if (!android.ext.PackageId.PLAY_STORE_NAME.equals(res) + && InstallSourceSpoofingHooks.shouldSpoof(mContext, this, packageName) != null) { + return android.ext.PackageId.PLAY_STORE_NAME; + } + return res; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -2753,6 +2758,12 @@ public InstallSourceInfo getInstallSourceInfo(String packageName) throws NameNot if (installSourceInfo == null) { throw new NameNotFoundException(packageName); } + if (!android.ext.PackageId.PLAY_STORE_NAME.equals(installSourceInfo.getInstallingPackageName())) { + var res = InstallSourceSpoofingHooks.shouldSpoof(mContext, this, packageName); + if (res != null) { + return InstallSourceSpoofingHooks.getPlayStoreInstallSourceInfo(res); + } + } return installSourceInfo; } diff --git a/core/java/android/app/InstallSourceSpoofingHooks.java b/core/java/android/app/InstallSourceSpoofingHooks.java new file mode 100644 index 0000000000000..24e5446176087 --- /dev/null +++ b/core/java/android/app/InstallSourceSpoofingHooks.java @@ -0,0 +1,90 @@ +package android.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.InstallSourceInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.ApplicationInfoFlags; +import android.content.pm.SigningInfo; +import android.ext.PackageId; +import android.ext.settings.BoolSetting; +import android.ext.settings.Setting; +import android.provider.Settings; +import android.util.Log; + +import java.util.Objects; + +/** @hide */ +public class InstallSourceSpoofingHooks { + static final String TAG = "InstallSourceSpoofingHooks"; + + private static final BoolSetting setting = new BoolSetting(Setting.Scope.PER_USER, + Settings.Secure.SPOOF_INSTALL_SOURCE_FOR_PLAY_STORE_APPS, true); + + // returns Play Store signing info if spoofing should be performed + static SigningInfo shouldSpoof(Context context, PackageManager pm, String pkgName) { + SigningInfo res = shouldSpoofInner(context, pm, pkgName); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "shouldSpoof result for " + pkgName + ": " + res); + } + return res; + } + + private static SigningInfo shouldSpoofInner(Context context, PackageManager pm, String pkgName) { + ApplicationInfo selfAppInfo = context.getApplicationInfo(); + if (selfAppInfo.isSystemApp()) { + return null; + } + switch (selfAppInfo.ext().getPackageId()) { + case PackageId.GMS_CORE: + case PackageId.PLAY_STORE: + return null; + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "shouldSpoof called for " + pkgName, new Throwable()); + } + if (!setting.get(context)) { + return null; + } + try { + ApplicationInfo appInfo = pm.getApplicationInfo(pkgName, + ApplicationInfoFlags.of(PackageManager.GET_PLAY_STORE_SOURCE_STAMP_STATE)); + if (appInfo.isSystemApp() || (!appInfo.hasPlayStoreSourceStamp() && !appInfo.ext().hasCompatChange( + com.android.server.os.nano.AppCompatProtos.SPOOF_INSTALLER_CHECKS))) { + return null; + } + } catch (PackageManager.NameNotFoundException e) { + return null; + } + PackageInfo playStorePkgInfo; + try { + playStorePkgInfo = pm.getPackageInfo(PackageId.PLAY_STORE_NAME, + PackageManager.GET_SIGNING_CERTIFICATES); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + ApplicationInfo playStoreAppInfo = playStorePkgInfo.applicationInfo; + if (playStoreAppInfo == null) { + return null; + } + if (playStoreAppInfo.ext().getPackageId() != PackageId.PLAY_STORE) { + return null; + } + return Objects.requireNonNull(playStorePkgInfo.signingInfo); + } + + static InstallSourceInfo getPlayStoreInstallSourceInfo(SigningInfo playStoreSigningInfo) { + return new InstallSourceInfo( + PackageId.PLAY_STORE_NAME, // initiatingPackageName + playStoreSigningInfo, // initiatingPackageSigningInfo + null, // originatingPackageName, which is hidden from packages without the + // privileged INSTALL_PACKAGES permission. Spoofing of installer checks can be + // enabled only for third-party apps, which are always unprivileged. + PackageId.PLAY_STORE_NAME, // installingPackageName + null, // updateOwnerPackageName + // Play Store doesn't set the package source value + PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index bcf423537bb66..78f1d7e75232a 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7523,6 +7523,11 @@ public static final class Secure extends NameValueTable { @Protected(readWrite = KnownSystemPackage.SETTINGS) public static final String DISALLOW_DELAYED_LOCKING_ON_USER_STOP = "disallow_delayed_locking_on_user_stop"; + /** @hide */ + @Readable + @Protected(restrictReads = false, readWrite = KnownSystemPackage.SETTINGS) + public static final String SPOOF_INSTALL_SOURCE_FOR_PLAY_STORE_APPS = "spoof_install_source_for_play_store_apps"; + // ExtSettings END // NOTE: If you add new settings here, be sure to add them to From eeb580e87825c6659e9fe2f8faad33e8256de25a Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Tue, 11 Aug 2026 18:16:23 +0000 Subject: [PATCH 09/10] add cert digest and Play Store source stamp indicator to App info footer --- .../settingslib/spa/widget/ui/CopyableBody.kt | 9 +- .../SpaPrivileged/res/values/strings_ext.xml | 6 +- .../spaprivileged/template/app/AppInfo.kt | 82 ++++++++++++++++--- 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt index 99b2524f0f9e6..6ca8962a4bc12 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt @@ -39,9 +39,10 @@ import androidx.compose.ui.unit.DpOffset import com.android.settingslib.spa.framework.theme.SettingsDimension @Composable -fun CopyableBody(body: String) { +fun CopyableBody(bodyCharSequence: CharSequence, showDropdownTitle: Boolean = true) { var expanded by remember { mutableStateOf(false) } var dpOffset by remember { mutableStateOf(DpOffset.Unspecified) } + val body = remember(bodyCharSequence) { bodyCharSequence.toString() } Box(modifier = Modifier .fillMaxWidth() @@ -54,14 +55,16 @@ fun CopyableBody(body: String) { ) } ) { - SettingsBody(body) + SettingsBody(bodyCharSequence) DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, offset = dpOffset, ) { - DropdownMenuTitle(body) + if (showDropdownTitle) { + DropdownMenuTitle(body) + } DropdownMenuCopy(body) { expanded = false } } } diff --git a/packages/SettingsLib/SpaPrivileged/res/values/strings_ext.xml b/packages/SettingsLib/SpaPrivileged/res/values/strings_ext.xml index 4a867c5a1c206..a5ae0b4cfb331 100644 --- a/packages/SettingsLib/SpaPrivileged/res/values/strings_ext.xml +++ b/packages/SettingsLib/SpaPrivileged/res/values/strings_ext.xml @@ -1,5 +1,7 @@ - Installed: %1$s - Updated: %1$s + installed: %1$s + updated: %1$s + app certificate SHA-256 digest: + has Play Store source stamp diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt index 3cfce4bc8431c..f2b5bf089e9f8 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt @@ -36,6 +36,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import com.android.settingslib.development.DevelopmentSettingsEnabler import com.android.settingslib.spa.framework.compose.rememberDrawablePainter @@ -131,7 +136,7 @@ class AppInfoProvider(private val packageInfo: PackageInfo) { if (footer.isBlank()) return if (!isSpaExpressiveEnabled) HorizontalDivider() Box(modifier = Modifier.padding(SettingsDimension.footerPadding)) { - CopyableBody(footer) + CopyableBody(footer, showDropdownTitle = false) } } @@ -147,7 +152,12 @@ class AppInfoProvider(private val packageInfo: PackageInfo) { get() = BidiFormatter.getInstance().unicodeWrap(versionName) } - private fun getFooterText(ctx: Context): String { + enum class FormattingCommand { + MonospaceTextStart, + MonospaceTextEnd, + } + + private fun getFooterText(ctx: Context): AnnotatedString { val pi = packageInfo val dateFormat = android.text.format.DateFormat.getMediumDateFormat(ctx) @@ -160,17 +170,17 @@ class AppInfoProvider(private val packageInfo: PackageInfo) { val appInfo = pi.applicationInfo - val lines = mutableListOf() + val lines = mutableListOf() + lines.add(pi.packageName) pi.versionNameBidiWrapped?.let { lines.add(ctx.getString(R.string.version_text, it)) - lines.add("") } - lines.add(pi.packageName) lines.add("versionCode ${pi.getLongVersionCode()}") - lines.add("") if (appInfo != null) { - lines.add("targetSdk ${appInfo.targetSdkVersion}") - lines.add("minSdk ${appInfo.minSdkVersion}") + lines.add("targetSdk ${appInfo.targetSdkVersion} | minSdk ${appInfo.minSdkVersion}") + if (appInfo.hasPlayStoreSourceStamp()) { + lines.add(ctx.getString(R.string.app_info_has_play_store_source_stamp)) + } } // some system apps report being installed in January 2009, skip showing install time for them @@ -188,13 +198,65 @@ class AppInfoProvider(private val packageInfo: PackageInfo) { if (pi.lastUpdateTime != pi.firstInstallTime) { if (!addedBlankLineBeforeTime) { lines.add("") - addedBlankLineBeforeTime = true } val s = formatDate(pi.lastUpdateTime, dateFormat, timeFormat) lines.add(ctx.getString(R.string.app_info_update_time, s)) } - return lines.joinToString(separator = System.lineSeparator()) + val signingInfo = pi.signingInfo + if (signingInfo != null) { + val certs = if (signingInfo.hasMultipleSigners()) { + signingInfo.apkContentsSigners + } else { + val certHistory = signingInfo.signingCertificateHistory + if (certHistory != null) { + arrayOf(certHistory.last()) + } else { + null + } + } + if (certs != null) { + lines.add("") + val hexFormat = HexFormat { + bytes { + bytesPerGroup = 2 + groupSeparator = " " + } + } + certs.forEach { + lines.add(ctx.getString(R.string.app_info_apk_cert_digest)) + val digest = it.sha256Digest + check(digest.size == 32) + lines.add(FormattingCommand.MonospaceTextStart) + lines.add(digest.copyOfRange(0, 16).toHexString(hexFormat)) + lines.add(digest.copyOfRange(16, 32).toHexString(hexFormat)) + lines.add(FormattingCommand.MonospaceTextEnd) + } + } + } + + return buildAnnotatedString { + var start = -1 + lines.forEachIndexed { idx, obj -> + when { + obj is String -> { + if (length != 0) { + append('\n') + } + append(obj) + } + obj == FormattingCommand.MonospaceTextStart -> { + start = length + } + obj == FormattingCommand.MonospaceTextEnd -> { + addStyle(SpanStyle(fontFamily = FontFamily.Monospace), start, length) + } + else -> { + error("unknown command $obj") + } + } + } + } } } From a0e5aa8ec599f44fc220cdd6171ca046ea60554a Mon Sep 17 00:00:00 2001 From: Dmitry Muhomor Date: Wed, 12 Aug 2026 09:24:16 +0000 Subject: [PATCH 10/10] aapt2: improve the "dump brief-package-info" command Add missing checks for rare edge cases. --- tools/aapt2/cmd/Dump.cpp | 52 ++++++++++++++++++++++++---------------- tools/aapt2/cmd/Dump.h | 5 ++-- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp index 2f2d661e8b7b9..113c5f8400425 100644 --- a/tools/aapt2/cmd/Dump.cpp +++ b/tools/aapt2/cmd/Dump.cpp @@ -343,7 +343,7 @@ static int32_t GetInt(BinaryPrimitive& bp) { static bool GetBool(BinaryPrimitive& bp) { CHECK(bp.value.dataType == android::Res_value::TYPE_INT_BOOLEAN); - return static_cast(bp.value.data); + return bp.value.data != 0u; } static bool GetBoolAttr(xml::Attribute& attr) { @@ -358,11 +358,19 @@ static int32_t GetIntAttr(xml::Attribute& attr) { return GetInt(*attr_prim); } +static xml::Attribute* GetName(xml::Element* element) { + xml::Attribute* name = element->FindAttribute(xml::kSchemaAndroid, "name"); + CHECK(name != nullptr && !name->value.empty()); + return name; +} + int DumpBriefPackageInfo::Action(const std::vector& args) { if (args.size() != 1) { diag_->Error(android::DiagMessage() << "should specify exactly one apk."); return 1; } + sdk_version_ = std::stoi(sdk_version_str_); + CHECK(sdk_version_ > 0); const std::string &apk_path = args[0]; android::Source source(apk_path); @@ -389,6 +397,7 @@ int DumpBriefPackageInfo::Action(const std::vector& args) { CHECK(attr != nullptr); CHECK(!attr->value.empty()); bpi.set_package_name(attr->value); + bpi.set_target_sdk(sdk_version_); } { xml::Attribute* ver_code_major_attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCodeMajor"); @@ -399,19 +408,17 @@ int DumpBriefPackageInfo::Action(const std::vector& args) { xml::Attribute* ver_code_attr = manifest_el->FindAttribute(xml::kSchemaAndroid, "versionCode"); CHECK(ver_code_attr != nullptr); int32_t ver_code = GetIntAttr(*ver_code_attr); - bpi.set_version_code((static_cast(ver_code_major) << 32) | ver_code); + bpi.set_version_code((static_cast(ver_code_major) << 32) | static_cast(ver_code)); } // initialized lazily, not needed in the vast majority of cases std::unique_ptr loaded_apk = nullptr; for (xml::Element* manifest_child : manifest_el->GetChildElements()) { if (manifest_child->name == "permission" || manifest_child->name == "permission-tree") { - xml::Attribute* name = manifest_child->FindAttribute(xml::kSchemaAndroid, "name"); - CHECK(!name->value.empty()); + xml::Attribute* name = GetName(manifest_child); bpi.add_permission(name->value); } else if (manifest_child->name == "permission-group") { - xml::Attribute* name = manifest_child->FindAttribute(xml::kSchemaAndroid, "name"); - CHECK(!name->value.empty()); + xml::Attribute* name = GetName(manifest_child); bpi.add_permission_group(name->value); } else if (manifest_child->name == "uses-permission" || manifest_child->name == "uses-permission-sdk-23" || manifest_child->name == "uses-permission-sdk-m") { xml::Attribute* max_sdk_attr = manifest_child->FindAttribute(xml::kSchemaAndroid, "maxSdkVersion"); @@ -425,7 +432,7 @@ int DumpBriefPackageInfo::Action(const std::vector& args) { loaded_apk = LoadedApk::LoadApkFromPath(apk_path, diag_); } ConfigDescription config; - config.sdkVersion = std::stoi(sdk_version_); + config.sdkVersion = sdk_version_; Value* val = FindValueById(loaded_apk->GetResourceTable(), res_id, config); CHECK(val != nullptr); auto *bp = ValueCast(val); @@ -435,24 +442,27 @@ int DumpBriefPackageInfo::Action(const std::vector& args) { max_sdk = GetIntAttr(*max_sdk_attr); } - if (max_sdk < std::stoi(sdk_version_)) { + if (max_sdk < sdk_version_) { continue; } } - xml::Attribute* name = manifest_child->FindAttribute(xml::kSchemaAndroid, "name"); - CHECK(!name->value.empty()); + xml::Attribute* name = GetName(manifest_child); bpi.add_uses_permission(name->value); } else if (manifest_child->name == "uses-sdk") { xml::Attribute* target_sdk = manifest_child->FindAttribute(xml::kSchemaAndroid, "targetSdkVersion"); - if (target_sdk == nullptr) { - bpi.set_target_sdk(std::stoi(sdk_version_)); - } else { + if (target_sdk != nullptr) { bpi.set_target_sdk(GetIntAttr(*target_sdk)); + } else { + xml::Attribute* min_sdk = manifest_child->FindAttribute(xml::kSchemaAndroid, "minSdkVersion"); + if (min_sdk != nullptr) { + bpi.set_target_sdk(GetIntAttr(*min_sdk)); + } } } else if (manifest_child->name == "application") { for (xml::Element* app_child : manifest_child->GetChildElements()) { if (app_child->name == "provider") { xml::Attribute* authorities = app_child->FindAttribute(xml::kSchemaAndroid, "authorities"); + CHECK(authorities != nullptr); // authorities can be specified with a string resource reference auto* ref = ValueCast(authorities->compiled_value.get()); if (ref == nullptr) { @@ -467,18 +477,18 @@ int DumpBriefPackageInfo::Action(const std::vector& args) { loaded_apk = LoadedApk::LoadApkFromPath(apk_path, diag_); } ConfigDescription config; - config.sdkVersion = std::stoi(sdk_version_); + config.sdkVersion = sdk_version_; Value* val = FindValueById(loaded_apk->GetResourceTable(), res_id, config); CHECK(val != nullptr); auto str = ValueCast(val); + CHECK(str != nullptr); for (std::string part : util::Split(*str->value, ';')) { bpi.add_content_provider_authority(part); } } } else if (app_child->name == "uses-library") { - xml::Attribute* name = app_child->FindAttribute(xml::kSchemaAndroid, "name"); - CHECK(!name->value.empty()); + xml::Attribute* name = GetName(app_child); bool required = true; xml::Attribute* required_attr = app_child->FindAttribute(xml::kSchemaAndroid, "required"); if (required_attr != nullptr) { @@ -491,15 +501,17 @@ int DumpBriefPackageInfo::Action(const std::vector& args) { bpi.add_optional_uses_library(name->value); } } else if (app_child->name == "library") { - xml::Attribute* name = app_child->FindAttribute(xml::kSchemaAndroid, "name"); - CHECK(!name->value.empty()); + xml::Attribute* name = GetName(app_child); bpi.add_library(name->value); } } } } - bpi.SerializeToOstream(&std::cout); - return 0; + if (!bpi.SerializeToOstream(&std::cout)) { + return 1; + } + std::cout.flush(); + return std::cout.good() ? 0 : 1; } int DumpPackageNameCommand::Dump(LoadedApk* apk) { diff --git a/tools/aapt2/cmd/Dump.h b/tools/aapt2/cmd/Dump.h index 132911544ce7b..c291687d40606 100644 --- a/tools/aapt2/cmd/Dump.h +++ b/tools/aapt2/cmd/Dump.h @@ -176,13 +176,14 @@ class DumpBriefPackageInfo : public Command { : Command("brief-package-info"), diag_(diag) { SetDescription("Print BriefPackageInfo protobuf."); AddRequiredFlag("--sdk-version", - "ro.build.version.sdk value. Some package IDs are version-specific.", &sdk_version_); + "ro.build.version.sdk value. Some package IDs are version-specific.", &sdk_version_str_); } int Action(const std::vector& args) override; private: - std::string sdk_version_; + std::string sdk_version_str_; + int32_t sdk_version_; android::IDiagnostics* diag_; };