Android 12 “致命”崩潰解決之路

阿里巴巴移動技術發表於2022-02-17

作者:林作健

UC核心在Android 12上發現一個致命的崩潰。約有10%的使用者在冷啟動的時候會遇到這個問題,嚴重影響了UC核心的釋出。它的呼叫棧是這樣的:

10-12 19:03:21.461  1038  2723 I id.AlipayGphon: Rejecting re-init on previously-failed class java.lang.Class<com.uc.webkit.impl.WebViewChromiumFactoryProvider>: java.lang.VerifyError: Verifier rejected class com.uc.webkit.impl.WebViewChromiumFactoryProvider: com.uc.webkit.an com.uc.webkit.impl.WebViewChromiumFactoryProvider.g() failed to verify: com.uc.webkit.an com.uc.webkit.impl.WebViewChromiumFactoryProvider.g(): [0x15]  can't resolve returned type 'Unresolved Reference: com.uc.webkit.an' or 'Unresolved Reference: com.uc.webkit.impl.ak' (declaration of 'com.uc.webkit.impl.WebViewChromiumFactoryProvider' appears in /data/user/0/com.eg.android.AlipayGphone/app_h5container/uc/3.22.2.28.21092218119_64/so/core.jar)
10-12 19:03:21.461  1038  2723 I id.AlipayGphon: (Throwable with empty stack trace)
10-12 19:03:21.464  1038  2723 E WebViewEntry: init error and prepare native crash
10-12 19:03:21.464  1038  2723 E WebViewEntry: java.lang.NoClassDefFoundError: com.uc.webkit.impl.WebViewChromiumFactoryProvider
10-12 19:03:21.464  1038  2723 E WebViewEntry:     at com.uc.webkit.impl.WebViewChromiumFactoryProvider.i(Unknown Source:0)
10-12 19:03:21.464  1038  2723 E WebViewEntry:     at com.uc.webkit.WebViewEntry.p(U4Source:193)
10-12 19:03:21.464  1038  2723 E WebViewEntry:     at com.uc.webkit.bg.run(Unknown Source:0)
10-12 19:03:21.464  1038  2723 E WebViewEntry:     at android.os.Handler.handleCallback(Handler.java:938)
10-12 19:03:21.464  1038  2723 E WebViewEntry:     at android.os.Handler.dispatchMessage(Handler.java:99)
10-12 19:03:21.464  1038  2723 E WebViewEntry:     at android.os.Looper.loopOnce(Looper.java:201)
10-12 19:03:21.464  1038  2723 E WebViewEntry:     at android.os.Looper.loop(Looper.java:288)
10-12 19:03:21.464  1038  2723 E WebViewEntry:     at android.os.HandlerThread.run(HandlerThread.java:67)
10-12 19:03:21.464  1038  2723 E WebViewEntry: Caused by: java.lang.VerifyError: Verifier rejected class com.uc.webkit.impl.WebViewChromiumFactoryProvider: com.uc.webkit.an com.uc.webkit.impl.WebViewChromiumFactoryProvider.g() failed to verify: com.uc.webkit.an com.uc.webkit.impl.WebViewChromiumFactoryProvider.g(): [0x15]  can't resolve returned type 'Unresolved Reference: com.uc.webkit.an' or 'Unresolved Reference: com.uc.webkit.impl.ak' (declaration of 'com.uc.webkit.impl.WebViewChromiumFactoryProvider' appears in /data/user/0/com.eg.android.AlipayGphone/app_h5container/uc/3.22.2.28.21092218119_64/so/core.jar)

不解決這個問題我們的核心可能無法在Android 12上啟用了,對於核心來說又是一個生死攸關的問題。這個問題正常操作無法重現,只能通過monkey瘋狂冷啟動才能偶現。

另外一個背景是UC瀏覽器把sdk level提高到了30才引發這個問題。

呼叫棧分析

從呼叫棧的資訊我們看到最頂層的Error是NoClassDefFoundError,但他是由下面的VerifyError引起的。這個呼叫棧顯示正在進行正常的啟動過程。

Rejecting re-init on previously-failed class 顯示com.uc.webkit.impl.WebViewChromiumFactoryProvider應該已經嘗試過Verify,但是Error了。按照常理應該還有一個VerifyError的丟擲。但找了多個崩潰日誌都沒有發現第一次VerifyError丟擲的位置。

