文書化されていない引数でNtQuerySystemInformation関数を使用する際の問題

その日の朝は、「ifs」が壊れたという事実から始まりました。 この式はかつて同僚が作成したもので、コードが段階的に渡されたときにデバッガーがifブロックに入る方法を実証しましたが、ifがチェックされているという条件はまったく偽でした。 そのときの問題は些細なものであることが判明しました-彼はリリースに最適化されたビルドを使用し、このシナリオでは、もちろん、段階的なデバッグへの信頼は不可能です。 しかし、「もし壊れた」という表現そのものが定着し、それ以来、非常に根本的なものが機能しなくなって信じられない状況を示すためにここで使用されています。



そのため、その日、 NtQuerySystemInformation関数が故障しました。これは、プロセス、スレッド、システム記述子などに関する情報を返すWindows OSの最も重要な関数の1つです。 この機能を使用する利点についてこの記事を書いたことがあります。 しかし、そのようなシステムの基礎でさえ失敗する場合があることが判明しました。



それで、何が起こった。



かなり長い間(数年前から)、SystemHandleInformation引数を指定したNtQuerySystemInformation関数の呼び出しを使用して、システム内のすべての記述子に関する情報を取得しました。 はい、この引数は正式には文書化されていないものを指しますが、現在実行中のすべてのWindowsアプリケーションですべての記述子をリストする方法に関する情報を探し始める場合、NtQuerySystemInformation + SystemHandleInformationの組み合わせが最もよく提案されるオプションになります。 そして、Windows NT以降のすべてのOSで、本当に機能します。



すべてのプロセスで記述子を検索する必要があるのはなぜですか? まあ、さまざまな理由で。 Process Hackerなどのユーティリティは、単に情報目的で表示します。 現在誰かによってブロックされているリソース(ファイルなど)を検索するためにこれを行うプログラムがあります。 また、たとえば、外部プロセスで、プログラムのコピーを1つだけ実行し、それを閉じて、そのようなアプリケーションの2つのインスタンスを起動できるようにするために使用されるミューテックスを見つけることができます。 または、サンドボックスを整理するために、記述子をリストしてそれらを複製します。 一般的に、多くのタスクがあります。



ここでは、ディスクリプタ列挙の完全な説明は行いません。一般的に、次のような一般的な例に似ているとだけ言います。



while ((status = NtQuerySystemInformation( SystemHandleInformation, handleInfo, handleInfoSize, NULL )) == STATUS_INFO_LENGTH_MISMATCH) handleInfo = (PSYSTEM_HANDLE_INFORMATION)realloc(handleInfo, handleInfoSize *= 2); // NtQuerySystemInformation stopped giving us STATUS_INFO_LENGTH_MISMATCH. if (!NT_SUCCESS(status)) { printf("NtQuerySystemInformation failed!\n"); return 1; } for (i = 0; i < handleInfo->HandleCount; i++) { ... }
      
      





しかし、その後、アプリケーションを起動します-そして、突然、必要な記述子が存在することがわかりました(そして、それが存在することは確かです!)NtQuerySystemInformation()関数によって返されるリストにありません。 それだけです、彼らは来ました-「壊れたなら」。



オフィス内の他のコンピューターで問題を再現しようとしています。 一部では、ほとんどの場合、再現されていますが、そうではありません。 私たちは、それが再現するものと、すべてが良いものとがどのように異なるかを理解しようとしています。 Windowsのバージョンはどこでも同じであり、アップデート、プログラムのビルド、すべてが同じです。 突然、問題が再現されたすべてのラップトップが同じモデルであることに気づいた人がいます。 ハードウェアの非互換性? しかし、なぜ突然、以前は動作していたのですか...さらに、現在動作している同じモデルの他のラップトップがオフィスにあります。 デバイスドライバーのバージョンでさえ比較されました-すべては同じようです。 しかし、すべては一部のラップトップでは機能しますが、他のラップトップでは機能しません。



