株式会社クリアコード > ククログ

ククログ


公開中のソフトウェアがWindows Defenderでマルウェアとして判定された場合の対応

当社ではIE View WEというアドオンを開発・公開しています。これはFirefox上で閲覧中のページやマッチングパターンに当てはまるURLのページをIEもしくは任意の外部アプリケーションで開くという物で、Firefox法人サポートでの需要を想定して、既存のアドオン「IE View」の仕様を参考にFirefox 57以降で使用できる形(WebExtensionsベース)でスクラッチで開発したという物です。

WebExtensionsではアドオンが直接任意の外部アプリケーションを起動する事はできず、Native Messaging Hostと呼ばれるヘルパープログラムを介する必要があります。Native Messaging Hostは任意の言語で実装することができますが、IE View WEのNative Messaging Hostの場合はgoで開発しており、一般のユーザのためにコンパイル済みのバイナリも公開・配布しています。

このNative Messaging Hostのバイナリについて、先日、「Windows Defenderでトロイ(マルウェア)と判定されている」というご指摘を頂きました。

結果的にはただの誤判定だったのですが、配布物がそのような警告を受ける状態となった事は当社では過去に無かったため、対応のノウハウが無く手探りでの対応とならざるを得ませんでした。本記事では、本件に際して実際に行った対応の内容をご紹介します。同様の事態に遭遇された方の参考になりましたら幸いです。

第一報〜状況確認〜バイナリ公開停止

最初に受け取ったのは、以下のような内容の指摘のメール(英語)でした。

件名: Native Messaging Host……
本文: Windows 10 32bit版のWindows Defenderで、トロイと判定されます。

これを受け、まず、本当に配布ファイルがWindows Defenderでマルウェアと判定されるのかどうかを確認しました。

IE View WEのNative Messaging Hostのバイナリは、現在の所GitHubのリリースページでのみ配布しています。まず公開中のファイルをダウンロードし、実際にWindows Defenderに判定させてみました*1。その結果、確かに Trojan:Win32/Azden.A!cl として判定される結果となりました。

そこで、公開中のバイナリをリリースページから削除し、以下の文言を表示するようにしました。

Note: ieview-we-host.zip is undistributed due to security reason for now. Please wait for a while.

註:ieview-we-host.zipはセキュリティ上の理由により現在公開を停止中です。しばらくお待ち下さい。

さらなる状況確認

次に確認したのは、ダウンロードした配布ファイルはこちらで作成してアップロードした物と同一なのかどうかという点です。

ダウンロード用のURLはHTTPSとなっており、サーバーからのダウンロード過程でファイルが攻撃者によって置き換えられてしまう可能性はありません。しかし、バイナリ自体に電子署名は施していないため、サーバー上にあるファイル自体がこちらでアップロードした物から差し替えられてしまっている可能性は0とは言えません*2。そのため、次の段階としてダウンロード後のファイルとアップロード前のファイルのハッシュ値をsha256sumコマンドで算出し、両者が一致するかどうかを確認しました。その結果、ハッシュ値は一致し、アップロードの前後でファイルは変化していない事を確認できました。

(なお、仮にこの段階でファイルのハッシュ値が一致しなかった場合、アップロードされた後でファイルが改竄されたという事になります。それが可能となるのは、当社エンジニアが使用しているSSH秘密鍵が流出したか、GitHubのWebサービスが攻撃を受けてファイルが改竄されたというケースにあたり、また別の対応が必要になってきます。)

アップロード前のファイルがマルウェア判定された場合の対応

アップロード前の状態と同一のファイルがマルウェアと判定されるということは、ファイルが作成された時点からそのような内容が含まれていたという事になります。そうなると、今度は以下の可能性が疑われます。

  • バイナリ作成者のPCがすでに攻撃者によって乗っ取られていた。
  • バイナリに含まれる外部ライブラリに攻撃コードが混入していた。

作業者のPCが攻撃者によって乗っ取られるというのは、現代でもあり得る脅威です。例えば、いわゆるフリーWi-Fiの使用時は、同一のLAN内に全く素性の知れない他人が接続してきている状況で、攻撃の踏み台として標的にされるという事は十分に起こり得ます。

ただ、今回の事例ではこの可能性はそう高くないと考えられました。作業者のPCはUbuntuのデスクトップ環境が動作しており、外部からの接続も公開鍵認証のSSH以外は受け付けないよう設定されていたからです。絶対数が少ない環境の上、フリーWi-Fiに接続する機会もほぼ無いため、あらかじめ攻撃を受けていて踏み台にされていたという事は考えにくいです。

また、現代のソフトウェアは多数の依存ライブラリを引用する形で成り立っている事が多く、その中にこっそり悪意あるコードが混入していたとしても容易に気付けないという状況にあります。実際、そのような攻撃が行われた事例や、攻撃が行われうる脆弱性が指摘された事例は複数あります。

こちらの可能性を疑い始めると、調査は困難になってきます。依存ライブラリもまた別のライブラリに依存しているという事が多く、場合によっては調査範囲が際限なく広がってしまうからです。

Microsoftへの連絡

ここで初めて、それまで意図的に敢えて除外していた、擬陽性であった可能性を考え始める事にしました。

現代のマルウェアは日々多種多様な亜種が発生しており、単純なパターンマッチングでは「本当はマルウェアなのにマルウェアとして判定されない」というケース(擬陰性)が発生しやすいです。そのため各セキュリティ対策ソフトのベンダーは、機械学習を用いた分析や実際のソフトウェアの動作の様子を監視するなどして、疑わしい振る舞いをするソフトウェアは積極的に脅威判定するようになっています。その結果、本来は無害なソフトウェアであってもマルウェアの疑い有りとして検出されるというケース(擬陽性)が発生してしまう事があります。

Microsoftではこのようなケースを想定して、提出されたファイルを詳細に分析する受付窓口を提供しています。Windows Defenderでマルウェアとして判定されたソフトウェアをここで提出すると、専門家が分析した上で「本当にマルウェアである」あるいは「マルウェアではない」といった判定を行った結果を連絡してくれます。

今回のように自作ソフトウェアがマルウェア判定されたケースでは、「Software Developer(ソフトウェア開発者)」を選択して報告します。実際の報告内容は以下のようにしました。

  • 会社名:ClearCode Inc.

  • 判定名:Trojan:Win32/Azden.A!cl

  • 定義ファイルバージョン:1.289.911.0

  • 追加の情報:

    This program is a native messaging host for a Firefox addon "IE View WE", and it provides following features:
    1) Read configurations from local config files placed under specific locations, distributed by the system administrator.
    2) Read registry to know where the executable file of the Internet Explorer (or Edge) is.
    3) Launch the Internet Explorer (or Edge) with some options.
    The source codes of the file are published under https://github.com/clear-code/ieview-we/tree/master/host and https://github.com/clear-code/mcd-go
    

    参考訳(※送信した報告は英語の方のみです)

    子のプログラムはFirefox用アドオン「IE View WE」用のNative Messaging Hostで、以下の機能を提供します:
    1) システム管理者によって特定の位置に置かれたローカル設定ファイルから設定情報を読み取る。
    2) レジストリからIE(またはEdge)の実行ファイルの位置を調べる。
    3) いくつかのオプションを指定してIE(またはEdge)を起動する。
    ソースコードは以下のサイトで公開しています: https://github.com/clear-code/ieview-we/tree/master/host および https://github.com/clear-code/mcd-go
    

送信した報告内容はキューに溜められ、順次処理されていきます。有償のサポート契約や開発者ライセンスを持っている場合、高い優先度でキューに割り込む事もできるようですが、当社はそのようなライセンスを特に保持していないので、一般的なソフトウェア開発者として報告するに留まりました。

報告者への連絡と、追加の確認

急いで取れる対応はここまでということで、この時点で一旦報告者の方に状況を伝える事にしました。

Hello,

Thank you for the information. We've removed the downloadable file for now, until those files are confirmed as completely safe. We've report those files to Microsoft security researchers, and waiting for responses.

regards,

こんにちは

情報をありがとうございます。安全が確認できるまで、ダウンロード可能なファイルは削除する事にしました。これらのファイルはすでにMicrosoftのセキュリティリサーチャーに報告済みで、返事を待っている状態です。

それでは

