【Unity】Androidの特定機種でWebViewをnewすると落ちる


Unityで作ったアプリから、AndroidのWebViewをnewするだけのコードが特定機種で落ちる不具合を発見してから、原因を特定するための調査をした際の備忘録です。

## 問題
### 問題の再現方法

Unity 4.6.6で、以下のコードをGalaxy系の4.3（S4, Jなど）で走らせる。

```cs
AndroidJavaObject activity = null;
using (var cls = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
	activity = cls.GetStatic<AndroidJavaObject>("currentActivity");
}
ativity.Call("runOnUiThread", new AndroidJavaRunnable(() => {
	using (var webView = new AndroidJavaObject("android.webkit.WebView", activity)) {}
	activity.Dispose();
}));
```

そうすると、以下のようなダンプをLogcatに吐いて落ちる。

```logcat
I/DEBUG   (  202): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG   (  202): Build fingerprint: 'samsung/SC-04E/SC-04E:4.3/JSS15J/SC04EOMUBNB1:user/release-keys'
I/DEBUG   (  202): Revision: '15'
I/DEBUG   (  202): pid: 8741, tid: 8895, name: WebViewCoreThre  >>> com.hoge <<<
I/DEBUG   (  202): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr bbadbeef
I/DEBUG   (  202):     r0 0000000b  r1 00000000  r2 00000001  r3 bbadbeef
I/DEBUG   (  202):     r4 00000000  r5 741aa3e4  r6 78a7c468  r7 78a7c398
I/DEBUG   (  202):     r8 78bf6558  r9 20100001  sl 78bc9d18  fp 1da00005
I/DEBUG   (  202):     ip 00000001  sp 7c41fb60  lr 73c9847d  pc 73c9847e  cpsr 60000030
I/DEBUG   (  202):     d0  0000000000000000  d1  0000000000000000
I/DEBUG   (  202):     d2  0000000000000000  d3  0000000000000000
I/DEBUG   (  202):     d4  000000003f800000  d5  0000000000000000
I/DEBUG   (  202):     d6  bf80000000000000  d7  000000003f800000
I/DEBUG   (  202):     d8  0000000000000000  d9  0000000000000000
I/DEBUG   (  202):     d10 0000000000000000  d11 0000000000000000
I/DEBUG   (  202):     d12 0000000000000000  d13 0000000000000000
I/DEBUG   (  202):     d14 0000000000000000  d15 0000000000000000
I/DEBUG   (  202):     d16 3ff0000000000000  d17 322f796470730631
I/DEBUG   (  202):     d18 0064007200610068  d19 0065007200610077
I/DEBUG   (  202):     d20 007300690064002e  d21 00790061006c0070
I/DEBUG   (  202):     d22 006900440049002e  d23 0061006c00700073
I/DEBUG   (  202):     d24 3fc74721cad6b0ed  d25 3fc2f112df3e5244
I/DEBUG   (  202):     d26 ffffffffffffffff  d27 ffffffffffffffff
I/DEBUG   (  202):     d28 0000000000000000  d29 0000000000000000
I/DEBUG   (  202):     d30 0000000000000000  d31 0000000000000000
I/DEBUG   (  202):     scr 60000010
I/DEBUG   (  202):
I/DEBUG   (  202): backtrace:
I/DEBUG   (  202):     #00  pc 000ed47e  /system/lib/libwebcore.so
I/DEBUG   (  202):     #01  pc 000ed4cb  /system/lib/libwebcore.so
I/DEBUG   (  202):     #02  pc 000ede7b  /system/lib/libwebcore.so
I/DEBUG   (  202):     #03  pc 000ef447  /system/lib/libwebcore.so
I/DEBUG   (  202):     #04  pc 000ef479  /system/lib/libwebcore.so
I/DEBUG   (  202):     #05  pc 001670a9  /system/lib/libwebcore.so
I/DEBUG   (  202):     #06  pc 0016352f  /system/lib/libwebcore.so
I/DEBUG   (  202):     #07  pc 0028298b  /system/lib/libwebcore.so
I/DEBUG   (  202):     #08  pc 00020c0c  /system/lib/libdvm.so (dvmPlatformInvoke+112)
I/DEBUG   (  202):     #09  pc 00051773  /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+398)
I/DEBUG   (  202):     #10  pc 0002a0a0  /system/lib/libdvm.so
I/DEBUG   (  202):     #11  pc 0002ea6c  /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+184)
I/DEBUG   (  202):     #12  pc 00063905  /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+292)
I/DEBUG   (  202):     #13  pc 0006392f  /system/lib/libdvm.so (dvmCallMethod(Thread*, Method const*, Object*, JValue*, ...)+20)
I/DEBUG   (  202):     #14  pc 00058683  /system/lib/libdvm.so
I/DEBUG   (  202):     #15  pc 0000cfa0  /system/lib/libc.so (__thread_entry+72)
I/DEBUG   (  202):     #16  pc 0000d11c  /system/lib/libc.so (pthread_create+208)
I/DEBUG   (  202):
I/DEBUG   (  202): stack:
I/DEBUG   (  202):          7c41fb20  00000017
I/DEBUG   (  202):          7c41fb24  00000040
I/DEBUG   (  202):          7c41fb28  798630e0
I/DEBUG   (  202):          7c41fb2c  00000000
I/DEBUG   (  202):          7c41fb30  741aa3e4
I/DEBUG   (  202):          7c41fb34  7c41fc14  [stack:8895]
I/DEBUG   (  202):          7c41fb38  78a7c468
I/DEBUG   (  202):          7c41fb3c  73c98389  /system/lib/libwebcore.so
I/DEBUG   (  202):          7c41fb40  78a7c468
I/DEBUG   (  202):          7c41fb44  73c98389  /system/lib/libwebcore.so
I/DEBUG   (  202):          7c41fb48  0000000b
I/DEBUG   (  202):          7c41fb4c  400a153c  /system/lib/libc.so (pthread_key_create+208)
I/DEBUG   (  202):          7c41fb50  00000000
I/DEBUG   (  202):          7c41fb54  741aa3e4
I/DEBUG   (  202):          7c41fb58  df0027ad
I/DEBUG   (  202):          7c41fb5c  00000000
I/DEBUG   (  202):     #00  7c41fb60  7c41fba4  [stack:8895]
I/DEBUG   (  202):          7c41fb64  00000000
I/DEBUG   (  202):          7c41fb68  7c41fc14  [stack:8895]
I/DEBUG   (  202):          7c41fb6c  73c984cf  /system/lib/libwebcore.so
I/DEBUG   (  202):     #01  7c41fb70  7c41fba4  [stack:8895]
I/DEBUG   (  202):          7c41fb74  73c98e7f  /system/lib/libwebcore.so
I/DEBUG   (  202):     #02  7c41fb78  78a7c398
I/DEBUG   (  202):          7c41fb7c  74096410  /system/lib/libwebcore.so
I/DEBUG   (  202):          7c41fb80  7430e0de  /system/lib/libchromium_net.so
I/DEBUG   (  202):          7c41fb84  32588cc7
I/DEBUG   (  202):          7c41fb88  78b40e9c
I/DEBUG   (  202):          7c41fb8c  00000000
I/DEBUG   (  202):          7c41fb90  73f206e5  /system/lib/libwebcore.so
I/DEBUG   (  202):          7c41fb94  78a7dca0
I/DEBUG   (  202):          7c41fb98  741a835c  /system/lib/libwebcore.so
I/DEBUG   (  202):          7c41fb9c  73c9a44b  /system/lib/libwebcore.so
```

