在Android平台上加载本地库的危险性

November 15, 2015

在2012年KeepSafe的创业初期,我们试图找到一种为Android应用加密的方案,通过多次迭代与原型设计,我们最终找到了最佳方案——利用JNI(Java本地接口)。我们决定将接口写入Java加密库中,完全通过JNI来调用加密库,以实现加密与解密。我们选用了即时解决的方案,以期将对用户体验的影响减到最小,并决定在方案通过后就部署到生产应用上。我们严格测试代码,确认一切顺利,直到事情超出了控制。

遇到可怕的“UnsatisfiedLinkError”错误

在版本发布、大家焦虑地刷新错误报告之际,我们开始注意到,有一个错误反复重现。用户遇到“UnsatisfiedLinkError”错误的意思是,要么A) 我们调用的本地库不存在;要么B) 我们调用的本地方法不存在。鉴于B) 可能性在编译与基础测试时一般都能被发现,我们立即困惑于这一事实:用户并未安装我们打包在APK内的本地库。

下面有一些我们碰到过的异常样例,从标准类型的:

java.lang.UnsatisfiedLinkError: Couldn’t load stlport_shared from loader dalvik.system.PathClassLoader[dexPath=/data/app/com.kii.safe-1.apk,libraryPath=/data/app-lib/com.kii.safe-1]: findLibrary returned null
at java.lang.Runtime.loadLibrary(Runtime.java:365)
at java.lang.System.loadLibrary(System.java:535)
at com.kii.safe.Native.<clinit>(Native.java:16)
… 63 more
Caused by: java.lang.UnsatisfiedLinkError: Library stlport_shared not found
at java.lang.Runtime.loadLibrary(Runtime.java:461)
at java.lang.System.loadLibrary(System.java:557)
at com.kii.safe.Native.<clinit>(Native.java:16)
… 5 more
Caused by: java.lang.UnsatisfiedLinkError: Cannot load library: get_lib_extents[760]: 1305 — /mnt/asec/com.kii.safe-1/lib/libstlport_shared.so is not a valid ELF object
at java.lang.Runtime.loadLibrary(Runtime.java:434)
at java.lang.System.loadLibrary(System.java:554)
at com.kii.safe.Native.<clinit>(Native.java:15)
Caused by: java.lang.UnsatisfiedLinkError: Library cryptopp not found
at java.lang.Runtime.loadLibrary(Runtime.java:461)
at java.lang.System.loadLibrary(System.java:557)
at com.kii.safe.Native.<clinit>(Native.java:17)
到更为诡异的……

