v8の誰も困らないバグに当たった話

ACCESS の三原と言います。今日は、Chrome ブラウザの JavaScript JIT コンパイラ v8 の、誰も困らないバグに当たった実話を語らせていただきます。

# 誰も困らないバグに当たるまでの経緯

エンジニアが JavaScript 言語に関わる立場は、大きく分けて3つあると思います。

- Web アプリを開発するために JavaScript プログラムを書く
- OSS JavaScript エンジンを開発するために主に C++ プログラムを書く
- プロプライエタリ JavaScript エンジンを開発するために C/C++ プログラムを書く

JavaScript 言語に関わる人のおそらく 99% 以上が、Web アプリを開発するために JavaScript プログラムを書いていると思います。

そして極めて少数の人が、OSS として公開される JavaScript エンジンを開発していらっしゃいます。日本でも Google オフィスは結構大きいですし、JavaScript エンジン開発者も皆無ではないと思います。

ところが、そんな中、プロプライエタリな JavaScript エンジンというのが未だにあるんです。というか弊社に残っています。

かつて FOMA 端末等に搭載された NetFront Browser v3.x は、携帯電話上では JavaScript プログラムが動作しません(細かい話はゴニョゴニョ……)が、オプションで JavaScript インタプリタをリンクさせることができました。その JavaScript インタプリタだけをインタプリタとしては高速なものに差し替えた NetFront Browser v4.x もあり、日本のガラケーには採用されませんでしたが海外のフィーチャーフォン向けにはありがたいぐらい売れました。そこから派生した paneE という UI ツールや NetFront Agent という言語処理系だけ抜き出したものも作りました。ですからメンテナンスは続いています。

この話に至る直接の契機は、今年(2017年)の春に、paneE を使用するお客様からコンテンツ動作確認を 64bit Linux PC で行いたいと要望をいただいたことでした。いくら組み込み端末が 32bit CPU を使い続けても、もう開発機は 64bit CPU。世の流れです。そこで Linux の LP64 環境で動くよう改造を行ったのですが、コンテンツ確認用なのですから、JavaScript インタプリタが正しく動作しなければいけません。当然、動作試験を行います。

リファレンスは PC/Mac の Chrome です。paneE を LP64 環境向けにビルドして、社内のテストコンテンツを走らせると、Chrome と挙動が違うところがボロボロと出ます。まずいです。そこで1つ1つ潰していきました。

そのとき、ふと思い立って、社内のテストコンテンツを Firefox でも走らせてみました。すると1つのテストケースだけ、paneE と同じ結果を返したのです。

そのテストケースは洗練されていませんでしたが、後に Google のエンジニアが書き直したテストコードは次の1行です。

```
(function() { return eval('with ({a: 1}) { function a() {} }; a') })()
```

このコードを実行すると、当時の Chrome 58.0.3029.96 (macOS) は ```undefined``` を返すのに、Firefox と paneE は ```function a()``` を返すのです。

弊社の JavaScript インタプリタがおかしいのはいいんです。ですが Firefox が弊社と同じミスをしているとは思えません。

そこで気がつきました。

「もしかして v8(Chrome の JavaScript JIT コンパイラ)がバグってる?」

これが v8 の誰も困らないバグに当たった瞬間でした。

# 何を間違えたのか

みなさまは、JavaScript の変数が、変数を保持するオブジェクトのプロパティということになっているのをご存知ですか?

JavaScript は、情報を保持するところはオブジェクトとそのプロパティしか存在しない、という原理原則で言語仕様が設計されています。

関数のローカル変数も、ローカル変数を保持するオブジェクトが存在して、そのプロパティとして値が保存されている、というルールになっています。

以下のコード

```
function hoge() {
    var a = 1;
    function b() { console.log(‘function b()’); }
}
```

には、関数実行時に作られる隠れたオブジェクトが存在します。

ES5 まではスコープの制限がほとんどなく関数の途中で宣言した変数が宣言前にも存在だけはする、という挙動も、関数のローカル変数を保持するオブジェクトが1つしかないのでプロパティは存在しているという理由によります。

そして、ローカル変数からグローバル変数までは、変数を探索する対象オブジェクトのリストが繋がっています。リストの末尾はブラウザなら window オブジェクトです。前からオブジェクトのプロパティを探索して、一致したところの値が変数の値として返る決まりになっています。

「オブジェクトのプロパティをローカル変数にする」with 文があります。他のプログラミング言語では分かりにくいですが、JavaScript では変数を探索する対象オブジェクトのリストの先頭にオブジェクトを1つ追加するだけ、なのです。

