• [OTA] 系统加密后Recovery是如何读取OTA升级包的


    目前很多Android手机采用的FUSE方案,也就是内部SD卡不单独占用一个文件系统而实际上占用的是userdata的空间。 当系统加密后,解密需要VOLD的参于。而在Recovery模式下,是没有VOLD的启动的。因此,若是OTA升级包保存在了usrdata或内部存储器中时,Recovery是没有法子直接读取的。

    那么,Android 5.0上, 是怎么处理这个问题的呢? 我来从头一一分析起来:

    首先,安装升级包一般是调用

    frameworks/base/core/java/android/os/RecoverySystem.java 中的installPackage来触发的。

        public static void installPackage(Context context, File packageFile)
            throws IOException {
            String filename = packageFile.getCanonicalPath();
            Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
    
            final String filenameArg = "--update_package=" + filename;
            final String localeArg = "--locale=" + Locale.getDefault().toString();
            bootCommand(context, filenameArg, localeArg);
        }

    最终调用到了bootCommand中,在/cache/recovery/command写入 “--update_package=升级包的文件路径”。然后调用PowerManager, 触发REBOOT_RECOVERY

        /**
         * Reboot into the recovery system with the supplied argument.
         * @param args to pass to the recovery utility.
         * @throws IOException if something goes wrong.
         */
        private static void bootCommand(Context context, String... args) throws IOException {
            RECOVERY_DIR.mkdirs();  // In case we need it
            COMMAND_FILE.delete();  // In case it's not writable
            LOG_FILE.delete();
    
            FileWriter command = new FileWriter(COMMAND_FILE);
            try {
                for (String arg : args) {
                    if (!TextUtils.isEmpty(arg)) {
                        command.write(arg);
                        command.write("
    ");
                    }
                }
            } finally {
                command.close();
            }
    
            // Having written the command file, go ahead and reboot
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            pm.reboot(PowerManager.REBOOT_RECOVERY);
    
            throw new IOException("Reboot failed (no permissions?)");
        }

    至于pm.reboot, 最后会调用到PowerManagerService的lowLevelReboot:

        /**
         * Low-level function to reboot the device. On success, this
         * function doesn't return. If more than 20 seconds passes from
         * the time a reboot is requested (120 seconds for reboot to
         * recovery), this method returns.
         *
         * @param reason code to pass to the kernel (e.g. "recovery"), or null.
         */
        public static void lowLevelReboot(String reason) {
            if (reason == null) {
                reason = "";
            }
            long duration;
            if (reason.equals(PowerManager.REBOOT_RECOVERY)) {
                // If we are rebooting to go into recovery, instead of
                // setting sys.powerctl directly we'll start the
                // pre-recovery service which will do some preparation for
                // recovery and then reboot for us.
                //
                // This preparation can take more than 20 seconds if
                // there's a very large update package, so lengthen the
                // timeout.  We have seen 750MB packages take 3-4 minutes
                SystemProperties.set("ctl.start", "pre-recovery");
                duration = 300 * 1000L;
            } else {
                SystemProperties.set("sys.powerctl", "reboot," + reason);
                duration = 20 * 1000L;
            }
            try {
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

    当重启原因是REBOOT_RECOVERY是,居然不是直接重启,而是启动了pre-recovery这个服务。 好吧,还要继续追踪,这个pre-recovery是啥东西?

    在init.rc里有定义:

    service pre-recovery /system/bin/uncrypt
        class main
        disabled
        oneshot
    

    继续找 uncrypt,路漫漫啊。 

    uncrypt在这里 “bootable/recovery/uncrypt/”, 看到Recovery了吧,开心了吧,快到头了。

    int main(int argc, char** argv)
    {
        const char* input_path;
        const char* map_file;
        int do_reboot = 1;
    
        if (argc != 1 && argc != 3) {
            fprintf(stderr, "usage: %s [<transform_path> <map_file>]
    ", argv[0]);
            return 2;
        }
    
        if (argc == 3) {
            // when command-line args are given this binary is being used
            // for debugging; don't reboot to recovery at the end.
            input_path = argv[1];
            map_file = argv[2];
            do_reboot = 0;
        } else {
            input_path = parse_recovery_command_file();
            if (input_path == NULL) {
                // if we're rebooting to recovery without a package (say,
                // to wipe data), then we don't need to do anything before
                // going to recovery.
                ALOGI("no recovery command file or no update package arg");
                reboot_to_recovery();
                return 1;
            }
            map_file = CACHE_BLOCK_MAP;
        }
    
        ALOGI("update package is %s", input_path);
    
        // Turn the name of the file we're supposed to convert into an
        // absolute path, so we can find what filesystem it's on.
        char path[PATH_MAX+1];
        if (realpath(input_path, path) == NULL) {
            ALOGE("failed to convert %s to absolute path: %s", input_path, strerror(errno));
            return 1;
        }
    
        int encryptable;
        int encrypted;
        if (read_fstab() == NULL) {
            return 1;
        }
        const char* blk_dev = find_block_device(path, &encryptable, &encrypted);
        if (blk_dev == NULL) {
            ALOGE("failed to find block device for %s", path);
            return 1;
        }
    
        // If the filesystem it's on isn't encrypted, we only produce the
        // block map, we don't rewrite the file contents (it would be
        // pointless to do so).
        ALOGI("encryptable: %s
    ", encryptable ? "yes" : "no");
        ALOGI("  encrypted: %s
    ", encrypted ? "yes" : "no");
    
        // Recovery supports installing packages from 3 paths: /cache,
        // /data, and /sdcard.  (On a particular device, other locations
        // may work, but those are three we actually expect.)
        //
        // On /data we want to convert the file to a block map so that we
        // can read the package without mounting the partition.  On /cache
        // and /sdcard we leave the file alone.
        if (strncmp(path, "/data/", 6) != 0) {
            // path does not start with "/data/"; leave it alone.
            unlink(RECOVERY_COMMAND_FILE_TMP);
        } else {
            ALOGI("writing block map %s", map_file);
            if (produce_block_map(path, map_file, blk_dev, encrypted) != 0) {
                return 1;
            }
        }
    
        wipe_misc();
        rename(RECOVERY_COMMAND_FILE_TMP, RECOVERY_COMMAND_FILE);
        if (do_reboot) reboot_to_recovery();
        return 0;
    }

    看看uncrypt干吗了:

    1.) 看下是不是有额外参数,如果有,就以为是debug模式。。嗯,这个不管它。只看正常模式

    2.) 调用 parse_recovery_command_file(), 就是读一下/cache/recovery/command了是的update-package。 这个是RecoverySystem.java写入的

    3.) 读不到update-package,重启到recovery,如果是Factory Data Reset触发的wipe-data就走到这里。

    4.) 看看系统是不是加密的,我是感觉原生代码上这里少了块逻辑,如果系统没有加密,真接重启进recovery就是了,为什么还要继续转换?

    5.)    看看 update-pacakge是不是以"/data"开始的。如果是的话,就调用produce_block_map

    6.) 删除中间文件,然后重启。

    至于 produce_block_map干什么了,也是一目了然的。

    1.) 生成了一个block map,把/cache/recovery/command中的update_package的值改成 @/cache/recovery/block.map

    2.) 如果系统是加密的,就解密,嗯,uncrypt.c 注释里写的很详细,我就不瞎扯了(其实是我也没细看这逻辑)

    // If the filesystem is using an encrypted block device, it will also
    // read the file and rewrite it to the same blocks of the underlying
    // (unencrypted) block device, so the file contents can be read
    // without the need for the decryption key.

    不过,看这注释讲,如果加密了,这个文件等升级完系统重启后,己经是被废掉了。

    先说到这吧。其实还有块,Recovery下面是怎么能识别 @/cache/recovery/block.map这样的update_package的。

    必须提一下有个作死的BUG。

    当uncrypt的selinux权限不够读原升级包文件时,会出错并退出,退出就退出吧,偏偏不重启进Recovery,此时会造成用户点系统升级后,能看到关机动画,然后就是黑屏卡住不动了。当升级包是内置sd卡时,无论是不是加密, 几乎是必出现的。

    此时,会有selinux 的 denied的log.

    04-14 09:32:50.094W/uncrypt ( 5524): type=1400 audit(0.0:25): avc: denied { getattr } forpath="/data/media" dev="mmcblk0p31" ino=567841scontext=u:r:uncrypt:s0 tcontext=u:object_r:media_rw_data_file:s0 tclass=dirpermissive=0 
    

    要怪就怪原生uncrypt没有media_rw的权限吧! (莫非google的OTA不用内卡?) 解决方法也很简单,在uncrypt.te中加所需权

    allow uncrypt media_rw_data_file:dir r_dir_perms; 
    allow uncrypt media_rw_data_file:file r_file_perms; 
      
    
  • 相关阅读:
    vue学习笔记(四)---- 品牌管理案例
    vue学习笔记(三)---- vue-resource
    vue学习笔记(二) ---- vue实例的生命周期
    vue学习笔记(一) ---- vue指令(总体大纲)
    vue学习笔记(一)---- vue指令(在vue中使用样式的方式)
    【问题记录】—.NetCore 编译问题
    Docker学习—概念及基本应用
    Consul 学习笔记-服务注册
    认证授权:IdentityServer4
    认证授权:IdentityServer4
  • 原文地址:https://www.cnblogs.com/codeking100/p/10338111.html
Copyright © 2020-2023  润新知