Set up fs-verity in system_server process

fs-verity setup no longer needs CAP_SYS_ADMIN, so move the setup into
system_server to avoid extra hop to installd.  The old path still needs
to be kept for legacy mode unfortunately.

Merkel tree is now also built in private memory instead of shared
memory.

Test: adb install foo.apk
Test: adb install-multiple foo.apk foo.apk.apk_sig
Bug: 112037636
Change-Id: I5c7ac50052fdb242aa60c8eadf97c24a1655b006
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 2dc5437..597f5b3 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -16911,19 +16911,29 @@
             final String filePath = entry.getKey();
             final String signaturePath = entry.getValue();
 
-            final VerityUtils.SetupResult result = VerityUtils.generateApkVeritySetupData(
-                    filePath, signaturePath, legacyMode);
+            if (!legacyMode) {
+                // fs-verity is optional for now.  Only set up if signature is provided.
+                if (new File(signaturePath).exists()) {
+                    try {
+                        VerityUtils.setUpFsverity(filePath, signaturePath);
+                    } catch (IOException | DigestException | NoSuchAlgorithmException
+                            | SecurityException e) {
+                        throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
+                                "Failed to enable fs-verity: " + e);
+                    }
+                }
+                continue;
+            }
+
+            // In legacy mode, fs-verity can only be enabled by process with CAP_SYS_ADMIN.
+            final VerityUtils.SetupResult result = VerityUtils.generateApkVeritySetupData(filePath);
             if (result.isOk()) {
                 if (Build.IS_DEBUGGABLE) Slog.i(TAG, "Enabling verity to " + filePath);
                 final FileDescriptor fd = result.getUnownedFileDescriptor();
                 try {
                     mInstaller.installApkVerity(filePath, fd, result.getContentSize());
-
-                    // In legacy mode, fs-verity can only be enabled by process with CAP_SYS_ADMIN.
-                    if (legacyMode) {
-                        final byte[] rootHash = VerityUtils.generateApkVerityRootHash(filePath);
-                        mInstaller.assertFsverityRootHashMatches(filePath, rootHash);
-                    }
+                    final byte[] rootHash = VerityUtils.generateApkVerityRootHash(filePath);
+                    mInstaller.assertFsverityRootHashMatches(filePath, rootHash);
                 } finally {
                     IoUtils.closeQuietly(fd);
                 }
diff --git a/services/core/java/com/android/server/security/VerityUtils.java b/services/core/java/com/android/server/security/VerityUtils.java
index 839ed30..b1db46f 100644
--- a/services/core/java/com/android/server/security/VerityUtils.java
+++ b/services/core/java/com/android/server/security/VerityUtils.java
@@ -36,6 +36,7 @@
 import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -59,6 +60,8 @@
     /** The maximum size of signature file.  This is just to avoid potential abuse. */
     private static final int MAX_SIGNATURE_FILE_SIZE_BYTES = 8192;
 
+    private static final int COMMON_LINUX_PAGE_SIZE_IN_BYTES = 4096;
+
     private static final boolean DEBUG = false;
 
     /** Returns true if the given file looks like containing an fs-verity signature. */
@@ -71,6 +74,48 @@
         return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION;
     }
 
+    /** Generates Merkle tree and fs-verity metadata then enables fs-verity. */
+    public static void setUpFsverity(@NonNull String filePath, String signaturePath)
+            throws IOException, DigestException, NoSuchAlgorithmException {
+        final PKCS7 pkcs7 = new PKCS7(Files.readAllBytes(Paths.get(signaturePath)));
+        final byte[] expectedMeasurement = pkcs7.getContentInfo().getContentBytes();
+        if (DEBUG) {
+            Slog.d(TAG, "Enabling fs-verity with signed fs-verity measurement "
+                    + bytesToString(expectedMeasurement));
+            Slog.d(TAG, "PKCS#7 info: " + pkcs7);
+        }
+
+        final TrackedBufferFactory bufferFactory = new TrackedBufferFactory();
+        final byte[] actualMeasurement = generateFsverityMetadata(filePath, signaturePath,
+                bufferFactory);
+        try (RandomAccessFile raf = new RandomAccessFile(filePath, "rw")) {
+            FileChannel ch = raf.getChannel();
+            ch.position(roundUpToNextMultiple(ch.size(), COMMON_LINUX_PAGE_SIZE_IN_BYTES));
+            ByteBuffer buffer = bufferFactory.getBuffer();
+
+            long offset = buffer.position();
+            long size = buffer.limit();
+            while (offset < size) {
+                long s = ch.write(buffer);
+                offset += s;
+                size -= s;
+            }
+        }
+
+        if (!Arrays.equals(expectedMeasurement, actualMeasurement)) {
+            throw new SecurityException("fs-verity measurement mismatch: "
+                    + bytesToString(actualMeasurement) + " != "
+                    + bytesToString(expectedMeasurement));
+        }
+
+        // This can fail if the public key is not already in .fs-verity kernel keyring.
+        int errno = enableFsverityNative(filePath);
+        if (errno != 0) {
+            throw new IOException("Failed to enable fs-verity on " + filePath + ": "
+                    + Os.strerror(errno));
+        }
+    }
+
     /** Returns whether the file has fs-verity enabled. */
     public static boolean hasFsverity(@NonNull String filePath) {
         // NB: only measure but not check the actual measurement here. As long as this succeeds,
@@ -87,36 +132,18 @@
     }
 
     /**
-     * Generates Merkle tree and fs-verity metadata.
+     * Generates legacy Merkle tree and fs-verity metadata with Signing Block skipped.
      *
      * @return {@code SetupResult} that contains the result code, and when success, the
      *         {@code FileDescriptor} to read all the data from.
      */
