pm: Require manifest.json for .dm (dex-metadata) files.

Example file:
  $> unzip CtsApkVerityTestAppPrebuilt.dm manifest.json ; cat manifest.json
  {
    "packageName": "android.appsecurity.cts.apkveritytestapp",
    "versionCode": 30
  }
  
Cherry-picked from 12af11d6f5

Test: adb install-multiple -r foo.apk foo.dm
Test: atest FrameworksServicesTests:com.android.server.pm.dex.DexMetadataHelperTest
Bug: 179295368
Merged-In: I7a3c442bb27da5948bc9ead146c77213de6b56cc
Change-Id: I7a3c442bb27da5948bc9ead146c77213de6b56cc
This commit is contained in:
Igor Murashkin
2021-02-12 10:51:13 -08:00
parent c50447ad57
commit abe2776ddf
3 changed files with 293 additions and 15 deletions

View File

@@ -22,17 +22,26 @@ import static android.content.pm.PackageParser.APK_FILE_EXTENSION;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.PackageLite;
import android.content.pm.PackageParser.PackageParserException;
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).

View File

@@ -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);
}
}

View File

@@ -41,6 +41,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.frameworks.servicestests.R;
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;
@@ -57,6 +58,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;
@@ -66,6 +69,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();
@@ -78,12 +84,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;
@@ -98,17 +138,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.getBaseCodePath());
assertNotNull(baseDexMetadata);
assertTrue(isDexMetadataForApk(baseDexMetadata, pkg.getBaseCodePath()));
// Should throw no exceptions.
validatePackageDexMetatadataVaryingRequireManifest(pkg);
}
@Test
@@ -118,7 +179,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());
@@ -129,6 +190,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
@@ -137,7 +201,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());
@@ -145,6 +209,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
@@ -153,9 +220,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);
}
@@ -171,9 +246,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);
}
@@ -186,7 +364,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.
}
@@ -202,7 +380,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.
}
@@ -214,7 +392,7 @@ public class DexMetadataHelperTest {
copyApkToToTmpDir("install_split_base.apk", R.raw.install_split_base);
File dm = createDexMetadataFile("install_split_base.apk");
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());
}