dnsHostName属性が無いサーバがいる環境でS.DS.AD.Utils.Compare実行するとNullReferenceException

皆さんごきげんよう。ういこうです。某サイトで本ブログの人気記事ランキングを見たら上位 4 記事が全部 Windbg というあたりにアイデンティティクライシスを感じる今日この頃ですが皆様いかがお過ごしでしょうか。今日は ADSI の問題についてひとつ事例を御紹介します。

【今日のお題】
Windows NT 4 ドメインがひっそりいる環境で .NET Framework 3.5 までのバージョンの System.DirectoryServices.ActiveDirectory.DirectoryServer.GetDirectoryEntry() を実行された際に System.NullReferenceException が発生する場合があります。.NET Framework 4 では発生しません。

【問題の概要】 某雑誌の某漫画「女子だらけの人気絶好調のアイドルグループに男子一人がひそかにまぎれてドキッ★」のように、「Windows Server 2000 以降の OS のドメイン コントローラの中に一人だけまぎれた NT4 サーバちゃん」という環境など、dnsHostName 属性が無いドメイン コントローラがいる環境を対象とした .NET Framework のバージョンが 3.5 までの環境上で上記メソッドを実行した場合に発生します。

-- 例外発生時のコール スタック (マネージのスタック) 2010/9/26 08:06:23 System.NullReferenceException is System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。
場所 System.DirectoryServices.ActiveDirectory.Utils.Compare(String s1, String s2, UInt32 compareFlags)
場所 System.DirectoryServices.ActiveDirectory.Utils.Compare(String s1, String s2)
場所 System.DirectoryServices.ActiveDirectory.DomainController.GetDomainControllerInfo()
場所 System.DirectoryServices.ActiveDirectory.DomainController.get_ServerObjectName()
場所 System.DirectoryServices.ActiveDirectory.DirectoryServer.GetDirectoryEntry()

【原因】 同メソッドが実行される際、ドメイン コントローラから dnsHostName 属性が取得できない場合、null チェックを行いハンドリングする実装が .NET Framework 3.5 、まで存在しないため、上記の System.NullReferenceException が発生します。.NET Framework 4.0 からこのチェック機構を入れるようになりましたため、 .NET Framework 4.0 以降では発生しません。

【詳細】
この現象は、dnsHostName 属性が何らかの原因で存在しなくなってしまったドメイン コントローラあるいは Windows NT 4 がドメイン コントローラ中に混じっている場合等に発生することが報告されています。

まず、System.DirectoryServices 名前空間 (S.DS) の問題となっている ActiveDirectory.Utils.Compare() メソッドおよびこれ以降 OS 内部で最終的にコールされる ADSI を含む Win32 API 群が Windows 2000 以上を対象として設計されているため、Windows NT 4 ベースのドメイン環境はそもそも動作保証外となります。

Windows NT は WINS で名前解決などを行うように基本設計が為されているため、そもそも DNS で使用される dnsHostName といった属性は既定では持たないため、ADSI ではこの環境をターゲットとした場合、値を取得できません。一方、今回問題が発生している Compare() メソッドでは、処理の過程で dnsHostName の確認を実施する実装となっています。このため、NT 4 サーバが対象の環境に存在する場合は動作しないことがあるということになります。(※接続先のサーバがたまたま NT4 出なかった場合は起きないです) また、ADSIEDIT 等で見て、Windows 2000 以降のサーバであっても dnsHostName 属性が存在しない場合は、正しい値を設定してみてください。ちなみに NT4 に対しても同属性を追加すればおそらく動作しますが、少なくとも製品開発部はそのようなシナリオで試していないので、動作保障外です。というより、windows NT 4 も、Windows 2000 もサポートは終了しておりますので、もしご利用の場合はマイグレーションの計画をお早めに立てられることをお勧めいたします。

【おまけ : 問題を特定するのに Windbg を使ってみる】
まずこの問題っぽい、あやしそうな場合は、ドメイン コントローラに対し、ADSIEDIT などで dnsHostName 属性が存在するか確かめてみますが、ドメイン コントローラに触らせてもらえない場合などは、現象発生時、Windbg ツールで、プログラムに渡されるデータを見ることでこの現象か判断してみるという手もあります。Windbg ってなあに?という方や、プログラムにアタッチして見てみる方法については、以下を参考にしてみてくださいね。

