NSFItemInfoNext対応の悪影響

1年半前の記事で、「NSFItemInfoNext」関数についての対応策について書いた。

chiburusystems.hatenablog.com

これについて、その後特に問題もなく順調にいっていたのだが、つい先日、とうとうこの対応が別の形で悪さをする事案に陥った。

以前の記事の概略

前回の対応をかいつまんで説明する

NSFItemInfoNext関数の第2引数はBLOCKIDという構造体を指定する。

typedef WORD BLOCK; /* pool block handle */

typedef struct /* Pointer to any block in any pool */
{
  DHANDLE pool; /* pool handle */
  BLOCK block; /* block handle */
} BLOCKID;

ところが、DominoサーバWindows64ビット版においては、この「BLOCK」は2バイトの「WORD」型ではなく、4バイトの「DWORD」型を指すという事実だ。

#ifdef W64
typedef DWORD BLOCK;
#else
typedef WORD BLOCK;
#endif

こうしないと、Win64においては必ずクラッシュする。幸い、Win32版、Mac版やLinux版ではこれを意識しなくてもいい。

今回発生した問題

このまま1年半使い続けてきたが、極めて限定的な条件の時にのみ、この対応がDominoサーバのクラッシュを生むらしい。私の認識している範囲で言えば、レプリカ直後の文書において、「特殊な」アクセスを試みたときに起こる(詳細は割愛する)。

NSFItemInfo関数は、文書内のアイテム情報(フィールド)を取得する。NSFItemInfoNext関数は、同一フィールド名の2番目以降を取得する。添付ファイルなどの特殊な場合を除き、1つのアイテムには64キロバイトの制限があるので、大きいサイズのフィールドはいくつかのブロックに分割して保存する。前出の「BLOCKID」はそれらを識別するための仕組みである。

ブロックからフィールドデータを得るためには、ブロックをロックして、ポインタを取得する。

void far * LNPUBLIC OSLockObject (DHANDLE Handle);

#define OSLock(blocktype,handle) ((blocktype far *) OSLockObject(handle))

#define    OSLockBlock(type,blockid) \
  ((type far *)(OSLock(char,(blockid).pool) + (blockid).block))

OSLockObject関数でハンドルからポインタを得る。

OSLockマクロは、単に得られたポインタを指定した型のポインタにキャストする。

OSLockBlockマクロは、BLOCKIDのpoolハンドルから得たcharポインタから、blockバイト分移動した場所を指すようにして返す。

一般的な使い方であればこれで問題がなかった。しかし、「特殊なアクセス」でこのOSLockBlockマクロを使ってポインタを得ると、とんでもないことになる。

問題の根本は、BLOCKID.blockの型を2バイトから4バイトにしたことにある。特殊なアクセスをすると、この4バイトの上位2バイトに得体の知れない値「0xc000」が含まれてしまうのである。元々blockは2バイトなので、OSLockBlockマクロが64キロバイト以上移動した場所を指すことはない。しかし、NSFItemInfoNext関数の「実装バグ」対応でBLOCKID.blockを4バイトにしたWin64 C APIプログラムでは、4バイトのblock値を受け取ってしまう。上位に「0xc000」という値が入ってしまえば、OSLockBlockマクロはとんでもない場所をポインタとして返してしまう。もちろんこのポインタでメモリアクセスすれば、保護違反でクラッシュする。幸い今まで上位2バイトに0以外が入ったことがなかっただけで、0以外が入らない保証はなく、この危険性は大いにあったわけである。

DBLOCK、DBLOCKIDの存在

この事案に直面した折、あらためてC APIヘッダファイルを確認したところ、BLOCK、BLOCKID以外に4バイト版とも言うべき以下の型を発見した。いずれもpool.hヘッダファイルである。

typedef DWORD DBLOCK; /* dpool block handle */

typedef struct /* Pointer to any block in any pool */
{
  DHANDLE pool; /* pool handle */
  DBLOCK block; /* block handle */
} DBLOCKID;

これらがWindows64ビット版で活用されてもいいところだが、残念ながら、APIツールキット9.0.1上では、これらは宣言されているだけで、NSFItemInfoNext関数などには全く利用されていない。

う〜む、またしても小手先で対応しなければならないではないか!

対応策

今回も、Windows64ビット版限定の対応とする。

// pool.hの54行目
#define    OSLockBlock(type,blockid) \
  ((type far *)(OSLock(char,(blockid).pool) + (blockid).block))

これを次のように書き換える。

#ifdef W64
#define    OSLockBlock(type,blockid) \
  ((type far *)(OSLock(char,(blockid).pool) + ((blockid).block & 0x0000ffff)))
#else
#define    OSLockBlock(type,blockid) \
  ((type far *)(OSLock(char,(blockid).pool) + (blockid).block))
#endif

BLOCKID.blockの値上位ビットをビットマスクでバッサリ切り捨てる。そもそもマクロやビットマスクの対応も、C++11以降のモダンC++の世界でどうかと思うが、まあこれはCのためのものなので、出しゃばったまねはしないようにする。

まとめ

クラッシュの原因特定まで時間を要した。「特殊なアクセス」という限定的な条件のときだけ起こるクラッシュ。その差分からとんちんかんな方向にも時間を費やした。最終的に、自分が書いた昔の記事に助けられた。こんな状況、海外も含めて同志がいないのもなかなかさみしいものだが、また一つ、自分の成長を実感できたことは嬉しい。

最後にもう一つ、HCLのNotes/Domino担当の皆さん、C APIのサポートってどうなっていますか?