Caused by: java.lang.UnsatisfiedLinkError: Cannot load library: reloc_library[1286]: 1748 cannot locate ‘쯰ҷЦf1Ϙ˗˞ք᣼0Ⱉض夘Ϛ.͏闑㥁ج뭫ර⓻в^ӎ3c`+W#Ҽ?-Bַˌ֕꼠’…
at java.lang.Runtime.loadLibrary(Runtime.java:370)
at java.lang.System.loadLibrary(System.java:535)
at com.kii.safe.Native.<clinit>(Native.java:17)
Caused by: java.lang.UnsatisfiedLinkError: Cannot load library: reloc_library[1312]: 1327 cannot locate ‘Pܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭXߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭXߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#׶ʭX Ϛߐܝ#
at java.lang.Runtime.loadLibrary(Runtime.java:434)
at java.lang.System.loadLibrary(System.java:554)
at com.kii.safe.Native.<clinit>(Native.java:17)

出问题的库没有明确的模式可循,似乎所有库都可能会抛出异常,并不拘于某个版本的Android OS,也不拘于特定型号的设备。此外在特定情况下,一些本地库加载正常,但并非所有库都能正常加载。我们在互联网上就这个问题疯狂地搜索,寻找答案或帮助,却一无所获。我们开始发布专门的修补程序,大多都是测试性修复补丁,并附有跟踪数据,以便让我们更好地了解故障出现时的具体环境,补丁中还包括一块代码,专门负责检查本地库是否安装在正确的位置上。

此外在特定情况下,一些本地库加载正常,但并非所有库都能正常加载。

结果发现,果然是本地库缺失所导致的。这些错误并不是系统类或文件系统所导致的偶然性奇特错误,而且用户的设备似乎也是正常的。

想法1—用户设备空间用尽

在猜测异常可能原因的时候,我们最开始想到的就是:也许用户设备的空间不足而导致本地库未能正确安装。在快速检测后,我们很快发现这个想法是错误的,用户设备空间足够安装我们打包的库。

想法2—本地库未包含在更新中

第二个看似合理的猜测:在Google Play提供给用户的版本中,我们的APK被破坏掉了。在阅读了类似的一些报告后,我们更加确信这一点:据说Google Play向所有受到影响的应用开发者发送了通知,让他们通知用户在升级后先不要运行应用,因为本地库安装错误。唯一的问题在于,这个报告出现在8月份,而我们遇到问题的时候已经是好几个月以后了,而且我们也从未收到Google Play为这类错误负责的通知。此外,这一点很难证实。

想法3—与真实用户直接联调

由于在手头的十几台各种类型的设备上都无法重现这一问题,我们决定找一个遇到这个问题的用户来联调。一位友好用户决定要帮助我们,他表示应用在最近一次更新前都能正常使用。而这里有一个问题,用户表示能正常运行的应用版本正是包含了我们加密代码与本地库的那个版本,这让我们更加困惑。我们决定直接提供给这名用户一个验证过的APK包,其中包含了所有的本地库。在他安装了APK包之后,再次运行应用,又遇到了同样的UnsatisfiedLinkError错误。这证明Google Play不是问题的根源,Android的PackageManager安装过程才是问题所在,在安装期间会发生某种错误,导致APK中的本地库无法被提取。

问题在于Android的PackageManager安装过程

得出解决方案

鉴于我们发现了问题是安装过程所导致的,我们决定将该部分的安装过程复制下来,并将本地库提取到应用代码中。幸运地是,通过以下代码就能很容易的获得应用的APK文件参考:

Context.getApplicationInfo().sourceDir;

并用来将本地库提取到内部存储中。由于APK文件只不过是ZIP文件,写个ZIP提取代码只是小事一桩。我们很快实现了代码提取和打包,大幅降低了报错率。


日均UnsatisfiedLinkError错误的总量

尽管这是件好事,我们还是发现了一些用户时不时遇到异常抛出,在此我们寻求了Google的帮助。幸运地是,他们给了我们建议:用户可以通过除了Play Store以外的其他渠道来安装我们的应用,并告诉我们了一个技巧:通过

Context.getPackageManager().getInstallerPackageName(packageName);

就可以得知是哪个package安装了我们的应用。

为了减少APK大小,并确保应用尽可能在所有设备上可用,我们有支持x86、Armv7和Arm架构的应用包。每种只包含相应架构的本地库,所以很有可能某个用户安装的APK包并非是支持相应设备架构的那种。

我们开始在崩溃中记录安装包的名称,并且很快发现,问题产生的确是因为用户从各种途径安装应用,新增的UnsatisfiedLinkError错误都是由于用户手动安装应用时,选择了不适用他们设备架构的应用所致。这是最终的“问题揭秘”,我们都放下心来,这个解释简单明了。

引入ReLinker

我们决定将提取的代码打包成一个人人适用的小型库。鉴于我们所经历过的这个debug过程,不应当再有人重蹈覆辙,尤其是其中还涉及到了超出应用开发者控制能力之外的Android基础功能。

使用ReLinker十分简单,就像用
ReLinker.loadLibrary(context, “mylibrary”)

来代替标准的

System.loadLibrary(“mylibrary”);

Github库中 你能找到更多ReLinker的相关内容。

问题解决

在发布修复补丁的时候,有约10万的独立用户持续不断地遇到这种崩溃性错误。我们希望ReLinker有用,不再让大家遇到UnsatisfiedLinkerError错误。

感谢

感谢谷歌的支持团队为我们指出正确的方向,还有他们在这个问题上对我们持续不断地支持!

我们的代码主要是基于Chromium类似的权宜方案。在Chromium的源代码中有一条注释,解释了Android package manager中的具体问题:

/**
* Try to load a native library using a workaround of
* http://b/13216167.
*
* Workaround for b/13216167 was adapted from code in
* https://googleplex-android-review.git.corp.google.com/#/c/433061
*
* More details about http://b/13216167:
* PackageManager may fail to update shared library.
*
* Native library directory in an updated package is a symbolic link
* to a directory in /data/app-lib/<package name>, for example:
* /data/data/com.android.chrome/lib -> /data/app-lib/com.android.chrome[-1].
* When updating the application, the PackageManager create a new directory,
* e.g., /data/app-lib/com.android.chrome-2, and remove the old symlink and
* recreate one to the new directory. However, on some devices (e.g. Sony Xperia),
* the symlink was updated, but fails to extract new native libraries from
* the new apk.
+
* We make the following changes to alleviate the issue:
* 1) name the native library with apk version code, e.g.,
* libchrome.1750.136.so, 1750.136 is Chrome version number;
* 2) first try to load the library using System.loadLibrary,
* if that failed due to the library file was not found,
* search the named library in a /data/data/com.android.chrome/app_lib
* directory. Because of change 1), each version has a different native
* library name, so avoid mistakenly using the old native library.
*
* If named library is not in /data/data/com.android.chrome/app_lib directory,
* extract native libraries from apk and cache in the directory.
*
* This function doesn’t throw UnsatisfiedLinkError, the caller needs to
* check the return value.
*/

(编译/孙薇 责编/翟方庆)

原文地址:Medium