V8 的 typeof null 返回 "undefined" 的 bug 是怎麼回事

1997 年,IE 4.0 發佈,帶來的衆多新特性中有一個對將來「影響深遠」的 DOM API:document.all。在隨後的 6 年裏,IE 的市場佔有率愈來愈高,直到 2003 年的 95%。javascript

在這段時間裏,產生了兩種成千上萬的頁面。第一種:IE only 的頁面,因爲超高的市場佔有率,開發人員以爲根本不須要考慮兼容性,因而直接使用 document.all,好比:html

document.all("foo").style.visibility = "visible"

甚至不少網站直接在服務器端判斷客戶端的 UA,不是 IE 的直接返回一句話:「本站只支持 IE。。。html5

第二種頁面:開發人員使用 document.all 來識別 IE,只對 IE 展示特殊的效果:java

var isIE = !!document.all
if (isIE) {
  // 使用 IE 私有的 DOM API,私有 CSS 特性
}

那個年代的不少書也都在講這種判斷方法,直到如今,2016年,估計還有少數人這麼寫,國內出版的垃圾書估計也有可能從別處複製粘貼這樣的代碼。node

因爲第一種頁面的大量存在,Opera 在 2002 年發佈的 6.0 版本里實現了 document.all,這致使的結果就是,第一種 IE only 的頁面有很多能夠在 Opera 中正常瀏覽了,這是好消息,但壞消息是,第二種頁面反而都報錯了,!!document.all 是 true 了,Opera 被當成 IE 了,Opera 不可能支持 IE 全部的私有特性。當時有很多人給 Opera 反饋 bug,開發人員表示沒法修復。git

這段時間裏,爲了搶佔市場佔有率,Mozilla 的人也在持續討論要不要實現 document.all,bugzilla 上有不少歷史帖子可查。最終,在 2004 年,JavaScript 之父 Brendan Eich 在 Firefox 0.10 預覽版實現了 document.all,但有了 Opera 的前車可鑑,Brendan 在實現 document.all 的時候玩了個小技巧,那就是你能夠正常的使用 document.all,但你沒法檢測到它的存在:github

> document.all + "" 
"[object HTMLAllCollection]"
> typeof document.all
"undefined"
> !!document.all
false

Brendan 取名爲「undetected document.all」,但在當時不少人也發現了,document.all 並非真的檢測不到,好比:web

> document.all === undefined
false
> "all" in document
true

當時 Mozilla 的人也回覆了:「這不是 bug,由於作這個改動是被迫的,並且這個改動是違反 ECMAScirpt 規範的,改動越小越好,in 和 === 就不去管了,畢竟極少數的人用 === 和 in 判斷 document.all 存在與否」。現現在全部的瀏覽器都是這樣的實現,HTML 5 規範裏也是這麼規定的chrome

那段時間 Safari 纔剛剛起步,但也有收到來自用戶的不支持 document.all 的 bug,2005 年末,Safari 學 Firefox,實現了 undetectable document.allc#

2008 年,Opera 在 9.50 Beta 2 版本將本身直接暴露了多年的 document.all 也改爲了 undetectable 的,變動記錄裏是這麼寫的:「Opera now cloaks document.all」。 Opera 的工程師當年還專門寫了一篇文章講了 document.all 在 Opera 裏的變遷,還說到 document.all 絕對值得被展覽進「Web 技術博物館」。

2008 年末,Chrome 1.0 發佈,Chrome 是基於 Webkit 和 V8 的,V8 固然得配合 Webkit 裏的 document.all 的實現。

很戲劇性的是,在 2013 年,連 IE 本身(IE 11)也隱藏掉了 document.all,也就是說,全部現代瀏覽器裏 document.all 都是個假值了。

在 V8 裏的實現是:一個對象均可以被標記成 undetectable 的,不少年來只有 document.all 帶有這個標記,這是相關的代碼片斷和註釋

// Tells whether the instance is undetectable.
// An undetectable object is a special class of JSObject: 'typeof' operator
// returns undefined, ToBoolean returns false. Otherwise it behaves like
// a normal JS object.  It is useful for implementing undetectable
// document.all in Firefox & Safari.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=248549.
inline void set_is_undetectable();
inline bool is_undetectable();

而後在 typeof 的實現裏,若是 typeof 的參數是 undefined 或者是 undetectable 的,就返回 "undefined":