-    public static SetupResult generateApkVeritySetupData(@NonNull String apkPath,
-            String signaturePath, boolean skipSigningBlock) {
+    public static SetupResult generateApkVeritySetupData(@NonNull String apkPath) {
         if (DEBUG) {
-            Slog.d(TAG, "Trying to install apk verity to " + apkPath + " with signature file "
-                    + signaturePath);
+            Slog.d(TAG, "Trying to install legacy apk verity to " + apkPath);
         }
         SharedMemory shm = null;
         try {
-            byte[] signedVerityHash;
-            if (skipSigningBlock) {
-                signedVerityHash = ApkSignatureVerifier.getVerityRootHash(apkPath);
-            } else {
-                Path path = Paths.get(signaturePath);
-                if (Files.exists(path)) {
-                    // TODO(112037636): fail early if the signing key is not in .fs-verity keyring.
-                    PKCS7 pkcs7 = new PKCS7(Files.readAllBytes(path));
-                    signedVerityHash = pkcs7.getContentInfo().getContentBytes();
-                    if (DEBUG) {
-                        Slog.d(TAG, "fs-verity measurement = " + bytesToString(signedVerityHash));
-                    }
-                } else {
-                    signedVerityHash = null;
-                }
-            }
-
+            final byte[] signedVerityHash = ApkSignatureVerifier.getVerityRootHash(apkPath);
             if (signedVerityHash == null) {
                 if (DEBUG) {
                     Slog.d(TAG, "Skip verity tree generation since there is no signed root hash");
@@ -124,8 +151,8 @@
                 return SetupResult.skipped();
             }
 
-            Pair<SharedMemory, Integer> result = generateFsVerityIntoSharedMemory(apkPath,
-                    signaturePath, signedVerityHash, skipSigningBlock);
+            Pair<SharedMemory, Integer> result =
+                    generateFsVerityIntoSharedMemory(apkPath, signedVerityHash);
             shm = result.first;
             int contentSize = result.second;
             FileDescriptor rfd = shm.getFileDescriptor();
@@ -156,7 +183,7 @@
      * {@see ApkSignatureVerifier#getVerityRootHash(String)}.
      */
     public static byte[] getVerityRootHash(@NonNull String apkPath)
-            throws IOException, SignatureNotFoundException, SecurityException {
+            throws IOException, SignatureNotFoundException {
         return ApkSignatureVerifier.getVerityRootHash(apkPath);
     }
 
@@ -172,9 +199,8 @@
      *         includes SHA-256 of fs-verity descriptor and authenticated extensions.
      */
     private static byte[] generateFsverityMetadata(String filePath, String signaturePath,
-            @NonNull TrackedShmBufferFactory trackedBufferFactory)
-            throws IOException, SignatureNotFoundException, SecurityException, DigestException,
-                   NoSuchAlgorithmException {
+            @NonNull ByteBufferFactory trackedBufferFactory)
+            throws IOException, DigestException, NoSuchAlgorithmException {
         try (RandomAccessFile file = new RandomAccessFile(filePath, "r")) {
             VerityBuilder.VerityResult result = VerityBuilder.generateFsVerityTree(
                     file, trackedBufferFactory);
@@ -184,6 +210,7 @@
 
             final byte[] measurement = generateFsverityDescriptorAndMeasurement(file,
                     result.rootHash, signaturePath, buffer);
+            buffer.flip();
             return constructFsveritySignedDataNative(measurement);
         }
     }
@@ -243,6 +270,7 @@
         return md.digest();
     }
 
+    private static native int enableFsverityNative(@NonNull String filePath);
     private static native int measureFsverityNative(@NonNull String filePath);
     private static native byte[] constructFsveritySignedDataNative(@NonNull byte[] measurement);
     private static native byte[] constructFsverityDescriptorNative(long fileSize);
@@ -256,18 +284,13 @@
      * for fsverity setup. The data is aligned to the beginning of {@code SharedMemory}, and has
      * length equals to the returned {@code Integer}.
      */
-    private static Pair<SharedMemory, Integer> generateFsVerityIntoSharedMemory(
-            String apkPath, String signaturePath, @NonNull byte[] expectedRootHash,
-            boolean skipSigningBlock)
-            throws IOException, SecurityException, DigestException, NoSuchAlgorithmException,
+    private static Pair<SharedMemory, Integer> generateFsVerityIntoSharedMemory(String apkPath,
+            @NonNull byte[] expectedRootHash)
+            throws IOException, DigestException, NoSuchAlgorithmException,
                    SignatureNotFoundException {
         TrackedShmBufferFactory shmBufferFactory = new TrackedShmBufferFactory();
-        byte[] generatedRootHash;
-        if (skipSigningBlock) {
-            generatedRootHash = ApkSignatureVerifier.generateApkVerity(apkPath, shmBufferFactory);
-        } else {
-            generatedRootHash = generateFsverityMetadata(apkPath, signaturePath, shmBufferFactory);
-        }
+        byte[] generatedRootHash =
+                ApkSignatureVerifier.generateApkVerity(apkPath, shmBufferFactory);
         // We only generate Merkle tree once here, so it's important to make sure the root hash
         // matches the signed one in the apk.
         if (!Arrays.equals(expectedRootHash, generatedRootHash)) {
@@ -345,7 +368,7 @@
         private ByteBuffer mBuffer;
 
         @Override
-        public ByteBuffer create(int capacity) throws SecurityException {
+        public ByteBuffer create(int capacity) {
             try {
                 if (DEBUG) Slog.d(TAG, "Creating shared memory for apk verity");
                 // NB: This method is supposed to be called once according to the contract with
@@ -378,4 +401,30 @@
             return mBuffer == null ? -1 : mBuffer.limit();
         }
     }
+
+    /** A {@code ByteBufferFactory} that tracks the {@code ByteBuffer} it creates. */
+    private static class TrackedBufferFactory implements ByteBufferFactory {
+        private ByteBuffer mBuffer;
+
+        @Override
+        public ByteBuffer create(int capacity) {
+            if (mBuffer != null) {
+                throw new IllegalStateException("Multiple instantiation from this factory");
+            }
+            mBuffer = ByteBuffer.allocate(capacity);
+            return mBuffer;
+        }
+
+        public ByteBuffer getBuffer() {
+            return mBuffer;
+        }
+    }
+
+    /** Round up the number to the next multiple of the divisor. */
+    private static long roundUpToNextMultiple(long number, long divisor) {
+        if (number > (Long.MAX_VALUE - divisor)) {
+            throw new IllegalArgumentException("arithmetic overflow");
+        }
+        return ((number + (divisor - 1)) / divisor) * divisor;
+    }
 }
diff --git a/services/core/jni/com_android_server_security_VerityUtils.cpp b/services/core/jni/com_android_server_security_VerityUtils.cpp
index 3c87e42..7dd30bd 100644
--- a/services/core/jni/com_android_server_security_VerityUtils.cpp
+++ b/services/core/jni/com_android_server_security_VerityUtils.cpp
@@ -75,6 +75,23 @@
     jbyte* mElements;
 };
 
+int enableFsverity(JNIEnv* env, jobject /* clazz */, jstring filePath) {
+#if HAS_FSVERITY
+    const char* path = env->GetStringUTFChars(filePath, nullptr);
+    ::android::base::unique_fd rfd(open(path, O_RDONLY | O_CLOEXEC));
+    if (rfd.get() < 0) {
+      return errno;
+    }
+    if (ioctl(rfd.get(), FS_IOC_ENABLE_VERITY, nullptr) < 0) {
+      return errno;
+    }
+    return 0;
+#else
+    LOG_ALWAYS_FATAL("fs-verity is used while not enabled");
+    return ENOSYS;
+#endif  // HAS_FSVERITY
+}
+
 int measureFsverity(JNIEnv* env, jobject /* clazz */, jstring filePath) {
 #if HAS_FSVERITY
     auto raii = JavaByteArrayHolder::newArray(env, sizeof(fsverity_digest) + kSha256Bytes);
@@ -82,14 +99,17 @@
     data->digest_size = kSha256Bytes;  // the only input/output parameter
 
     const char* path = env->GetStringUTFChars(filePath, nullptr);
-    ::android::base::unique_fd rfd(open(path, O_RDONLY));
+    ::android::base::unique_fd rfd(open(path, O_RDONLY | O_CLOEXEC));
+    if (rfd.get() < 0) {
+      return errno;
+    }
     if (ioctl(rfd.get(), FS_IOC_MEASURE_VERITY, data) < 0) {
       return errno;
     }
     return 0;
 #else
     LOG_ALWAYS_FATAL("fs-verity is used while not enabled");
-    return -1;
+    return ENOSYS;
 #endif  // HAS_FSVERITY
 }
 
@@ -172,6 +192,7 @@
 }
 
 const JNINativeMethod sMethods[] = {
+    { "enableFsverityNative", "(Ljava/lang/String;)I", (void *)enableFsverity },
     { "measureFsverityNative", "(Ljava/lang/String;)I", (void *)measureFsverity },
     { "constructFsveritySignedDataNative", "([B)[B", (void *)constructFsveritySignedData },
     { "constructFsverityDescriptorNative", "(J)[B", (void *)constructFsverityDescriptor },