- Ladybirdは通常のWebコンテンツはある程度処理できるが、Google Project ZeroのDOMファザー Domato を回すと、ブラウザエンジンに潜むエッジケースがすぐに露呈した
- JavaScriptでパーサールールを回避して作ったDOM、windowのない文書、循環するSVG参照のような 現実に起こりうる異常入力 から、実際のバグ5件が見つかって修正された
- `` のtable祖先という前提、
DOMParser 文書にwindowがあるという前提、Element.before() の兄弟探索ミスのように、実装内部の暗黙の前提がクラッシュや無限ループにつながっていた
- 削除されたiframeの
contentWindow へのアクセス問題はLadybird固有の不具合ではなく、HTML仕様の browsing context の前提とも絡み、WHATWG HTMLのissueにつながった
- Domatoのようなファザーは、通常のWebページテストだけでは見つけにくいセキュリティ・安定性の問題を露出させる。Ladybirdの次の課題は、継続的なファジングに耐えられる程度まで安定化したうえで自動実行することだ
DomatoでLadybirdをストレステスト
- Ladybird は整ったWebコンテンツならある程度処理できるが、セキュリティ研究用ツールで異常入力を投げ、どんな問題が出るかを確認した
- 使用したツールはGoogle Project ZeroのDOMファザー Domato である
- Domatoは、ほとんどは有効だが奇妙な HTML, CSS, JavaScript が混ざったランダムなWebページを生成する
- 生成されたページをLadybirdの デバッグビルド に読み込ませ、挙動を観察した
- DomatoのREADMEでは主要ブラウザで見つかった多くのバグが挙げられており、Ladybirdでも意味のある欠陥を見つけられると判断した
が の中にあるときのヌルポインタ逆参照
- 最初の問題は1秒もたたずに見つかり、562KiBのDomato出力は以下の形まで縮小できた
let mfrac = document.createElement("mfrac");
mfrac.appendChild(document.createElement("th"));
document.body.appendChild(mfrac);
- UBSANを有効にしたLadybirdビルドでは、
HTMLTableCellElement.cpp の table_containing_cell 呼び出しが ヌルポインタ逆参照 を引き起こした
- 原因は、Ladybirdの
と の実装が、DOMツリーを上にたどると常に `` があると仮定していたことにあった
- HTMLパーサーは `` のようなマークアップを許可しない
- 仕様に従うブラウザでは、このマークアップを読み込むと中身が空の `` が1つ作られる
- しかしJavaScriptのDOM APIでノードを直接作ると、パーサールールの一部を回避して
の中に を入れられる
- 問題のコードは、
と がテーブルボックスだけでなく各セルにもCSSのborderとpaddingを適用する古い挙動を実装するために使われていた
- 修正は、
と が常に `` 祖先を持つという前提を取り除く形で行われた
table_containing_cell(*this) の代わりに first_ancestor_of_type() を使用する
- テーブル祖先がなければ即座に戻る
- 修正コミットは こちら
windowのない文書で `` イベントハンドラーを代入する
- 2番目の問題も1秒以内に見つかり、472KiBのDomato出力は次のコードまで縮約された
var parser = new DOMParser();
var doc = parser.parseFromString("", "text/html");
var body = doc.createElement("body");
body.onblur = null;
- Ladybirdは
GCPtr 検証失敗で停止した
- 核心は、`` の
onfoo イベントハンドラー属性が持つ 特殊な挙動 にある
- 古いWebコンテンツとの互換性のため、
document.body.onfoo の代入は window.onfoo に転送される必要がある
- しかし
DOMParser で作られた文書には windowオブジェクトがない
- Ladybirdの内部オブジェクトモデルは、すべてのdocumentが常にwindowを持つ前提で誤って設計されていた
- 修正後、
Document::window() はnullableな値を返すようになり、複数箇所でnullが処理されるようになった
- windowのない文書で
document.body.onblur を代入すると、他のブラウザと同様に何も起きない
SVG `` の循環参照
- 3番目の問題は、SVGグラデーションが自分自身を参照したときに起きる 無限再帰 だった
- SVGはHTML内のインラインSVGと外部画像フォーマットの両方をサポートする必要があり、グラデーションは別のグラデーションを参照して色を継承できる
- Ladybirdの実装は、グラデーションが自分自身を参照するケースを考慮しておらず、参照チェーンをたどるうちにループし続けていた
- 単に自分自身を参照するケースだけを防いでも、多段の循環参照には対応できない
- 正しい処理は、訪問済みのグラデーションをすべて追跡し、すでに訪れたグラデーションに再び出会ったらチェーン追跡を打ち切ることだ
- Firefoxはこの種のグラデーションに対して開発者コンソールに警告を表示する
削除されたiframeのwindowプロパティアクセスとHTML仕様バグ
- 4番目の問題は、iframeを削除したあと、あらかじめ保持しておいた
contentWindow で getSelection() を呼び出したときに発生した
window.onload = function() {
let iframe = document.querySelector("iframe")
let iframeWindow = iframe.contentWindow;
iframe.remove();
iframeWindow.getSelection();
}
- Ladybirdは
WindowProxy.cpp で、BrowsingContext に対するヌルポインタ参照バインディングのランタイムエラーを出した
- iframeがDOMから削除されると、そのcontent documentは自身の browsing context から切り離される
- windowオブジェクトのプロパティを取得または設定すると、HTML仕様のアルゴリズム
"check if an access between two browsing contexts should be reported" が実行される
- このアルゴリズムは、アクセス元windowとアクセス先windowのbrowsing contextを検査する
- 仕様は、プロパティアクセス時点で両方のwindowが接続済みのbrowsing contextを持つと誤って仮定していた
- HTML仕様に対する issue が立てられ、Ladybirdにはひとまずnullチェックが追加された
- Ladybirdの作業中に仕様バグを見つけた場合、バグ報告や修正提案によって皆のために仕様を改善できる
Element.before() の無限ループ
- 5番目の問題は、ページの読み込みが終わらずCPUを100%使用し続けるという形で現れた
two.before(one);
- 原因は、
before() 実装で `` の前の兄弟のうち、引数に含まれない最初の兄弟を探すロジックのミスだった
- 既存のループは毎回
node->previous_sibling() を取り直していた
while (auto previous_sibling = node->previous_sibling()) {
// check if previous_sibling is one of the arguments
}
- 実際には兄弟チェーンをたどり、
previous_sibling->previous_sibling() で進み続ける必要があった
for (auto sibling = node->previous_sibling(); sibling; sibling = sibling->previous_sibling()) {
// check if previous_sibling is one of the arguments
}
ファジング結果と次のステップ
- 今回のセッションでは実際のバグを5件見つけ、そのうち1件は HTML仕様バグ で、すべて修正された
- 奇妙で予想外の入力に遭遇すると、Ladybirdが非常に速く崩れることが明らかになった
- Domatoのようなファザーは、ソフトウェアをより堅牢にしたい人にとって有用な資源である
- 次の段階は、Ladybirdを継続的なファジング入力に耐えられる水準まで安定化することだ
- 十分に安定したら、クラウド上のどこかで自動実行して、さらに多くの問題を見つける計画だ
1件のコメント
Hacker Newsのコメント
仕様に複数の独立した実装があることになぜ価値があるのかをよく示している
この記事ひとつだけでも仕様の穴が1つ見つかっており、ほかにもあった、あるいは今後さらに出てくる気がする
Webプラットフォームの長期的な健全性には複数の独立実装が重要なので、私たちもその役割を担おうとしている
たとえば「ナスは私の一番好きな野菜」とツイートしたら誰かが「実は果物」と訂正してくれて、それをもって「Twitterの価値が証明された」と言うような感じに近い
この作業自体や仕様の複数実装に価値がないという意味ではないが、この特定の例だけではまだその含意は成り立たないと思う
このプロジェクトが小さなチームでも驚くようなものを作れると示し続けているのが良い
利害関係者の多い会社の中では、こうしたことをやり遂げるのはずっと難しかっただろう
趣味プロジェクトならいつでも戻って作り直せるが、こうしたものの一部は最初からアーキテクチャに組み込まれているべきではなかったか、という感覚を拭いにくい
もうSVGを実装したのか? 思っていたよりずっと速いペースで進んでいて興味深く見ている
特にアニメーションが大きな未実装部分だ
issue #3については、別のグラデーションを参照するグラデーションに最大深度制限を設けるのもよさそうだ
「この参照を以前見たか」というロジックのバグや限界に備える多層防御になる
SVGグラデーションには詳しくないが、参照チェーンが1000個も続く正当な理由があるのかもしれない一方で、実運用でそんなものを見たら、攻撃かファザー入力である可能性が高いと思う
このコメントはLadybirdで書いている
もうHacker NewsはLadybirdで動く
1日に数分ほどHacker NewsやOSnewsのようなサイトを眺めるときにLadybirdを使っている
遅くて壊れやすいが、動いてはいる。プロジェクトがこれほど初期段階で、文字どおり全部をゼロから書いたことを考えると、それだけでも大したものだ
Ladybirdが成熟していくのが本当に楽しみだ
興味深いが、ほとんどすべての開発者が issue #1 に見られるように「見つけた! 修正コミットした、終わり!」で済ませてしまうのが気になる
そうではなく、何が正確に間違っていたのかを理解すべきだ。たとえば「親は必ず存在する」という前提が問題だったなら、コードベース全体で同種のミスを探すべきだ
想像力を使って、同じことがほかのどこで起こりうるかを見つける必要がある。絶対に1か所だけではない
現代のソフトウェアが信頼しにくいバグだらけの悪夢になっているのは、たいてい資本主義的な制約のせいだが、それでももっとうまくやることはできる
今年のWeb Engines Hackfestに Ladybird が登場する予定なのか気になる
少し話はそれるが、YouTubeのハッキング動画はどうなったのか気になる
以前は新しい動画を楽しみに待っていたが、しばらく見ていない気がする
月次アップデート動画はまだ上げているが、最後のハッキング動画からはもう数か月たっている
それでも Ladybird の作業は毎日続けていて、昨年は Shopify やほかのところからの手厚い支援のおかげで、今ではフルタイムのエンジニア2人もマネジメントしている