(android-12)app私有*.so被置零

  • <app_basedir>:apk在/data/app下的安装路径,示例/data/app/~~07kR77IGzQfrbWwdbtkBJQ==/com.kos.launcher-6kugzuV2AqFjUibzOJm1eQ==
  • <data_CE_dir>:表示apk在/data/data内部存储路径,示例/data/data/com.kos.launcher

工作中遇到这么个问题,com.kos.launcher是自写app,主要编程语言是C/C++,这些C/C++代码编译成数个*.so,然后打包进apk。在运行着android-12的firefly-rk3588s-pc上安装这apk,安装成功,之后运行也成功。但是,重启设备后,会随机出现这些*.so被置0,但文件长度保持不变。举个例子,图1显示了libhidapi.so正常时数据,图2是出现错误后这个so被清零后的数据。出问题后,此文件中字节都变成了0,但前、后两libhidapi.so文件长度保持不变。

图1 正确的libhidapiso

 

图2 出问题时libhidapi.so

把这apk安装到其它android设备上,像android7.1的aio-3399j,没有这问题。安装到同样是android12的华为手机,也没这问题。

但是,在这出问题的firefly-rk3588s-pc(android-12),安装带有*.so的其它产商apk,像bilibili的iBiliPlayer-bili.apk,好像也没这问题。

以下是更多出问题时现象。

  1. 一旦出了问题,在android启动创段,PackageManagerService开始执行scanDirLI安装/data/app下的app时,com.kos.launcher相关*.so已经被破坏了。
  2. 出问题后,如果把正确的so替换掉被破坏的so,再运行apk,能成功。
  3. 不论是32位的armeabi-v7a,还是64位的arm64-v8a,都有这问题。
  4. 用目前最新NDK-r25编译*.so,也有这问题。
  5. 不论是按reset键的热重启,还是拔、插电源线的冷重启,都有这问题。。
  6. 这问题好像只在安装后的第一次重启时出现。也就是说,要是第一次重启后,没出问题,那后面就不出了。
  7. 出问题的*.so存放在<app_basedir>/lib/arm64下,但出问题时,上级目录下的<app_basedir>/base.apk没有被破坏。
  8. [这点不知是出题时连带现象,还是android就有这特点]往<data_CE_dir>写入文件时,重启设备,这文件长度会随机变成0字节。举个例子,创建目录<data_CE_dir>/leagorlib,并把<app_basedir>/lib/arm64下的libhidapi.so复制这里。设备重启后,libhidapi.so变成了0字节。

这破坏发生在启动阶段的PackageManagerService之前,到现在没找到原因。这是目前猜测:1)自写的apk在处理*.so方面有问题,是编译有问题,还是android studio相关配置有问题,不知道。2)相比华为手机android-12,firefly-rk3588s-pc可能要求apk中的*.so较严格,发现不满足要求,就清零了。

 

一、临时解决办法

没找到原因,但问题总得解决,只能找个临时办法。要修改android源码,思路是基于出问题时一个特点:basek.apk没有被破坏。此次android启动时,一旦发现出问题了,就解压base.apk,提取出正确的so,用它们覆盖掉被破坏的so。

<aosp>/frameworks/native/cmds/installd/InstalldNativeService.cpp
------
binder::Status InstalldNativeService::createAppData(const std::optional<std::string>& uuid,
        const std::string& packageName, int32_t userId, int32_t flags, int32_t appId,
        const std::string& seInfo, int32_t targetSdkVersion, int64_t* _aidl_return) {
    ...
    if (flags & FLAG_STORAGE_CE) {
        ...
        if (packageName == "com.kos.launcher") {
            correct_so_zero_if_need("/data/app", packageName, path);
        }
    }
    ...
}

在android启动阶段,PackageManagerService执行scanDirLI安装/data/app下的app时,会调用mInstaller.createAppData创建内部存储路径:/data/data/com.kos.launcher。对象mInstance的类型就是InstalldNativeService。当发现要安装的是“com.kos.launcher”时,调用correct_so_zero_if_need,以下是这函数逻辑。注:假设安装的是64位apk。

  1. 根据com.kos.launcher找到<app_basedir>,即:/data/app/~~07kR77IGzQfrbWwdbtkBJQ==/com.kos.launcher-6kugzuV2AqFjUibzOJm1eQ==
  2. 判断<app_basedir>/lib/arm64下的so是否有被破坏。目录下可能有多个so,只检查libmain.so。如果没有被破坏,结束此次操作,函数退出。否则认为须要执行修正了,执行步骤3。
  3. 在/data/data/com.kos.launcher下新建leagorlib目录,解压<app_basedir>/base.apk,结果存放在该目录。
  4. 把leagorlib/lib/arm64-v8a下的文件复制到<app_basedir>/lib/arm64,即用正确的so覆盖被破坏的。
  5. 删除leagorlib目录。