私が偶然2つのことに気づくまで、頭を引っ張った髪は約半日続きました:



  1. プロセスPIDは、通常、何らかの理由でコンピューター上の3桁、4桁、または5桁の数字ですが、6桁になります。 タイプ780936のPIDを見るのは十分に奇妙でした。私はこれらに以前気づいていませんでした。 さらに、実行中のプロセスの総数は非常に適切でした(最大100)。
  2. [CPU]タブのタスクマネージャーには、システム内の記述子の合計数が表示されました。これは800,000を超える巨大なものでした。


通常のアプリケーションでは、100つか2つの記述子を開くのが普通です。 まあ、千。 積極的に使用すると、クロムは約2000を開くことができ、大規模プロジェクトのVisual Studioは3000を開くことができます。 幸いなことに、前述のProcess Hackerを使用すると、各プロセスの記述子の数を表示したり、使用されている記述子の数でプロセスのリストをソートしたりできます。



そして、私たちは何を見ますか? そして、この写真のようなものを見ます:







上記のスクリーンショットを作成したばかりであるため、プロセスリストの最初に約20,000の記述子があります。 そして、私が初めて問題を見たとき、そこには約65万人がいました。 ビンゴ! これはプロセスSynTPEnhService.exeです。



そして、私の頭の中でパズル全体が発展します。 SynTPEnhService.exeはSynapticsタッチパッドドライバーの一部です。 問題が発生したオフィスの特定のモデルのラップトップにのみインストールされました。 短い観察では、このプロセスが5秒ごとに子プロセスSynTPEnh.exeを開始し、1〜2秒後に閉じることが示されました。 同時に、親プロセスは子プロセスのハンドルを保持し続けるため、記述子のリークが発生します。 5秒ごとに1つずつ。 これは1日あたり17,280記述子です。 コンピューターを1週間オンのままにしておくと、10万を超えるハング記述子があります。 私のパーソナルコンピューターは1か月以上再起動しませんでした。したがって、50万を超える数の新しいプロセスのPIDです。 これは、なぜオフィスの一部のラップトップで問題が再現されたのかを説明しますが、他のラップトップでは発生しませんでした。同僚の何人かは毎日PCを再起動し、 。



ところで、この場所で、Synapticsタッチパッドドライバーに関する何らかの問題について既に読んだことを思い出しました。 少し掘り下げた後、Bruce Dawsonによって書かれたこの記事を見つけました(彼の記事の多くの翻訳がHabréで異なる時期に公開されましたが、この特定の記事ではありません)。 そこで彼は、SynTPEnh.exeプロセスのこの無限の再起動によるメモリリークの問題について説明していますが、ハンドルリークの問題については何も言っていないため、私の発見はそれとは異なります。



問題解決



それで、タッチパッドドライバは何十万もの記述子を「食べ」ます。 そして、Windows NTの時代に書き戻された関数NtQuerySystemInformation(SystemHandleInformation、...)には、かなり限られた内部バッファーがありました(そして持っていました)。 サイズの正確な指標はどこにも見つかりませんでしたが、明らかに、100万個の記述子用に設計されていませんでした。 その結果、関数は「できる限り」それらを返します。つまり、それらの中に目的のものがある場合とない場合があります。



どうする? アニメ化されたシリーズ「Rick and Morty」のRickが言ったように、「テレポートを発明するとき、あなたはすぐに不快なものを発見します。あなたはそれを発明した宇宙の最後の人です。」 判明したように、20年前にSystemHandleInformation引数で呼び出されたときに、MicrosoftはNtQuerySystemInformationの制限されたバッファーでこの問題を認識しました。 NtQuerySystemInformation(SystemExtendedHandleInformation、...)を呼び出すと、システム内のすべての記述子が、それがいくつあっても返されます。 まあ、というか、これは確かにわかりません。この引数にはいくつかの制限があるかもしれませんが、ある状態で800,000個の記述子を返すことができるのは確かです。



ネット上で、SystemExtendedHandleInformationの使用例、たとえばこれを見つけることができます。 一般に、すべてがそこで類似しており、他の構造が単純に使用され、それだけです。



これは、Widnows OSの文書化されていない引数の使用に関する有益なストーリーでした。これは非常に便利ですが、非標準の問題に対する注意深いテストと準備が必要です。



All Articles