```
var a = 1;
var o = { a : 2 };
with (o) {
    console.log(a);

}
```

上記のコードでは 2 が出力されます。

しかし with 文には、内部で宣言された変数は、with 文で追加したオブジェクトではなく、元の変数スコープを管理するオブジェクトのプロパティになるという規定があります。

下記のコードでは、変数 ```b``` は with 文で追加したオブジェクトではなく元の変数スコープを管理するオブジェクトにプロパティになるので、```o.b``` と ```b```の値が異なります。

```
var a = 1;
var o = { a : 2 };
with (o) {
    var b = 3;
}
console.log(o.b); => ‘undefined’ が出力されます
console.log(b); => ‘3’ が出力されます
```

ここに、さらに厄介な eval 文を組み合わせます。

eval 文は、関数内で呼び出されると、呼び出し元の関数のスコープを管理するオブジェクトを引き継ぎます。ですから、eval 文内部で呼び出し元の関数にローカル変数を追加できます。

下記のコードでは

```
function hoge() {
    eval(‘var a = 1;’);
    console.log(a); => ‘1’ が出力されます
}
```

eval 文が呼び出し元の関数にローカル変数を追加しています。

これらを組み合わせても、同様に動くはずです。

しかし v8 はこの通りには動かなかったのです。

- 関数内で eval() を呼び出し
- eval() 内部で with 文を実行し
- with 文内部でオブジェクトに存在するプロパティと同じ名前の変数を宣言すると

**変数の値が with 文で付加されたオブジェクトに付きました。**

問題の発端となったコードを分解します。

```
(function() {
     return eval(            // 関数内で eval()
          'with (            // eval() 内で with 文
              {a: 1}         // オブジェクトにプロパティ
           ) { function a() {} }; // プロパティと同じ名前の変数定義
           a')  // <= 変数がwith文のオブジェクトにひきづられ、ここで値が入っていない
 })();
```

with 文も eval 文も、今では言語の仕様バグと言われるほど、問題が多くて使われない機能です。そんなレアケースのテストが抜けていたようです。

なお、この当たりの話は下記の記述を自己流で解釈したものです。

https://www.ecma-international.org/ecma-262/7.0/#sec-performeval
https://www.ecma-international.org/ecma-262/7.0/#sec-evaldeclarationinstantiation
https://www.ecma-international.org/ecma-262/7.0/#sec-with-statement

# 結局どうなったか

このことを社内で打ち明けたところ、開発元に報告した方がいいという話になりました。そこで2017年5月10日に報告しました。

https://bugs.chromium.org/p/chromium/issues/detail?id=720247

そこからの流れは

> Able to reproduce the issue using latest Chrome version 58.0.3029.96 on Mac 10.12.4, Win 10 and Ubuntu 14.04

https://bugs.chromium.org/p/chromium/issues/detail?id=720247#c3

再現できたようでなによりです。

> Good build: 49.0.2613.0 (Revision: 367537)
> Bad build:  49.0.2615.0 (Revision: 367975)

https://bugs.chromium.org/p/chromium/issues/detail?id=720247#c3

Chrome 49 というと、2016年3月あたりでしょうか。1年気づかれませんでした。

> Given how old this bug is (present since M49), I don't see the urgency to get this into M60.

https://bugs.chromium.org/p/chromium/issues/detail?id=720247#c7

誰も困ってないので、急ぐ必要ありませんよね……

> Thanks for reporting this issue. This seems like something we should fix--although eval isn't exactly the same as no-eval, I agree with your spec reading here.

https://bugs.chromium.org/p/chromium/issues/detail?id=720247#c9

拙い仕様書読みが、どうにかこうにかいい結果をもたらしたようです。

というわけで、2017年6月22日にパッチが作成され
https://bugs.chromium.org/p/chromium/issues/detail?id=720247#c11
2017年8月8日に解決済みとされました
https://bugs.chromium.org/p/chromium/issues/detail?id=720247#c12

# 最後に

という経緯で v8 のバグ報告者として名を残す名誉をいただいたのですが、これはひとえに、弊社の JavaScript インタプリタのバグを洗い出すテストコンテンツのおかげです。かつて JavaScript インタプリタを開発していた方々はほとんどが弊社を去りました。ここにお礼を申し上げます。

「じゃあ ACCESS の JavaScript インタプリタにはバグはないのか?」と言われると、きっとブーメランが返ってくるのだろうと思っています。

明日は @aTadatoshiTokutake さんです。どうぞご期待ください。