ククログ

株式会社クリアコード > ククログ > WebExtensionsのNative Messaging Hostが動作しなくなる問題と、その予防方法

WebExtensionsのNative Messaging Hostが動作しなくなる問題と、その予防方法

結城です。

先日、当社製ThunderbirdアドオンのFlexConfirmMailList Addons in Windows' Programs(以下、LAWP)について、「これらを同時に使用するとFlexConfirmMailが動作しなくなる」というお問い合わせを頂きました。 調査の結果、この現象はアドオン開発時に使用するNative Messagingという機能の使い方に起因するものであったことが分かりました。

この記事では、主に開発者向けの情報として、トラブルシューティング事例としてこの不具合の原因調査の過程を紹介しつつ、FirefoxおよびThunderbirdのアドオンの開発時のNative Messaging使用時の注意点を説明します。

原因の調査

「FlexConfirmMailが動作しない」状態の確認

調査のため手元の検証環境で現象の再現を試みたところ、FlexConfirmMailとLAWPの両方をインストールした状態において、Thunderbirdの起動直後の状態で何度かに1回程度の割合で、FlexConfirmMailが動作しない現象が再現しました。 具体的には、メールの送信直前に割り込んで宛先確認を行う動作のうち「宛先確認を行う」部分が機能せず、「メールの送信処理の中断」だけが行われている(その結果、メールを送信できなくなっている)ように見えました。

処理が停止している箇所の特定

現象発生時の状況をデバッグログで詳しく追ったところ、メール送信を検知して行うFlexConfirmMailの設定の読み込み処理の中で処理が停止してしまっている(その直前のログは記録されている一方で、その直後に出るはずのログが記録されていなかった)様子が窺えました。 ただ、該当箇所をtry-catchで囲ってみても例外は発生していませんでした。 そこで、より深く掘り下げてみたところ、処理が停止しているのはWebExtensionsの非同期APIであるbrowser.runtime.sendNativeMessage()が返したPromiseの解決をawaitで待っている箇所であることが分かりました。 これはつまり、APIが返したPromiseがいつまで待っても解決されていない状況だということです。

browser.runtime.sendNativeMessage()は、FirefoxやThunderbirdのアドオンがWebExtensionsのAPIが用意されていないことを行うために、外部のネイティブアプリケーション(Native Messaging Host)を呼び出して実行する、Native Messagingという仕組みのためのAPIです。 Thunderbird版FlexConfirmMailは、Outlook版FlexConfirmMail用のGPOの設定を読み込むためにNative Messagingを使っており、この一行を実行する過程で

  1. FlexConfirmMailがbrowser.runtime.sendNativeMessage()を呼ぶ。
  2. 事前にFlexConfirmMail Native Messaging Hostのインストーラが登録した情報に基づいて、ThunderbirdがFlexConfirmMail Native Messaging Host(事前に登録された小型のローカルアプリ)の情報を把握する。
  3. ThunderbirdがNative Messaging Hostを起動する。
  4. Native Messaging Hostが起動し、レジストリ上からGPOの設定内容を読み込んでThunderbirdに返却する。
  5. ThunderbirdがNative Messaging Hostから受け取った情報をアドオンに返却する。
  6. FlexConfirmMailがbrowser.runtime.sendNativeMessage()の戻り値として、Native Messaging Hostから返された情報を受け取る。

という一連の処理がThunderbirdの内部で行われます。 このどこかで処理が躓いてしまっているようですが、どこで止まっているのかまではまだ分かりません。

処理が停止している状況で起こっていることの把握

Native Messaging Hostの呼び出し時に起こっていることは前述の通りですが、これを「処理が停止する原因」という観点で捉えると、以下のような原因が考えられます。

  1. LAWPのNative Messaging Hostの登録情報が間違っていて、ThunderbirdがFlexConfirmMailのNative Messaging Hostを起動しようとしても起動できなかった。
  2. 各Native Messaging Hostの登録情報は正しいが、Thunderbirdがその情報通りにFlexConfirmMailのNative Messaging Hostを起動しようとしても起動できなかった。
  3. Native Messaging Hostは起動されたが、Native Messaging Hostが異常終了した。
  4. Native Messaging Hostは正常に終了したが、結果をFlexConfirmMailに返せなかった。

まず、1の可能性を疑って調査を行いました。

当社では複数のNative Messaging Hostを開発していますが、コード流用時の識別子の更新忘れなどによって「LAWPのNative Messaging Hostをインストールしたら、誤ってFlexConfirmMailのNative Messaging Hostとして登録されてしまった」といったことが起こる可能性はあります。 具体的には、本来であればWindowsのレジストリー上のキー HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\NativeMessagingHosts\com.clear_code.list_addons_in_win_programs_we_host に値を設定するべき所で、誤って HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\NativeMessagingHosts\com.clear_code.flexible_confirm_mail_we_host などのキーに値を設定してしまっている可能性が疑われます。 そこで、検証環境でそれぞれのNative Messaging Hostのインストールを試行し、これらのレジストリキーの状態をレジストリエディターで確認しましたが、登録情報が混ざるということはなく、すべて期待通りの情報が登録される結果を得られました。

次に、2の可能性を疑って調査を行いました。

当社では「Native Messaging Hostの登録情報が正しいのにプロセスを起動できない」過去事例はありませんでしたが、「アドオンが読み込まれた順番によって、片方がもう片方の動作に意図しない影響を与えた」事例はありました。 そこで、FlexConfirmMailとLAWPのそれぞれの有効・無効状態を切り替えてリロードしたり、アドオン自体をアンインストール・再インストールしてみたりして、状況に変化が見られるかを確認したところ、以下の事が分かりました。

  • LAWPを先に読み込んだ場合でもFlexConfirmMailを先に読み込んだ場合でも、どちらの場合も現象が再現する。 よって、「LAWPが有効であること」だけが必須の再現条件だと言える。
  • 現象が発生した後、FlexConfirmMailをリロードしても、現象は発生し続ける。 よって、アドオンのリロードで解放・再確保される種類のリソース(JavaScriptの名前空間など)以外の部分で何かが起こっていると考えられる。
  • Thunderbirdを再起動すると、現象が発生しなくなる。 よって、問題はThunderbirdというアプリケーションの制御下の範囲で起こっていて、Windowsのレベルで起こっているわけではないように思われる。

これらのことから「LAWPの初期化処理の過程で何かが起こっているようだ」と検討をつけて、LAWPのデバッグログ出力を有効にした状態でLAWPのリロードを繰り返しながらログを採取してみたところ、「LAWPの正常動作時に出力されるはずの一連のログが、途中までしか出力されなくなる場合がある」「そのような状態が発生すると、それ以後FlexConfirmMailのNative Messaging Hostを呼び出せなくなる」ことが分かりました。

このとき、出力されなくなったのは「LAWPがLAWP自身のNative Messaging Hostへの問い合わせ結果を受けて出力する」ログでした。 このことからようやく、起こっていた現象は実は「FlexConfirmMailのNative Messaging Host呼び出しだけが失敗している」のではなく、「FlexConfirmMailも含めた複数の(恐らく、すべての)アドオンのNative Messaging Host呼び出しが失敗している」というものであったと分かりました。 LAWPは「アドオンが動作すると、その時点でThunderbirdにインストールされているアドオンの一覧をWindowsのレジストリーに登録する」アドオンなので、インストールされているアドオンの一覧に変化がなければ、LAWPが動作していてもいなくても表面上の「動作結果」は変わりません。 そのため、「実はLAWP自身のNative Messaging Host呼び出しも失敗していた」とは気付きにくい状況だったのでした。

Native Messaging Hostを呼び出せなくなっている時に起こっていることの詳細

Native Messaging Host呼び出しが失敗している箇所として前項で1から4までを仮定し、1については可能性を否定できましたが、2から4までのどれが実際の原因箇所かはまだ分かりません。 そこで、Native Messaging Host呼び出しに失敗している時に何が起こっているのかをより詳細に調べてみました。

当社製アドオンのNative Messaging Hostは、それ自身もまたデバッグ用のログを出力する機能を持っています。 そこでNative Messaging Host側のログを確認してみたところ、現象の発生時には、そもそも全くログが出力されていないことが分かりました。 このことから、現象の発生原因となっている箇所は、前項で述べた2、すなわち「ThunderbirdがNative Messaging Hostのプロセスを起動できていない」という点であると判断できました。