Windbg Tips 初級編 (1) : Install、Setting
https://blogs.technet.com/jpilmblg/archive/2009/02/21/debugging-windbg-1.aspx
Windbg Tips 初級編 (2) : 実践サンプル付き
https://blogs.technet.com/jpilmblg/archive/2009/02/25/debugging-windbg-2.aspx
Windbg Tips 初級編 (3) : Break Point の設定など
https://blogs.technet.com/jpilmblg/archive/2009/03/06/debugging-windbg-3-tips.aspx

さて、Windbg ツールをプログラムにアタッチさせてから、例外が発生するまで g コマンドで実行してみると、やがて例外が発生します。

(2a24.2234): Access violation - code c0000005 (first/second chance not available)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=068c8418 ecx=00000001 edx=0000042b esi=00000000 edi=013eeb80
eip=71fe02ae esp=0021e9f8 ebp=0021ea28 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
system_directoryservices_ni!System.DirectoryServices.ActiveDirectory.Utils.Compare(System.String, System.String, UInt32) +0x52:
71fe02ae 8b7608 mov esi,dword ptr [esi+8] ds:0023:00000008= ????????

例外コードは、c0000005。気持ちいいくらい一般的なコードです。これだけで原因を特定することなんてできません。
ただ、これを見ると、system_directoryservices_ni!System.DirectoryServices.ActiveDirectory.Utils.Compare(System.String, System.String, UInt32) で問題が発生したらしいということ、なんか変な値 ( ???????? ) が嫌な感じというのは判ります。

コール スタックとデータを見てみる。
さて、ではどんな人生を歩んだ結果、このプログラムは頓死するに至ったか。それを見るためにはまずコール スタックです。

ただし、コール スタックと言っても、昔の Visual Basic 6.0 とか、Visual C++ とかで作ったプログラムではなくて、Visual C# / Visual Basic .NET など、.NET Framework 上で動作させることが前提のプログラムの場合、調査の一番最初の時点では以下の二つの世界を同時に考えていく必要があります。

まぁ要は、.NET Framework 上でどうやって動いているかと、その下の OS 上でどう動いているかを見て、どっちの世界から優先して見てみればよいか判断する、ということです。

では、その両者の世界ってどう違うのか?…良くわからないですよね。私も色々探していて、難しいドキュメントばっかりで嫌になりました。もっと簡単に、私見たいな察しの悪い人とか、開発者じゃない人でも判るような説明はないのか。とちょっと唸ってしまいましたが、以下のページにざっくり度数が良い感じの図があったのでいただきストリートしてしまいました。

これをみてみると、以下のようなざっくりとした世界で動いていることがわかります。ちなみに以下の概念は、イメージをつかむことを優先しているので、技術的に厳密な説明などではないのに注意ください。厳密さが必要な場合は、後述の MSDN などご参照ください。

image

(1) .NET Framework (共通言語ランタイム、クラスライブラリ等)… ピンク色の部分。 C# / Visual Basic .NET / F# とかはここで動いてます。ここで動作するコードは、マネージコードともいいます。共通言語ランタイム = CLR がメモリの割り当て、解放など、あれこれ世話を焼いてくれる(マネージしてくれる)からです。
(2) Windows (オペレーティング システム)… 青色の部分。 COM、Win32 API や VB6 / C++ で作ったプログラムなどがここで動いています。ここで動くコードは、アンマネージコードといいます。(要は CLR が面倒を見てくれない = マネージしてくれない = アンマネージ)

プログラムの動作は最終的に Win32 API の世界に行きつくのですが、CLR の中で帰結する問題なのか、それともその下の Win32 API などのアンマネージ界を見るべきなのかといった部分を見分けるのに Windbg 等のツールを使っていきます。
ちなみに、System.DirectoryServices 名前空間のクラスは、最終的には Win32 の ADSI / WLDAP32 の世界にリダイレクトされ、動作しますので ADSI の世界でおきる問題は、System.DirectoryServices のメソッドでも起きることになります。

参考)

.NET Framework のディレクトリ サービス
https://msdn.microsoft.com/ja-jp/library/ms180826.aspx

image

ADSI がリークすれば S.DS アプリもリークする例
[ADSI] 障害情報 : WinNT プロバイダを用いて ADsOpenObject() をコールするとメモリリーク発生
https://blogs.technet.com/b/jpilmblg/archive/2010/05/14/adsi-winnt-adsopenobject.aspx

さて、大分脱線しましたが、マネージ、アンマネージのスタックを見てみましょう。
アンマネージのスタックは、Windbg の k コマンドを使います。色々 v とか n とか入れると表示され方や表示される情報量が変化します。私のお気に入りは kvn です。~* をつけると、全部のスタックが表示されて楽ちんです。ついでにスレッドの ID もわかります。

-- アンマネージのスタック
0:000> ~*kvn

.  0 Id: 2a24.2234 Suspend: 4096 Teb: 7ffde000 Unfrozen # ChildEBP RetAddr  Args to Child             
00 0021ea28 71fe03bf 00000000 00000000 00000000 system_directoryservices_ni!System.DirectoryServices.ActiveDirectory.Utils.Compare(System.String, System.String, UInt32) +0x52 (Managed)
01 0021ea40 71fe7369 00000000 00000000 00000000 system_directoryservices_ni!System.DirectoryServices.ActiveDirectory.Utils.Compare(System.String, System.String)+0x3b (Managed)
02 0021eaa4 71fe90c9 00000000 00000000 00000000 system_directoryservices_ni!System.DirectoryServices.ActiveDirectory.DomainController.GetDomainControllerInfo()+0x255 (Managed)
03 0021eab4 71fe9c8d 00000000 00000000 00000000 system_directoryservices_ni!System.DirectoryServices.ActiveDirectory.DomainController.get_ServerObjectName()+0x21 (Managed)
04 0021eac0 00cb1016 00000000 00000000 00000000 system_directoryservices_ni!System.DirectoryServices.ActiveDirectory.DirectoryServer.GetDirectoryEntry()+0x6d (Managed)
… (以下省略) …

これを見ると、呼び出し履歴(コール スタック) の最上位は、 (Managed) とあるので、どうも CLR 上で動作している部分が最後の部分のようですね。今度は CLR 上でどんな動作をしているのか見てみましょう。.NET Framework 上のスタックを見たい場合は、 .loadby sos mscorwks と入力して Enter を押してから、!clrstack コマンドを入力し、Enter を押します。

-- マネージのスタック(.NET Framework の共通言語ランタイム上のスタック)
0:000> .loadby sos mscorwks
0:000> !clrstack
OS Thread Id: 0x2234 (0)
ESP       EIP    
0021e9f8 71fe02ae System.DirectoryServices.ActiveDirectory.Utils.Compare(System.String, System.String, UInt32)
0021ea34 71fe03bf System.DirectoryServices.ActiveDirectory.Utils.Compare(System.String, System.String)
0021ea48 71fe7369 System.DirectoryServices.ActiveDirectory.DomainController.GetDomainControllerInfo()
0021eaac 71fe90c9 System.DirectoryServices.ActiveDirectory.DomainController.get_ServerObjectName()
0021eabc 71fe9c8d System.DirectoryServices.ActiveDirectory.DirectoryServer.GetDirectoryEntry()
… (以下省略) …

これを見ると、ほとんどアンマネージと、マネージとで動作が一致していますね。
スレッドの ID も、それぞれ 2234 と一致しています。

-- アンマネージ
.  0  Id: 2a24.2234 Suspend: 4096 Teb: 7ffde000 Unfrozen # ChildEBP RetAddr  Args to Child             
-- マネージ
OS Thread Id: 0x2234 (0)

ここで問題が起きたと見てよいでしょう。
次に、!clrstack -a コマンドを実行します。-a を与えると、関数に渡されている引数と、ローカル変数に関する情報を一気に表示してくれます。
コマンドの詳細は以下でご覧いただけます。

SOS.dll (SOS Debugging Extension)
https://msdn.microsoft.com/ja-jp/library/bb190764.aspx

0:000> !clrstack -a
OS Thread Id: 0x2234 (0)
ESP       EIP    
ESP/REG  Object   Name
0021e9f8 71fe02ae System.DirectoryServices.ActiveDirectory.Utils.Compare(System.String, System.String, UInt32)
    PARAMETERS:
        s1 = 0x00000000 ← ★ ここ
        s2 = 0x013eeb80
        compareFlags = 0x00031003
    LOCALS:
        0x0021ea00 = 0x00000000
        0x0021e9fc = 0x00000000
        0x0021e9f8 = 0x00000000
        <no data>
        <no data>