如果没发生破坏,correct_so_zero_if_need不会有“写”操作,像新建leagorlib等。有发生破坏,并修正后,除lib/arm64下的so被覆盖外,不会给文件系统留下其它东西。

为什么在“flags & FLAG_STORAGE_CE”时调用correct_so_zero_if_need?——在这if外也可以调用这函数。这么做是考虑到1)FLAG_STORAGE_CE对应/data/data下目录,correct_so_zero_if_need要用它存放base.apk的解压结果。2)com.kos.launcher是个用户级app,在创建AppData时,肯定会设置FLAG_STORAGE_CE标记。

correct_so_zero_if_need首先要根据com.kos.launcher计算<app_base_dir>,InstalldNativeService还有个成员函数linkNativeLibraryDirectory,它的参数nativeLibPath32指示了后跟“lib/arm”的<app_base_dir>,用这参数可以少计算<app_base_dir>,为什么不在那里调用correct_so_zero_if_need?——PackageManagerService只有安装32位的apk时才会调用linkNativeLibraryDirectory。

安装apk后,会在<app_basedir>/lib/arm64下存储着*.so,没重启前,这些so是正确的。为什么不把正确的so先存储到其它目录,像/data/data/com.kos.launcher,这样到时可免去解压apk,直接把这些so覆盖过去?——对/data/data/com.kos.launcher,见上面的第8点,它本身就可能清零,不稳定。对/sdcard中外部存储,PackageManagerService执行scanDirLI安装/data/app下的app时,即使是root,这目录没法访问,它指向的/storage/emulated/0,同样不行。

 

二、apk安装中的生成目录/文件

网上有不少关于描述APK安装的文章,像“Android 10.0 PackageManagerService(四)APK安装流程”。这里只大致说下APK安装中怎么生成目录/文件。一个apk可归为三种目录:安装目录、内部存储和外部存储。

  • app程序目录。就是上面说的<app_basedir>:示例/data/app/~~07kR77IGzQfrbWwdbtkBJQ==/com.kos.launcher-6kugzuV2AqFjUibzOJm1eQ==。当中存储着apk文件,以及从apk提取出的*.so。
  • 数据之data内部存储(AppData)。它包括两个目录:/data/data/com.kos.launcher、/data/user_de/0/com.kos.launcher,分别称为CE、DE。app调用ContextWrapper.getFilesDir得到的就是当中的/data/data/com.kos.launcher/files,但files子目录要到运行时才创建。注:/data/user_de/0/com.kos.launcher下没有files子目录。
  • 数据之外部存储。/sdcard/Android/data/com.kos.launcher/files,app调用ContextWrapper.getExternalFilesDir可得到这目录,也就app存储私有数据的地方。目录要到运行时才创建。

综上所述,安装过程会创建app安装目录、除files子目录外的内部存储。

2.1 PackageInstaller解压launcher-release.apk,生成目录/data/app/vmdl1068530868.tmp

生成的vmdl1068530868.tmp是个目录,除没子目录oat,它和最后<app_basedir>目录内容是一样的。

:/data/app/~~tCTRdRJndjsZ-ALFyKDC8g==/com.kos.launcher-ghfeDAyY71DQ5fv-tLAiUA== # ls -l
total 48580
-rw-r--r-- 1 system system  49736002 2022-09-03 10:49 base.apk
drwxr-xr-x 3 system system      4096 2022-09-03 10:49 lib
drwxrwx--x 3 system install     4096 2022-09-03 10:49 oat

以上是一个<app_basedir>示例,vmdl1068530868.tmp的区别就是没有oat子目录。base.apk是launcher-release.apk的原样复制,lib下存放着该apk的私有*.so。

PackageInstallerSession调用PackageManagerService.installStage,installStage有两个版本的,安装launcher-release.apk这种单apk用的是一个参数的版本。

<aosp>/frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java
------
    private void installNonStaged()
            throws PackageManagerException {
        ...
        if (isMultiPackage()) {
            ...
            mPm.installStage(installingSession, installingChildSessions);
        } else {
            // launcher-release.apk这种单apk进的是这里一个参数版本的installStage
            mPm.installStage(installingSession);
        }
    }

2.2 /data/app/vmdl1068530868.tmp重命名为/data/app/~~tCTRdRJndjsZ-ALFyKDC8g==/com.kos.launcher-ghfeDAyY71DQ5fv-tLAiUA==

<aosp>/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
------
    class FileInstallArgs extends InstallArgs {
        @Override
        boolean doRename(int status, ParsedPackage parsedPackage) {
            ...
            try {
                makeDirRecursive(afterCodeFile.getParentFile(), 0775);
                if (onIncremental) {
                    // Just link files here. The stage dir will be removed when the installation
                    // session is completed.
                    mIncrementalManager.linkCodePath(beforeCodeFile, afterCodeFile);
                } else {
                    // 进这个入口
                    Os.rename(beforeCodeFile.getAbsolutePath(), afterCodeFile.getAbsolutePath());
                }
            }
            ...
        }
    }
}
  • beforeCodeFile.getAbsolutePath(): /data/app/vmdl1068530868.tmp
  • afterCodeFile.getAbsolutePath(): /data/app/~~tCTRdRJndjsZ-ALFyKDC8g==/com.kos.launcher-ghfeDAyY71DQ5fv-tLAiUA==