また、複数のセキュリティ対策ソフトでの検知結果を横断して確認できるVirusTotalというサイトでも確認を行った所、Windows Defender以外にもいくつかのソフトで脅威判定されている様子が窺えました。具体的な結果は以下の通りです。

  • 32bit版バイナリ: 検出率: 2/70(現時点では0/70
    • Cylance: Unsafe (20190312)
    • Microsoft: Trojan:Win32/Azden.A!cl (20190307)
  • 64bit版バイナリ: 検出率: 1/65(現時点でも変わらず
    • Jiangmin: Trojan.Ebowla.k (20190312)

安全確認〜再公開

そうこうするうちに、Microsoftから分析完了の通知メールが届きました。メールに記載されていたリンクを開くと、以下のように記載されていました。

Submission details
host.exe

Submission ID: xxxx-xxxx-xxxx-xxxx-xxxx

Status: Completed

Submitted by: (送信者のMicrosoftアカウントのメールアドレス)

Submitted: Mar 12, 2019 11:19:43

User Opinion: Incorrect detection

Analyst comments:
We have removed the detection. Please follow the steps below to clear cached detection and obtain the latest malware definitions.

  1. Open command prompt as administrator and change directory to c:\Program Files\Windows Defender
  2. Run “MpCmdRun.exe -removedefinitions -dynamicsignatures”
  3. Run “MpCmdRun.exe -SignatureUpdate”

Alternatively, the latest definition is available for download here: https://www.microsoft.com/en-us/wdsi/definitions

Thank you for contacting Microsoft.

報告の詳細
host.exe

報告ID: xxxx-xxxx-xxxx-xxxx-xxxx

状態: 完了

送信者: (送信者のMicrosoftアカウントのメールアドレス)

送信日時: 2019年3月12日 11:19:43

ユーザーの意見: 誤判定

分析者のコメント:
私達は判定を削除しました。以下の手順で判定結果のキャッシュを消去し、最新のマルウェア定義を入手して下さい。

  1. コマンドプロンプトを管理者として開き、ディレクトリを「c:\Program Files\Windows Defender」に切り替える。
  2. 「MpCmdRun.exe -removedefinitions -dynamicsignatures」を実行する。
  3. 「MpCmdRun.exe -SignatureUpdate」を実行する。

もしくは、最新の定義ファイルは以下からもダウンロード可能です:https://www.microsoft.com/en-us/wdsi/definitions

表示されている時刻は日本時間です。11時台に報告して、結果が返ってきたのは14時〜16時にかけてでした*3。数日待たされる事も覚悟していたのですが、予想よりずっと早い回答でした。

案内のあった手順の通りに操作して再確認した所、バイナリはマルウェアとは検出されないようになりました。よって、これを以て安全の確認が取れたと見なし、以下の文言を添えて、公開を取り下げていたバイナリを再公開しました

Files named host.exe in ieview-we-host.zip may be detected as Trojan:Win32/Azden.A!cl by Microsoft Windows Defender, but it is false positive and actually safe. Those misdetectons have been reported to Microsoft, and go away after you update the malware definitions.

ieview-we-host.zipに含まれるhost.exeという名前のファイルがMicrosoft Windows DefenderによってTrojan:Win32/Azden.A!clと判定される事がありますが、これは擬陽性で、実際には安全です。この誤判定はMicrosoftに報告済みで、マルウェア定義の更新後は警告されなくなります。

また、報告者の方にも以下の通り連絡しました。

Hello,

The trojan alert was a false positive, and Microsoft researchers decided to remove them from detection. > After you update malware definitions, the alert will go away. Detailed steps described by Microsft:

(中略)

So we've re-published native messaging host binaries. Thanks again!

regards,

こんにちは

トロイ警告は擬陽性で、Microsoftのリサーチャーはこの判定結果を取り除く決定をしました。
マルウェア定義の更新後は、この警告は表示されなくなります。Microsoftによって案内された手順は以下の通りです:

(中略)

よって、Native Messaging Hostのバイナリを再公開しました。重ねてありがとうございます!

それでは

以上を以て、本件へのWindows Defenderに関する対応は完了としました。

まとめ

以上、Windows Defenderでマルウェアとして誤判定された場合の対応フローの例をご紹介しました。

被害の拡大を防ぐ事を第一に置くと、最初に取るべき対応は「実際にマルウェア判定されるか確認する」ではなく「サンプルとしてファイルをダウンロードした上で、バイナリの公開を停止する」だったと言えます。実際にマルウェア判定されるかどうかは公開を取り下げた後で行えば良かったにも関わらず、それを後回しにして「マルウェア判定されるのは事実か?」を先に確認してしまったのは不徳の致すところです。

今回は擬陽性での誤判定という事で決着しましたが、実際にマルウェアだったという場合にはまた別の対応が必要になってきます。そのような事は起こらないに越した事はありませんが、もし万が一そのような事態が発生した際には、被害の拡大を防ぐ事を最優先として対応したいです。

なお、2019年3月18日現在、VirusTotalで確認した限りでは、他の製品では脅威判定はなされない状態になっているようなのですが、Jiangminという製品で依然として脅威として判定されている模様です。Jiangminは中国系企業の製品で、JUSTセキュリティのバックエンドにも採用されているそうなのですが、公式サイトが中国語のみで提供されているため、報告の窓口が分からずお手上げとなっています。もし報告窓口をご存じの方がいらっしゃいましたら、情報をお寄せいただければ幸いです。

*1 この段階でもしマルウェアとして判定されなければ、報告者がファイルをダウンロードしたWebサイトで配布されているファイルが改竄済みの物である(改竄されたファイルが第三者によって配布されている)という事になります。

*2 電子署名を施していた場合、「署名した時点のファイルから改竄されていない事」が保証されるため、この確認は不要になります。

*3 32bit版と64bit版の各バイナリについて、結果が出るまでに若干タイムラグがありました。

タグ: Mozilla
2019-03-18

Firefox 67 以降でPolicy Engineによるポリシー設定でPOSTメソッドの検索エンジンを追加できるようになります

Firefoxのポリシー設定では、以下の要領でロケーションバーやWeb検索バー用の検索エンジンを登録することができます。

{
  "policies": {
    "SearchEngines": {
      "Add": [
        {
          "Name":               "検索エンジンの表示名",
          "IconURL":            "https://example.com/favicon.ico",
          "Alias":              "ex",
          "Description":        "検索エンジンの説明文",
          "Method":             "GET",
          "URLTemplate":        "https://example.com/search?q={searchTerms}",
          "SuggestURLTemplate": "https://example.com/suggest?q={searchTerms}"
        }
      ]
    }
  }
}

Firefox 66以前のバージョンではこの時、HTTPのメソッドとしてGETのみ使用可能で、POSTメソッドを要求する検索エンジンは登録できないという制限事項がありました。OpenSearchの検索プラグインではPOSTの検索エンジンも登録できたので、これはPolicy Engineだけの制限ということになります。

RESTの原則からいうと「何回検索しても同じ結果が返る事が期待される検索結果一覧ページを表示するためのリクエストはGETが当然」という事になるのですが、実際にはFirefoxのロケーションバーやWeb検索バーは「検索」に限らず、入力された語句を含む任意のHTTPリクエストを送信する汎用のフォームとして使えます。よって、適切な内容の「検索エンジン」を登録しておきさえすれば、FirefoxのUI上から直接「Issueの登録」や「チャットへの発言」を行うといった応用すらも可能となります。しかし、そういった「新規投稿」にあたるリクエストはPOSTメソッドを要求する場合が多いため、それらは残念ながらPolicy Engine経由では登録できませんでした。

この件について 1463680 - Policy: SearchEngines.Add cannot add effective search engine with POST method として報告していたのですが、最近になって「バックエンドとなる検索エンジン管理の仕組みの改良によって、仕組み的にはPOSTメソッドも受け取るようになったので、パッチを書いてみては?」と促されました。そこでさっそく実装してみた所、変更は無事マージされ、Firefox 67以降では以下の書式でPOSTメソッドとそのパラメータを指定できるようになりました。

{
  "policies": {
    "SearchEngines": {
      "Add": [
        {
          "Name":        "検索エンジンの表示名",
          "IconURL":     "https://example.com/favicon.ico",
          "Alias":       "ex",
          "Description": "検索エンジンの説明文",
          "Method":      "POST",
          "URLTemplate": "https://example.com/search",
          "PostData":    "q={searchTerms}"
        }
      ]
    }
  }
}

"Method"の指定を"POST"にする事と、"URLTemplate"ではなく"PostData"の方に{searchTerms}というパラメータを指定する事がポイントです。

今回の変更自体は別のBugでの改良に依存しているため、ESR60に今回の変更がupliftされるためには、依存する変更も併せてupliftされる必要があります。ESR版自体のメジャーアップデートも視野に入りつつあるこの時期ですので、そこまでの手間をかけてこの変更がESR60に反映されるかどうかについては、現状では何とも言えません。とはいえ、次のESRのメジャーアップデート以降で使用可能になる事は確実です。もしこの機能をお使いになりたいというESR版ユーザーの方がいらっしゃいましたら、期待してお待ちいただければと思います。

2019-03-01

Firefox ESR60でタイトルバーのウィンドウコントロールボタンが機能しなくなる事がある問題の回避方法

Firefox ESR60をWindows 7で使用していると、ウィンドウコントロール(タイトルバーの「最小化」「最大化」「閉じる」のボタン)が動作しなくなる、という現象に見舞われる場合があります。Firefoxの法人サポート業務の中でこの障害についてお問い合わせを頂き、調査した結果、Firefox自体の不具合である事が判明しました。

この問題は既にMozillaに報告済みで、Firefox 67以降のバージョンで修正済みですが、Firefox ESR60では修正されない予定となっています。この記事では、Firefox ESR60をお使いの方向けに暫定的な回避方法をご案内します。

問題の状況とその原因

この現象は、FirefoxのUIの設計に由来する物です。

一般的なWindowsアプリケーションは、タイトルバーなどを含めたウィンドウの枠そのものはWindowsに描画や制御を任せて、枠の内側だけでUIを提供します。それに対し、Firefoxのブラウザウィンドウでは、タイトルバー領域に食い込む形でタブを表示させるため、タイトルバーを含むウィンドウの枠まで含めた全体を自前で制御しています。そのため、タイトルバー領域に食い込む形でタブが表示されている場面では、Windowsが標準で提供しているウィンドウコントロールのボタンは実は使われておらず、それを真似る形で置かれた独自のUI要素で代用しています。

通常、これらの代用ボタンは他のUI要素よりも全面に表示されるため、タブなどの下に隠れる事はなく、ユーザーは見たままの位置にあるボタンをクリックできます。しかし、特定の条件下ではこれらの代用ボタンが他のUI要素の下に隠れてしまう形となり、ボタンをクリックできなくなってしまいます。

この問題が起こる条件は、以下の通りです。

  • Windows 7で、Windows自体のテーマとして「Classic」テーマを使用している。
  • ツールバー上の右クリックメニューから「メニューバー」にチェックを入れており、メニューバーが常時表示される状態になっている。
  • ウィンドウがwindow.open()で開かれ、その際、メニューバーを非表示にするように指定された。

Windowsのテーマ設定とFireofxのツールバーの設定を整えた状態で、w3schools.comのwindow.oepn()の各種指定のサンプルを開き「Try it」ボタンをクリックしてみて下さい。実際に、ボタンが機能しないためウィンドウを閉じられなくなっている*1事を確認できるはずです。

問題が再現した時のFirefoxの内部状態を詳しく調査すると、Classicテーマにおいては、この状況下ではタブバーのz-index(重ね合わせの優先順位)が2になるのに対し、ウィンドウコントロールのz-indexは常に1になるために、タブバーがウィンドウコントロールの上に表示されてしまっている状態である*2という事が分かりました。

なお、ClassicテーマはWindows 7以前のバージョンでのみ使用できる機能で、Windows 10以降では使えません*3。そのため、この問題はWindows 10以降では再現しません。

回避方法

原因が分かれば対策は容易です。

最も単純な対策は、ユーザープロファイル内にchromeという名前でフォルダを作成し、以下の内容のファイルをuserChrome.cssの名前で設置するというものです。

@namescape url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");

/* WindowsでClassicテーマが反映されている場合、 */
@media (-moz-windows-classic) {
  /* タイトルバーにタブを表示する設定で、フルスクリーン表示でない時は、 */
  #main-window[tabsintitlebar]:not([sizemode=fullscreen]) #titlebar-buttonbox {
    /* ウィンドウコントロールの重ね合わせ順位をタブバーよりも上にする */
    z-index: 3 !important;
  }
}

ただ、ユーザープロファイルの位置はFirefoxの実行環境ごとに異なる*4ため、管理者側でこの対応を全クライアントに反映するのは難しいです。クライアント数が多い場合は、Firefoxのインストール先のchromebrowser\chrome配下に置かれたCSSファイルを自動的に読み込むためのスクリプトMCD用設定ファイルに組み込むなどの方法をとるのがおすすめです。

修正の状況

原因が判明した時点で、本件はFirefox本体の問題としてbugzilla.mozilla.orgに以下の通り報告しました。

また、調査を行った時期にちょうどFirefoxのタブバー・タイトルバー周りの実装の仕方が変化していたため、最新の開発版でも状況を確認した所、Firefox 65以降ではWindows 7でなくても同様の問題が起こり、しかも今度はそもそもウィンドウコントロールが表示されなくなってしまっているという、より酷い状況でした。そのため、そちらは別の問題として以下の通り報告しました。

Firefox ESR60およびFirefox 64以前での問題(本記事で解説している問題)については、Firefox 65でタブバーの設計が変わったために現象としては再現しなくなった事と、セキュリティに関わる問題ではない事から、修正はされないという決定がなされています。

Firefox 65以降での問題については、既に修正のためのパッチを提供し、Firefox 67以降のバージョンに取り込まれる事が確定しています。Firefox 65に対しては修正はバックポートされず、Firefox 66については特に誰も働きかけなければ修正は反映されないままとなる見込みです。

まとめ

以上、Firefoxの法人向け有償サポートの中で発覚したFirefoxの不具合について暫定的な回避方法をご案内しました。

当社のフリーソフトウェアサポート事業では、当社が開発した物ではないソフトウェア製品についても、不具合の原因究明、暫定的回避策のご提案、および(将来のバージョンでの修正のための)開発元へのフィードバックなどのサポートを有償にて行っております。Firefoxのようなデスクトップアプリケーションだけでなく、サーバー上で動作する各種ソフトウェアについても、フリーソフトウェアの運用でトラブルが発生していてお困りの企業のシステム管理担当者さまがいらっしゃいましたら、メールフォームよりご相談下さい。

*1 そのため、ウィンドウを閉じるにはCtrl-F4などのキーボードショートカットを使うなどの、ボタンを使わない方法を使う必要があります。

*2 タブバーの右端はあらかじめウィンドウコントロールを重ねて表示するために余白領域が設けられていますが、現象発生時には、この余白領域の上ではなく下にウィンドウコントロールが表示されており、「ボタンは見えているのにその手前に透明な壁があってクリックできない」というような状況が発生しているという状況です。

*3 Classicテーマ風のテーマは存在していますが、Windows 7以前のそれとは異なり、単に配色等をClassicテーマ風にするだけの物です。

*4 安全のため、パスにランダムな文字列が含まれる形になっています。

2019-02-15

Gecko Embedded: 60ESR対応のフィードバック

60ESR対応フィードバックの顛末

クリアコードでは Gecko(Firefox)を組み込み機器向けに移植する取り組みを行っています。

この対応にあたっては OSSystems/meta-browser というYoctoレイヤをフォークして作業を行っていますが、対応が落ちついたバージョンについてはアップストリーム(OSSystems/meta-browser)に成果をフィードバックしています。例えば52ESRへのメジャーバージョンアップの対応はクリアコードの成果が元となっています。

さて、60ESRへのメジャーバージョンアップ対応についてもフィードバックをしていたのですが、最後まで自分たちでは解決できなかった問題が一つありました。今回は、OSSにはこんなフィードバックの仕方もあるのだという事例として紹介したいと思います。

meta-rustへの依存

52ESRから60ESRへの更新にあたって一番大きな問題となったのが、Firefox本体にRustで書かれたコードが導入された点です。通常、Firefoxをビルドする際にはrustupというコマンドでツールチェーンを導入します。クリアコードで60ESRのYoctoへのポーティングを開始した当初も、Rustのツールチェーンについてはrustupで導入したものを使用していました。

しかし、Yoctoでは一貫したクロスビルド環境を提供するために、ツールチェーン類も全てYoctoでビルドするのが基本です。このため、RustのツールチェーンについてもYoctoでビルドした物を使う方がよりYoctoらしいと言えます。調べてみたところ、既にmeta-rustというYoctoレイヤが存在していることが分かりましたので、これを利用することとしました。

ターゲット環境用のlibstd-rsを参照できない問題

meta-rustへの対応作業を進めていく中で、Firefoxのビルドシステムがターゲット環境用のlibstd-rsを発見してくれないという問題に遭遇しました。しかし、我々はRustのビルドの仕組みには不慣れであったため、それがどこの問題であるかをはっきりと切り分けることができていませんでした。一方で、meta-rustでのビルドの際に、libstd-rsのみビルド済みのものを導入することで、ひとまずこの問題を突破できることが分かりました。

その他発生していた問題については解決することができ、Firefoxのビルドが全て通るところまでは到達しました。

上記の回避策が正しい修正方法ではないということは分かっていたのですが、ではどこでどう修正をするのが正しいのかという点については、少し調査をしてみた限りでは判断がつかない状態でした。時間をかければいずれは解明できるのは間違いないのですが、我々には他にも取り組まなければいけない問題が山積していたため、その時点では、これ以上時間をかけることはできませんでした。

ひとまずOSSystems/meta-browserに対しては「こういう問題があってまだ調査中だから、マージするのはもうちょっと待ってね」という形でpull requestを投げておきました。

未解決のままマージされる

ところがしばらくすると、 Yocto/Open EmbeddedプロジェクトのKhem Raj氏が、OSSystems/meta-browserのメンテナであるOtavio Salvador氏に働きかけ、上記のlibstd-rsの問題が未解決であるにも関わらず、このpull requestをmasterにマージしてしまいました。上記の回避策はmeta-rustにはマージされていませんので、そのままではビルドを通すことすらできません。私の目から見るとちょっとした暴挙にも見えたのですが、状況についてはKhem Raj氏にも間違いなく伝わっているので、何か算段があるのだろうということでしばらく経過を見守ることにしました。

あとから分かったのですが、ちょうどYoctoの次のリリース向けの開発が始まるタイミングであったため、とりあえずmasterにマージしてCIに組み込んでしまってからの方が問題に取り組みやすいという事情があったようです。しばらく時間はかかりましたが、無事Khem Raj氏から問題を解決するpull requestが投げられました。

我々の方ではmeta-rustとmeta-browserのどちらで対処すべき問題なのかすら切り分けることができていませんでしたが、meta-browserで対処すべき問題だったようです。

本事例の感想

今回の件については、以下のような懸念から、ともすればフィードバックを躊躇してしまいがちな事例ではないかと思います。

  • このような中途半端なpull requestを送ると、アップストリームの開発者から罵倒されたりはしないか?
  • 「この程度の問題すら自分たちで解決できていない」ということを晒すことになるので、恥ずかしいなぁ。

前者については、確かにプロジェクトによってはそのようなpull requestが歓迎されないこともあるでしょう。とはいえ、今回の事例についてはそこそこ工数がかかる60ESRポーティング作業を粗方済ませており、その成果が有益と感じる開発者も多いはずです。また、後者については「聞くのは一時の恥、聞かぬは一生の恥」の良い事例ではないかと思います。結果的にはアップストリームの開発者の協力を得て問題を解決することもできて、成果をより多くの人に使ってもらえる状態にできたので、中途半端でもフィードバックをしておいて良かったなと感じています。

皆さんも「この程度のものは誰の役にも立たないだろう」「この程度のものを世に出すのは恥ずかしい」などと一人で勝手に思い込まないで、手元にある成果を積極的に公開してみてはいかがでしょうか?ひょっとすると、それが世界のどこかで悩んでいる開発者の問題を解決して、感謝してもらえるかもしれませんよ。

2019-01-24

Firefox 64およびFirefox ESR60.4以降で可能になる、ルート証明書の自動インポートの方法

Firefox 52以降で可能になったエンタープライズの証明書の自動インポート機能は、WindowsでActive Directoryのグループポリシー機能を使って配布された証明書をFirefoxから認識できるようになるという物でした。そのため、Windows以外の環境では使用できず、また、WindowsでもActive Directoryを運用していない場合はレジストリを直接編集して証明書を配布する必要がありました。

Firefox 64およびFirefox ESR60.4では新たに、PEM形式の証明書ファイルとして配布されたルート証明書の自動インポートが可能になりました。この機能はプラットフォームを問わず動作するため、LinuxやmacOSを企業で運用する場合にも使用できます。

以下、Ubuntu 16.04LTSの環境でCert Importerアドオンのテスト用のダミーのルート証明書(cacert.pemをインポートさせる場合を例として、具体的な手順を解説します。

ステップ1:証明書ファイルの配布

PEM形式の証明書ファイルは、以下の位置に設置します。

  • Windows:
    • %AppData%\MozillaC:\Users\(ユーザー名)\AppData\Roaming\Mozilla\Certificates
    • %LocalAppData%\MozillaC:\Users\(ユーザー名)\AppData\Local\Mozilla\Certificates
  • macOS
    • /Users/(username)/Library/Application Support/Mozilla/Certificates
    • /Library/Application Support/Mozilla/Certificates
  • Linux
    • ~/.mozilla/certificates
    • /usr/lib/mozilla/certificates

これら以外の位置に置かれた証明書ファイルはインポートできませんので、注意して下さい。

それでは実際に、cacert.pemをダウンロードして、上記のいずれかの位置に設置します。
ここでは ~/.mozilla/certificates の位置に置く事にしました。

$ mkdir -p ~/.mozilla/certificates
$ curl https://raw.githubusercontent.com/clear-code/certimporter/master/doc/cacert.pem > ~/.mozilla/certificates/cacert.pem

ステップ2:証明書ファイルをインポートするためのポリシー設定の作成と配布

以下の形式でポリシー設定ファイルを作成し、policies.json というファイル名で保存します。

{
  "policies": {
    "Certificates": {
      "Install": [
        "ファイル名1",
        "ファイル名2",
        ...
      ]
    }
  }
}

今回は cacert.pem 一つだけをインポートするため、以下のようになります。

{
  "policies": {
    "Certificates": {
      "Install": [
        "cacert.pem"
      ]
    }
  }
}

このファイルを、Firefoxの実行ファイルと同じ位置の distribution ディレクトリに設置します。
具体的には以下の要領となります。

  • Windows:
    • C:\Program Files\Mozilla Firefox\distribution\policies.json など
  • macOS
    • /Applications/Firefox.app/Contents/MacOS/distribution/policies.json など
  • Linux
    • /usr/lib/firefox/distribution/policies.json など

今回の実験環境では ~/opt/firefox-nightly/ 配下に置いたFirefox(Nightly)を使用するため、設置先は ~/opt/firefox-nightly/distribution/policies.json とします。

$ mkdir -p ~/opt/firefox-nightly/distribution
$ cat << END > ~/opt/firefox-nightly/distribution/policies.json
> {
>   "policies": {
>     "Certificates": {
>       "Install": [
>         "cacert.pem"
>       ]
>     }
>   }
> }
> END

ステップ3:Firefoxの起動と結果の確認

以上で証明書の自動インポート機能の準備は完了です。Firefoxを起動すると policies.json に列挙された証明書ファイルが自動的に検出され、ルート証明書としてFirefoxの証明書データベースにインポートされます。

以上の手順でインポートされた証明書は、設定画面の「プライバシーとセキュリティ」配下の「証明書を表示...」ボタンをクリックして開かれる証明書マネージャーの一覧上に現れます(グループポリシーで配布されたエンタープライズの証明書はこの一覧上には現れません)。
先の cacert.pem は「!example」という組織の「example.com」という名前の証明書になっていますので、実際に証明書の一覧に現れている事を確認してみて下さい。

(画像:証明書一覧を表示した所)

まとめ

以上、Firefox 64およびFirefox ESR60.4で可能となった、PEM形式の証明書ファイルによるルート証明書の自動インポートの手順をご案内しました。

Firefox 64では他にもポリシー設定にいくつかの新機能が加わっています。また、ポリシー設定に関する変更はFirefox ESR60へも随時反映されています。5月12日付の記事のポリシー設定の解説に現時点で判明している変更点を反映済みですので、併せてご覧下さい。

また、当社ではFirefoxの法人での利用中に発生した様々なトラブルに対するテクニカルサポートを有償でご提供しています。Firefoxの運用にお悩みのシステム管理者さまは、お問い合わせフォームよりお気軽にお問い合わせ下さい。

タグ: Mozilla
2018-11-30

PhabricatorでのFirefoxへのパッチ投稿方法

以前の記事で、Firefoxへのパッチ投稿の一手段としてMozReviewという仕組みがあることと、その使用方法を紹介しました。しかし、その後すぐにMozillaのコードレビューシステムがPhabricator完全移行してしまい、MozReviewの運用は止まってしまったようです。記事公開時点ではまだMozReviewを使う開発者が大半のように見えていたため、MozReviewの運用停止が近づいているということを把握できていませんでした。

今回は改めてPhabricatorでのパッチ投稿を実践してみたため、その方法を紹介します。

セットアップ

本記事で紹介するセットアップ方法はMozilla Phabricator User Guideを元にしています。詳細はそちらを参照して下さい。

事前に用意するもの
  • BMO(bugzilla.mozilla.org)のアカウント
    • まだBMOアカウントを取得していない場合は https://bugzilla.mozilla.org/createaccount.cgi で作成します。
    • Real nameにはYour Name [:ircnick]のような形で末尾にニックネームを付けておきましょう。
      • Phabricator上ではこのニックネームがユーザー名として表示されます。
      • ただし、既に存在するニックネームは使用できません。
    • Phabricatorにアクセスするためには、Bugzilla側で2段階認証を有効化しておく必要があります。
  • mozilla-centralのワーキングコピー
    • Firefoxへのフィードバックの仕方:Windows編等を参考に、hg clone https://hg.mozilla.org/mozilla-centralで取得して下さい。
    • 本記事ではバージョン管理システムとしてMercurialを使用する場合のみを対象とします
      • PhabricatorはGitでも使用できるようですが、本記事では対象としていません。
Phabricatorへのログイン確認

MozillaのPhabricatorは https://phabricator.services.mozilla.com/ でアクセスできます。
Loginボタンを押して、ログインを試みます。

Phablicatorログイン

認証はBugzilla側で行われるため、以下のようなボタンが表示されます。

Bugzilla認証

最初のアクセス時には、Bugzilla側での認証成功後、Phabricator上でのアカウント登録を促されます。

Phabricatorユーザー登録

アカウントの登録を完了させて、Phabricatorを使用できる状態にしておきましょう。

Arcanistおよびmoz-phabのセットアップ

パッチをコマンドラインから投稿するためには、ArchanistというPhablicatorのコマンドラインツールと、そのラッパーコマンドであるmoz-phabをインストール必要があります。

Archanist

Archanistのインストール方法は以下に記載されています。

Ubuntuの場合は比較的簡単で、依存パッケージをインストール後、ArchanistのGitリポジトリをcloneして、パスを通します。

$ sudo apt install php php-curl
$ mkdir somewhere/
$ cd somewhere/
$ git clone https://github.com/phacility/libphutil.git
$ git clone https://github.com/phacility/arcanist.git
$ export PATH="$PATH:/somewhere/arcanist/bin/"

Windowsの場合のインストール方法は以下に記載されています。

こちらについては筆者の手元で検証できていないため、本記事では省略します。

MozillaBuild を使用している場合の具体的な手順は以下の通りです。

  1. Visual Studio 2017 の Microsoft Visual C++ 再頒布可能パッケージをダウンロードし、インストールする。

  2. Windows版PHPのzipファイルをダウンロードし、展開した物をC:\PHPに置く。(検証はPHP 7.2の「VC15 x64 Non Thread Safe」と書かれている物で行いました)

  3. C:\PHP\php.ini-development の位置にあるファイルを C:\PHP\php.ini にコピーし、以下の通り編集する。

    • ;extension=php_curl.dll または ;extension=curl と書かれた行の行頭の ; を削除する。
    • ;extension_dir = "ext" と書かれた行を extension_dir = "C:\PHP\ext" に書き換える(行頭の ; を削除し、実際のパスを記入する)。
  4. MozillaBuild のシェル上で /c/PHP/php.exe -i | grep curl と実行し、結果に curl という行が含まれている事を確認する。

  5. Windows版Gitをダウンロードし、インストールする。

  6. GitBash を起動し、以下の操作で必要なツールをダウンロードする。

    $ mkdir ~/phabricator
    $ cd ~/phabricator
    $ git clone https://github.com/phacility/libphutil.git
    $ git clone https://github.com/phacility/arcanist.git
    
  7. MozillaBuild で以下のコマンド列を実行し、各ツールにパスを通す。

    $ echo 'export PATH=${PATH}:/c/PHP:${HOME}/phabricator/arcanist/bin' >> ~/.bash_profile
    $ echo 'export EDITOR=/usr/bin/vim' >> ~/.bash_profile
    $ source ~/.bash_profile
    

    MozillaのドキュメントにはテキストエディタとしてVim以外を使用する場合の手順も書かれていますので、他のテキストエディタを使いたい場合はそちらも併せて参照して下さい。

Ubuntu、Windowsのそれぞれの方法でArchanistのインストールが完了したら、APIキーを設定します。mozilla-centralのソースディレクトリ下で以下のコマンドを実行します。

$ arc install-certificate

ブラウザでAPIキーが表示されますので、そのAPIキーをコマンドラインにコピー&ペーストすると、APIキーが取り込まれます。
APIキーは以下のようなJSON形式で~/.arcrcに書き込まれます。

{
  "hosts": {
    "https://phabricator.services.mozilla.com/api/": {
      "token": "xxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  }
}
moz-phab

moz-phabのインストール方法は、moz-phabREADME.mdに記載されていますので、そちらを参照して下さい。

単にmoz-phabコマンドをパスの通ったディレクトリにコピーし、実行権限を付けるだけで良いようです。以下はその操作例です。

$ cd ~/phabricator
$ git clone https://github.com/mozilla-conduit/review.git
$ echo 'export PATH=${PATH}:${HOME}/phabricator/review' >> ~/.bash_profile
$ source ~/.bash_profile

レビューリクエストの作成

以前の記事で紹介したように、Firefoxで何かパッチを投稿したい場合は、全てBugzilla上の該当Bugを起点に作業を行います。何か新機能を追加したり、不具合修正を行いたい場合は、まず該当するBugが既に存在するかどうかを確認し、無い場合は新たに自分で新しいBugをfileします。

該当Bugでソースコードに対して何か変更を行って、いざPhabricatorにパッチを投稿したいという状況になった場合、まずはMercurialで手元のリポジトリに変更をコミットします。

$ hg branch fix-bug-xxxxx
$ hg commit

このとき、コミットメッセージの形式に注意しましょう。具体的には以下のような形式にする必要があります。

Bug [Bug番号] - [Bugの概要(一行)]

以下、Bugの詳細についての記述...

Mercurialでリモートリポジトリにpushする際、上記のコミットメッセージのBug番号から自動的にBugzillaの該当Bugにパッチが投稿されます。

また、末尾にr?ircnickという形式でレビュアーを指定すると、push後に自動的に該当レビュアーにレビューリクエストを投げることもできます。このレビュアーの指定は、パッチを送信した後にPhabricatorのWeb UIから行うこともできますので、必ずしもコミットメッセージに含める必要はありません。

以下に、筆者が実際にパッチを投稿した際のコミットメッセージを示します。

Bug 1502786 - Break cycle between PureOmxPlatformLayer and OmxDataDecoder r?jya

OmxDataDecoder, OmxPromiseLayer and PureOmxPlatformLayer consist
circular reference by RefPtr, and no one sever the reference. As a
result their refcount never decrease to 0.
This commit sever it at PureOmxPlatformLayer::Shutdown() which is
called by OmxDataDecoder.

詳細な議論はBug番号から辿ることができるため、コミットメッセージには必ずしも詳細な記述は必要ないようです。有った方が好ましいとは思いますが、慣れていない場合には、まずはBug番号と一行サマリを適切に記載することに注力すると良いでしょう。

ローカルリポジトリへのコミットが完了したら、リモートリポジトリにsubmitします。

$ moz-phab submit

submitが完了した後、先ほどコミットした内容をhg exportで確認してみると、以下のようにDifferential Revision:という行が追加されていることに気が付きます。

# HG changeset patch
# User Takuro Ashie <ashie@clear-code.com>
# Date 1541472583 -32400
#      Tue Nov 06 11:49:43 2018 +0900
# Node ID 25c8e78baa9aa8189ca7026d7ac7868c69d483f3
# Parent  9f9a9234959f114825f58beee0cffbab82d0bb29
Bug 1502786 - Break cycle between PureOmxPlatformLayer and OmxDataDecoder r?jya

OmxDataDecoder, OmxPromiseLayer and PureOmxPlatformLayer consist
circular reference by RefPtr, and no one sever the reference. As a
result their refcount never decrease to 0.
This commit sever it at PureOmxPlatformLayer::Shutdown() which is
called by OmxDataDecoder.

Differential Revision: https://phabricator.services.mozilla.com/D10028

...

この行はレビュー結果を受けてパッチを修正する際に必要になります。また、この行に記載されているURLをブラウザで開くと、Phabricator上でレビューリクエストを参照することができます。以後、レビュアーとのやりとりはこのページで行うことになります。

パッチの修正

レビュアーによってパッチがレビューされ、Phabricator上で修正箇所を指摘されたら、パッチを修正して再度Phabricatorにsubmitすることになります。この際、同一のレビューリクエストに対する修正であることを指定するために、先ほどと同じDifferential Revisionをコミットメッセージに含めてhg commitし、moz-phab submitします。

Mercurialでのパッチ管理方法は本記事のスコープ外のため割愛しますが、パッチ(コミット)が1つのみで、ローカルリポジトリに過去のバージョンが不要である場合、もっとも簡単な修正方法はhg commit --amendで前回のコミットをやり直す方法でしょう。この方法の場合、コミットメッセージは特に修正しなければ前回のままとなりますので、Differential Revisionも前回と同じものが使用されます。ローカルリポジトリの修正は上書きされてしまいますが、リモートリポジトリ上では過去のバージョンも管理され、その差分を確認することもできます。

Phabricator diffリビジョン

修正をsubmitしたら、Phabricator上でその修正に対応するコメントの「Done」にチェックを入れ、レビュアーのコメントに返信をします。この際も、最後にSubmitボタンを押すことを忘れないで下さい。なお、MozReviewの時とは違い、Phabricator上での会話が自動的にBugzillaにも投稿されるという機能は無いようです。

Bugzilla上でパッチを添付する場合とは事なり、Phabricator上でレビュアーの情報が紐付けられているため、変更の度に改めてレビュー依頼をし直す必要はありません。再度レビューしてもらえるのをおとなしく待ちましょう。

レビューが通ったら

レビュアーによってパッチに問題ないと判断された場合、以下のようにAcceptedのマークが付きます。

Phabricator Accepted

この状態になったら、パッチのランドが可能になります。Mozilla Phabricator User GuideのLanding Patchesの項によると、パッチのランドにはLandoというシステムを使うことを強く推奨するとなっていますが、mozilla-centralへのコミット権限が無い場合、このシステムを使用することはできません。実際に試してみたところ、以下のように弾かれてしまいました(筆者の権限はLevel 1)。

Landoエラー

コミット権限が無い場合は、これまでと同様に、Bugzilla側で「Keywords」欄にcheckin-neededというキーワードを付加しておいて、権限のある開発者にコミットしてもらえば良いようです。この際、Bugzilla側ではレビュー承認済みであるr=ではなくレビューリクエスト中であるr?のマークのままになっていることがあるようですが、Phabricator側でAcceptedになっていれば、構わずcheckin-neededにしてしまって問題無いようです。

Accept後のパッチ修正

単にAcceptされただけであればそのままランドしてしまえば良いだけですが、場合によっては「Acceptするけど、こことここだけは修正しておいてね」と言われる場合があります。この場合はAcceptedのマークは付きますが、パッチは修正して再度送信する必要があります。すると、マークが以下のように変わります。

Phabricator Other Diff Accepted

この場合、修正版のパッチを再度レビューしてもらう必要があるのか疑問に思うところでしょう。結論から言えば、特にレビューしてもらう必要は無いようです。自分で修正できたと判断すれば、そのままランドしてしまうことができます。ただし、指摘された箇所は全て「Done」にチェックを入れておきましょう。

Phabricator Doneフラグ

この時も、「Done」のチェック後にSubmitボタンを押す必要があります(チェックを付けただけでは送信されません)。

Backoutされたパッチの修正

一旦ランドされたパッチが自動テストの失敗によってBackoutされる事もあります。ここのような場合、Phabricator上のパッチは既に一旦Closedになってしまっているため、そのままでは修正を継続できません。

FAQによると、このようなケースでは以下の手順でパッチをReopenする必要があります。

  1. ページ最下部のコメント入力欄までスクロールする。
  2. 「Add Action...」のドロップダウンリストを開き、「Revision Actions」配下の「Reopen Revision」を選択する。
  3. 「Submit」をクリックしてアクションを確定する。

この操作を行うとパッチが「Accepted(レビュー完了済み)」の状態に戻りますので、hg commit --amendでパッチを修正してmoz-phab submitし、再度レビュアーの反応を待つ事になります。

まとめ

PhabricatorでのFirefoxへのパッチ投稿方法について紹介しました。

なお、本記事内で紹介した実例はBug 1502786: Memory leaks in OpenMAX PDMになります。以前Firefox本体にフィードバックしたOpenMAX IL対応パッチにバグがあることを発見したので、その修正を再度フィードバックしています。

元となるOpenMAX対応パッチについては、特にレビュアーを指定せずにとりあえずMozReviewで上げてみただけだったのですが、Mozillaの開発者の目に止まって勝手にレビューされ、本体にマージされるところまで進みました。やはりコードレビューシステムで登録しておいた方が開発者としてもレビューが捗るのかもしれませんね。

2018-11-15

Tokyo WebExtensions Meetup #3

クリアコードでFirefoxやThunderbirdの法人サポートに従事している結城です。

去る10月30日、御茶ノ水・デジタルハリウッド大学院の1教室をお借りして、Firefox(やその他様々なブラウザ)の拡張機能に関心を持つ人達の交流イベントであるTokyo WebExtensions Meetup #3が開かれました。日にちが日にちだったため、会場にはハロウィン的な仮装のための小道具が用意されていたり、Mozillaのスポンサードにより提供されたピザがハロウィン仕様(黒い生地にオレンジのチーズ)だったりと、ささやかながらお祭り気分を味わいつつの進行でした。

筆者も発表者の一人として参加しましたので、このエントリでは発表の内容の概要をお伝えします。

発表1:タブの複数選択APIのつかいかた

最初は筆者による、Firefox 63以降で使える「タブの複数選択機能」に関する発表です。発表資料のスライドはQiitaにて公開中です。また、公開に際しては口頭で発表した詳細な説明も書き加えてあります。

タブの複数選択機能はFirefox 64からの新機能(予定)ですが、基本的な機能はFirefox 63の時点でも既に使える状態になっています。about:configを開いてbrowser.tabs.multiselectを検索し、値をtrueに設定すれば、機能が使えるようになります*1

筆者は予てよりマルチプルタブハンドラというアドオンを個人的に開発しています。これはFirefox 56以前においては、Firefoxのタブにまさにそのような性質を加える物でした。しかしWebExtensionsではFirefox自体のタブの振る舞いを変えることはAPIの制約上できません。そのため、WebExtensions移行に際しては、このアドオンの内部でタブの選択状態を管理し、browser.runtime.sendMessage()による明示的なAPI呼び出しに対してその情報を返すという形で、他のアドオンに対して「タブの複数選択」相当の事ができるようにするAPIプロバイダのように振る舞う*2アドオンとして再出発したという経緯があります。

今回、Firefox本体にタブの複数選択機能が入り、同時にその機能を呼び出すためのWebExtensions APIが追加された事によって、アドオン同士がお互いの存在を知らないまま暗黙的に連携しあえる場面がまた一つ増えたと言う事ができます。また、次のリリースとなるFirefox 64ではタブやブックマークのコンテキストメニューに機能を追加するアドオン同士の暗黙的な連携も可能になる予定です。このように暗黙的な連携が図られる場面が増えることで、各アドオンが単独で動作するだけの状態に比べて、機能の有用さは何倍にも高まります。

XUL時代のアドオンの有用性を支えていた要素の一つであったその性質が、WebExtensionsへの移行で大きく失われてしまった、という体験をした身としては、今後もこういった形でアドオン同士の暗黙的な連携が促進されるような改良が続く事を切に期待するばかりです。

発表2:Webページ中のテキストの動的な置換

2番目の発表は、あきみね氏による「Webページ中に現れる特定の文字列を、任意の文字列に強制的に置き換える」アドオンの紹介でした。

このアドオンでは、コンテントスクリプトでDOMツリーを走査して個々のテキストノードを編集するという基本の処理に加えて、MutationObserverを使って動的に挿入されるテキストも編集するという工夫により、Facebookなどのような「スクロールに応じてページの内容が次々と継ぎ足されていく」形式のWebサービスにも対応できるようにした事が特長であるという紹介がありました。

ただ、Webページ中の全テキストノードを走査するという事は、それだけ処理に時間がかかるという事でもあります。そのため、処理に時間がかかるようなページのための対策には悩まれているそうです。

これについて、browser.findのAPIを使用すればより効率よく指定の文字列の置き換えができるのではないかという情報提供を筆者から行いましたが、このAPIは今のところFirefoxが対応しているのみのため、Chromeの使用を考えると残念ながら使う事はできないとの事でした*3

ブラウザを問わずに行えそうな別方向での最適化としては、worker-domを活用するという方法も考えられます。これはDOMツリーを丸ごとWorker上に再現して、ボトルネックになりがちなDOMの操作を別スレッドに逃がしてしまうという試みです。一般的なWebページの処理にどこまで使えるかというのは未知数ですが、もし使えるのであれば、Webページの内容と密接に関わる種類のアドオンの大幅な高速化が可能になるかも知れず、興味深い技術です。

発表3:WebExtensionsとテスト

3番目の発表は、Firefox上でvim風の操作体系を実現するアドオンの一つであるVim Vixenの作者のUeoka氏による、アドオンの自動テストの実現方法についてのお話でした。
こちらは発表資料がSpeakerDeckにて公開されています

Vim Vixenの開発にあたっては一般的なJavaScriptでのアプリケーション開発のノウハウが活かされていて、自動テストもKarmaMochaWebExtensionsの各種APIをモックするライブラリを組み合わせて大量のユニットテストを行っているそうです。また、自動テストやESLintによる検証をCircleCIで継続的に実行されているとの事でした。

プロダクト自体を自動テストが容易になるように設計するという事は、純粋なロジック部分と外部要因になる部分とをなるべく切り離すという事です。例えばタブを指定の条件でフィルタリングしてリストアップするためのフィルター処理自体browser.tabsに一切触れておらず、その自動テストも純粋なロジックのみに特化して検証できています。Vim Vixenでは全体をこのように設計し、テストをなるべくしやすいようにされているとのことでした。

発表の後半はE2Eテスト(ユーザーの実操作に近いテスト)にフォーカスした話でした。Vim Vixenでは163あるE2Eテスト項目のうち現時点で45件を自動化できているとのことですが、その実現にあたっては、ambassadorと名付けられたテスト実行専用のアドオンを使われているそうです。WebExtensionsベースのアドオンの開発・テスト支援ツールであるweb-extの改造版を用いてVim Vixenとambassadorを同時に読み込ませ、テストケース内で指示された操作をambassadorで行ってVim Vixenの動作をテストしているとの事でした。

ユーザーの入力を再現するような箇所は非同期処理が多くなりがちですが、スクロール操作のE2Eテストなどを見ると分かるように、非同期処理が絡む自動テストもawaitを使うとシーケンシャルに記述できます。async/awaitはテストの実装にも有用という事を示す一例と言えるでしょう。

Vim Vixenはコンテンツ領域内にUIを挿入する設計のためこのようなテスト手法が可能となっているそうですが、言い換えると、サイドバーやツールバー上のポップアップ内でUIを提供するアドオンについては、このアプローチでのテストは行えないという事でもあります。Ueoka氏自身も、より広い範囲のE2Eテストを自動化する方法について試行錯誤を続けられているそうです。

アドオンの自動テストについては、Firefox本体におけるWebExtensions APIそのものの自動テストの実装方法が参考になりそうな所ではあるのですが、Firefox内のWebExtensions APIのテスト群は専用のテストランナー上で専用のユーティリティを使って実行する形を取っており、しかもその内容はFirefoxの名前空間上で動作するという、現状のFirefoxの実装に強く依存した形式となっています。web-extやその後継ツールには、Firefox本体の内部事情に詳しくなくても同等の事ができるような自動テスト支援の仕組みを提供してくれる事が期待されます。

ambassadorや実際の自動テストも含めて、この発表の中で紹介された要素はすべてVim Vixenのリポジトリで公開されています。拡張機能の自動テストに関心がある方は必見ですね。

11月8日追記:発表をされたUeokaさんご自身による発表内容の解説記事も併せてご覧下さい。

発表4:開発ツールを拡張しよう!

最後の発表は、Mozillaで主にJavaScriptエンジンの開発を担当されているarai氏による、Webページのソースを任意の内容で置き換えるDITMというアドオンの紹介でした。

DITMとは「Developer In The Middle」の略ですが、これは「Man In The Middle Attack(中間者攻撃)」という攻撃手法をもじった物です。

MITM攻撃では、ユーザーが操作するクライアントとWebサーバの間に何らかの方法で中継者が入り込み、ユーザーが送るリクエストの内容を書き換えてWebサーバーに送ったり、Webサーバーから返された内容を書き換えてクライアントに送ったりすることで、ユーザーが入力したパスワードやクレジットカード番号などの機密情報を盗み出したり、ユーザーを望ましくない行動に誘導したりといった事が行われます。昨今のいわゆる「常時SSL化」は、このような攻撃を防ぐ事を目的の1つとしています。

このようにMITM攻撃を防ぐ事のできるSSL(TLS)ですが、アドオンの権限ではTLSで通信して取得した後の内容を任意の物に置き換える事ができます*4。これを応用して、Webサーバー上のファイルを実際に編集せず手元で書き換えてデバッグするという事を可能にするのが、DITMです。

発表では、「Minifyされたライブラリのソースを一旦スクラッチパッドに貼り付けてpretty printし、それを元のファイルの代わりに使うようにDITMで設定した上で、任意のコードをそこに追加して実行してみる」というデモが行われていました*5。「Firefoxでだけ動かないページ」に遭遇した時の原因調査などに活用できるかもしれません。

DITMの主要な機能はbrowser.webRequest.filterResponseData()によって実現されているため、Web上のコンテンツをクライアント側で置き換える*6実装の例と言う事ができます。また、開発ツールに独自のタブを追加する例としても参考になるでしょう。

次回以降のイベントについて

メインの発表が終わった後は、このイベント自体の今後の方向性についてヒアリングも兼ねたディスカッションが行われました。以下はその中で聞かれた意見です。

  • 一般的なセミナー形式ではなくワークショップをやりたい。
    • アドオンを作ってみたい、という初心者向けの内容はどうか。
    • 既存のアドオンの不具合の修正や機能追加のプルリクエスト、WebExtensionsに移行できていないアドオンの移行をやってみるのはどうか。
    • 日本語ロケールが無いアドオンを翻訳してプルリクエストするのはどうか。
    • Firefox本体に対するWebExtensionsの新しいAPIの提案や、実装が行われないまま停止しているAPI案へのパッチの提供など、やや高度な事に挑戦してみるのはどうか。
    • モブプログラミング形式(手を動かす人一人に対し周囲に複数人が集まって意見を出し合う形式)にしてはどうか。
  • 温泉旅館などでハッカソンのように泊まりがけてディープな事をやってみたい。
  • セミナー形式のイベントにニーズが無いわけではない。今回の発表も刺激的な内容だった。

ひとまず、次回開催は1月下旬から2月上旬頃を目標として、どのようなイベントが望まれているかのアンケートなどを近日中にMozilla Japan コミュニティのSlackで行う事を予定しています。次回の参加を検討されている方は、自分が参加したいと思えるイベントにしていく機会として、是非Slack上で意見を表明してみて下さい。

また、「OSSであるFirefoxやアドオンにフィードバックしてみたいが、やり方が分からなかったり、恐怖感があったりして、一人でやるのは尻込みしてしまう」という人向けに、Firefox関係に限らず広くOSS一般を対象にしたOSS Gateという取り組みも行っています。こちらはワークショップが定期的に開催されていますので、開催予定のイベント一覧から、参加しやすいエリア・日次のワークショップを探してビギナーとして参加してみてはいかがでしょうか。

まとめ

以上、Tokyo WebExtensions Meetup #3で行われた発表やディスカッションの内容をご紹介しました。

*1 なお、タブの複数選択機能はChromeには遅くとも2013年頃の時点で既に実装されていた模様です。

*2 とはいえ実際には、大多数の他のアドオンはこのアドオンの存在自体を知らないために、明示的な連携が図られる事はほぼ無く、「アドオン同士の連携」といってもほとんど絵に描いた餅に過ぎないという状態が長く続いていました。

*3 同様の事をクロスブラウザで行う物ための技術としては、かつてW3Cで提案されていたRangeFinder APIという仕様もありますが、こちらは現在の所実装しているブラウザは存在しません。

*4 そのため、怪しいアドオンを不用意にインストールするのは非常に危険な事と言えます。

*5 Minifyやコンパイルされたコードに対するデバッグ方法としては、変換前のソースとの対応表を提供するSource Mapという技術もあります。Source Mapが提供されていない場合でもそれと同様の事を行えるというのが、DITMの利点の一つという事になります。

*6 あきみね氏の発表はDOMツリーが構築された後の置き換えなのに対し、arai氏のDITMはDOMとして解釈されるより前の時点での置き換えという違いがあります。

タグ: Mozilla
2018-11-02

Firefoxのメモリ消費量が右肩上がりで増加する場合の対策

FirefoxをWindows Serverベースのシンクライアント環境で使っている組織において、Firefoxを更新したりWindows Serverをリプレースしたりといったタイミング以降で、急にパフォーマンスが低下しだしたというお問い合わせを複数頂きました。調査の結果、それらの現象は同一の原因である可能性が高い事、同様の対策が有効に作用する事が分かりましたので、その際に得られた調査結果を知見としてご紹介します。

なお、本エントリで紹介する設定はすべてFirefox ESR52とFirefox ESR60向けの物です。 将来のバージョンのFirefoxでは有効でない可能性がありますので、ご注意下さい。

パフォーマンス低下の原因

お問い合わせを頂いた複数の事例において、サーバー全体のリソース状況を監視して頂いた所、業務の開始と共にメモリの消費量が右肩上がりに増大しており、退勤時刻になってユーザーがログオフし始めるまでメモリ不足の状況が続くという状況である事が分かりました。また、この時メモリを最も消費しているのはFirefoxのプロセスであるという事も判明しました。

(メモリ消費量の傾向のイメージ)

いくつかの事例では、FirefoxやWindows Serverのリプレース以前にはこのような現象は発生していなかったという事が分かっていました。そのため、これは単純にメモリ消費量が増大したというよりも、 使い終わったメモリ領域がいつまでも開放されないという問題(いわゆるメモリリーク) であると考えられました。

一般に、プログラムの動作速度と消費リソースの量は比例します。現在のFirefoxは体感速度の向上に努めているため、キャッシュを多めに保持したり、複数のプロセスを並行動作させたりといった形でリソースの消費が多くなりがちです。その過程で必ずしも必要でないような大量のメモリ領域を確保する事があり、それが何らかの理由で解放されないままとなってしまうと、物理メモリの枯渇に繋がり、ディスクドライブ上の仮想メモリとの間でスワップが頻発して却ってパフォーマンスが悪化してしまう事になります。

よって、この問題を解消するためには何らかの方法でメモリ消費量を削減する必要があります。

端的にメモリ消費量を抑える

Firefoxが消費するメモリの上限を強制的に設定する事はできないのでしょうか? 実は、できます。 Firefox全体での消費メモリ量の上限は設定できませんが、FirefoxのJavaScript実行エンジンが使用できるメモリの量は上限を設定できるようになっており、例えば以下のようにするとJavaScript実行エンジンの使用可能なメモリの上限が1GBに設定されます。

// 単位はbytes。
// 1GB=1024MB、1MB=1024KB、1KB=1024bytesのため、
// 1024 * 1024 * 1024 で1GBを表す。
lockPref("javascript.options.mem.max", 1024 * 1024 * 1024);

このlockPref()という書き方は、MCDと呼ばれる設定方法に特有の物です。Windows Serverであれば、そのマシン上のFirefoxのインストール先に上記の内容を記載したautoconfig.cfgを設置することで、そのWindows Severにログオンするすべてのユーザーに設定が反映されます。

ただし、この方法で上限を設定した場合に上限を超える量のメモリが必要な状況が発生すると、Firefoxがクラッシュしたり、フリーズしたり、Webページを表示できなくなったりといったトラブルが発生する可能性があります。 現代のWebページは1ページあたり数百MBのメモリを消費する事も多く、安心して動作できるだけの数値を設定すると、多人数でリソースを共有するWindows Serverにおいては結局は各ユーザーで物理メモリを容易に食い潰してしまうという結果になるでしょう。 ですので、この設定は今回のような事例では実際には有用ではありません。

Firefoxのプロセス数を減らす

安全に行える範囲の設定では、マルチプロセス機能の無効化がメモリ消費量の削減にある程度有効です。

現在のFirefoxは、体感速度や安定性、セキュリティを向上する事を目的に、処理を複数のプロセスで並行して行う設計となっています。 しかし基本的にFirefoxはそれなりの量のメモリを消費する設計のため、プロセス数が増えれば増えるほどその分余計なメモリを消費する事になります。 マルチプロセス機能を無効化すると、前述の利点を失う事と引き替えにメモリ消費量を削減する事ができます。

マルチプロセス機能についてはFirefoxの設定画面からもある程度の設定は変更できますが、抜本的な無効化には隠し設定の変更が必要です。 前述のMCDで行う場合は以下のようになります。

// マルチプロセス機能の無効化
lockPref("browser.tabs.remote.autostart", false);
lockPref("browser.tabs.remote.desktopbehavior", false);
lockPref("dom.ipc.multiOptOut", 1);
lockPref("browser.tabs.remote.force-enable", false);

ただ、メモリ消費量の増大がメモリリークに起因している状況では、これも根本的な対策とはなりません。 プロセス数が少ない分、メモリ消費量の増大ペースは緩やかになる事が予想されますが、それでもいつかはメモリの枯渇が発生します。

開けるタブの最大数を制限する

プロセス数の削減と似た対策として、Firefox上で開けるタブの最大数を制限するという方法もあります。 これはFirefox本体の設定のみでは不可能で、Lean Tab Limiterというアドオンを導入する事で行えます。 全体向けに導入する場合、ポリシー設定でpolicies.Extensions.Installを設定するか、特定のフォルダにファイルを設置することによるサイドローディング若干情報が古い日本語での解説)でインストールする必要があります。

ただし、2018年10月24日現在公開されているLean Tab Limiter 0.1は管理者による設定の変更に対応していないため、タブの最大数の制限は各ユーザーが設定しなくてはなりません。 管理者側でタブの最大数を設定したい場合は、Storage Manifestを使った管理者による設定に対応した改造版であるLean Tab Limiter Advancedを使う必要があります。以下は、タブの最大数を5に設定しつつ、制限を超えた数のタブを開こうとした場合に通知メッセージを端的に表示する場合の設定例です。

{
  "name": "lean-tab-limiter-advanced@clear-code.com",
  "description": "Managed storage manifest for lean-tab-limiter-advanced",
  "type": "storage",
  "data": {
    "tab-limit": 5,
    "notify-blocked": true,
    "notify-blocked-title": "%s+1個以上のタブ及びウィンドウは開けません",
    "notify-blocked-message": " "
  }
}

原因を踏まえてメモリの過大な消費を抑制する

Firefoxにおいてメモリリークに類する現象が発生する場合、ガーベジコレクションが適切に実行されていない事が原因となっている場合があります。

ガーベジコレクションはJavaScriptなどの言語において不要になったメモリ領域の回収・解放を行う仕組みですが、ガーベジコレクションを行うためには基本的に他のすべてを停止する必要があるため、単純に頻繁に実行すると体感のパフォーマンスが低下します。 そのため初期状態では、一定時間以上操作が行われていなかったり、改修が可能そうなメモリの量がある程度以上の量溜まっていると検出されたり、といった条件が満たされた時に初めて実行されるという設定になっています。 よって、何らかの理由からガーベジコレクションの実行条件が満たされにくい状態にあると、未使用メモリが回収されないまま新たにメモリが確保されるという事が繰り返されるために、見た目のメモリ消費量が右肩上がりに増大してしまうという状況が発生します。

また、Firefoxの見た目のメモリ消費量が急上昇する背景には、Firefoxの「こまめにメモリ領域を確保するのではなく、ある程度のまとまった量のメモリ領域を先んじて確保しておき、必要に応じてFirefox内部でそれを小分けにして使う」という戦略があります。 これは、OSに対してのメモリ確保要求は一般的に時間がかかる事が多いため、このような戦略をとった方が体感速度の向上に繋がるという理由によるものですが、この事とガーベジコレクションが実行されないという現象が重なると、見た目のメモリ消費量が異常な速度で上昇していくという状況が発生します。

よって、以下のような設定を行えば、体感速度の低下と引き替えにメモリ消費量の異常な増大を防ぐ事ができると考えられます。

  • ガーベジコレクションの実行頻度を上げる。
  • 一度に確保するメモリ領域の大きさを小さくする。

実際に、デスクトップPCに比べて搭載メモリ量が小さいAndroid端末用の初期設定は、Firefox 52時点ではそのような趣旨に沿った内容になっています*1

以上の事を踏まえての、効果が大きく且つ体感速度の低下度合いが小さいと考えられる設定項目とその設定値の例は以下の通りです。

// GCを実行する基準にするメモリ消費量(単位:MB)。0〜1000の間で設定。
// 数値を小さくする事で、GCの実行頻度を上げ、
// パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。
// 初期値は30。
lockPref("javascript.options.mem.gc_allocation_threshold_mb", 3);

// 高頻度でGCとメモリ領域の確保が実行される場面での、最大の
// メモリ確保増加量(パーセンテージ)。
// 値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を
// 削減できる可能性がある。
// 初期値は300。
lockPref("javascript.options.mem.gc_high_frequency_heap_growth_max", 150);

// 高頻度でGCとメモリ領域の確保が実行される場面での、最小の
// メモリ確保増加量(パーセンテージ)。
// 値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を
// 削減できる可能性がある。
// 初期値は150。
lockPref("javascript.options.mem.gc_high_frequency_heap_growth_min", 150);

// 高頻度でGCとメモリ領域の確保が実行される場面での、最大の
// メモリ確保増加量(単位:MB)。
// 値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を
// 削減できる可能性がある。
// 初期値は500。
lockPref("javascript.options.mem.gc_high_frequency_high_limit_mb", 100);

// 高頻度でGCとメモリ領域の確保が実行される場面での、最小の
// メモリ確保増加量(単位:MB)。
// 値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を
// 削減できる可能性がある。
// 初期値は100。
lockPref("javascript.options.mem.gc_high_frequency_low_limit_mb", 100);

// GCを行うしきい値となるゾーンごとのGC対象のメモリ使用量(単位:MB)。
// 数値を小さくする事で、より頻繁にGCが実行されるようになり、
// パフォーマンス低下と引き替えにメモリ使用量を削減できると考えられる。
// 初期値は128。
lockPref("javascript.options.mem.high_water_mark", 16);

また、まだお客様環境への反映実績はないものの、Firefox ESR60の時点でデスクトップ版とAndroid版の間で初期設定が変更されている項目として以下の設定も有効である可能性があります。

// GC対象になりやすい短命なオブジェクトを保持するためのメモリ領域の最大サイズ(単位:KB)。
// 数値を小さくする事で、単純に必要なメモリ領域の量を削減できると考えられる。
// また、空きメモリ領域がこの値より小さい時は常にアイドル時にGCが実行される。
// デスクトップ版Firefoxでの初期値は16384、Android版の初期値は4096。
lockPref("javascript.options.mem.nursery.max_kb", 4096);

実際に、お問い合わせがあったお客様の環境ではこれらの設定によってメモリ消費量の右肩上がりの増大が収まり、旧環境とほぼ同水準のメモリ消費量に収まるようになったという結果が得られています。 しかしその一方で、消費リソースを削減するという事から事前に予想された通り、体感速度の低下も発生しており、ユーザー体験の悪化が許容範囲内となるバランスを模索する必要があったようです。 本エントリを参考に設定を反映する場合でも、上記の設定を叩き台として、ピーク時にもFirefox以外のアプリケーションを実行できる程度の空きメモリ領域を確保できる程度の範囲で、なるべく体感速度が損なわれないよう数値を調整する事を強くお勧めします。

その他、メモリ消費量と体感速度に関係している可能性がある設定

なお、Firefox ESR52を対象とした調査の過程で、メモリ消費量と体感速度に影響を与える可能性がある設定項目として、前項に挙げた項目の他にも以下の物がある事が分かりました。 それぞれどの程度の効果があるかは不明ですが、参考として記載しておきます。

  • javascript.options.asmjs(初期値:true):asm.jsのコンパイラを有効にする。無効化する事で、パフォーマンス低下と引き替えに若干メモリ消費が減る可能性がある。
  • javascript.options.baselinejit(初期値:true):Firefox上で実行されるJavaScriptの全てについて、実行時コンパイルを行い処理を高速化する。無効化する事で、パフォーマンス低下と引き替えに若干メモリ消費が減る可能性がある。
  • javascript.options.compact_on_user_inactive(初期値:true):ユーザーが離席している時にGCを行う。無効化するとGCの実行頻度が下がる。
  • javascript.options.compact_on_user_inactive_delay(初期値:300000):ユーザーが離席していると判断する時間(ミリ秒)。初期値は30秒だが、これを小さくするとより頻繁にGCが実行され、メモリ消費が減る可能性がある。
  • javascript.options.discardSystemSource(初期値:false):コンパイルした関数のソースをメモリ上から破棄するかどうか。有効化する事でメモリ消費が減る可能性があるが、Function.prototype.toSource()が機能しなくなるため、この機能を使用しているスクリプトを実行する場合は期待通りに動作しなくなる恐れがある。
  • javascript.options.gc_on_memory_pressure(初期値:true):空きメモリが逼迫して充分な使用メモリ領域を確保できなかった時にGCを実行するかどうか。無効化するとGCの実行頻度が下がる。
  • javascript.options.ion(初期値:true):Firefox上でのJavaScriptの実行時コンパイルについて、新型のエンジンであるIonMonkeyを使用する。無効化する事で、パフォーマンス低下と引き替えに若干メモリ消費が減る可能性がある。
  • javascript.options.ion.offthread_compilation(初期値:true):IonMonkeyにおいて別スレッドでコンパイルを実行する。無効化する事で、パフォーマンス低下と引き替えに若干メモリ消費が減る可能性がある。
  • javascript.options.mem.gc_compacting(初期値:true):GCの実行時に使用メモリの縮小も行うかどうか。インクリメンタルGCの場合には影響しない。無効化するとメモリ使用量が増大する可能性があり、変更しない事が望ましいと考えられる。
  • javascript.options.mem.gc_dynamic_heap_growth(初期値:true):使用メモリ領域を確保する時の一度に確保するメモリの量を動的に変えるかどうか。無効化すると「300%」固定になる。使用メモリを最小化する事を優先する場合、300%固定にされるよりは、動的な計算でgc_low_frequency_heap_growthgc_high_frequency_heap_growth_maxで上限を110%などに設定した方が有効と思われる。
  • javascript.options.mem.gc_dynamic_mark_slice(初期値:true):高頻度でメモリ領域の確保が行われている時に、インクリメンタルGCにおいて処理単位を大きくする。無効化するとインクリメンタルGCの効率が落ちメモリ使用量が減りにくくなると予想される。
  • javascript.options.mem.gc_high_frequency_time_limit_ms(初期値:1000):GCとメモリ領域の確保が頻繁に行われていると判断する基準の時間(ミリ秒)。値を大きくすることで、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。
  • javascript.options.mem.gc_incremental(初期値:true):インクリメンタルGC(段階的なGC)を行うかどうか。無効化するとGCの実行頻度が下がりメモリ使用量の増大に繋がる可能性がある。
  • javascript.options.mem.gc_incremental_slice_ms(初期値:10):インクリメンタルGCを行う時間間隔(ミリ秒)。既に充分小さい間隔なので、間隔を小さくして頻度をこれ以上上げてもメモリ使用量の削減という効果はあまり変わらないと予想される。逆に、間隔を大きくして頻度を下げるとメモリ使用量が増大すると考えられる。以上の事から、変更しない事が望ましいと考えられる。
  • javascript.options.mem.gc_low_frequency_heap_growth(初期値:150):低頻度(1秒に1回レベル)でGCとメモリ領域の確保が実行される場面での、メモリ確保増加量(パーセンテージ)。値を小さくする事で、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。
  • javascript.options.mem.gc_max_empty_chunk_count(初期値:30):GCに使用するメモリ領域について、キャッシュ的に確保しておく領域の最大数。小さい値を設定すると、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。
  • javascript.options.mem.gc_min_empty_chunk_count(初期値:1):GCに使用するメモリ領域について、キャッシュ的に確保しておく領域の最小数。小さい値(0)を設定すると、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。
  • javascript.options.mem.gc_per_zone(初期値:true):GCをゾーンごとに行うかどうか。インクリメンタルGCが有効な場合はこの設定に関わらずインクリメンタルGCが行われれるので、設定変更の必要はないと考えられる。
  • javascript.options.mem.gc_refresh_frame_slices_enabled(初期値:true):インクリメンタルGCの進行中に、メモリの回収を細かい単位で行うかどうか。無効化するとメモリ使用量が増大する可能性がある。
  • javascript.options.parallel_parsing(初期値:true):JavaScriptの解釈を別スレッドで行う。無効化すると、パフォーマンス低下と引き替えにメモリ使用量を削減できる可能性がある。
  • javascript.options.showInConsole(初期値:true):JavaScriptの内部エラーをコンソールに表示する。無効化するとコンソールのためのバッファの消費が減るため、デバッグ情報を収集しにくくなる事と引き替えにメモリ使用量を削減できると考えられる。
  • javascript.options.wasm_baselinejit(初期値:false):WebAssemblyの実行をさらに高速化するかどうか。有効化するとパフォーマンス改善に繋がるが、メモリ使用量は増大すると予想される。

まとめ

以上、Firefoxのメモリ使用量が右肩上がりに増大する状況でのメモリ使用量削減に効果があると思われる対策をご紹介しました。

当社のOSSサポートサービスでは、このエントリに記載したような、企業でFirefoxを運用中に発生したトラブルについての原因および解決策の有無の調査を有償にて承っております。 FirefoxやThunderbirdの運用でお困りの情報システム担当社の方は、お問い合わせフォームよりお問い合わせ下さい。

*1 一方、現在はこれらのAndroid版用の設定はなくなっていて、一部を除くとデスクトップ版と共通の設定値になっています

タグ: Mozilla
2018-10-24

macOS版FirefoxのIPCとドラッグ&ドロップ周りのバグを調査する

クリアコードの結城です。

先日、動画でバグ報告する方法の中で例として挙げていたmacOS版Firefoxのサイドバーの操作に関するバグですが、最終的には原因の特定とパッチの提出にまで至る事ができました。このエントリでは当該バグの調査の過程を辿りながら、macOS上のFirefoxにおいてマルチプロセスが絡む部分やドラッグ&ドロップに関わる部分のデバッグをどのように進めたかの一例をご紹介します。

問題の概要

当該バグはツリー型タブというアドオンに寄せられた報告に端を発しています。このアドオンはFirefoxのサイドバー内でタブバーの代替となるUIを提供するという物で、項目をクリックするとタブが切り替わり、ドラッグ&ドロップでタブを並べ替える事もできます。このサイドバーパネル上において「セッション復元後にまだ読み込まれていないタブをドラッグしようとするとドラッグできず、また、それ以後サイドバー内で一切のドラッグ操作が行えなくなる」という現象が発生する、というのが報告の要旨でした。

報告者の方と連絡を取りながら調査を進めた結果、この問題はmacOSでのみ発生するらしいという事と、アドオン側で可能と思われる対策を講じても現象を回避できない、Firefox自体のバグに起因する問題であるらしいという事が分かりました。当該アドオンの実装を参考に最小のテストケースを作成して検証した結果、確かに現象を再現できたため、Firefox自体のバグとしてBugzillaに報告しました。またその過程において、上記の再現手順の他に以下の事も事が分かりました。

  • サイドバーの内容(=アドオンが提供するコンテンツ)が別プロセスで動作している時だけ現象が再現する。
  • 既にセッションが完全に復元された状態のタブでは、現象は再現しない。
  • 現象発生時は、dragstartイベントは発生するが、その後に発生するはずのdragoverdragenterdragleavedragendの各イベントは発生しない。

調査前の時点での見立て

本格的に調査を始める前に、この時点で分かっていた情報から、Firefoxの内部で何が起こりどうしてこの問題が起こっているのかという事について、いくつかの仮説を立てました。

  • 仮説1:タブを切り替えてセッション復元処理が行われる時に、親プロセスと子プロセス群との間の「繋ぎ替え」が発生し、サイドバーのプロセスが「アクティブな子プロセス」でなくなるせいで、サイドバーのプロセスから親プロセスにドラッグ操作が伝わらなくなる。
  • 仮説2:タブを切り替えてセッション復元処理が行われる時に、ドラッグ&ドロップの処理に対して何らかの割り込みをかけるせいで、「何らかのフラグ」が内部で立ってしまい、以後ドラッグ&ドロップのイベントが発生しなくなる。

ただ、そもそも一連の処理に関わる実装がどのような設計になっているのか、「繋ぎ替え」や「フラグ」とここで呼んでいるような物が実際にあるのかどうかも、この時点では不明なままでした。これらはあくまで、過去の経験に基づく推測での見立てとなります。

また、筆者はmacOSのネイティブアプリ開発の知見を全く持っていないという問題もありました。筆者はWindows版のFirefoxの低レイヤ部分の調査は若干経験しているものの、macOSネイティブの開発経験は皆無です。今回のように特定プラットフォームでしか発生しない問題は、そのプラットフォーム向けのコード(WindowsではWin32 API、LinuxではGTK+、macOSではCocoa APIを使う部分)を調査する必要が出てきます。調査対象の事を全く知らない状態での手探りの調査は泥沼化しがちですので、これは大きなリスクです。

そのリスクを回避するために「macOSアプリでのドラッグ&ドロップはどのように実装するのか」という事を先に勉強してから調査に臨むというやり方もあります。しかしながら、今回の調査に必要な知識がその知識体系のどのあたりに存在しているかは全く予想が付きませんし、そこまでガッツリとmacOSでの開発を学びたいという訳でもありません*1。そもそもこれは「解決できれば儲け物だが、自身がmacOSユーザーではないので究極的には直らなくても困らない問題」という性質の物でしたので、失敗に終わったとしてもそれほどダメージは無いと言えます。ですので、知見の不足には一旦目を瞑って、Mozilla製品でのクロスプラットフォームな開発の知識だけを元に調査していく事にしました。

調査の出発点を探す

全く手がかりがない所から調査を行うのは、Firefoxほどの規模のソフトウェアではほぼ不可能です。そこで、ここまでに分かっていた事からヒントになりそうな要素はないかを検討してみました。

成功ケースと失敗ケースの両方の再現手順がはっきりしているという事は、その両者で起こっている事を子細に比較していけば、現象が起こる原因が分かる可能性は高いと考えられます。一連の操作の中で「共通してこのモジュールのこのメソッドが呼ばれている」という部分が特定できれば、そこを手がかりにして調査範囲を広げていけるはずです。

(ある一点を起点にして、成功ケースにまで辿り着くルート上のどこかで、失敗ケースへと分岐するポイントがあるはず)

ここで着目したのは、正常な場合でも異常な場合でもDOMのdragstartイベントは発行されているという点でした。Firefoxの内部ではこういったDOMイベントの名前は定数で定義されていますので、その定数を伴ってイベントを初期化したり発行したりしていそうな所を特定できれば、そこを起点に調査を進められます。

"dragstart"という文字列でざっとソースコード内を検索してみた所、dragstartを定数として定義しているらしい箇所がすぐに見つかりました。ここで、このイベント名は他の箇所からeDragStartという定数名で参照されているらしいという事が分かりました。

次に、eDragStartという定数名でソースコード内を検索してみた所、自動テスト用に擬似的にイベントを生成しているらしい箇所に混ざって、EventStateManagerというモジュールの中でイベントを生成しているらしい箇所が1箇所だけ見つかりました。この処理が含まれているのはEventStateManager::GenerateDragGesture()というメソッドでしたので、メソッド名から見ても、どうやらドラッグ&ドロップの操作が行われた時には必ずここを通過すると考えて良さそうです。

同様にeDragOverという定数名でソースコード内を検索した結果、成功ケースにおける「dragoverイベントが発行されるのはここ」という場所も特定できました*2

ところで、今回の問題は厳密に言うと、以下の2つの現象が起こっています。

  • セッション復元後にまだ読み込まれていないタブをドラッグしようとするとドラッグできない。(最初の問題)
  • それ以後、サイドバー内で一切のドラッグ操作が行えなくなる。(2回目以降の問題)

2つの現象は「タブをドラッグできない」「どちらもdragstartイベントだけが発行される」という症状は共通していますが、原因が同じかどうかは分かりません。ただ、再現が容易なのは2つ目の現象の方(1つ目の現象が再現したら、以後は何度やっても2つ目の現象が起こる事になるので)なので、先にこちらの方から原因を調べる事にしました。こちらの原因が分かれば、今度はそれが「その原因となっている状況を引き起こした犯人=1つ目の現象の原因」を特定する手がかりとなります。

低レベルのログを収集する

Firefoxには環境変数を指定する事でモジュール単位の低レベルのログを収集する機能があるため、成功ケースと失敗ケースの両方についてこのモジュールのログを取得して比較すれば、何か分かるかも知れません。この方法であれば、子プロセスが出力したログもそれぞれ個別に収集する事ができます。

低レベルのログを収集するには、macOSではターミナルを使います。Firefoxのアプリケーションファイルはファイルシステム上はフォルダ(ディレクトリ)になっているので、以下のようにして環境変数を設定して実行します。

$ MOZ_LOG=timestamp,sync,nsCocoaWidget:5 MOZ_LOG_FILE=~/debug.log /Applications/Nightly.app/Contents/MacOS/firefox

環境変数MOZ_LOGには、ログを取得したいモジュール名とログレベルを:で繋げたものを、,区切りのリストとして指定します。このようにしてFirefoxを起動すると、メインプロセスのログがMOZ_LOG_FILEで指定した名前のファイルに出力され、子プロセスのログはdebug.log.child-1のような名前で同じ位置に出力されます。ログ出力が始まった状態から、ターミナルの新しいタブでtail -F debug.log*と実行すれば、それぞれのログに出力される内容をリアルタイムで見る事もできます。

しかし、先程特定した「成功時と失敗で共通して呼ばれているモジュール」であったEventStateManager.cppのソース内をlogで検索してみても、ログ出力を行っているらしきコードは見つかりませんでした。また、dragoverイベントが発行される契機になっている処理の方についてはnsCocoaWidgetというログモジュールでログを収集できましたが、こちらは成功ケースのログしか出力されず、しかも知りたい核心部分の処理が全て終わった後の時点のログでした。これではログを取っても調査の役には立ちません。

モジュール単位でのログが役に立たないとなると、何か別の方法で「成功ケースと失敗ケースのそれぞれで、内部的に何が起こっているのか」を調べる必要があります。

デバッグビルドとデバッガを使用した調査

このような場面で使えるツールの1つがデバッガです。問題が発生するケース・期待通りに動くケースで必ずこの行を通る、という事がはっきり分かっている場合には、デバッガ上でそこにブレークポイントを仕掛けておく事で、処理を一時停止して各変数の値を詳細に見る事ができます。また、そこから1行ずつ処理を進めて流れをじっくり追うという事(ステップ実行)もできます。

デバッグビルドの準備

ただ、デバッガを使うにはmacOS版Firefoxのデバッグ用ビルドが必要です。オフィシャルに公開されているバイナリは無い様子でしたので、「Firefox macOS debug」と検索して出てきたMDNのmacOSでのデバッグ手順の解説ビルド手順の解説を見ながら自分でデバッグビルドを作成することにしました。

  • macOS版FirefoxのビルドのためにはXcodeが必要です。しかし、調査に使える環境はmacOS 10.12 Sierraだったため、App Storeにある最新のmacOS向けのXcodeはインストールできませんでした。そこで「Xcode for old macOS」などのキーワードで検索して見つかった情報を参考に、開発者向けのページからXcodeの旧バージョンをダウンロードしてインストールしました。
  • Homebrewなどのツール類については、解説にある通りbootstrap.pyを使って一括インストールする事ができました。
    • 初期設定時に選択するビルド対象は、Artifact Buildではないデスクトップ版Firefoxとしました。Artifact Buildとは、ビルド済みバイナリをダウンロードしてきて使う事によって、ビルドに要する時間を短縮できるという特殊なビルドです。しかし、今回はバイナリ部分をデバッグ用にビルドしたいので、Artifact Buildは不適当という事になります。
  • 既にNightlyで現象を再現できていたので、cloneするリポジトリはNightlyに対応する https://hg.mozilla.org/mozilla-central/ を使いました。

解説の通りに進めてデバッグビルドができた時点で*3./mach runでテスト実行してみました。最適化がなされないためなのかデバッグビルドだからなのか、一挙手一投足がもたつく程に非常に低速ではありますが、一応動く事は動いているため、準備が整った事にして次の行程に進みます。

ブレークポイントの設定とステップ実行

macOSでのデバッグ手順解説に記載の通りに準備した上で、Xcodeの「Product」メニューから「Run」を選ぶと、デバッグビルドのFirefoxが起動します。

(Xcodeでブレークポイントを設定した様子)

ブレークポイントを設定するためには、まずXcodeの左ペインのフォルダーアイコンをクリックし、ソースコードのファイル一覧から「ここで処理を止めたい」という処理が含まれているファイル(今回は dom/events/EventStateManager.cpp )を選択します。 右上のペインにソースコードが表示されますので、止めたい処理の行番号をクリックします。すると、行番号の部分に青いマークが付きます。これをブレークポイントといい、Xcode経由で起動したデバッグビルドのFirefoxの内部で処理がこの行に到達すると、処理がその場所で止まってXcodeに制御が移るようになります。

ブレークポイントで処理が止まった後は、左のペインの内容が関数呼び出しのスタック表示に切り替わり、ソースコードが表示されているペインの左下ペインには停止位置での各変数の値が表示されます。

(ブレークポイントで処理が停止した様子)

この時には、ツールバー上の各ボタンで以下の操作を行えます。

  • Deactivate breakpoints:全てのブレークポイントを一時的に無効化する。もう一度クリックすると、ブレークポイントを有効化する。
  • Continue program execution:次のブレークポイントまで処理を一気に進める(ブレークポイントでの一時停止を解いて、通常の実行に戻す)
  • Step over:処理をソースコード上で1行分次に進める(ステップ実行)。
  • Step into:関数の呼び出し行において、関数の中に入る(呼ばれた関数の1行目からステップ実行を行う)。
  • Step out:関数の外に出る(現在ステップ実行中の処理を関数の最終行またはreturnに到達するまで進めて、呼び出し元の関数で元の関数が呼ばれている行からステップ実行を行う)。

変数の値を調べても有用な情報を見付けられなかった場合には、ステップ実行を繰り返したり、別の位置にブレークポイントを設定したり、ブレークポイントを削除*4したりして、解析を続けていきます。同じ箇所で「期待通りの結果が得られている時の内部状態」と「問題が起こっている時の内部状態」を詳細に比較していけば、「何が原因で、処理が期待と異なる方向に進んでいってしまったのか」を明らかにできるというわけです。

ただ、実際には各変数の値はツリー構造になっていて、奥の奥の方に原因が潜んでいるという場合も多々あるため、余程「ここをピンポイントで調べたい」という事が事前にはっきり分かっている場合でもない限りは、この方法で一発で原因を見付けるというのは難しいです。今回の調査でも、闇雲に調べるだけでは残念ながら決定打となる情報に辿り着く事はできませんでした。

期待外れだったのは、成功ケースで実際にdragoverイベントが発行されている場面の詳細を調べられなかったという点です。この処理はCocoaに対して登録しておくイベントハンドラのような関数の中にあり、Cocoaから通知されたイベントをトリガーに実行されるため、ここにブレークポイントを置いても(関数呼び出しのスタックが切れてしまっているので)デバッガでは処理の呼び出し元を辿る事ができないのです。「成功ケースと失敗ケースで明らかに異なる部分」が目の前にあるにも関わらず、そこからは何の情報も得られないという、非常に残念な結末でした。

ただ、ここまでの調査過程で以下の事は分かりました。

(プロセス間でのドラッグデータの受け渡しの様子の図)

  • 成功ケースでは親プロセス側で認識できたドラッグデータ(アドオンで定義したdragstartイベントのリスナにおいて、event.dataTransfer.setData()で追加したデータ)の個数が1以上になっているが、失敗ケースでは0になっている。子プロセス側で設定したはずのドラッグデータが親プロセスからは認識されていない。そのため、EventStateManager::DoDefaultDragStart()falseを返して終了する形となっており、ドラッグセッションを開始するnsDragService::InvokeDragSessionWithImage()が呼ばれていない。
  • このドラッグデータはEventStateManager::DetermineDragTargetAndDefaultData()nsContentAreaDragDrop::GetDragData()DragDataProducer::Produce()TabParent::AddInitialDnDDataTo()TabParentmInitialDataTransferItemsというメンバ変数の値を取得する形で初期化されている。
  • TabParentmInitialDataTransferItemsの値は、TabParent::RecvInvokeDragSession()で設定されている。このメソッドは、プロセス間通信でPBrowser::Msg_InvokeDragSession__IDというメッセージを受け取った時に実行されている
    • この時のIPC周りのコードは自動生成されているため、デバッガでブレークポイントを設定できない。
  • PBrowser::Msg_InvokeDragSession__IDというメッセージは、子プロセス側でnsDragServiceProxy::InvokeDragSessionImpl()が呼ばれた時に(IPC周りの自動生成されたコードを経由して)親プロセスに向けて送出される模様。

以上の通り、成功するケースでは子プロセス側のInvokeDragSessionと親プロセス側のInvokeDragSessionが両方とも期待通りに処理されるのに対し、失敗するケースでは親プロセス側のInvokeDragSessionに到達する前に(ドラッグデータが0個という事で)処理が終わってしまっている、という事が分かりました。という事は、失敗ケースでは「子プロセスが送ったメッセージを何らかの理由で親プロセスが受け取れていない」か、もしくは「子プロセスがそもそもそのメッセージを送出していない」かのどちらかであるという事が言えます。

標準出力・標準エラー出力に現れるメッセージの監視

状況の把握のためにPBrowser::Msg_InvokeDragSession__IDというメッセージやその他のメッセージを親プロセスが受け取れているかどうかを調べたいと思ってコードを見ていると、TabParent::RecvInvokeDragSession()呼び出し元箇所mozilla::ipc::LogMessageForProtocol("PBrowserParent",...というコードがある事に気付きました。関数の定義を調べてみた所、これはデバッグビルドの実行時にMOZ_IPC_MESSAGE_LOGという環境変数にPBrowserParentまたは1が設定されている時に、fputsで標準エラー出力にログメッセージを出力するという物であることが分かりました。このログメッセージが現れるかどうかを調べれば、上記のIPCのメッセージを親プロセスが受け取っているかどうかが分かります。

fputsで直接標準出力や標準エラー出力に出力されたメッセージは、MOZ_LOGで収集するログには出力されません。ではどこを見ればよいかというと、Xcodeの右下のペインです。標準出力や標準エラー出力に出力されたメッセージは、ここで確認する事ができます。

デバッグ実行時の環境変数は、Xcodeのメニューの「Product」→「Scheme」→「Edit Scheme」→「Run」→「Environment Variables」で設定できます。ここでMOZ_IPC_MESSAGE_LOGPBrowserParentを設定して再度デバッグ実行して確認した所、成功ケースでは上記のログメッセージが出るのに対し失敗ケースでは出なかったという事から、親プロセス側がPBrowser::Msg_InvokeDragSession__IDというメッセージを受け取れていないという事が分かりました。

そうなると今度は、子プロセス側がIPCのメッセージを送出しているにも関わらず親プロセスが受け取れていない(IPCの仕組みの中での問題)のか、それとも子プロセス側でnsBaseDragService::InvokeDragSession()が実行されていないかのどちらかという点が問題になります。

任意のログを任意のタイミングで出力させる

ここで子プロセス側の処理に対してデバッガでブレークポイントを設定できればよかったのですが、弊社調査環境では何故か、MDNのマルチプロセス有効時のデバッグに関する説明通りに設定しているにも関わらず、設定したブレークポイントで処理を止める事ができないという状況でした。処理の流れを追いたい肝心のモジュールは前述した通りMOZ_LOGでの指定でログを出力してくれないため、これでは調査のしようがありません。

そのため、ここで初めてカスタムログに頼る事にしました。

MOZ_LOGで出力されるログは既存の物以外に、全く新しくログを出力する事もできます。C++製コンポーネントの開発に関わる人向けにMOZ_LOGで出力可能なログをC++製のモジュール内で定義する手順の説明が用意されていますので、これを参考に、EventStateManager.cppの冒頭で以下のようなログモジュールを定義するようにしました。

using mozilla::LogLevel;
static mozilla::LazyLogModule sEventStateManagerLog("EventStateManager");

その上で、ここまでに調査した範囲の中で特定していたドラッグを開始するかどうかの判定を何段階も行っている関数において、returnする直前にMOZ_LOG(sEventStateManagerLog, LogLevel::Info, ("DoDefaultDragStart - no drag service"));のような行を追加して、ログを見ればどのreturnで関数が終了したのかを分かるようにしました。

このようにして収集したログを調べて分かったのは、2回目以降の失敗ケースにおいては「現在進行中のドラッグセッションがまだあるならば、新たなドラッグセッションは開始しない」という判断の結果親プロセス側でドラッグセッションが開始されていない、という事でした。

つまり、1回目の失敗ケースから2回目以降の失敗ケースにかけての間では以下の事が起こっていたと考えられます。

  • 成功ケースや1回目の失敗ケースでは、子プロセス側では通常通りにドラッグセッションが開始されている。
  • その処理が親プロセスに引き継がれるまでの処理のどこかに問題があって、「子プロセスはドラッグセッションが開始されているが、親プロセスはドラッグセッションが始まっていない(終了した)と認識している」状態が発生した。
  • 本来であれば子プロセス側のドラッグセッションを終了させるための処理が行われるはずなのに、この一連の処理がどこかの時点で中断されてしまったために、子プロセス側のドラッグセッショが終了処理が呼ばれず、ゾンビドラッグセッションが残留してしまっている。
  • このゾンビドラッグセッションがあるせいで、親プロセスでドラッグセッションが開始されなくなっている。

ここまでで分かった事をまとめて、1回目の失敗ケースの原因を探る

ここまでの調査で、2回目以降の失敗ケースは「1回目の失敗ケースにおいて、子プロセス側のドラッグセッションが開始されたにも関わらず、親プロセス側ではドラッグセッションが開始されなかったために、子プロセス側で残ってしまったゾンビドラッグセッション」が原因で発生している事が分かりました。

そうなると今度は、1回目の失敗ケースで何故ゾンビドラッグセッションができてしてしまうのか、何故親プロセス側でドラッグセッションを開始できなかったのか(処理が中断されたのか)、という事を明らかにしなくてはいけません。

現時点までで、1回目の失敗ケースでは子プロセスから親プロセスへドラッグデータがきちんと引き渡されているという事が分かっています。そうなると、「ドラッグデータは受け取れているが、ドラッグセッションを開始できない」という状況が発生する条件は、一体何なのでしょうか?

以上の事を念頭に置きながら関連モジュールのコードを眺めていると、各プラットフォームで共通のドラッグセッション開始のための処理であるnsBaseDragService::InvokeDragSession()から呼び出されるmacOS版固有のnsDragService::InvokeDragSessionImpl()の実装の冒頭に、何らかの条件が満たされなかった時にNS_ERROR_FAILUREというエラーコードを返却するというコードがある事に気がつきました。

さらにその先を読み進めていくとbeginDraggingSessionWithItemsというCocoaのAPIを呼んでいる箇所があります。調べてみると、beginDraggingSessionWithItemsはまさにドラッグセッションを開始するためのCocoaのAPIであると書かれています。

ここまで分かってやっと、失敗ケースでdragoverイベントを発行するためのコードが何故呼ばれないかの謎が解けました。このコードはCocoaでのドラッグセッションが進行中である場合に呼ばれるdraggingUpdateというイベントハンドラの中にありますが、ドラッグセッションが開始されていなければ当然これらのイベントハンドラも呼ばれないという訳です。つまり最初から、beginDraggingSessionWithItemsの呼び出しに至るまでのコードパスを調査すればよかったのでした。Cocoa APIの知識を持たない状態で調査を始めたために、ひどく遠回りをしてしまった事になります。

ともあれ、これで調査が必要な範囲はだいぶ絞り込めました。今度はbeginDraggingSessionWithItemsが呼ばれなくなるパターンの分岐に絞ってMOZ_LOGを仕込んで再びログを収集してみた所、1回目の失敗ケースはまさに先程見付けたNS_ERROR_FAILUREを返す分岐に入っている、という事が分かりました。

その分岐に入る条件はgLastDragViewというグローバル変数が空である事で、これは意味としては、「どのビューでドラッグが開始されたか分からなければ、ドラッグセッションを開始せずにエラーを返す」という事です。

そこでこの変数に値を代入している箇所を検索した所、変数の初期化時を除くと、Cocoa API用のmouseDraggedというイベントハンドラの中でのみ値を設定している事が分かりました。具体的には、4728行目で「そのビュー自身」を代入し、イベントの処理を挟んだわずか10行後にnilを再代入しているという状況でした。そこでこの前後に絞ってMOZ_LOGをさらに仕込んで調査した所、成功ケースと失敗ケースでは非同期処理の実行順が異なっており、そのせいで失敗ケースでは、ドラッグが開始されたビューが分かるより前にドラッグセッションを開始しようとしてドラッグセッションを開始できずにいるらしいという事が分かりました。

詳しい人の意見を聞いてみる

Cocoa APIまわりの非同期処理の話になると、ますますこちらに知見がないため、これ以上の調査は難しく思えます。そこで一旦、識者に意見を求めてみる事にしました。

調査対象にしているファイルのコミット履歴を見ると、複数人が関与している様子が窺えます。この時に注目するのは、コミットした人やパッチを書いた人ではなく、パッチのレビューを行った人です。

パッチが投入されるまでの流れを見ると分かりますが、パッチのコミット自体はcheckin-neededという目印に沿って作業担当者が行っているだけなので、コミットした人は必ずしもそのモジュールのエキスパートという訳ではありません。また、パッチを書いた人もたまたま関わっただけの協力者という立場である可能性があります。それらに比べると、レビューはある程度の知見がないとできないため、複数のパッチでレビュー担当者として指名されている人ほど、信頼できる識者である可能性が高くなります。

Bugzillaではコメントを追記する時に「needinfo」という情報を設定できます。これはバグの報告者など他の人に情報を求める、つまり質問するときに使われる機能です。「Need more information from...」というラベルのチェックボックスをONにして、質問先の種別を「other」にし、上記の方法であたりを付けた識者の人のメールアドレスを入力して投稿すれば、これで「質問されている」という事がその人に通知されます。

数日待った所、その方が反応して下さり、「それぞれのケースのスタックトレースを収集してみてはどうか」というようなコメントを頂けました。

スタックトレースはXcodeのメイン画面の左ペインに表示されますが、これをそのままコピーする事はできません。文字列としてコピーできる形でスタックトレースを取得するには、ブレークポイントで処理を止めた状態でXcodeのウィンドウ右下のペインのコンソールの「(lldb)」と表示されている箇所(これが実はプロンプトになっています)にbtというコマンドを入力して実行すると、コンソール内にスタックトレースの情報が出力されます。これを選択してコピー&ペーストすれば、スタックトレースを容易に収集できます。

この方法でスタックトレースを収集してじっくり比較してみた所、成功ケースではCocoa APIのmouseDraggedハンドラからそのまま呼び出しが続いているのに対し、失敗ケースではそうなっておらず、RefreshDriverTimer::Tickなどのメソッドの呼び出しが親となっていたという事が分かりました。

(1つのイベントループの中で処理が完結する場合は成功する) (1つのイベントループで処理が完結しなかった場合は失敗する)

成功ケースでも失敗ケースでもmouseDraggedから全ての処理が始まっているとばかり思い込んでいたため、これは盲点でした。

どうやら、何らかの条件に合致した場合には、ドラッグ時のイベントの処理が実際には後のイベントループに回されており、その場合は必要な情報であるgLastDragViewが既に失われてしまっているのでドラッグセッションを開始できない、という事がこの問題の根本的な発生原因である模様です。このような状況は、元々同期処理で書かれていた物を後から非同期処理の形に改修したというような場面でよく見られる物です。Firefoxは現在、体感的なパフォーマンスの向上のための改良が続いていますので、その中でそういった事が起こる事は十分にあり得そうです。

パッチの作成

ともかく、原因が分かったことでようやく問題を修正する段階に移れます。

ここまでの調査で分かった原因からは、以下のような改修案が考えられます。

  1. gLastDragViewをすぐにnilで破棄してしまわずに、ドラッグイベントの処理が終了するまで待ってから破棄するようにする。
  2. gLastDragViewに相当する情報をイベントの情報の一部として引き回す、または保持しておき、後のイベントループで続きが処理される時にその情報を参照するようにする。

1の方法は非常に単純なやり方です。確実にこの問題は直りますが、gLastDragViewがメモリ上に保持される期間が長くなるため、メモリリークや、ドラッグ開始操作として認識されて欲しくない物が誤ってドラッグ開始操作として扱われてしまうといった新しい別の問題の原因になる恐れがあります。

2は、そういった副作用の恐れがない安全な方法に思えます。一般的にはこちらの方針で改修を行う事が望ましいと言えるでしょう。ただ、今回はこの方針を取るのがためらわれる理由があります。それは、gLastDragViewを設定している箇所や参照している箇所がmacOS固有の実装の中であるのに対し、設定箇所から参照箇所までの間に通過する処理はほとんど全てクロスプラットフォームな実装であるという点です。クロスプラットフォームな実装の中に特定プラットフォーム向けのコードを入れるという変更はあまり行儀が良いとは言えず、また、その中には非同期処理の基盤的な実装も含まれていたため、変更の影響が想像以上に広い範囲に及ぶ恐れがあります。副作用の恐れがないはずの方法の方が、実際にはリスクが大きいという困った状況です。

以上の検討結果を踏まえ、1の方が調査が必要な範囲は狭い(macOS固有の実装だけに変更が閉じる)と考えられたため、まずはその前提で影響範囲を調査・検討しました。その結果、想定外のタイミングでドラッグセッションの開始処理が呼ばれてしまう事は理論上あり得ないという結論に至ったため、実際に1の方針でパッチを作成しました。しかし安全であると言い切れる確証は持てなかったため、前のコメントで紹介して頂けた別のこの件に詳しい方にneedinfoで意見を求めてみる事にしました。

時間はかかったものの無事にその方にパッチを見て頂けて、方針はこれで問題ないというコメントと、追加で修正が必要な箇所(コメント形式で書かれた説明文が実装と食い違うようになるため、それらも併せて更新する必要がある)の指摘を頂きました。現在は、その指摘に基づいて更新したパッチを再提出してレビューを待っているという状況です。この後特に問題がなければ、パッチが取り込まれるまでの流れなどで紹介ている通りに進行してマージに至ると期待されます。

まとめ

以上、プロセス間通信やmacOSでのドラッグ&ドロップといった箇所の実装を調査して、バグの原因を特定しパッチを提出するまでの一通りの経緯をご紹介しました。

最終的なパッチは実質的には1行削除・2行追加しただけの内容ですが、そこに辿り着くまでの調査にはかなりの時間を要する結果となってしまいました。要因の1つに、Cocoa APIを使ったmacOSネイティブアプリにおけるドラッグ&ドロップの実装の一般的な作法を知らないまま調査を進めた事がある事は否めず、その部分の調査をスキップする選択をしたという調査初期の判断ミスが悔やまれます。

この記事で述べた情報そのものが直接的に役に立つ場面はあまり無いと思われますが、未知の部分が大きい調査対象に取り組む際の様々なアプローチの仕方や、調査方針の見直しのタイミングなど、メタな部分で知見を得るための資料、あるいは「しくじり先生」的な反面教師として参考にしていただけると、この紆余曲折の記録も無駄にはならないのではないかと思っております。

OSSの利点の1つとして、今回のように、不可解なトラブルに遭遇した場合でも第三者の立場で詳細な調査を実施できる余地があるという点が挙げられます。本体の開発チームは通常のリリースに向けての作業に注力している事が多く、今回の問題のように希な条件下でしか発生しない・一般のユーザーに与える影響の小さい問題の解決はどうしても後回しになってしまいがちです*5。しかし、その問題に遭遇した当事者にとってはまさに今直面している問題で、ともすれば死活問題ともなり得ます。そのような場合でもただ待つだけ以上の事ができるというのは、OSSならではの事と言えるでしょう。

当社は、そのようにOSS開発により積極的に関われるようになりたい人を支援するOSS Gateという取り組みを支援しています。このエントリを見て「普段使っているOSSに自分でもフィードバックできるのか! 自分もやってみたい!」と新鮮な驚きを感じた方や、「そうそう、こんな感じで調査するんだよね。このやり方が分からなくて困ってる人を手助けできればいいんだけど……」とお考えの方は、ぜひワークショップへの参加をご検討下さい。

また、当社ではFirefoxやThunderbirdの他、Fluentd、Groonga等のOSSの法人利用において発生する様々なトラブルや不具合について有償でのサポートサービスを提供しており、このエントリに記載しているようなソースコードレベルでの調査も承っております。業務上でのOSSの利用でお困りの場合、お問い合わせフォームよりお問い合わせ下さい。

*1 そこで学ぶ知識を今後も活用していける目処があるなら話は別ですが……

*2 ただ、こちらはCocoa(macOSでGUIアプリを実装する際に使われるAPIセット)のイベントハンドラにあたる関数で、単にCocoaでの`draggingUpdate`というイベントを`dragover`というDOMイベントにマッピングするためのものでしかありません。後述しますが、この事が後の調査を難航させる1つの原因になりました。

*3 使用した検証機ではフルビルドに4〜5時間程を要しました。

*4 ブレークポイントを左クリックすると、そのブレークポイントだけ一時的に無効化できます。右クリックして「Delete Breakpoint」を選択すると、ブレークポイントを削除できます。

*5 とはいえ、今回取り組んだBugはpriorityがP3(中程度)と設定されており、それなりに重要な問題と認識されてはいたようです。

2018-10-17

グループポリシーでエンタープライズの証明書を配布した場合、IE(Edge)では閲覧できるページがFirefoxではエラーで閲覧できない場合があります

Firefox ESR52以降のバージョンは、security.enterprise_roots.enabledtrueに設定しておく事で、Active Directoryのグループポリシー機能を使って配布された証明書をFirefoxに自動的にインポートできます *1。また、Firefox ESR60ではグループポリシー経由である程度の設定の制御も行えるようになりました。以前のバージョンのFirefoxでは証明書のインポートや管理者による設定の制御にはCCK2や独自形式の設定ファイルなどを別途用意する必要がありましたが、Firefox ESR52以降では設定も証明書の管理もActive Directoryのグループポリシーに一元化できるようになったという事で、Internet Explorer(およびその後継ブラウザであるEdge)をメインで運用しつつFirefoxも併用するという使い方をしやすくなってきていると言えます。

ただ、そのために、「IEではこれで問題なく閲覧できるのに、Firefoxでは何故かエラーになる」という状況が顕在化しやすくなっています。組織内のWebサイトにSSL/TLSで接続できるようにするために証明書をグループポリシーで配布した場合に、IE(Edge)では期待通りにそのWebサイトを閲覧できるのにも関わらず、Firefoxでは何故か証明書のエラーになって閲覧できない場合がある、というのもその一つです。

この現象が発生する原因として典型的なのは、配布されている証明書自体に問題があるというものです。

証明書の安全性がFirefoxの求める要件を満たしていない場合

Firefoxをアップデートしたら急に、今まで問題なく閲覧できていたWebサイトで「安全でない接続」というエラーが表示されるようになり、閲覧できなくなってしまった。という場合、これはFirefoxの許容する証明書の安全性の水準が見直された事が原因となっている可能性があります

「この水準の安全性が確保されていれば妥当な証明書として認める」という基準は、IE/EdgeとFirefoxで異なります。また同じFirefoxであっても、バージョンによってその基準が見直される事があります。Firefoxのアップデート直後からSSL/TLSに関するトラブルが急増したという場合には、Firefoxの各バージョンのリリースノートや、開発者向けリリースノートの「セキュリティ」の見出し配下を確認し、問題が起こるようになったバージョンにおいて何らかのアナウンスがなされていないかを確認してみて下さい。

GoogleやYahooなどの一般的なWebサイトを閲覧しようとして「安全でない」というエラーが表示されるようになったというケースも、証明書の安全性が低い事が原因である場合が多いです。これは、以下の例のような「SSLロガー」や「SSLプロキシ」と言われるようなネットワーク機器やサービスを使用している場合によく見られるトラブルです。

この種の機器やサービスは、使用にあたっては原理上必ず、各クライアントに専用の認証局証明書をインポートする必要があります。この認証局証明書がFirefoxの求める安全性の水準を満たさなくなると、SSL/TLSを使用したあらゆるWebサイトで「安全でない接続」の問題が起こるようになります。
(いわゆるSSLプロキシがある場合の通信)
導入手順の一環として流されがちなためか、「証明書をインポートして使っている」という事実を忘れてしまいやすい模様ですので、殊更注意が必要です。

認証局証明書として本来は不正な証明書を、気付かずに使っている場合

証明書の安全性の水準に問題がなくても「安全でない接続」のエラーが発生する場面がもう1つあります。それは、認証局証明書(CA*2証明書、あるいはルート証明書)として使えないはずの証明書をエンタープライズの証明書として配布してしまっている場合です。

具体的には、実験や組織内での一時的な使用のために作られる事の多い、「サーバーの署名用」ではあるが「認証局証明所用」ではない自己署名証明書をエンタープライズの証明書として配布している場合に問題が起こります。実際に、各種の証明書エラーの様子を確認できるbadssl.comというサイトにおいて自己署名証明書を使ったサイトの例で使われている証明書の詳細な情報を見ると、

  • Subject(この証明書の識別子と証明する対象):C=US, ST=California, L=San Francisco, O=BadSSL, CN=*.badssl.com
  • Issuer(この証明書の署名者):C=US, ST=California, L=San Francisco, O=BadSSL, CN=*.badssl.com
  • X509v3 extensions(拡張属性), X509v3 Basic Constraints(基本の制約事項):CA:FALSE(認証局ではない

と、まさにそのような形式になっています。

このような証明書を使っているWebサイトを訪問した場合には、必ず「安全でない接続」のエラーが表示され、内容を閲覧するためにはそのサイトをセキュリティの例外として登録しなくてはなりません。しかしながら一般的に、セキュリティの例外はユーザー個々人が手動で操作して行う必要があります。組織内で多くのユーザーが共通して使用するWebサーバーでこの種の証明書を使っている場合*3、全ユーザーに手動で例外登録をさせるというのは非現実的です。そこで取られる事があるのが、この証明書自体をエンタープライズの証明書として配布するという方法です。

この証明書がエンタープライズの証明書として読み込まれているクライアントPCでは、IE/Edgeで当該Webサイトを訪問すると、

  1. そのWebサイトが送ってくる証明書の署名者を確認する。
  2. エンタープライズの証明書に登録されている証明書(Webサイトが送ってきた証明書と同じ物)が見つかる。
  3. 見つかった証明書が認証局証明書として使われ、Webサイトが送ってきた証明書が正しく認証局証明書によって発行された物であると判定される。

という形で検証され、安全な接続であるとしてWebサイトのコンテンツがそのまま表示される結果になります。

Firefoxでsecurity.enterprise_roots.enabledtrueに設定されていている場合も、当然IE/Edgeと同様の検証が行われて当該Webサイトの内容が警告無しに表示できるはず……と思ってしまう所ですが、実際にはそうはなりません。Firefoxでは、

  1. そのWebサイトが送ってくる証明書の署名者を確認する。
  2. エンタープライズの証明書に登録されている証明書(Webサイトが送ってきた証明書と同じ物)が見つかる。
  3. 見つかった証明書にCA:TRUE(認証局証明書であるというメタ情報)が無いため、認証局証明書ではないと判定される。
  4. 1の証明書の署名者の認証局証明書が見つからないため、証明書の妥当性を検証できず、エラーになる。

という形で検証が行われるため、安全な接続ではないと判定され、Webサイトのコンテンツの代わりにエラーページが表示される結果になります*4

ユーザーが手動操作でFirefoxの証明書マネージャから認証局証明書をインポートする場合、本来であれば、用途が認証局証明書でない証明書は「この証明書は認証局の証明書ではないため、認証局の一覧には追加できません。」というエラーメッセージが出て拒絶されるため、誤って認証局に登録してしまうという事も起こりません。しかしながら、グループポリシー経由でエンタープライズの証明書として配布された物を security.enterprise_roots.enabled の機能でインポートする場合には、このチェックが行われません。また、厄介な事に、そうしてエンタープライズの証明書として一度認識されてしまうと、この証明書を証明書マネージャから手動操作で認証局証明書として再インポートしようとした場合にも、用途のチェックがスキップされてインポートに成功してしまうという性質があります。

この問題がそもそも発生しないようにするためには、以下のどちらかの方法を取るしかありません。

  • サーバーの証明書を再発行する。その時に、認証局証明書としての用途も併せて設定することで、認証局証明書で、且つサーバーの証明書という状態にしておく。
  • 認証局証明書を新たに作成し、その認証局で署名された証明書としてサーバーの証明書を再発行する。エンタープライズの証明書としては、認証局証明書の方を配布する。

一般的な認証局で発行してもらう証明書には適切な用途情報が設定されていますが、組織内でのみ使用するために独自に証明書を発行する場合、この点のチェックがなおざりになりがちです。証明書の用途は適切に使い分けるように気をつけましょう。

まとめ

以上、Active Directoryのグループポリシー機能を使って配布したエンタープライズの証明書をIE/EdgeとFirefoxで共用する場合に起こりがちなトラブルについて解説しました。

当社では、Firefoxの法人での運用における不明点の調査、ご要望に合わせた適切な設定のご案内、本体に含まれない特別な運用を実現するアドオンの開発などを広く承っております。Firefoxの運用でお困りのシステム管理・運用ご担当者さまは、お問い合わせフォームよりお気軽にお問い合わせ下さい。

*1 なお、この機能はActive Directoryを運用していない環境でも、Windowsのレジストリを編集することで動作の検証が可能です

*2 Certificate Authority=認証局

*3 本来であればそのような運用を取るべきではありません。

*4 これはIE/EdgeとFirefoxで証明書の取り扱いのポリシーの厳格さに差があるためで、どちらかが正しくてどちらかが間違っているという事はありません。IE/Edgeは証明書の用途に関する検証がルーズなので、そちらを基準にして運用していると支障が生じる場合がある、という事です。

2018-10-02

タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|