From 702b79ba770866d45e99bb429ad8d9d5e1d1b51d Mon Sep 17 00:00:00 2001 From: Jiakai Zhang Date: Mon, 11 Nov 2024 11:29:26 +0000 Subject: Add a dump option to dump SDM status. This will be used in CompilationTest in order to test SDM support. Bug: 377474232 Test: atest ArtServiceTests:DumpHelperTest Test: adb shell pm art dump --include-sdm-status Change-Id: I1693ed0b3873e58ff4e611fba377eda42deb74b3 --- libartservice/service/Android.bp | 1 + libartservice/service/jarjar-rules.txt | 5 + .../com/android/server/art/ArtManagerLocal.java | 29 +++++- .../com/android/server/art/ArtShellCommand.java | 22 ++++- .../java/com/android/server/art/DumpHelper.java | 95 +++++++++++++++++-- .../com/android/server/art/DumpHelperTest.java | 102 ++++++++++++++++++++- 6 files changed, 238 insertions(+), 16 deletions(-) (limited to 'libartservice') diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp index 62bdda9edd..9df3d4ef7b 100644 --- a/libartservice/service/Android.bp +++ b/libartservice/service/Android.bp @@ -101,6 +101,7 @@ java_defaults { "sdk_module-lib_current_framework-connectivity", ], static_libs: [ + "android.content.pm.flags-aconfig-java-export", "art-statslog-art-java", "artd-aidl-java", "dexopt_chroot_setup-aidl-java", diff --git a/libartservice/service/jarjar-rules.txt b/libartservice/service/jarjar-rules.txt index 54ff0a13d2..014ff22579 100644 --- a/libartservice/service/jarjar-rules.txt +++ b/libartservice/service/jarjar-rules.txt @@ -1,3 +1,8 @@ # Repackages static libraries to make them private to ART Services. +rule android.content.pm.CustomFeatureFlags com.android.server.art.jarjar.@0 +rule android.content.pm.FakeFeatureFlagsImpl com.android.server.art.jarjar.@0 +rule android.content.pm.FeatureFlags com.android.server.art.jarjar.@0 +rule android.content.pm.FeatureFlagsImpl com.android.server.art.jarjar.@0 +rule android.content.pm.Flags com.android.server.art.jarjar.@0 rule com.android.modules.utils.** com.android.server.art.jarjar.@0 rule com.google.protobuf.** com.android.server.art.jarjar.@0 diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java index 881a09f5ac..0afdf03d9c 100644 --- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java +++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java @@ -996,8 +996,19 @@ public final class ArtManagerLocal { @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public void dump( @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) { + dump(pw, snapshot, false /* verifySdmSignatures */); + } + + /** + * Same as above, but allows to specify options. + * + * @hide + */ + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public void dump(@NonNull PrintWriter pw, + @NonNull PackageManagerLocal.FilteredSnapshot snapshot, boolean verifySdmSignatures) { try (var pin = mInjector.createArtdPin()) { - new DumpHelper(this).dump(pw, snapshot); + new DumpHelper(this).dump(pw, snapshot, verifySdmSignatures); } } @@ -1013,9 +1024,21 @@ public final class ArtManagerLocal { @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public void dumpPackage(@NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName) { + dumpPackage(pw, snapshot, packageName, false /* verifySdmSignatures */); + } + + /** + * Same as above, but allows to specify options. + * + * @hide + */ + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public void dumpPackage(@NonNull PrintWriter pw, + @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName, + boolean verifySdmSignatures) { try (var pin = mInjector.createArtdPin()) { - new DumpHelper(this).dumpPackage( - pw, snapshot, Utils.getPackageStateOrThrow(snapshot, packageName)); + new DumpHelper(this).dumpPackage(pw, snapshot, + Utils.getPackageStateOrThrow(snapshot, packageName), verifySdmSignatures); } } diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java index ca8f48a0ad..1c19dad9c4 100644 --- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java +++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java @@ -171,11 +171,22 @@ public final class ArtShellCommand extends BasicShellCommandHandler { return 0; } case "dump": { + boolean verifySdmSignatures = false; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "--verify-sdm-signatures": + verifySdmSignatures = true; + break; + } + } + String packageName = getNextArg(); if (packageName != null) { - mArtManagerLocal.dumpPackage(pw, snapshot, packageName); + mArtManagerLocal.dumpPackage(pw, snapshot, packageName, verifySdmSignatures); } else { - mArtManagerLocal.dump(pw, snapshot); + mArtManagerLocal.dump(pw, snapshot, verifySdmSignatures); } return 0; } @@ -1014,10 +1025,13 @@ public final class ArtShellCommand extends BasicShellCommandHandler { pw.println(" Cleanup obsolete files, such as dexopt artifacts that are outdated or"); pw.println(" correspond to dex container files that no longer exist."); pw.println(); - pw.println(" dump [PACKAGE_NAME]"); - pw.println(" Dumps the dexopt state in text format to stdout."); + pw.println(" dump [--verify-sdm-signatures] [PACKAGE_NAME]"); + pw.println(" Dump the dexopt state in text format to stdout."); pw.println(" If PACKAGE_NAME is empty, the command is for all packages. Otherwise, it"); pw.println(" is for the given package."); + pw.println(" Options:"); + pw.println(" --verify-sdm-signatures Also verify SDM file signatures and include"); + pw.println(" their statuses."); pw.println(); pw.println(" dexopt-packages -r REASON"); pw.println(" Run batch dexopt for the given reason."); diff --git a/libartservice/service/java/com/android/server/art/DumpHelper.java b/libartservice/service/java/com/android/server/art/DumpHelper.java index 709c742231..56efac04f3 100644 --- a/libartservice/service/java/com/android/server/art/DumpHelper.java +++ b/libartservice/service/java/com/android/server/art/DumpHelper.java @@ -20,7 +20,12 @@ import static com.android.server.art.DexUseManagerLocal.CheckedSecondaryDexInfo; import static com.android.server.art.DexUseManagerLocal.DexLoader; import static com.android.server.art.model.DexoptStatus.DexContainerFileDexoptStatus; +import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.content.pm.PackageManager; +import android.content.pm.SigningInfo; +import android.content.pm.SigningInfoException; import android.os.Build; import android.os.RemoteException; import android.os.ServiceSpecificException; @@ -34,6 +39,7 @@ import com.android.server.pm.pkg.PackageState; import dalvik.system.VMRuntime; +import java.io.File; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Comparator; @@ -65,13 +71,13 @@ public class DumpHelper { } /** Handles {@link ArtManagerLocal#dump(PrintWriter, PackageManagerLocal.FilteredSnapshot)}. */ - public void dump( - @NonNull PrintWriter pw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot) { + public void dump(@NonNull PrintWriter pw, + @NonNull PackageManagerLocal.FilteredSnapshot snapshot, boolean verifySdmSignatures) { snapshot.getPackageStates() .values() .stream() .sorted(Comparator.comparing(PackageState::getPackageName)) - .forEach(pkgState -> dumpPackage(pw, snapshot, pkgState)); + .forEach(pkgState -> dumpPackage(pw, snapshot, pkgState, verifySdmSignatures)); pw.printf("\nCurrent GC: %s\n", ArtJni.getGarbageCollector()); } @@ -80,8 +86,8 @@ public class DumpHelper { * ArtManagerLocal#dumpPackage(PrintWriter, PackageManagerLocal.FilteredSnapshot, String)}. */ public void dumpPackage(@NonNull PrintWriter pw, - @NonNull PackageManagerLocal.FilteredSnapshot snapshot, - @NonNull PackageState pkgState) { + @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull PackageState pkgState, + boolean verifySdmSignatures) { if (pkgState.isApex() || pkgState.getAndroidPackage() == null) { return; } @@ -124,7 +130,7 @@ public class DumpHelper { ipw.increaseIndent(); for (List fileStatuses : primaryStatusesByDexPath.values()) { - dumpPrimaryDex(ipw, snapshot, fileStatuses, packageName); + dumpPrimaryDex(ipw, snapshot, fileStatuses, packageName, verifySdmSignatures); } if (!secondaryStatusesByDexPath.isEmpty()) { ipw.println("known secondary dex files:"); @@ -141,7 +147,8 @@ public class DumpHelper { private void dumpPrimaryDex(@NonNull IndentingPrintWriter ipw, @NonNull PackageManagerLocal.FilteredSnapshot snapshot, - List fileStatuses, @NonNull String packageName) { + List fileStatuses, @NonNull String packageName, + boolean verifySdmSignatures) { String dexPath = fileStatuses.get(0).getDexContainerFile(); ipw.printf("path: %s\n", dexPath); ipw.increaseIndent(); @@ -149,6 +156,7 @@ public class DumpHelper { dumpUsedByOtherApps(ipw, snapshot, mInjector.getDexUseManager().getPrimaryDexLoaders(packageName, dexPath), packageName); + dumpSdmStatus(ipw, dexPath, verifySdmSignatures); ipw.decreaseIndent(); } @@ -217,6 +225,66 @@ public class DumpHelper { } } + private void dumpSdmStatus(@NonNull IndentingPrintWriter ipw, @NonNull String dexPath, + boolean verifySdmSignatures) { + if (!android.content.pm.Flags.cloudCompilationPm()) { + return; + } + + String sdmPath = getSdmPath(dexPath); + String status = "not-found"; + String signature = "skipped"; + if (mInjector.fileExists(sdmPath)) { + // "Pending" means yet to be picked up by dexopt. For now, "pending" is the only status + // because SDM files are not supported yet. + status = "pending"; + // This operation is expensive, so hide it behind a flag. + if (verifySdmSignatures) { + signature = getSdmSignatureStatus(dexPath, sdmPath); + } + } + ipw.printf("sdm: [sdm-status=%s] [sdm-signature=%s]\n", status, signature); + } + + // The new API usage is safe because it's guarded by a flag. The "NewApi" lint is wrong because + // it's meaningless (b/380891026). We have to work around the lint error because there is no + // `isAtLeastB` to check yet. + // TODO(jiakaiz): Remove this workaround, change @FlaggedApi to @RequiresApi here, and check + // `isAtLeastB` at the call site after B SDK is finalized. + @FlaggedApi(android.content.pm.Flags.FLAG_CLOUD_COMPILATION_PM) + @SuppressLint("NewApi") + @NonNull + private String getSdmSignatureStatus(@NonNull String dexPath, @NonNull String sdmPath) { + SigningInfo sdmSigningInfo; + try { + sdmSigningInfo = + mInjector.getVerifiedSigningInfo(sdmPath, SigningInfo.VERSION_SIGNING_BLOCK_V3); + } catch (SigningInfoException e) { + AsLog.w("Failed to verify SDM signature", e); + return "invalid-sdm-signature"; + } + + SigningInfo apkSigningInfo; + try { + apkSigningInfo = + mInjector.getVerifiedSigningInfo(dexPath, SigningInfo.VERSION_SIGNING_BLOCK_V3); + } catch (SigningInfoException e) { + AsLog.w("Failed to verify SDM signature", e); + return "invalid-apk-signature"; + } + + if (!sdmSigningInfo.signersMatchExactly(apkSigningInfo)) { + return "mismatched-signers"; + } + + return "verified"; + } + + @NonNull + private static String getSdmPath(@NonNull String dexPath) { + return Utils.replaceFileExtension(dexPath, ArtConstants.SECURE_DEX_METADATA_FILE_EXT); + } + @NonNull private String getLoaderState( @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull DexLoader loader) { @@ -252,5 +320,18 @@ public class DumpHelper { public DexUseManagerLocal getDexUseManager() { return GlobalInjector.getInstance().getDexUseManager(); } + + public boolean fileExists(@NonNull String path) { + return new File(path).exists(); + } + + // TODO(jiakaiz): See another comment about "NewApi" above. + @FlaggedApi(android.content.pm.Flags.FLAG_CLOUD_COMPILATION_PM) + @SuppressLint("NewApi") + @NonNull + public SigningInfo getVerifiedSigningInfo( + @NonNull String path, int minAppSigningSchemeVersion) throws SigningInfoException { + return PackageManager.getVerifiedSigningInfo(path, minAppSigningSchemeVersion); + } } } diff --git a/libartservice/service/javatests/com/android/server/art/DumpHelperTest.java b/libartservice/service/javatests/com/android/server/art/DumpHelperTest.java index 45a7010d09..24e5ac5216 100644 --- a/libartservice/service/javatests/com/android/server/art/DumpHelperTest.java +++ b/libartservice/service/javatests/com/android/server/art/DumpHelperTest.java @@ -24,12 +24,19 @@ import static com.android.server.art.model.DexoptStatus.DexContainerFileDexoptSt import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.content.pm.SigningInfo; +import android.content.pm.SigningInfoException; import android.os.SystemProperties; import androidx.test.filters.SmallTest; @@ -70,6 +77,8 @@ public class DumpHelperTest { @Mock private ArtManagerLocal mArtManagerLocal; @Mock private DexUseManagerLocal mDexUseManagerLocal; @Mock private PackageManagerLocal.FilteredSnapshot mSnapshot; + @Mock private SigningInfo mSigningInfoA; + @Mock private SigningInfo mSigningInfoB; private DumpHelper mDumpHelper; @@ -99,6 +108,11 @@ public class DumpHelperTest { setUpForBar(); setUpForSdk(); + lenient().when(mSigningInfoA.signersMatchExactly(mSigningInfoA)).thenReturn(true); + lenient().when(mSigningInfoA.signersMatchExactly(mSigningInfoB)).thenReturn(false); + lenient().when(mSigningInfoB.signersMatchExactly(mSigningInfoB)).thenReturn(true); + lenient().when(mSigningInfoB.signersMatchExactly(mSigningInfoA)).thenReturn(false); + mDumpHelper = new DumpHelper(mInjector); } @@ -110,12 +124,14 @@ public class DumpHelperTest { + " [location is /somewhere/app/foo/oat/arm64/base.odex]\n" + " arm: [status=verify] [reason=install]\n" + " [location is /somewhere/app/foo/oat/arm/base.odex]\n" + + " sdm: [sdm-status=not-found] [sdm-signature=skipped]\n" + " path: /somewhere/app/foo/split_0.apk\n" + " arm64: [status=verify] [reason=vdex] [primary-abi]\n" + " [location is primary.vdex in /somewhere/app/foo/split_0.dm]\n" + " arm: [status=verify] [reason=vdex]\n" + " [location is primary.vdex in /somewhere/app/foo/split_0.dm]\n" + " used by other apps: [com.example2.bar (isa=arm)]\n" + + " sdm: [sdm-status=not-found] [sdm-signature=skipped]\n" + " known secondary dex files:\n" + " /data/user_de/0/foo/1.apk (removed)\n" + " arm: [status=run-from-apk] [reason=unknown]\n" @@ -123,7 +139,8 @@ public class DumpHelperTest { + " class loader context: =VaryingClassLoaderContexts=\n" + " com.example1.foo (isolated): CLC1\n" + " com.example3.baz: CLC2\n" - + " used by other apps: [com.example1.foo (isolated) (isa=arm64), com.example3.baz (removed)]\n" + + " used by other apps: [com.example1.foo (isolated) (isa=arm64), " + + "com.example3.baz (removed)]\n" + " /data/user_de/0/foo/2.apk (public)\n" + " arm64: [status=speed-profile] [reason=bg-dexopt] [primary-abi]\n" + " [location is /data/user_de/0/foo/oat/arm64/2.odex]\n" @@ -136,20 +153,96 @@ public class DumpHelperTest { + " [location is /somewhere/app/bar/oat/arm/base.odex]\n" + " arm64: [status=verify] [reason=install]\n" + " [location is /somewhere/app/bar/oat/arm64/base.odex]\n" + + " sdm: [sdm-status=not-found] [sdm-signature=skipped]\n" + "[com.example3.sdk]\n" + " path: /somewhere/app/sdk/base.apk\n" + " arm: [status=verify] [reason=install] [primary-abi]\n" + " [location is /somewhere/app/sdk/oat/arm/base.odex]\n" + " arm64: [status=verify] [reason=install]\n" + " [location is /somewhere/app/sdk/oat/arm64/base.odex]\n" + + " sdm: [sdm-status=not-found] [sdm-signature=skipped]\n" + "\n" + "Current GC: CollectorTypeCMC\n"; var stringWriter = new StringWriter(); - mDumpHelper.dump(new PrintWriter(stringWriter), mSnapshot); + mDumpHelper.dump(new PrintWriter(stringWriter), mSnapshot, false /* verifySdmSignatures */); assertThat(stringWriter.toString()).isEqualTo(expected); } + @Test + public void testDumpSdmStatusNotFound() throws Exception { + when(mInjector.fileExists(any())).thenReturn(false); + + var stringWriter = new StringWriter(); + mDumpHelper.dumpPackage(new PrintWriter(stringWriter), mSnapshot, + getPackageState(PKG_NAME_BAR), true /* verifySdmSignatures */); + assertThat(stringWriter.toString()).contains("sdm: [sdm-status=not-found]"); + } + + @Test + public void testDumpSdmStatusInvalidSdmSignature() throws Exception { + when(mInjector.fileExists("/somewhere/app/bar/base.sdm")).thenReturn(true); + when(mInjector.getVerifiedSigningInfo(eq("/somewhere/app/bar/base.sdm"), anyInt())) + .thenThrow(SigningInfoException.class); + + var stringWriter = new StringWriter(); + mDumpHelper.dumpPackage(new PrintWriter(stringWriter), mSnapshot, + getPackageState(PKG_NAME_BAR), true /* verifySdmSignatures */); + assertThat(stringWriter.toString()) + .contains("sdm: [sdm-status=pending] [sdm-signature=invalid-sdm-signature]"); + } + + @Test + public void testDumpSdmStatusInvalidApkSignature() throws Exception { + when(mInjector.fileExists("/somewhere/app/bar/base.sdm")).thenReturn(true); + doReturn(mSigningInfoA) + .when(mInjector) + .getVerifiedSigningInfo(eq("/somewhere/app/bar/base.sdm"), anyInt()); + doThrow(SigningInfoException.class) + .when(mInjector) + .getVerifiedSigningInfo(eq("/somewhere/app/bar/base.apk"), anyInt()); + + var stringWriter = new StringWriter(); + mDumpHelper.dumpPackage(new PrintWriter(stringWriter), mSnapshot, + getPackageState(PKG_NAME_BAR), true /* verifySdmSignatures */); + assertThat(stringWriter.toString()) + .contains("sdm: [sdm-status=pending] [sdm-signature=invalid-apk-signature]"); + } + + @Test + public void testDumpSdmStatusSignersNotMatch() throws Exception { + when(mInjector.fileExists("/somewhere/app/bar/base.sdm")).thenReturn(true); + doReturn(mSigningInfoA) + .when(mInjector) + .getVerifiedSigningInfo(eq("/somewhere/app/bar/base.sdm"), anyInt()); + doReturn(mSigningInfoB) + .when(mInjector) + .getVerifiedSigningInfo(eq("/somewhere/app/bar/base.apk"), anyInt()); + + var stringWriter = new StringWriter(); + mDumpHelper.dumpPackage(new PrintWriter(stringWriter), mSnapshot, + getPackageState(PKG_NAME_BAR), true /* verifySdmSignatures */); + assertThat(stringWriter.toString()) + .contains("sdm: [sdm-status=pending] [sdm-signature=mismatched-signers]"); + } + + @Test + public void testDumpSdmStatusVerified() throws Exception { + when(mInjector.fileExists("/somewhere/app/bar/base.sdm")).thenReturn(true); + doReturn(mSigningInfoA) + .when(mInjector) + .getVerifiedSigningInfo(eq("/somewhere/app/bar/base.sdm"), anyInt()); + doReturn(mSigningInfoA) + .when(mInjector) + .getVerifiedSigningInfo(eq("/somewhere/app/bar/base.apk"), anyInt()); + + var stringWriter = new StringWriter(); + mDumpHelper.dumpPackage(new PrintWriter(stringWriter), mSnapshot, + getPackageState(PKG_NAME_BAR), true /* verifySdmSignatures */); + assertThat(stringWriter.toString()) + .contains("sdm: [sdm-status=pending] [sdm-signature=verified]"); + } + private PackageState createPackageState(@NonNull String packageName, int appId, boolean isApex, boolean hasPackage, @NonNull String primaryAbi, @NonNull String secondaryAbi) { var pkgState = mock(PackageState.class); @@ -317,4 +410,9 @@ public class DumpHelperTest { PKG_NAME_SDK, "/somewhere/app/sdk/base.apk")) .thenReturn(Set.of()); } + + @SuppressLint("DirectInvocationOnMock") + private PackageState getPackageState(String packageName) { + return mSnapshot.getPackageState(packageName); + } } -- cgit v1.2.3-59-g8ed1b