Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit abe2776d authored by Igor Murashkin's avatar Igor Murashkin
Browse files

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 12af11d6

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
parent c50447ad
Loading
Loading
Loading
Loading
+100 −2
Original line number Diff line number Diff line
@@ -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).
+3 −1
Original line number Diff line number Diff line
@@ -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);
        }
    }

+190 −12
Original line number Diff line number Diff line
@@ -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());
        }