另外,這個VerifyError的Caused by: java.lang.VerifyError位置應該後面還跟著它第一次Verify的呼叫棧,但它卻顯示(Throwable with empty stack trace)

黑科技分析:手段一

帶著上述的諸多疑問,我們發現目前的資料不足以我們進行分析,我們需要更多的和Verify有關的資訊才能處理問題。

Android的art虛擬機器是帶著verbose log的。它是按照模組分類的,平時不會開啟。需要啟動art的時候通過傳參讓它開啟。

我們嘗試了wrapper技術,即在lib目錄加上檔案wrapper.sh,系統就會用wrapper.sh啟動虛擬機器,而不是通過Zygote。很遺憾這個手段沒有作用,分析了AndroidRuntime.cpp裡面的原始碼後,我們發現wrapper傳入的虛擬機器參賽會被它過濾掉,完全無視。

我們只能使用正經途徑之外的方法了。

上圖是Verbose log的結構,我們看到有個全域性變數gLogVerbosity控制這它們的開關。我們能不能通過修改gLogVerbosity達到啟動verbose log的目的?

UC核心有著一系列強大的黑科技組合。適應這種需求的黑科技是symbol_resolver模組。這個技術能夠從/proc/self/maps檔案裡面分析指名的so對映的位置,並通過elf解析拿到所有的符號,然後我們就能夠從Key-Value對裡面找到想要的符號的位置。

用這個技術我們很快定位了libart.so裡面的gLogVerbosity位置,並且當作一個bool陣列把verifier和verifier_debug項置為true。於是我們有了新的log:

Verification failed on class org.chromium.ui.base.WindowAndroid in /data/user/0/com.eg.android.AlipayGphone/app_h5container/uc/3.22.2.31.10191532_64/so/core.jar because: Verifier rejected class org.chromium.ui.base.WindowAndroid: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken() failed to verify: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken(): [0x10]  can't resolve returned type 'Unresolved Reference: android.os.IBinder' or 'Reference: android.os.IBinder'
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x0] : Processing const/4 v1, #+0
0:[Undefined],1:[Undefined],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x1] : Processing iget-object v0, v2, Ljava/lang/ref/WeakReference; org.chromium.ui.base.WindowAndroid.e // field@7982
0:[Undefined],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x3] : Processing invoke-virtual {v0}, java.lang.Object java.lang.ref.WeakReference.get() // method@7347
0:[Reference: java.lang.ref.WeakReference],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x6] : Processing move-result-object v0
0:[Reference: java.lang.ref.WeakReference],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x7] : Processing check-cast v0, android.content.Context // type@TypeIndex[61]
0:[Reference: java.lang.Object],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x9] : Processing invoke-static {v0}, android.app.Activity org.chromium.ui.base.WindowAndroid.a(android.content.Context) // method@17017
0:[Reference: android.content.Context],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0xc] : Processing move-result-object v0
0:[Reference: android.content.Context],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0xd] : Processing if-nez v0, +4
0:[Reference: android.app.Activity],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0xf] : Processing move-object v0, v1
0:[Reference: android.app.Activity],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x10] : Processing return-object v0
0:[Zero/null],1:[Conflict],2:[Conflict],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x11] : Processing invoke-virtual {v0}, android.view.Window android.app.Activity.getWindow() // method@26
0:[Reference: android.app.Activity],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x14] : Processing move-result-object v0
0:[Reference: android.app.Activity],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x15] : Processing if-nez v0, +4
0:[Reference: android.view.Window],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x17] : Processing move-object v0, v1
0:[Reference: android.view.Window],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x18] : Processing goto -8
0:[Zero/null],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x19] : Processing invoke-virtual {v0}, android.view.View android.view.Window.peekDecorView() // method@1459
0:[Reference: android.view.Window],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x1c] : Processing move-result-object v0
0:[Reference: android.view.Window],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x1d] : Processing if-nez v0, +4
0:[Reference: android.view.View],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x1f] : Processing move-object v0, v1
0:[Reference: android.view.View],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x20] : Processing goto -16
0:[Zero/null],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x21] : Processing invoke-virtual {v0}, android.os.IBinder android.view.View.getWindowToken() // method@1318
0:[Reference: android.view.View],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x24] : Processing move-result-object v0
0:[Reference: android.view.View],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x25] : Processing goto -21
0:[Reference: android.os.IBinder],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x25] : Merging at [0x25] to [0x10]: 
0:[Zero/null],1:[Conflict],2:[Conflict],  MERGE
0:[Reference: android.os.IBinder],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],  ==
0:[Reference: android.os.IBinder],1:[Conflict],2:[Conflict],
VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x10] : Processing return-object v0
0:[Reference: android.os.IBinder],1:[Conflict],2:[Conflict],
Rejecting opcode return-object v0
Register Types:
  0: Undefined
  1: Conflict
  2: null
  3: Boolean
  4: Byte
  5: Short
  6: Char
  7: Integer
  8: Long (Low Half)
  9: Long (High Half)
  10: Float
  11: Double (Low Half)
  12: Double (High Half)
  13: Precise Constant: -1
  14: Zero/null
  15: Precise Constant: 1
  16: Precise Constant: 2
  17: Precise Constant: 3
  18: Precise Constant: 4
  19: Reference: org.chromium.ui.base.WindowAndroid
  20: Reference: java.lang.Object
  21: Reference: java.lang.ref.WeakReference
  22: Reference: java.lang.ref.Reference
  23: Reference: android.content.Context
  24: Reference: android.app.Activity
  25: Unresolved Reference: android.os.IBinder
  26: Reference: android.view.Window
  27: Reference: android.view.View
  28: Reference: android.os.IBinder
Dumping instructions and register lines:
  0:[Undefined],1:[Undefined],2:[Reference: org.chromium.ui.base.WindowAndroid],
  0x0000: V-O-B-- const/4 v1, #+0
  0x0001: V-O---- iget-object v0, v2, Ljava/lang/ref/WeakReference; org.chromium.ui.base.WindowAndroid.e // field@7982
  0x0003: V-O---- invoke-virtual {v0}, java.lang.Object java.lang.ref.WeakReference.get() // method@7347
  0x0006: V-O---- move-result-object v0
  0x0007: V-O--G- check-cast v0, android.content.Context // type@TypeIndex[61]
  0x0009: V-O---- invoke-static {v0}, android.app.Activity org.chromium.ui.base.WindowAndroid.a(android.content.Context) // method@17017
  0x000c: V-O---- move-result-object v0
  0x000d: V-O---- if-nez v0, +4
  0x000f: V-O---- move-object v0, v1
  0:[Reference: android.os.IBinder],1:[Conflict],2:[Conflict],
  0x0010: VCO-B-R return-object v0
  0:[Reference: android.app.Activity],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
  0x0011: V-O-B-- invoke-virtual {v0}, android.view.Window android.app.Activity.getWindow() // method@26
  0x0014: V-O---- move-result-object v0
  0x0015: V-O---- if-nez v0, +4
  0x0017: V-O---- move-object v0, v1
  0x0018: V-O---- goto -8
  0:[Reference: android.view.Window],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
  0x0019: V-O-B-- invoke-virtual {v0}, android.view.View android.view.Window.peekDecorView() // method@1459
  0x001c: V-O---- move-result-object v0
  0x001d: V-O---- if-nez v0, +4
  0x001f: V-O---- move-object v0, v1
  0x0020: V-O---- goto -16
  0:[Reference: android.view.View],1:[Zero/null],2:[Reference: org.chromium.ui.base.WindowAndroid],
  0x0021: V-O-B-- invoke-virtual {v0}, android.os.IBinder android.view.View.getWindowToken() // method@1318
  0x0024: V-O---- move-result-object v0
  0x0025: V-O---- goto -21
Setting org.chromium.ui.base.WindowAndroid to erroneous.

這個log最值得關注的有兩點:

1、[0x10] can't resolve returned type 'Unresolved Reference: android.os.IBinder' or 'Reference: android.os.IBinder' VFY: android.os.IBinder org.chromium.ui.base.WindowAndroid.getWindowToken()[0x0] : Processing const/4 v1, #+0

[]()

根據打log的程式碼,我們看到return_type對應著'Unresolved Reference: android.os.IBinder'

但return_type的來源是:

GetMethodReturnType:

會呼叫FromDescriptor

會呼叫ResolveClassResolveClass會呼叫ClassLinker::FindClassFindClass有個顯而易見的失敗前提是:

也就是在當前執行緒是RuntimeThread的時候,會拒絕FindClass。因為這可能會導致class進入初始化過程,導致它呼叫class裡面static block中的class初始化函式。在RuntimeThread缺少允許java 函式的環境,不能允許它這麼做。

難道由於當前執行緒是Runtime Thread嗎?是的話這個Thread是哪個Runtime Thread?難道是gc thread嗎?