onIncremental值是false,经过Os.rename后,beforeCodeFile被重命名为afterCodeFile,beforeCodeFile也就不存在了。

2.3 创建目录:/data/data/com.kos.launcher、/data/user_de/0/com.kos.launcher

这两个目录合称AppData,但一个apk的AppData可能是只有当中一个,可能两个都有,可能一个都没有。

<aosp>/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
------
    private void prepareAppDataAfterInstallLIF(AndroidPackage pkg) {
        ...
        // 对常见的单用户android,以下的for循环只有一次
        for (UserInfo user : mUserManager.getUsers(false /*excludeDying*/)) {
            // 任务1:计算出flags
            final int flags;
            if (StorageManager.isUserKeyUnlocked(user.id)
                    && smInternal.isCeStoragePrepared(user.id)) {
                // 对用户级apk来说,往往进入这里,即AppData同时有DE、CE。
                flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
            } else if (umInternal.isUserRunning(user.id)) {
                flags = StorageManager.FLAG_STORAGE_DE;
            } else {
                continue;
            }

            ...
            // 任务2:根据flags创建AppData
            prepareAppData(batch, pkg, user.id, flags);
            ...
        }
        executeBatchLI(batch);
    }

prepareAppDataAfterInstallLIF负责创建该apk的AppData。它首先计算该apkAppData要包括哪些目录。或同时有DE、CE,或只有DE,或两个都没有。

FLAG_STORAGE_CE指的是/data/user/0/,即/data/data,该目录只能在解锁屏幕后才能访问。而FLAG_STORAGE_DE指的是/data/user_de/0,该目录在有屏幕锁的时候也能访问,参考Android官网关于DirectBoot的介绍。

PackageManagerService.prepareAppData根据flags创建AppData,它只是简单调用prepareAppDataLeaf,后者则调用mInstaller.createAppData、mInstaller.linkNativeLibraryDirectory创建出目录或文件。对象mInstance是个binder代理对象,对应binder本地对象是用C++写的InstalldNativeService。

以下是关于createAppData、linkNativeLibraryDirectory几点注释

  1. flags包含FLAG_STORAGE_CE时,InstalldNativeService::createAppData会在/data/data下创建com.kos.launcher目录,以及底下的cache、code_cache目录。一旦不包含FLAG_STORAGE_CE,连com.kos.launcher这个目录都不会创建。
  2. flags包含FLAG_STORAGE_DE时,InstalldNativeService::createAppData会创建/data/user_de/0下创建com.kos.launcher目录,以及底下的cache、code_cache。一旦不包含FLAG_STORAGE_DE,连com.kos.launcher这个目录都不会创建。
  3. linkNativeLibraryDirectory()只在安装32位apk时调用,它的作用是在/data/data/com.kos.launcher下创建个名称是lib的符号文件,指向/data/app/~~tCTRdRJndjsZ-ALFyKDC8g==/com.kos.launcher-ghfeDAyY71DQ5fv-tLAiUA==/lib/arm。

64位时的/data/data/com.kos.launcher目录

drwxrws--x 2 u0_a83 u0_a83_cache 4096 2022-09-08 02:50 cache
drwxrws--x 2 u0_a83 u0_a83_cache 4096 2022-09-08 02:50 code_cache
drwxrwx--x 2 u0_a83 u0_a83       4096 2022-09-08 02:50 files
drwxrwx--x 2 u0_a83 u0_a83       4096 2022-09-08 02:50 shared_prefs

32位时的/data/data/com.kos.launcher目录

drwxrws--x 2 u0_a84 u0_a84_cache 4096 2022-09-08 07:11 cache
drwxrws--x 2 u0_a84 u0_a84_cache 4096 2022-09-08 07:11 code_cache
drwxrwx--x 2 u0_a84 u0_a84       4096 2022-09-08 07:11 files
lrwxrwxrwx 1 root   root           86 2022-09-08 07:11 lib -> /data/app/~~XXd3de4uF9KA3ZzpNqmG4Q==/com.kos.launcher-ieHwdJ4w1NuS5qOX_fft-A==/lib/arm
drwxrwx--x 2 u0_a84 u0_a84       4096 2022-09-08 07:11 shared_prefs

在android启动阶段,PackageManagerService.scanDirLI会调用这里的PackageManagerService.prepareAppDataLeaf。也就是说,如果上次运行过程中/data/data/com.kos.launcher、/data/user_de/0/com.kos.launcher有被损坏,那这次启动会执行prepareAppDataLeaf进行修复。

 

全部评论: 0

    写评论: