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

Commit e41b029d authored by Todd Kennedy's avatar Todd Kennedy
Browse files

Release cblocks back to the free pool

Modern f2fs implementations starting in S support compression
natively within the file system. The data blocks of specific
installation artifacts [eg. .apk, .so, ...] can be compressed
at the file system level, making them look and act like any
other uncompressed file, but consuming a fraction of the space.

However, the unused space is not free'd automatically. Instead,
we must manually tell the file system to release the extra
blocks [the delta between the compressed and uncompressed block
counts] back to the free pool.

Because of how compression works within the file system, once
the blocks have been released, the file becomes read-only and
cannot be modified until the free'd blocks have again been
reserved from the free pool. For these installation artifacts,
that is not a concern.

Test: atest CtsAppSecurityHostTestCases:SplitTests
Test: atest CtsAppSecurityHostTestCases:IsolatedSplitsTests
Test: atest CtsAppSecurityHostTestCases:PkgInstallSignatureVerificationTest
Test: atest CtsLibnativehelperTestCases
Test: atest PackageManagerTests
Test: Manual. Boot the device and see cblocks for stub APKs are released
Test: Manual. Disable and re-enable stub APK and see cblocks are released
Bug: 188928405
Change-Id: I4b29dab263d02a79d7b1593d35ae28101d33679a
parent ee4d86d0
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -6623,6 +6623,20 @@ public final class Settings {
        @Readable
        public static final String COMPLETED_CATEGORY_PREFIX = "suggested.completed_category.";
        /**
         * Whether or not compress blocks should be released on install.
         * <p>The setting only determines if the platform will attempt to release
         * compress blocks; it does not guarantee that the files will have their
         * compress blocks released. Compression is currently only supported on
         * some f2fs filesystems.
         * <p>
         * Type: int (0 for false, 1 for true)
         *
         * @hide
         */
        public static final String RELEASE_COMPRESS_BLOCKS_ON_INSTALL =
                "release_compress_blocks_on_install";
        /**
         * List of input methods that are currently enabled.  This is a string
         * containing the IDs of all enabled input methods, each ID separated
+296 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.content;

import android.annotation.NonNull;
import android.content.ContentResolver;
import android.os.Environment;
import android.os.incremental.IncrementalManager;
import android.provider.Settings.Secure;
import android.text.TextUtils;
import android.util.Slog;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;

/**
 * Utility methods to work with the f2fs file system.
 */
public final class F2fsUtils {
    private static final String TAG = "F2fsUtils";
    private static final boolean DEBUG_F2FS = false;

    /** Directory containing kernel features */
    private static final File sKernelFeatures =
            new File("/sys/fs/f2fs/features");
    /** File containing features enabled on "/data" */
    private static final File sUserDataFeatures =
            new File("/dev/sys/fs/by-name/userdata/features");
    private static final File sDataDirectory = Environment.getDataDirectory();
    /** Name of the compression feature */
    private static final String COMPRESSION_FEATURE = "compression";

    private static final boolean sKernelCompressionAvailable;
    private static final boolean sUserDataCompressionAvailable;

    static {
        sKernelCompressionAvailable = isCompressionEnabledInKernel();
        if (!sKernelCompressionAvailable) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "f2fs compression DISABLED; feature not part of the kernel");
            }
        }
        sUserDataCompressionAvailable = isCompressionEnabledOnUserData();
        if (!sUserDataCompressionAvailable) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "f2fs compression DISABLED; feature not enabled on filesystem");
            }
        }
    }

    /**
     * Releases compressed blocks from eligible installation artifacts.
     * <p>
     * Modern f2fs implementations starting in {@code S} support compression
     * natively within the file system. The data blocks of specific installation
     * artifacts [eg. .apk, .so, ...] can be compressed at the file system level,
     * making them look and act like any other uncompressed file, but consuming
     * a fraction of the space.
     * <p>
     * However, the unused space is not free'd automatically. Instead, we must
     * manually tell the file system to release the extra blocks [the delta between
     * the compressed and uncompressed block counts] back to the free pool.
     * <p>
     * Because of how compression works within the file system, once the blocks
     * have been released, the file becomes read-only and cannot be modified until
     * the free'd blocks have again been reserved from the free pool.
     */
    public static void releaseCompressedBlocks(ContentResolver resolver, File file) {
        if (!sKernelCompressionAvailable || !sUserDataCompressionAvailable) {
            return;
        }

        // NOTE: Retrieving this setting means we need to delay releasing cblocks
        // of any APKs installed during the PackageManagerService constructor. Instead
        // of being able to release them in the constructor, they can only be released
        // immediately prior to the system being available. When we no longer need to
        // read this setting, move cblock release back to the package manager constructor.
        final boolean releaseCompressBlocks =
                Secure.getInt(resolver, Secure.RELEASE_COMPRESS_BLOCKS_ON_INSTALL, 1) != 0;
        if (!releaseCompressBlocks) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "SKIP; release compress blocks not enabled");
            }
            return;
        }
        if (!isCompressionAllowed(file)) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "SKIP; compression not allowed");
            }
            return;
        }
        final File[] files = getFilesToRelease(file);
        if (files == null || files.length == 0) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "SKIP; no files to compress");
            }
            return;
        }
        for (int i = files.length - 1; i >= 0; --i) {
            final long releasedBlocks = nativeReleaseCompressedBlocks(files[i].getAbsolutePath());
            if (DEBUG_F2FS) {
                Slog.d(TAG, "RELEASED " + releasedBlocks + " blocks"
                        + " from \"" + files[i] + "\"");
            }
        }
    }

    /**
     * Returns {@code true} if compression is allowed on the file system containing
     * the given file.
     * <p>
     * NOTE: The return value does not mean if the given file, or any other file
     * on the same file system, is actually compressed. It merely determines whether
     * not files <em>may</em> be compressed.
     */
    private static boolean isCompressionAllowed(@NonNull File file) {
        final String filePath;
        try {
            filePath = file.getCanonicalPath();
        } catch (IOException e) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "f2fs compression DISABLED; could not determine path");
            }
            return false;
        }
        if (IncrementalManager.isIncrementalPath(filePath)) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "f2fs compression DISABLED; file on incremental fs");
            }
            return false;
        }
        if (!isChild(sDataDirectory, filePath)) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "f2fs compression DISABLED; file not on /data");
            }
            return false;
        }
        if (DEBUG_F2FS) {
            Slog.d(TAG, "f2fs compression ENABLED");
        }
        return true;
    }

    /**
     * Returns {@code true} if the given child is a descendant of the base.
     */
    private static boolean isChild(@NonNull File base, @NonNull String childPath) {
        try {
            base = base.getCanonicalFile();

            File parentFile = new File(childPath).getCanonicalFile();
            while (parentFile != null) {
                if (base.equals(parentFile)) {
                    return true;
                }
                parentFile = parentFile.getParentFile();
            }
            return false;
        } catch (IOException ignore) {
            return false;
        }
    }

    /**
     * Returns whether or not the compression feature is enabled in the kernel.
     * <p>
     * NOTE: This doesn't mean compression is enabled on a particular file system
     * or any files have been compressed. Only that the functionality is enabled
     * on the device.
     */
    private static boolean isCompressionEnabledInKernel() {
        final File[] features = sKernelFeatures.listFiles();
        if (features == null || features.length == 0) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "ERROR; no kernel features");
            }
            return false;
        }
        for (int i = features.length - 1; i >= 0; --i) {
            final File feature = features[i];
            if (COMPRESSION_FEATURE.equals(features[i].getName())) {
                if (DEBUG_F2FS) {
                    Slog.d(TAG, "FOUND kernel compression feature");
                }
                return true;
            }
        }
        if (DEBUG_F2FS) {
            Slog.d(TAG, "ERROR; kernel compression feature not found");
        }
        return false;
    }

    /**
     * Returns whether or not the compression feature is enabled on user data [ie. "/data"].
     * <p>
     * NOTE: This doesn't mean any files have been compressed. Only that the functionality
     * is enabled on the file system.
     */
    private static boolean isCompressionEnabledOnUserData() {
        if (!sUserDataFeatures.exists()
                || !sUserDataFeatures.isFile()
                || !sUserDataFeatures.canRead()) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "ERROR; filesystem features not available");
            }
            return false;
        }
        final List<String> configLines;
        try {
            configLines = Files.readAllLines(sUserDataFeatures.toPath());
        } catch (IOException ignore) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "ERROR; couldn't read filesystem features");
            }
            return false;
        }
        if (configLines == null
                || configLines.size() > 1
                || TextUtils.isEmpty(configLines.get(0))) {
            if (DEBUG_F2FS) {
                Slog.d(TAG, "ERROR; no filesystem features");
            }
            return false;
        }
        final String[] features = configLines.get(0).split(",");
        for (int i = features.length - 1; i >= 0; --i) {
            if (COMPRESSION_FEATURE.equals(features[i].trim())) {
                if (DEBUG_F2FS) {
                    Slog.d(TAG, "FOUND filesystem compression feature");
                }
                return true;
            }
        }
        if (DEBUG_F2FS) {
            Slog.d(TAG, "ERROR; filesystem compression feature not found");
        }
        return false;
    }

    /**
     * Returns all files contained within the directory at any depth from the given path.
     */
    private static List<File> getFilesRecursive(@NonNull File path) {
        final File[] allFiles = path.listFiles();
        if (allFiles == null) {
            return null;
        }
        final ArrayList<File> files = new ArrayList<>();
        for (File f : allFiles) {
            if (f.isDirectory()) {
                files.addAll(getFilesRecursive(f));
            } else if (f.isFile()) {
                files.add(f);
            }
        }
        return files;
    }

    /**
     * Returns all files contained within the directory at any depth from the given path.
     */
    private static File[] getFilesToRelease(@NonNull File codePath) {
        final List<File> files = getFilesRecursive(codePath);
        if (files == null) {
            if (codePath.isFile()) {
                return new File[] { codePath };
            }
            return null;
        }
        if (files.size() == 0) {
            return null;
        }
        return files.toArray(new File[files.size()]);
    }

    private static native long nativeReleaseCompressedBlocks(String path);

}
+5 −0
Original line number Diff line number Diff line
# Bug component: 36137
include /core/java/android/content/pm/OWNERS

per-file ReferrerIntent.aidl = file:/services/core/java/com/android/server/am/OWNERS
per-file ReferrerIntent.java = file:/services/core/java/com/android/server/am/OWNERS
+1 −0
Original line number Diff line number Diff line
@@ -84,6 +84,7 @@ cc_library_shared {
        android: {
            srcs: [
                "AndroidRuntime.cpp",
                "com_android_internal_content_F2fsUtils.cpp",
                "com_android_internal_content_NativeLibraryHelper.cpp",
                "com_google_android_gles_jni_EGLImpl.cpp",
                "com_google_android_gles_jni_GLImpl.cpp", // TODO: .arm
+2 −0
Original line number Diff line number Diff line
@@ -190,6 +190,7 @@ extern int register_android_content_res_ObbScanner(JNIEnv* env);
extern int register_android_content_res_Configuration(JNIEnv* env);
extern int register_android_animation_PropertyValuesHolder(JNIEnv *env);
extern int register_android_security_Scrypt(JNIEnv *env);
extern int register_com_android_internal_content_F2fsUtils(JNIEnv* env);
extern int register_com_android_internal_content_NativeLibraryHelper(JNIEnv *env);
extern int register_com_android_internal_content_om_OverlayConfig(JNIEnv *env);
extern int register_com_android_internal_net_NetworkUtilsInternal(JNIEnv* env);
@@ -1621,6 +1622,7 @@ static const RegJNIRec gRegJNI[] = {

        REG_JNI(register_android_animation_PropertyValuesHolder),
        REG_JNI(register_android_security_Scrypt),
        REG_JNI(register_com_android_internal_content_F2fsUtils),
        REG_JNI(register_com_android_internal_content_NativeLibraryHelper),
        REG_JNI(register_com_android_internal_os_DmabufInfoReader),
        REG_JNI(register_com_android_internal_os_FuseAppLoop),
Loading