### 原因の推測

まず、logcatから落ちている箇所がlibwebcore.soの中で発生しているということが分かるので、WebViewをnewしている辺りから派生していると考えられる。
そこでWebViewの処理を調べてみると、どうやらnewした時にスレッドを数本立ち上げて初期化しているようです。

とりあえずJavaではなく、ネイティブコードの部分で落ちていそうなので、機種からlibwebcore.soを引っ張りだしてきて、objdumpで逆アセンブルしてみました。

```
   ed458:       4b18            ldr     r3, [pc, #96]   ; (ed4bc <JNI_OnLoad+0xef8>)
   ed45a:       4a19            ldr     r2, [pc, #100]  ; (ed4c0 <JNI_OnLoad+0xefc>)
   ed45c:       447b            add     r3, pc
   ed45e:       b570            push    {r4, r5, r6, lr}
   ed460:       589d            ldr     r5, [r3, r2]
   ed462:       682c            ldr     r4, [r5, #0]
   ed464:       b974            cbnz    r4, ed484 <JNI_OnLoad+0xec0>
   ed466:       2004            movs    r0, #4
   ed468:       f7fb ebc2       blx     e8bf0 <operator new(unsigned int)@plt>
   ed46c:       4915            ldr     r1, [pc, #84]   ; (ed4c4 <JNI_OnLoad+0xf00>)
   ed46e:       4479            add     r1, pc
   ed470:       4606            mov     r6, r0
   ed472:       f7fb ec84       blx     e8d7c <pthread_key_create@plt>
   ed476:       b120            cbz     r0, ed482 <JNI_OnLoad+0xebe>
   ed478:       f20c ff00       bl      2fa27c <ucnv_open_emoji+0x7bfac>
   ed47c:       4b0e            ldr     r3, [pc, #56]   ; (ed4b8 <JNI_OnLoad+0xef4>)
   ed47e:       601c            str     r4, [r3, #0]
   ed480:       47a0            blx     r4
   ed482:       602e            str     r6, [r5, #0]
   ed484:       682d            ldr     r5, [r5, #0]
   ed486:       6828            ldr     r0, [r5, #0]
   ed488:       f7fb ec7e       blx     e8d88 <pthread_getspecific@plt>
   ed48c:       b108            cbz     r0, ed492 <JNI_OnLoad+0xece>
   ed48e:       6804            ldr     r4, [r0, #0]
   ed490:       b984            cbnz    r4, ed4b4 <JNI_OnLoad+0xef0>
   ed492:       2008            movs    r0, #8
```