この時の様子をWindowsのタスクマネージャーで確認し、正常動作時と比較したところ、以下の事が分かりました。

  • Thunderbirdは直接Native Messaging Hostのプロセスを起動するのではなく、conhost.exeというプロセスを経由して起動している。 これは、WindowsアプリケーションがGUIを持たないプロセスを起動する時に共通して使う、Windowsの機能である。
  • 正常は、LAWPの初期化処理中にconhost.exeのプロセスが大量に起動され、その後しばらくして、それらのconhost.exeのプロセスが一斉に終了する。
  • 現象発生時は、LAWPの初期化処理中にconhost.exeのプロセスが大量に起動された後、それらのconhost.exeのプロセスが終了せずにそのまま残留している

Google Chrome拡張機能のNative Messagingの仕様では、Native Messaging Hostの動作として「1つのプロセスを起動して、ソケット通信で継続的にデータをやり取りする」モードと「要求の度にプロセスを起動して、処理が終わったらプロセスが終了する」モードの2つがあることになっていますが、Thunderbirdはそのうちの後者にのみ対応しています。 LAWPのNative Messaging Hostは「このアドオンの情報を1つだけWindowsのレジストリーに登録せよ・レジストリーから削除せよ」というごく単純な指示を受け取って動作するよう設計されているため、インストールされているアドオンの数だけNative Messaging Hostが起動されることになります。 conhost.exeのプロセスが大量に起動するのは、そのためです。

このとき使用するAPIのbrowser.runtime.sendNativeMessage()はPromiseを返すため、戻り値を受け取るためには、一般的には以下の例のように、awaitで解決を待つことになります。

const addons = await browser.management.getAll();
for (const addon of addons) {
  await browser.runtime.sendNativeMessage(...);
}

ただ、この例のようにそれぞれの呼び出しで毎回awaitで解決を待つと、無駄な待ち時間が生じてしまいます。 そのため、こういった非同期APIのそれぞれの呼び出し同士に依存関係が無い場合は、以下のようにPromise.all()を使うのがセオリーとなっています。

const addons = await browser.management.getAll();
await Promise.all(
  addons.map(
    async addon => browser.runtime.sendNativeMessage(...)
  )
);

後者のような実装の仕方では、ごく短時間(数マイクロ秒~数ミリ秒)の間にconhost.exeが大量に起動されることになります。 JavaScriptは基本的にシングルスレッドのため、このような実装であってもメモリ破壊などは原則として発生し得ませんが、JavaScriptのコードの先にいるconhost.exeやその呼び出し箇所(C++で実装されていると思われる)はその限りではありません。 もしconhost.exeやその呼び出し箇所に、ごく短時間の間に複数回実行されることで問題が発生する何らかの不具合があるならば、後者のような実装を前者のように改めることで、処理に要する時間が増大する代わりに安定した動作を得られるようになる可能性があります。

そこで、後者のような効率重視の実装になっていた箇所を、前者のようなベタな実装に変更して動作を検証してみたところ、これがまさに大当たりで、当社検証環境においては現象が再現しなくなりました。 Thunderbirdとconhost.exe(Windows)のどちらのに責任があるのかは現時点では不明ですが、状況から見て、「Thunderbirdがconhost.exeのプロセスを一度に大量に起動すると、conhost.exeが正常に動作・終了しなくなる」という根本の問題があり、LAWPが前述のような実装の仕方になっているためにその問題が表面化した、というのがこの現象の正体である模様です。

以上の調査はあくまで当社の検証環境で行っており、実際の環境で起こっている現象は全く別の原因によるものである可能性もあります。 そこでお客さまには、現象発生時のconhost.exeのプロセスの状態(conhost.exeのプロセスが大量に残留するかどうか)の確認と、前述の改修を行ったバージョンのLAWPの動作テストをお願いしました。 果たして、お客さま環境においても現象発生時にはconhost.exeのプロセスが大量に残留していること、改修版のLAWPでは現象が発生しなくなったことをご確認頂けました。

問題の解決

とりあえずの対応

お客様環境での検証結果を承け、改修を反映したLAWPをバージョン2.1としてリリースしました。 今回の改修範囲はアドオン本体のみに留まっており、Native Messaging Host側には変更が無いため、アドオンの自動更新を有効にしている環境ではすでに更新が反映されているものと思われます。 企業内などでアドオンの自動更新を無効化している場合は、配布ページからXPIパッケージを入手して各端末にインストールして頂く必要があります。