… (以下省略) …

パラメータとして渡される s1 = 0x00000000 (NULL) となっています。ここは残念ながら、内部実装の世界になってしまうのですが、今回紹介させて頂いているパターンにはまっている場合、この s1 にあたる部分は System.DirectoryServices.ActiveDirectory.DsDomainControllerInfo2 の dnsHostName となります。この部分が NULL (00000000) となってしまうことにより問題が発生しているのです。

まず、スタック上のオブジェクトをダンプするために !dso コマンドを実行します。

0:000> !dso
OS Thread Id: 0x2234 (0)
ESP/REG  Object   Name
edi      013eeb80 System.String
0021ea20 013eeb80 System.String    dsfimnt4.microsoft.com
0021ea48 013f4164 System.DirectoryServices.ActiveDirectory.DomainController
0021ea50 013f5b34 System.DirectoryServices.ActiveDirectory.DsDomainControllerInfo2
0021ea54 013f51bc System.DirectoryServices.ActiveDirectory.NativeMethods+DsGetDomainControllerInfo

次に、DsDomainControllerInfo2 = 013f5b34 の中身を !do コマンドで見てみます。

0:000> !do 013f5b34
Name: System.DirectoryServices.ActiveDirectory.DsDomainControllerInfo2
MethodTable: 72025e74
EEClass: 71f5a778
Size: 104(0x68) bytes
GC Generation: 0
(C:\Windows\assembly\GAC_MSIL\System.DirectoryServices\2.0.0.0__b03f5f7f11d50a3a\System.DirectoryServices.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
713608ec  400049b        4        System.String  0 instance 013f5b9c netBiosName
713608ec 400049c 8 System.String 0 instance 00000000 dnsHostName ← ★ ここ
713608ec  400049d        c        System.String  0 instance 00000000 siteName
713608ec  400049e       10        System.String  0 instance 00000000 siteObjectName
713608ec  400049f       14        System.String  0 instance 013f5bbc computerObjectName
713608ec  40004a0       18        System.String  0 instance 00000000 serverObjectName
713608ec  40004a1       1c        System.String  0 instance 00000000 ntdsaObjectName
713343b8  40004a2       20       System.Boolean  1 instance        0 isPdc
713343b8  40004a3       21       System.Boolean  1 instance        0 dsEnabled
713343b8  40004a4       22       System.Boolean  1 instance        0 isGC
7135c7fc  40004a5       24          System.Guid  1 instance 013f5b58 siteObjectGuid
7135c7fc  40004a6       34          System.Guid  1 instance 013f5b68 computerObjectGuid
7135c7fc  40004a7       44          System.Guid  1 instance 013f5b78 serverObjectGuid
7135c7fc  40004a8       54          System.Guid  1 instance 013f5b88 ntdsDsaObjectGuid

うーん、null ですね。
これが今回の問題の特徴です。

dnsHostName なんて、ADSIEDIT とかで見ればいいじゃん!というあなた、いえいえ、もしかしたらそもそもドメインコントローラ様にあわせて頂けないかもしれません。そんな時は、こっそり現象発生時にダンプを取ってみて上のパターンに一致するか見てみるってこともできます。
手段の一つとしてちょっとお試し頂くのは如何でしょうか?

それではまた。

おまけ :
CLR のハンドルされない例外の処理
https://msdn.microsoft.com/ja-jp/magazine/cc793966.aspx
.NET Framework 4CLR デバッグのアーキテクチャ
https://msdn.microsoft.com/ja-jp/library/bb384548.aspx
Visual Studio 2010 - Visual C#Introduction to the C# Language and the .NET Framework
https://msdn.microsoft.com/ja-jp/library/z1zx9t92.aspx
共通言語ランタイム (CLR)
https://msdn.microsoft.com/ja-jp/library/cc825639.aspx

ういこう@ニュースで「乳がんに対する正しい知識が乳がんに対する最高の対抗手段」を「UMA (ゆーま)に対する正しい知識が UMA に対する最高の対抗手段」と聞き違えましたがあながち間違ってないとも思います。