2、對這個日誌前後的Verify動作進行分析。發現正常能Verify過的執行緒,都有load class的日誌。但出問題的這條執行緒一條load class的日誌都沒有,後面它還因為同樣的原因Verify失敗了好幾個class。這更加肯定失敗的執行緒是一個Runtime Thread。另外前面提到的VerifyError沒有呼叫棧記錄的現象也在側面印證這是個Runtime Thread。因為Runtime Thread沒有Java環境,不能呼叫Java函式,所以沒有記錄。但我們還是需要找到這個執行緒是什麼。為此我們動用了第二個黑科技。

黑科技分析:手段二

通過觀察程式碼,我們發現VerifyError都是通過同一個函式丟擲的:

[]()

我們也能找到它的全域性符號,所以我們只需要在這個符號的位置加上執行馬上崩潰的程式碼,然後讓monkey觸發這個問題就能處理它了。

這裡有個問題:android為了安全的原因禁止我們把程式碼段的許可權改為可寫。

如何安全的把程式碼段改了呢?我們使用了/prof/self/mem技術:開啟/proc/self/mem檔案,然後用pwrite api往符號的位置寫入必崩程式碼。

這樣我們就發現了Verify失敗的那個執行緒:

[]()

根本原因分析

我們拿到了執行緒名Verification th。也拿到了執行緒啟動的呼叫棧。他是從ThreadPool啟動的,ThreadPool中的Thread都是RuntimeThread,坐實了之前的猜測。執行緒執行的任務是BackgroundVerificationTask。可以迅速找到它啟動的位置:

[]()

再找一下是這個提交出的問題:

commit 0d5f6402ff925ac1385ccb349f8a2798a4816458 Author: Nicolas Geoffray ngeoffray@google.com Date: Tue Apr 13 13:05:36 2021 +0100

Only run background verification when dexPathList is set.

Otherwise, the runtime will not be able to find the classes.

Test: 692-vdex-secondary-loader
Bug: 185088679
Change-Id: Idd39eabe00faa017aa5254f7188e7adbcaa23c74

diff --git a/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java b/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
index 710a88cc6d0..afbc9ec9de7 100644
--- a/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
+++ b/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
@@ -128,6 +128,9 @@ public class BaseDexClassLoader extends ClassLoader {
                 : Arrays.copyOf(sharedLibraryLoaders, sharedLibraryLoaders.length);
         this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
 
+        // Run background verification after having set 'pathList'.
+        this.pathList.maybeRunBackgroundVerification(this);
+
         reportClassLoaderChain();
     }
 
@@ -186,6 +189,8 @@ public class BaseDexClassLoader extends ClassLoader {
         this.sharedLibraryLoaders = null;
         this.pathList = new DexPathList(this, librarySearchPath);
         this.pathList.initByteBufferDexPath(dexFiles);
+        // Run background verification after having set 'pathList'.
+        this.pathList.maybeRunBackgroundVerification(this);
     }
 
     @Override

git tag --contain命令找了下,發現確實是android 12 beta版開始帶上的。

解決方案

除了向谷歌報告問題,抱怨一通之外我們還是要找到解決方案。谷歌說他們下一版android 12的12月更新就會解決這個問題,但很多老機器根本不更新,所以他們是指望不上的了。

我們必須從OatFileManager::RunBackgroundVerification函式裡面找到逼迫它不要啟動後臺驗證執行緒的方法。我們的目光很快落在了:

上面。因為我們還是能控制檔名的。前面的邏輯也有判斷sdk level,只要sdk level<=29也不會啟動這個執行緒,但UC瀏覽器已經把sdk level開啟到30了(這也印證了背景提到UC瀏覽器把sdk level提高到30才出現)。

觀察了函式DexLocationToOdexFilename,發現一行很有幫助:

// Get the base part of the file without the extension.
  std::string file = location.substr(pos+1);
  pos = file.rfind('.');
  if (pos == std::string::npos) {
    *error_msg = "Dex location " + location + " has no extension.";
    return false;
  }

只要我們讓它找不到suffix separator "."就能迫使它退出了。

結果

對android 12使用了軟連結core.jar為corejar的方法後, 這個問題就消失了。威脅UC核心的怪獸被打敗了,世界又恢復往日的和平。

關注【阿里巴巴移動技術】微信公眾號,每週 3 篇移動技術實踐&乾貨給你思考!

相關文章