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

Commit 4fc6b1a8 authored by Kohsuke Yatoh's avatar Kohsuke Yatoh Committed by Android (Google) Code Review
Browse files

Merge "Use fs-verity for protecting updatable font files."

parents bfb16f06 755199ea
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -50,9 +50,11 @@ public final class SystemFonts {
    private static final String DEFAULT_FAMILY = "sans-serif";

    private static final String FONTS_XML = "/system/etc/fonts.xml";
    private static final String SYSTEM_FONT_DIR = "/system/fonts/";
    /** @hide */
    public static final String SYSTEM_FONT_DIR = "/system/fonts/";
    private static final String OEM_XML = "/product/etc/fonts_customization.xml";
    private static final String OEM_FONT_DIR = "/product/fonts/";
    /** @hide */
    public static final String OEM_FONT_DIR = "/product/fonts/";

    private SystemFonts() {}  // Do not instansiate.

+53 −9
Original line number Diff line number Diff line
@@ -31,6 +31,8 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.security.FileIntegrityService;
import com.android.server.security.VerityUtils;

import java.io.File;
import java.io.FileDescriptor;
@@ -39,6 +41,7 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.NioUtils;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.Map;

/** A service for managing system fonts. */
@@ -78,7 +81,17 @@ public final class FontManagerService {

    private static class OtfFontFileParser implements UpdatableFontDir.FontFileParser {
        @Override
        public long getVersion(File file) throws IOException {
        public String getPostScriptName(File file) throws IOException {
            ByteBuffer buffer = mmap(file);
            try {
                return FontFileUtil.getPostScriptName(buffer, 0);
            } finally {
                NioUtils.freeDirectBuffer(buffer);
            }
        }

        @Override
        public long getRevision(File file) throws IOException {
            ByteBuffer buffer = mmap(file);
            try {
                return FontFileUtil.getRevision(buffer, 0);
@@ -95,18 +108,48 @@ public final class FontManagerService {
        }
    }

    private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil {
        @Override
        public boolean hasFsverity(String filePath) {
            return VerityUtils.hasFsverity(filePath);
        }

        @Override
        public void setUpFsverity(String filePath, byte[] pkcs7Signature) throws IOException {
            VerityUtils.setUpFsverity(filePath, pkcs7Signature);
        }

        @Override
        public boolean rename(File src, File dest) {
            // rename system call preserves fs-verity bit.
            return src.renameTo(dest);
        }
    }

    @Nullable
    private final UpdatableFontDir mUpdatableFontDir;

    @GuardedBy("FontManagerService.this")
    @Nullable SystemFontSettings mCurrentFontSettings = null;
    @Nullable
    private SystemFontSettings mCurrentFontSettings = null;

    private FontManagerService() {
        mUpdatableFontDir = ENABLE_FONT_UPDATES
                ? new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser()) : null;
        mUpdatableFontDir = createUpdatableFontDir();
    }

    @NonNull private SystemFontSettings getCurrentFontSettings() {
    @Nullable
    private static UpdatableFontDir createUpdatableFontDir() {
        if (!ENABLE_FONT_UPDATES) return null;
        // If apk verity is supported, fs-verity should be available.
        if (!FileIntegrityService.isApkVeritySupported()) return null;
        return new UpdatableFontDir(new File(FONT_FILES_DIR),
                Arrays.asList(new File(SystemFonts.SYSTEM_FONT_DIR),
                        new File(SystemFonts.OEM_FONT_DIR)),
                new OtfFontFileParser(), new FsverityUtilImpl());
    }

    @NonNull
    private SystemFontSettings getCurrentFontSettings() {
        synchronized (FontManagerService.this) {
            if (mCurrentFontSettings == null) {
                mCurrentFontSettings = SystemFontSettings.create(mUpdatableFontDir);
@@ -115,13 +158,14 @@ public final class FontManagerService {
        }
    }

    private boolean installFontFile(String name, FileDescriptor fd) {
    // TODO(b/173619554): Expose as API.
    private boolean installFontFile(FileDescriptor fd, byte[] pkcs7Signature) {
        if (mUpdatableFontDir == null) return false;
        synchronized (FontManagerService.this) {
            try {
                mUpdatableFontDir.installFontFile(name, fd);
                mUpdatableFontDir.installFontFile(fd, pkcs7Signature);
            } catch (IOException e) {
                Slog.w(TAG, "Failed to install font file: " + name, e);
                Slog.w(TAG, "Failed to install font file");
                return false;
            }
            // Create updated font map in the next getSerializedSystemFontMap() call.
@@ -194,5 +238,5 @@ public final class FontManagerService {
            }
            return null;
        }
    };
    }
}
+213 −42
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.graphics.fonts;

import android.annotation.Nullable;
import android.os.FileUtils;
import android.util.Base64;
import android.util.Slog;
@@ -28,71 +29,158 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

final class UpdatableFontDir {

    private static final String TAG = "UpdatableFontDir";
    private static final String RANDOM_DIR_PREFIX = "~~";
    // TODO: Support .otf
    private static final String ALLOWED_EXTENSION = ".ttf";

    /** Interface to mock font file access in tests. */
    interface FontFileParser {
        long getVersion(File file) throws IOException;
        String getPostScriptName(File file) throws IOException;

        long getRevision(File file) throws IOException;
    }

    /** Interface to mock fs-verity in tests. */
    interface FsverityUtil {
        boolean hasFsverity(String path);

        void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException;

        boolean rename(File src, File dest);
    }

    /** Data class to hold font file path and version. */
    static final class FontFileInfo {
        final File mFile;
        final long mVersion;
    /** Data class to hold font file path and revision. */
    private static final class FontFileInfo {
        private final File mFile;
        private final long mRevision;

        FontFileInfo(File file, long version) {
        FontFileInfo(File file, long revision) {
            mFile = file;
            mVersion = version;
            mRevision = revision;
        }

        public File getFile() {
            return mFile;
        }

        /** Returns the unique randomized font dir containing this font file. */
        public File getRandomizedFontDir() {
            return mFile.getParentFile();
        }

        public long getRevision() {
            return mRevision;
        }
    }

    /**
     * Root directory for storing updated font files. Each font file is stored in a unique random
     * dir. The font file path would be {@code mFilesDir/~~{randomStr}/{fontFileName}}.
     * Root directory for storing updated font files. Each font file is stored in a unique
     * randomized dir. The font file path would be {@code mFilesDir/~~{randomStr}/{fontFileName}}.
     */
    private final File mFilesDir;
    private final List<File> mPreinstalledFontDirs;
    private final FontFileParser mParser;
    private final FsverityUtil mFsverityUtil;
    /**
     * A mutable map containing mapping from font file name (e.g. "NotoColorEmoji.ttf") to {@link
     * FontFileInfo}. All files in this map are validated, and have higher revision numbers than
     * corresponding font files in {@link #mPreinstalledFontDirs}.
     */
    @GuardedBy("UpdatableFontDir.this")
    private final Map<String, FontFileInfo> mFontFileInfoMap = new HashMap<>();

    UpdatableFontDir(File filesDir, FontFileParser parser) {
    UpdatableFontDir(File filesDir, List<File> preinstalledFontDirs, FontFileParser parser,
            FsverityUtil fsverityUtil) {
        mFilesDir = filesDir;
        mPreinstalledFontDirs = preinstalledFontDirs;
        mParser = parser;
        mFsverityUtil = fsverityUtil;
        loadFontFileMap();
    }

    private void loadFontFileMap() {
        // TODO: SIGBUS crash protection
        synchronized (UpdatableFontDir.this) {
            boolean success = false;
            mFontFileInfoMap.clear();
            try {
                File[] dirs = mFilesDir.listFiles();
                if (dirs == null) return;
                for (File dir : dirs) {
                if (!dir.getName().startsWith(RANDOM_DIR_PREFIX)) continue;
                    if (!dir.getName().startsWith(RANDOM_DIR_PREFIX)) return;
                    File[] files = dir.listFiles();
                if (files == null || files.length != 1) continue;
                addFileToMapLocked(files[0], true);
                    if (files == null || files.length != 1) return;
                    FontFileInfo fontFileInfo = validateFontFile(files[0]);
                    if (fontFileInfo == null) {
                        Slog.w(TAG, "Broken file is found. Clearing files.");
                        return;
                    }
                    addFileToMapLocked(fontFileInfo, true /* deleteOldFile */);
                }
                success = true;
            } finally {
                // Delete all files just in case if we find a problematic file.
                if (!success) {
                    mFontFileInfoMap.clear();
                    FileUtils.deleteContents(mFilesDir);
                }
            }
        }
    }

    void installFontFile(String name, FileDescriptor fd) throws IOException {
        // TODO: Validate name.
    /**
     * Installs a new font file, or updates an existing font file.
     *
     * <p>The new font will be immediately available for new Zygote-forked processes through
     * {@link #getFontFileMap()}. Old font files will be kept until next system server reboot,
     * because existing Zygote-forked processes have paths to old font files.
     *
     * @param fd             A file descriptor to the font file.
     * @param pkcs7Signature A PKCS#7 detached signature to enable fs-verity for the font file.
     */
    void installFontFile(FileDescriptor fd, byte[] pkcs7Signature) throws IOException {
        synchronized (UpdatableFontDir.this) {
            // TODO: proper error handling
            File newDir = getRandomDir(mFilesDir);
            if (!newDir.mkdir()) {
                // TODO: Define and return an error code for API
                throw new IOException("Failed to create a new dir");
            }
            File newFontFile = new File(newDir, name);
            try (FileOutputStream out = new FileOutputStream(newFontFile)) {
            boolean success = false;
            try {
                File tempNewFontFile = new File(newDir, "font.ttf");
                try (FileOutputStream out = new FileOutputStream(tempNewFontFile)) {
                    FileUtils.copy(fd, out.getFD());
                }
            addFileToMapLocked(newFontFile, false);
                // Do not parse font file before setting up fs-verity.
                // setUpFsverity throws IOException if failed.
                mFsverityUtil.setUpFsverity(tempNewFontFile.getAbsolutePath(), pkcs7Signature);
                String postScriptName = mParser.getPostScriptName(tempNewFontFile);
                File newFontFile = new File(newDir, postScriptName + ALLOWED_EXTENSION);
                if (!mFsverityUtil.rename(tempNewFontFile, newFontFile)) {
                    // TODO: Define and return an error code for API
                    throw new IOException("Failed to rename");
                }
                FontFileInfo fontFileInfo = validateFontFile(newFontFile);
                if (fontFileInfo == null) {
                    // TODO: Define and return an error code for API
                    throw new IllegalArgumentException("Invalid file");
                }
                if (!addFileToMapLocked(fontFileInfo, false)) {
                    // TODO: Define and return an error code for API
                    throw new IllegalArgumentException("Version downgrade");
                }
                success = true;
            } finally {
                if (!success) {
                    FileUtils.deleteContentsAndDir(newDir);
                }
            }
        }
    }

@@ -114,27 +202,110 @@ final class UpdatableFontDir {
        return dir;
    }

    private void addFileToMapLocked(File file, boolean deleteOldFile) {
        final long version;
    /**
     * Add the given {@link FontFileInfo} to {@link #mFontFileInfoMap} if its font revision is
     * higher than the currently used font file (either in {@link #mFontFileInfoMap} or {@link
     * #mPreinstalledFontDirs}).
     */
    private boolean addFileToMapLocked(FontFileInfo fontFileInfo, boolean deleteOldFile) {
        String name = fontFileInfo.getFile().getName();
        FontFileInfo existingInfo = mFontFileInfoMap.get(name);
        final boolean shouldAddToMap;
        if (existingInfo == null) {
            // We got a new updatable font. We need to check if it's newer than preinstalled fonts.
            // Note that getPreinstalledFontRevision() returns -1 if there is no preinstalled font
            // with 'name'.
            shouldAddToMap = getPreinstalledFontRevision(name) < fontFileInfo.getRevision();
        } else {
            shouldAddToMap = existingInfo.getRevision() < fontFileInfo.getRevision();
        }
        if (shouldAddToMap) {
            if (deleteOldFile && existingInfo != null) {
                FileUtils.deleteContentsAndDir(existingInfo.getRandomizedFontDir());
            }
            mFontFileInfoMap.put(name, fontFileInfo);
            return true;
        } else {
            if (deleteOldFile) {
                FileUtils.deleteContentsAndDir(fontFileInfo.getRandomizedFontDir());
            }
            return false;
        }
    }

    private long getPreinstalledFontRevision(String name) {
        long maxRevision = -1;
        for (File dir : mPreinstalledFontDirs) {
            File preinstalledFontFile = new File(dir, name);
            if (!preinstalledFontFile.exists()) continue;
            long revision = getFontRevision(preinstalledFontFile);
            if (revision == -1) {
                Slog.w(TAG, "Invalid preinstalled font file");
                continue;
            }
            if (revision > maxRevision) {
                maxRevision = revision;
            }
        }
        return maxRevision;
    }

    /**
     * Checks the fs-verity protection status of the given font file, validates the file name, and
     * returns a {@link FontFileInfo} on success. This method does not check if the font revision
     * is higher than the currently used font.
     */
    @Nullable
    private FontFileInfo validateFontFile(File file) {
        if (!mFsverityUtil.hasFsverity(file.getAbsolutePath())) {
            Slog.w(TAG, "Font validation failed. Fs-verity is not enabled: " + file);
            return null;
        }
        if (!validateFontFileName(file)) {
            Slog.w(TAG, "Font validation failed. Could not validate font file name: " + file);
            return null;
        }
        long revision = getFontRevision(file);
        if (revision == -1) {
            Slog.w(TAG, "Font validation failed. Could not read font revision: " + file);
            return null;
        }
        return new FontFileInfo(file, revision);
    }

    /**
     * Returns true if the font file's file name matches with the PostScript name metadata in the
     * font file.
     *
     * <p>We check the font file names because apps use file name to look up fonts.
     * <p>Because PostScript name does not include extension, the extension is appended for
     * comparison. For example, if the file name is "NotoColorEmoji.ttf", the PostScript name should
     * be "NotoColorEmoji".
     */
    private boolean validateFontFileName(File file) {
        String fileName = file.getName();
        String postScriptName = getPostScriptName(file);
        return (postScriptName + ALLOWED_EXTENSION).equals(fileName);
    }

    /** Returns the PostScript name of the given font file, or null. */
    @Nullable
    private String getPostScriptName(File file) {
        try {
            version = mParser.getVersion(file);
            return mParser.getPostScriptName(file);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to read font file", e);
            return;
            return null;
        }
        if (version == -1) {
            Slog.e(TAG, "Invalid font file");
            return;
        }
        FontFileInfo info = mFontFileInfoMap.get(file.getName());
        if (info == null) {
            // TODO: check version of font in /system/fonts and /product/fonts
            mFontFileInfoMap.put(file.getName(), new FontFileInfo(file, version));
        } else if (info.mVersion < version) {
            if (deleteOldFile) {
                FileUtils.deleteContentsAndDir(info.mFile.getParentFile());
    }
            mFontFileInfoMap.put(file.getName(), new FontFileInfo(file, version));

    /** Returns the non-negative font revision of the given font file, or -1. */
    private long getFontRevision(File file) {
        try {
            return mParser.getRevision(file);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to read font file", e);
            return -1;
        }
    }

@@ -142,7 +313,7 @@ final class UpdatableFontDir {
        Map<String, File> map = new HashMap<>();
        synchronized (UpdatableFontDir.this) {
            for (Map.Entry<String, FontFileInfo> entry : mFontFileInfoMap.entrySet()) {
                map.put(entry.getKey(), entry.getValue().mFile);
                map.put(entry.getKey(), entry.getValue().getFile());
            }
        }
        return map;
+6 −2
Original line number Diff line number Diff line
@@ -60,8 +60,7 @@ public class FileIntegrityService extends SystemService {
    private final IBinder mService = new IFileIntegrityService.Stub() {
        @Override
        public boolean isApkVeritySupported() {
            return Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.R
                    || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
            return FileIntegrityService.isApkVeritySupported();
        }

        @Override
@@ -111,6 +110,11 @@ public class FileIntegrityService extends SystemService {
        }
    };

    public static boolean isApkVeritySupported() {
        return Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.R
                || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
    }

    public FileIntegrityService(final Context context) {
        super(context);
        try {
+6 −1
Original line number Diff line number Diff line
@@ -73,7 +73,12 @@ abstract public class VerityUtils {
        if (Files.size(Paths.get(signaturePath)) > MAX_SIGNATURE_FILE_SIZE_BYTES) {
            throw new SecurityException("Signature file is unexpectedly large: " + signaturePath);
        }
        byte[] pkcs7Signature = Files.readAllBytes(Paths.get(signaturePath));
        setUpFsverity(filePath, Files.readAllBytes(Paths.get(signaturePath)));
    }

    /** Enables fs-verity for the file with a PKCS#7 detached signature bytes. */
    public static void setUpFsverity(@NonNull String filePath, @NonNull byte[] pkcs7Signature)
            throws IOException {
        // This will fail if the public key is not already in .fs-verity kernel keyring.
        int errno = enableFsverityNative(filePath, pkcs7Signature);
        if (errno != 0) {
Loading