Handle<String> Object::TypeOf(Isolate* isolate, Handle<Object> object) {
  if (object->IsNumber()) return isolate->factory()->number_string();
  if (object->IsUndefined() || object->IsUndetectableObject()) {     return isolate->factory()->undefined_string();
  }
  if (object->IsBoolean()) return isolate->factory()->boolean_string();
  if (object->IsString()) return isolate->factory()->string_string();
  if (object->IsSymbol()) return isolate->factory()->symbol_string();
  if (object->IsString()) return isolate->factory()->string_string();
#define SIMD128_TYPE(TYPE, Type, type, lane_count, lane_type) \
  if (object->Is##Type()) return isolate->factory()->type##_string();
  SIMD128_TYPES(SIMD128_TYPE)
#undef SIMD128_TYPE
  if (object->IsCallable()) return isolate->factory()->function_string();
  return isolate->factory()->object_string();
} 

今年 2 月份,V8 作了一個改動,就是除了 document.all,要把 null 和 undefined 兩個值也標記成 undetectable 的。當時,開發人員清楚的知道這個改動會讓 typeof null 返回 "undefined",因此專門改動了 typeof 的實現,而且添加了個對應的測試文件

assertFalse(typeof null == "undefined")

看上去很完美,但其實這個改動產生了個 bug,這個 bug 後來流到了 50 和 51 的穩定版裏。

在 4 月份,有人發現了這個 bug,提煉一下重現代碼就是:

for (let n = 0; n < 10000; n++) {
  console.log(typeof null == "undefined")
}

Chrome 裏執行結果以下:

在 for 循環執行若干次後,typeof null 會從 "object" 變成 "undefined"。

我當時也關注了這個 bug,不到一週時間後就修復了。當時 master 分支是 Chrome 52,開發人員覺的 bug 影響不大(個人猜想),就沒有合進當時的穩定版 Chrome 50 裏。

其實這個 bug 產生的緣由是:V8 還有個優化編譯器(optimizing compiler),叫 crankshaft,當代碼執行次數夠多時,JavaScript 代碼會被這個編譯器從新編譯執行。crankshaft 裏有一個它單獨使用的 typeof 實現,和普通編譯器(full-codegen)用的不同:

String* TypeOfString(HConstant* constant, Isolate* isolate) {
  Heap* heap = isolate->heap();
  if (constant->HasNumberValue()) return heap->number_string();
  if (constant->IsUndetectable()) return heap->undefined_string();   if (constant->HasStringValue()) return heap->string_string();
  switch (constant->GetInstanceType()) {
    case ODDBALL_TYPE: {
      Unique<Object> unique = constant->GetUnique();
      if (unique.IsKnownGlobal(heap->true_value()) ||
          unique.IsKnownGlobal(heap->false_value())) {
        return heap->boolean_string();
      }
      if (unique.IsKnownGlobal(heap->null_value())) {
        return heap->object_string();
      }
      DCHECK(unique.IsKnownGlobal(heap->undefined_value()));
      return heap->undefined_string();
    }
    case SYMBOL_TYPE:
      return heap->symbol_string();
    case SIMD128_VALUE_TYPE: {
      Unique<Map> map = constant->ObjectMap();
#define SIMD128_TYPE(TYPE, Type, type, lane_count, lane_type) \
  if (map.IsKnownGlobal(heap->type##_map())) {                \
    return heap->type##_string();                             \
  }
      SIMD128_TYPES(SIMD128_TYPE)
#undef SIMD128_TYPE
      UNREACHABLE();
      return nullptr;
    }
    default:
      if (constant->IsCallable()) return heap->function_string();
      return heap->object_string();
  }
}

那次改動開發人員漏改了這個 typeof 實現,致使了上面的 bug,修復很簡單,就是把標紅的那句判斷挪下面,同時修 bug 的人專門新增了個測試文件,裏面 %OptimizeFunctionOnNextCall() 是個特殊函數,可讓某個函數直接被編譯進 crankshaft 裏執行。

6 月 8 號,那個 bug 裏又有人反饋,說他們的系統由於這個 bug 沒法運行了,問何時修復代碼能合進穩定版裏,當時沒有相關人員回覆。

6 月 20 號,有人在 reddit 上公開了這個 bug,被人屢次轉到 twitter 上,那個 bug 下面又有了更多人的回覆。

Chrome 的開發人員意識到問題有點嚴重了,因而將先前的 commit cherry-pick 進了 Chrome 51 裏,Node 也進行了一樣的操做

在這以後,又有三個不知道這個 bug 已經修復了的人報了重複的 bug,issue 621887issue 622628issue 5146

PS,V8 將來會淘汰 Full-codegen/Crankshaft,使用新的 Ignition(解釋器) + Turbofan(編譯器) 架構,在 Chrome 50 或者 51 裏打開 chrome://flags/#enable-ignition 選項,就會發現 bug 沒法重現了。

相關文章
相關標籤/搜索