今回の改修方法は、改修範囲がアドオン側のみに留まるメリットがありますが、インストールされているアドオンの数が増えるほど待ち時間が増えて、処理の完了までにかかる時間が長くなるというデメリットもあります。 今回は以下の理由から、この改修方法のままリリースすることとしました。

  • お客さまからのお問い合わせに基づく改修で、なるべく早く対応することが望ましい。改修範囲が広がると、それだけ検証にも時間がかかるので、なるべく小規模の改修で済ませたかった。
  • LAWPというアドオン(Thunderbirdのアドオンマネージャーが認識しているアドオンを、資産管理ソフト等で管理しやすくするようにWindowsのレジストリーに登録する)の性質上、それほど高い性能が要求されるわけではない。

ただ、これはあくまで、すでにある実装をそのまま使った方が改修コストを削減できるという前提があっての判断です。 新たにこのような機能を実装する場面では、より望ましい実装方法を模索するべきなのは間違い無いです。

アドオン開発においてNative Messagingの使用時に気をつけるべきこと

一般的に、メンテナンス性・開発の継続性の維持を重視する観点からは、ソフトウェアのモジュール同士は疎結合にしておくことが望ましいです。 LAWPにおける「1回のNative Messaging Host呼び出しで1件の情報をレジストリへ書き込む」設計は、モジュール同士を疎結合にする観点で取った選択でした。

しかしながら、今回の調査の過程で、Native Messaging Hostへの大量の呼び出しを一度に行う事には思わぬリスクがあることが分かりました。 Native Messagingを使用する場合は、安全のために、Native Messaging Hostの呼び出し回数がなるべく少なく済むような設計とすることが望ましいと言えるでしょう。 例えば、今回取り上げたLAWPのようなアドオンであれば、browser.runtime.sendNativeMessage()の引数で指定してNative Messaging Hostに渡すメッセージや、Native Messaging Hostが返却するメッセージは、「複数の処理対象を配列でまとめて指定する」「処理結果を配列で返す」といった形で一括処理を行える設計にするといったやり方1が考えられます。

ただ、そのような設計がそぐわないケースもあります。 例えば、当社開発のBrowserSelectorFirefox用アドオンでは、「Firefox上で遷移しようとしているWebページのURLがWindowsのセキュリティゾーン設定においてどのゾーンと判定されるかを、Windowsのネイティブアプリケーション向けAPIを用いて判定する」必要があるために、ページ遷移の要求が生じる度に必ずNative Messaging Hostを呼ぶようになっています。 読み込み要求は都度処理しなくてはならないため、「読み込み要求を一定数溜め込んでからバッチ処理する」ことは、残念ながらできません。

BrowserSelectorの事例では、Windowsのセキュリティゾーンに基づく判定が必要ないのであれば、個々の読み込み要求に対して必ずしもNative Messaging Hostを呼び出す必要はありません。 そのため、判定をアドオン側で行えるならそれで済ませて、Native Messaging Hostの呼び出しを減らす改修を検討しています。

まとめ

Thunderbird用のアドオンのFlexConfirmMailList Addons in Windows' Programs(以下、LAWP)について、これらを併用しているお客さまから寄せられたお問い合わせに基づいて行った調査の経緯と解決までの顛末をご紹介しました。 また、その際に得られた知見に基づいて、Firefox・Thunderbird用アドオン開発時においてNative Messagingを安全に使うための注意点もご紹介しました。

当社では、FirefoxやThunderbirdの法人運用におけるトラブルの原因究明や回避方法の調査、社内事情に合わせるためのアドオンの改修、アドオンの新規開発のご依頼など、様々なお問い合わせへの対応を有償にて承っております。 こういった事柄についてお悩みを抱えている企業のご担当者さまは、お問い合わせフォームよりご相談ください

  1. なお、仕様上も実装上も、アドオンからNative Messaging Hostへ送信できるメッセージの最大サイズは1回あたり4GBまで、Native Messaging Hostからアドオンへ返却できるメッセージの最大サイズは1回あたり1MBまでという制約があるので、これを超えるサイズのメッセージをやり取りする場合は、複数個のメッセージへの分割が必要となります。