000ed47eの落ちている部分は、これはr3のポインタに0を代入しようとしている模様です。
r3のポインタは0xbbadbeefを指しているので、意図的にクラッシュさせているようです。
その少し上を見てみると、pthread_key_createしてその結果をr0に入れ、0比較しているコードがあります。
r0を見てみると0xbです。
pthread_key_createが返すエラーの中で、0xbはEAGAINであり、これはThread Local Storage（TLS; スレッド固有データ）のキーが使い果たされているときに出るエラーです。

### まとめ

以上より、以下の流れで落ちていると考えられます。

1. new WebView(context)
2. WebViewCoreThreadのスレッドが立ち上がる
3. スレッドの初期化が始まる
4. System.load("libwebcore.so")
5. libwebcore.soのJNI_OnLoadが呼ばれる
6. ptherad_key_createできずクラッシュ

## なぜ特定機種だけ落ちるのか？

ここで、なぜ特定機種だけで落ちるのかを調査しました。

### Samsung改造説

まず、共通しているのがSamsung製端末だったので、Samsungがlibwebcore.so独自改造をしている線を疑いました。
そこでSamsungのサイトから該当端末の公開されているコードを拾ってきて読んでみました。
すると確かにThreadIdentifierDataPthreads.cpp`の落ちていると思われる以下の箇所は、Samsungの独自改造部分でした。

```cpp
#if ENABLE(SAMSUNG_WEBKIT_PERFORMANCE_PATCH)
// SAMSUNG CHANGE : Webkit Performance Patch Merge + r92154
void ThreadIdentifierData::initializeOnce()
{
    if (pthread_key_create(&m_key, destruct))
        CRASH();
}
// SAMSUNG CHANGE : Webkit Performance Patch Merge -
#endif
```

ですが、そもそも本家のコードも違うタイミングでpthread_key_createしているので、どうやらSamsungの改造が直接の原因ではなさそうです。

### TLSを使いすぎてる説

そもそもpthread_key_createでEAGAINエラーが帰ってきているので、TLSを使いきっているのが根本的な原因です。
そこで以下の様なコードをUnityアプリ起動直後に走らせ、pthread_key_createの余力をいろいろな端末で調べてみました。

```cs
[System.Runtime.InteropServices.DllImport("c")]
private static extern int pthread_key_create(IntPtr key, IntPtr destructor);
public void Init()
{
	var key = System.Runtime.InteropServices.Marshal.AllocHGlobal(sizeof(int));
	int count = 0;
	while (true)
	{
		var ret = pthread_key_create(key, IntPtr.Zero);
		if (ret != 0) break;
		count++;
	}
	Debug.Log(string.Format("pthread_key_create capacity={0}", count));
	System.Runtime.InteropServices.Marshal.FreeHGlobal(key);
}
```

#### 結果

**【追記】Unity 4.6.6で、development build + script debuggingを有効にした結果です。
リリースビルドにするとTLSは少し回復します（下の追記参照）**


|端末名|OSバージョン|TLS余力|
|---|---|---|
|Xperia AX|4.0.4|16|
|Galaxy S3|4.0.4|17|
|Xperia Z|4.1.2|16|
|Xperia Z1|4.2.2|13|
|Galaxy S4|4.2.2|9|
|Galaxy S4|4.3|3|
|Galaxy J|4.3|3|
|Galaxy S4|4.4.2|75|
|Galaxy Z3|4.4.2|78|
|Nexus7 (2013)|5.1.2|89|

#### 考察

4.3で余力3とか、そりゃあ落ちますよ・・・
というか4.4から急に凄い増えている・・・
これは本家にキャパ増やす修正入ってますね。（*1）

結論としては、UnityやらMonoやらできっとある程度TLSを使っているしまっている中で、WebViewを立ち上げ限界を超えたTLSを使おうとしてクラッシュというのが事の顛末のようです。
誰が悪いわけでもないので、対策としてはWebViewを使わないようにするという方法しかなく、現実的にはGalaxyの4.3系でのクラッシュを黙認せざるを得ないと思います。（他の4.3系の端末では未確認です）

## 追記（2015/06/30）

Unityのビルドオプションでリリースビルドにしたところ、TLSが6回復しました。
またSamusungの4.3でWebView立ち上げても落ちないTLSの閾値を探ってみたところ、5以上なら大丈夫そうです。
なのでリリースビルドでビルドすれば動くようになるはずです。
また回復量と閾値がどの端末でも同じなら、リリースビルドにさえしておけば大体の端末で落ちなくなるはずですが、もちろん4.3以下の端末でTLSが厳しいことに変わりはないので、使っているUnityのバージョンやライブラリ次第では落ちるかもしれません。

## 参考

*1: [bionicにおけるPTHREAD_KEYS_MAXの修正コミット](https://github.com/android/platform_bionic/commit/c03e1e74390c1b8a30341399cd85cc869d0842b0) で、4.4からTLSのキャパが64から128になった模様。