[ADSI] 障害情報 : WinNT プロバイダを用いて ADsOpenObject() をコールするとメモリリーク発生

本日は新規作成された ADSI 案件の障害情報についてのご報告です。WinNT プロバイダを用いて ADsOpenObject() をコールあるいは System.DirectoryServices.DirectoryEntry オブジェクトを生成すると CoUninitialize() がコールされるまでメモリが増加しつづけます。Release() をコールしても増加動作はコールされるごとに継続して発生しつづけるという現象となります。LDAP プロバイダを使用した場合は発生しません。

[技術情報]
文書番号: 2021561 - 最終更新日: 2010年5月12日 - リビジョン: 1.0
資格情報のセットと共に ADsOpenObject API を使用すると、メモリ リークが発生する
https://support.microsoft.com/default.aspx?kbid=2021561

[現象]
AdsOpenObject() を WinNT プロバイダを用いて呼び出すと、一回呼び出すごとに 8byte メモリが増加します。
この割り当てられた 8byte のメモリは ADsOpenObject() が返すオブジェクトの Release() を呼び出すことでは開放されず、増加分は CoUninitialize() を呼び出すまで開放されません。
メモリ使用量が上限に達してメモリ確保に失敗すると ERROR_BAD_NETPATH が返るなど、問題が発生することがあります。
なお、その時点のプログラムのメモリ割り当て / 動作状況によって、返されるエラーは異なる可能性があります。

[詳細]
内部にはサーバーの "IPC$" リソースに対するクレデンシャルが格納されているテーブルがありますが、クレデンシャルの取得が行われると、CCredTable にエントリが追加されます。この情報は基本的に配列で保持されます。サイズが満杯になると、配列サイズを 10 増やします。
このテーブルはこの配列の状態をトラックする変数としてテーブル エントリの割り当て状況を表すカウンタと、エントリの使用をつかさどる変数を持ちます。
テーブルにエントリが追加されると、エントリの割り当て状況を表すカウンタを 1 インクリメントします。ただし、エントリを削除する場合はエントリの無効化のみを実施し、領域の開放は行いません。したがって、エントリ追加時にインクリメントしたカウンタ変数のデクリメントは行われません。
さらに、新たにクレデンシャルを追加する際にはこれらのエントリ メモリのことは考慮されていない実装であるため、結果として、この無効化されたエントリがそのままとなるため、メモリ使用量が増加するという動作となります。Release() をコールしても開放されません。
一方、CoUninitialize() はメモリ全体を開放しますので、CoUninitialize() を実行すると、そのままにされた無効化エントリも開放対象となりますため、結果としてメモリ開放がなされることになります。

なお、 .NET Framework の System.DirectoryServices (S.DS) 名前空間を用いるアプリケーションもこの問題の影響を受けます。
S.DS 名前空間の実態は、ADSI のラッピングしているためです。つまり、最終的には OS 付属の ADSI モジュールに格納された問題の ADsOpenObject() がコールされるため、.NET Framework の全バージョンがこの問題の影響を等しく受けることになります。(1.0/1.1/2.0/3.5/4.0)
一例を挙げますと、WinNT プロバイダを用いる ADsPath 文字列 (例 : "WinNT://") を使用して作成される任意の System.DirectoryServices.DirectoryEntry オブジェクトを生成した場合、.NET CLR 以降のアンマネージの層でネイティブの IADsOpenDsObject::OpenDsObject メソッドを使用されることになるため、結果的に ADsOpenObject() もコールされ、メモリ リークが発生することになります。

ADsOpenObject Function
https://msdn.microsoft.com/en-us/library/aa772238(VS.85).aspx

.NET Framework クラス ライブラリ
System.DirectoryServices 名前空間
https://msdn.microsoft.com/ja-jp/library/system.directoryservices(VS.80).aspx

[ターゲット OS]
本件の発生 OS は以下となります。

Windows 2000
Windows XP
Windows Server 2003
Windows Vista
Windows 7 (Windows Server 2008 R2)

この現象は残念ながら、コード変更時のシステム全体の影響などサイドエフェクト(副作用)が懸念されるため、修正は見送られております。
弊社製品の問題により、影響を受けていらっしゃるお客様に深くお詫び申し上げます。

[補足情報]
以下、サポート技術情報の回避方法セクションにさらに α を加えた対処方法案をご案内いたします。

-- コードによる回避例 (1) : ADsOpenObject() 処理部分を LogonUser() + AdsGetObject() のセットに変更する

LDAP プロバイダ (例 : "LDAP://") を用いて AdsOpenObject() をコールした場合はこの問題は発生しません。
あるいは、ADsGetObject() による ADSI 接続へ変更いただくことも有効な対処となります。

上記技術情報のメモリ増加の原因は認証を伴う ADSI 接続の API である ADsOpenObject を利用したための事象であるため、認証を伴わない ADsGetObject であればメモリ リークの影響は発生しません。この場合、認証については別途 LogonUser() を用いていただき、スレッドに対して特定のユーザーでの認証をしていただく方式をとることになります。具体的には、該当のスレッドの先頭にて LogonUser による認証を一度実施した後、ADsOpenObject() 使用部分を ADsGetObject() に変更するという処理の流れになります。
Microsoft オペレーティング システムでユーザー資格情報の認証を行う方法についての参考文書は以下となります。

文書番号: 180548 - 最終更新日: 2005年5月20日 - リビジョン: 3.4
Microsoft オペレーティング システムでユーザー資格情報の認証を行う方法
https://support.microsoft.com/kb/180548/

プラットフォーム SDK
LogonUser
https://msdn.microsoft.com/ja-jp/library/cc447468.aspx

ADsGetObject Function
https://msdn.microsoft.com/en-us/library/aa772184(VS.85).aspx

-- コードによる回避例 (2) : ADSI 処理を Out Process の COM にする
ADSI 処理を別の COM に分ける
ことも回避策として有効です。プログラムの設計によって可能な場合はこちらも合わせてご検討ください。
具体的には、ADSI 処理の本体部分およびその処理部分の制御部分を別々の COM に分割し、通常は制御部分の COM のみをサービスとして動作させ、必要に応じて ADSI 処理の COM を呼び出していただく形にします。

切り離された ADSI 処理を実装する COM は、 Out Process として動作させ、処理が終了する毎に CoUninitialize() を呼び出し、解放する形にします。

-- ADSI 使用時の注意
ADSI はマルチスレッドで使用されることを基本的に想定していません。 また、スレッドを開放せずに複数回 ADsOpenObject() (あるいは最終的にこれをコールする S.DS 名前空間のクラスなど) を用いる設計も推奨いたしません。これは本件だけでなく、ADSI プログラミング全般で考慮すべき事項となります。上記の回避例 (2) の Out Process の COM にする方法を応用し、マルチスレッドではなく、マルチプロセスで並列動作させることでマルチスレッド同様の動作を結果的に得られるなどの対処もあります。

*******

改めて弊社製品の不具合についてお詫び申し上げます。

~ ADSI サポートチーム ~