diff --git a/core/java/android/content/pm/dex/DexMetadataHelper.java b/core/java/android/content/pm/dex/DexMetadataHelper.java index bf35c4d92d3ad5a1e1cc003cb08e179a274544cc..bc5d14a533e24fe2795dff54fc825a2a04b62ab7 100644 --- a/core/java/android/content/pm/dex/DexMetadataHelper.java +++ b/core/java/android/content/pm/dex/DexMetadataHelper.java @@ -22,17 +22,26 @@ import static android.content.pm.parsing.ApkLiteParseUtils.APK_FILE_EXTENSION; import android.content.pm.PackageParser.PackageParserException; import android.content.pm.parsing.ApkLiteParseUtils; import android.content.pm.parsing.PackageLite; +import android.os.SystemProperties; import android.util.ArrayMap; import android.util.jar.StrictJarFile; +import android.util.JsonReader; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.zip.ZipEntry; /** * Helper class used to compute and validate the location of dex metadata files. @@ -40,6 +49,12 @@ import java.util.Map; * @hide */ public class DexMetadataHelper { + public static final String TAG = "DexMetadataHelper"; + /** $> adb shell 'setprop log.tag.DexMetadataHelper VERBOSE' */ + public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + /** $> adb shell 'setprop pm.dexopt.dm.require_manifest true' */ + private static String PROPERTY_DM_JSON_MANIFEST_REQUIRED = "pm.dexopt.dm.require_manifest"; + private static final String DEX_METADATA_FILE_EXTENSION = ".dm"; private DexMetadataHelper() {} @@ -147,14 +162,31 @@ public class DexMetadataHelper { /** * Validate that the given file is a dex metadata archive. - * This is just a validation that the file is a zip archive. + * This is just a validation that the file is a zip archive that contains a manifest.json + * with the package name and version code. * * @throws PackageParserException if the file is not a .dm file. */ - public static void validateDexMetadataFile(String dmaPath) throws PackageParserException { + public static void validateDexMetadataFile(String dmaPath, String packageName, long versionCode) + throws PackageParserException { + validateDexMetadataFile(dmaPath, packageName, versionCode, + SystemProperties.getBoolean(PROPERTY_DM_JSON_MANIFEST_REQUIRED, false)); + } + + @VisibleForTesting + public static void validateDexMetadataFile(String dmaPath, String packageName, long versionCode, + boolean requireManifest) throws PackageParserException { StrictJarFile jarFile = null; + + if (DEBUG) { + Log.v(TAG, "validateDexMetadataFile: " + dmaPath + ", " + packageName + + ", " + versionCode); + } + try { jarFile = new StrictJarFile(dmaPath, false, false); + validateDexMetadataManifest(dmaPath, jarFile, packageName, versionCode, + requireManifest); } catch (IOException e) { throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, "Error opening " + dmaPath, e); @@ -168,6 +200,72 @@ public class DexMetadataHelper { } } + /** Ensure that packageName and versionCode match the manifest.json in the .dm file */ + private static void validateDexMetadataManifest(String dmaPath, StrictJarFile jarFile, + String packageName, long versionCode, boolean requireManifest) + throws IOException, PackageParserException { + if (!requireManifest) { + if (DEBUG) { + Log.v(TAG, "validateDexMetadataManifest: " + dmaPath + + " manifest.json check skipped"); + } + return; + } + + ZipEntry zipEntry = jarFile.findEntry("manifest.json"); + if (zipEntry == null) { + throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, + "Missing manifest.json in " + dmaPath); + } + InputStream inputStream = jarFile.getInputStream(zipEntry); + + JsonReader reader; + try { + reader = new JsonReader(new InputStreamReader(inputStream, "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, + "Error opening manifest.json in " + dmaPath, e); + } + String jsonPackageName = null; + long jsonVersionCode = -1; + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (name.equals("packageName")) { + jsonPackageName = reader.nextString(); + } else if (name.equals("versionCode")) { + jsonVersionCode = reader.nextLong(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + + if (jsonPackageName == null || jsonVersionCode == -1) { + throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, + "manifest.json in " + dmaPath + + " is missing 'packageName' and/or 'versionCode'"); + } + + if (!jsonPackageName.equals(packageName)) { + throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, + "manifest.json in " + dmaPath + " has invalid packageName: " + jsonPackageName + + ", expected: " + packageName); + } + + if (versionCode != jsonVersionCode) { + throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA, + "manifest.json in " + dmaPath + " has invalid versionCode: " + jsonVersionCode + + ", expected: " + versionCode); + } + + if (DEBUG) { + Log.v(TAG, "validateDexMetadataManifest: " + dmaPath + ", " + packageName + + ", " + versionCode + ": successful"); + } + } + /** * Validates that all dex metadata paths in the given list have a matching apk. * (for any foo.dm there should be either a 'foo' of a 'foo.apk' file). diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java index 37dfea4ee0f3bd6dd6dc0d0ddbdff14d0efa4ff7..5ee612b6d55f1d7a2a456e409b6017eb5eb032e8 100644 --- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java +++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java @@ -125,8 +125,10 @@ public class AndroidPackageUtils { public static void validatePackageDexMetadata(AndroidPackage pkg) throws PackageParserException { Collection<String> apkToDexMetadataList = getPackageDexMetadata(pkg).values(); + String packageName = pkg.getPackageName(); + long versionCode = pkg.toAppInfoWithoutState().longVersionCode; for (String dexMetadata : apkToDexMetadataList) { - DexMetadataHelper.validateDexMetadataFile(dexMetadata); + DexMetadataHelper.validateDexMetadataFile(dexMetadata, packageName, versionCode); } } diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java index 0dcd608e6bbdcbbe6965d6d9e31bceca333164bc..a1b2f38af473f90d1cddcd80aaa16e0b0df2e83e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexMetadataHelperTest.java @@ -39,6 +39,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.servicestests.R; import com.android.server.pm.PackageManagerException; import com.android.server.pm.parsing.TestPackageParser2; +import com.android.server.pm.parsing.pkg.AndroidPackage; import com.android.server.pm.parsing.pkg.AndroidPackageUtils; import com.android.server.pm.parsing.pkg.ParsedPackage; @@ -55,6 +56,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; +import java.nio.charset.StandardCharsets; +import java.util.Collection; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -64,6 +67,9 @@ import java.util.zip.ZipOutputStream; public class DexMetadataHelperTest { private static final String APK_FILE_EXTENSION = ".apk"; private static final String DEX_METADATA_FILE_EXTENSION = ".dm"; + private static final String DEX_METADATA_PACKAGE_NAME = + "com.android.frameworks.servicestests.install_split"; + private static long DEX_METADATA_VERSION_CODE = 30; @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); @@ -76,12 +82,46 @@ public class DexMetadataHelperTest { } private File createDexMetadataFile(String apkFileName) throws IOException { + return createDexMetadataFile(apkFileName, /*validManifest=*/true); + } + + private File createDexMetadataFile(String apkFileName, boolean validManifest) throws IOException + { + return createDexMetadataFile(apkFileName,DEX_METADATA_PACKAGE_NAME, + DEX_METADATA_VERSION_CODE, /*emptyManifest=*/false, validManifest); + } + + private File createDexMetadataFile(String apkFileName, String packageName, Long versionCode, + boolean emptyManifest, boolean validManifest) throws IOException { File dmFile = new File(mTmpDir, apkFileName.replace(APK_FILE_EXTENSION, DEX_METADATA_FILE_EXTENSION)); try (FileOutputStream fos = new FileOutputStream(dmFile)) { try (ZipOutputStream zipOs = new ZipOutputStream(fos)) { zipOs.putNextEntry(new ZipEntry("primary.prof")); zipOs.closeEntry(); + + if (validManifest) { + zipOs.putNextEntry(new ZipEntry("manifest.json")); + if (!emptyManifest) { + String manifestStr = "{"; + + if (packageName != null) { + manifestStr += "\"packageName\": " + "\"" + packageName + "\""; + + if (versionCode != null) { + manifestStr += ", "; + } + } + if (versionCode != null) { + manifestStr += " \"versionCode\": " + versionCode; + } + + manifestStr += "}"; + byte[] bytes = manifestStr.getBytes(StandardCharsets.UTF_8); + zipOs.write(bytes, /*off=*/0, /*len=*/bytes.length); + } + zipOs.closeEntry(); + } } } return dmFile; @@ -96,17 +136,38 @@ public class DexMetadataHelperTest { return outFile; } + private static void validatePackageDexMetadata(AndroidPackage pkg, boolean requireManifest) + throws PackageParserException { + Collection<String> apkToDexMetadataList = + AndroidPackageUtils.getPackageDexMetadata(pkg).values(); + String packageName = pkg.getPackageName(); + long versionCode = pkg.toAppInfoWithoutState().longVersionCode; + for (String dexMetadata : apkToDexMetadataList) { + DexMetadataHelper.validateDexMetadataFile( + dexMetadata, packageName, versionCode, requireManifest); + } + } + + private static void validatePackageDexMetatadataVaryingRequireManifest(ParsedPackage pkg) + throws PackageParserException { + validatePackageDexMetadata(pkg, /*requireManifest=*/true); + validatePackageDexMetadata(pkg, /*requireManifest=*/false); + } + @Test public void testParsePackageWithDmFileValid() throws IOException, PackageParserException { copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); createDexMetadataFile("install_split_base.apk"); - ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, 0 /* flags */, false); + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); Map<String, String> packageDexMetadata = AndroidPackageUtils.getPackageDexMetadata(pkg); assertEquals(1, packageDexMetadata.size()); String baseDexMetadata = packageDexMetadata.get(pkg.getBaseApkPath()); assertNotNull(baseDexMetadata); assertTrue(isDexMetadataForApk(baseDexMetadata, pkg.getBaseApkPath())); + + // Should throw no exceptions. + validatePackageDexMetatadataVaryingRequireManifest(pkg); } @Test @@ -116,7 +177,7 @@ public class DexMetadataHelperTest { copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a); createDexMetadataFile("install_split_base.apk"); createDexMetadataFile("install_split_feature_a.apk"); - ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, 0 /* flags */, false); + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); Map<String, String> packageDexMetadata = AndroidPackageUtils.getPackageDexMetadata(pkg); assertEquals(2, packageDexMetadata.size()); @@ -127,6 +188,9 @@ public class DexMetadataHelperTest { String splitDexMetadata = packageDexMetadata.get(pkg.getSplitCodePaths()[0]); assertNotNull(splitDexMetadata); assertTrue(isDexMetadataForApk(splitDexMetadata, pkg.getSplitCodePaths()[0])); + + // Should throw no exceptions. + validatePackageDexMetatadataVaryingRequireManifest(pkg); } @Test @@ -135,7 +199,7 @@ public class DexMetadataHelperTest { copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); copyApkToToTmpDir("install_split_feature_a.apk", R.raw.install_split_feature_a); createDexMetadataFile("install_split_feature_a.apk"); - ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, 0 /* flags */, false); + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); Map<String, String> packageDexMetadata = AndroidPackageUtils.getPackageDexMetadata(pkg); assertEquals(1, packageDexMetadata.size()); @@ -143,6 +207,9 @@ public class DexMetadataHelperTest { String splitDexMetadata = packageDexMetadata.get(pkg.getSplitCodePaths()[0]); assertNotNull(splitDexMetadata); assertTrue(isDexMetadataForApk(splitDexMetadata, pkg.getSplitCodePaths()[0])); + + // Should throw no exceptions. + validatePackageDexMetatadataVaryingRequireManifest(pkg); } @Test @@ -151,9 +218,17 @@ public class DexMetadataHelperTest { File invalidDmFile = new File(mTmpDir, "install_split_base.dm"); Files.createFile(invalidDmFile.toPath()); try { - ParsedPackage pkg = new TestPackageParser2() - .parsePackage(mTmpDir, 0 /* flags */, false); - AndroidPackageUtils.validatePackageDexMetadata(pkg); + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/true); + fail("Should fail validation: empty .dm file"); + } catch (PackageParserException e) { + assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); + } + + try { + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/false); + fail("Should fail validation: empty .dm file"); } catch (PackageParserException e) { assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); } @@ -169,9 +244,112 @@ public class DexMetadataHelperTest { Files.createFile(invalidDmFile.toPath()); try { - ParsedPackage pkg = new TestPackageParser2() - .parsePackage(mTmpDir, 0 /* flags */, false); - AndroidPackageUtils.validatePackageDexMetadata(pkg); + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/true); + fail("Should fail validation: empty .dm file"); + } catch (PackageParserException e) { + assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); + } + + try { + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/false); + fail("Should fail validation: empty .dm file"); + } catch (PackageParserException e) { + assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); + } + } + + @Test + public void testParsePackageWithDmFileInvalidManifest() + throws IOException, PackageParserException { + copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); + createDexMetadataFile("install_split_base.apk", /*validManifest=*/false); + + try { + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/true); + fail("Should fail validation: missing manifest.json in the .dm archive"); + } catch (PackageParserException e) { + assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); + } + } + + @Test + public void testParsePackageWithDmFileEmptyManifest() + throws IOException, PackageParserException { + copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); + createDexMetadataFile("install_split_base.apk", /*packageName=*/"doesn't matter", + /*versionCode=*/-12345L, /*emptyManifest=*/true, /*validManifest=*/true); + + try { + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/true); + fail("Should fail validation: empty manifest.json in the .dm archive"); + } catch (PackageParserException e) { + assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); + } + } + + @Test + public void testParsePackageWithDmFileBadPackageName() + throws IOException, PackageParserException { + copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); + createDexMetadataFile("install_split_base.apk", /*packageName=*/"bad package name", + DEX_METADATA_VERSION_CODE, /*emptyManifest=*/false, /*validManifest=*/true); + + try { + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/true); + fail("Should fail validation: bad package name in the .dm archive"); + } catch (PackageParserException e) { + assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); + } + } + + @Test + public void testParsePackageWithDmFileBadVersionCode() + throws IOException, PackageParserException { + copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); + createDexMetadataFile("install_split_base.apk", DEX_METADATA_PACKAGE_NAME, + /*versionCode=*/12345L, /*emptyManifest=*/false, /*validManifest=*/true); + + try { + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/true); + fail("Should fail validation: bad version code in the .dm archive"); + } catch (PackageParserException e) { + assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); + } + } + + @Test + public void testParsePackageWithDmFileMissingPackageName() + throws IOException, PackageParserException { + copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); + createDexMetadataFile("install_split_base.apk", /*packageName=*/null, + DEX_METADATA_VERSION_CODE, /*emptyManifest=*/false, /*validManifest=*/true); + + try { + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/true); + fail("Should fail validation: missing package name in the .dm archive"); + } catch (PackageParserException e) { + assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); + } + } + + @Test + public void testParsePackageWithDmFileMissingVersionCode() + throws IOException, PackageParserException { + copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); + createDexMetadataFile("install_split_base.apk", DEX_METADATA_PACKAGE_NAME, + /*versionCode=*/null, /*emptyManifest=*/false, /*validManifest=*/true); + + try { + ParsedPackage pkg = new TestPackageParser2().parsePackage(mTmpDir, /*flags=*/0, false); + validatePackageDexMetadata(pkg, /*requireManifest=*/true); + fail("Should fail validation: missing version code in the .dm archive"); } catch (PackageParserException e) { assertEquals(e.error, PackageManager.INSTALL_FAILED_BAD_DEX_METADATA); } @@ -184,7 +362,7 @@ public class DexMetadataHelperTest { try { DexMetadataHelper.validateDexPaths(mTmpDir.list()); - fail("Should fail validation"); + fail("Should fail validation: .dm filename has no match against .apk"); } catch (IllegalStateException e) { // expected. } @@ -200,7 +378,7 @@ public class DexMetadataHelperTest { try { DexMetadataHelper.validateDexPaths(mTmpDir.list()); - fail("Should fail validation"); + fail("Should fail validation: split .dm filename unmatched against .apk"); } catch (IllegalStateException e) { // expected. } @@ -211,7 +389,7 @@ public class DexMetadataHelperTest { copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base); final File dm = createDexMetadataFile("install_split_base.apk"); final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite( - ParseTypeImpl.forDefaultParsing().reset(), mTmpDir, 0 /* flags */); + ParseTypeImpl.forDefaultParsing().reset(), mTmpDir, /*flags=*/0); if (result.isError()) { throw new IllegalStateException(result.getErrorMessage(), result.getException()); } @@ -228,7 +406,7 @@ public class DexMetadataHelperTest { try (FileInputStream is = new FileInputStream(base)) { final ParseResult<ApkLite> result = ApkLiteParseUtils.parseApkLite( ParseTypeImpl.forDefaultParsing().reset(), is.getFD(), - base.getAbsolutePath(), /* flags */ 0); + base.getAbsolutePath(), /*flags=*/0); if (result.isError()) { throw new PackageManagerException(result.getErrorCode(), result.getErrorMessage(), result.getException());