libsmbclient + nss_wins の問題

2008-05-21 追記: 下記に(とくにメカニズム)間違いがありました。あとで書きます。

現象

nss_wins(bundled with Samba)を hosts の名前解決時に使うような下記の設定だとします。

# /etc/nsswitch.conf
hosts:          files mdns4_minimal [NOTFOUND=return] dns mdns4 wins

んで,たとえば単純に gethostbyname() を使うコードを書いてみます。

/* test.c: */
#include <stdio.h>
#include <netdb.h>

int
main(int argc, char *argv[])
{
    struct hostent *hp;

    hp = gethostbyname(argv[1]);
    if (hp) {
        fprintf(stderr, "h_name: %s\n", hp->h_name);
    }

    return 0;
}

これをビルドして実行すると,

$ gcc -o test test.c

$ ./test HOGEHOGE
h_name: HOGEHOGE

うまくいきます。

このときの処理のフローは下記のようになっています。

  1. main
  2. libc::gethostbyname()
  3. libc::gethostbyname_r()
  4. nss
  5. nsswitch/wins.c::_nss_wins_gethostbyname_r()

これを,無駄に libsmbclient とリンクしてビルド・実行してみます。

$ gcc -o test test.c -lsmbclient

すると

*** glibc detected *** ./test: realloc(): invalid pointer: 0xb7f8d06c ***

みたいにいわれて落ちます。

このときのコールスタックは下記のような感じ。

  1. main
  2. libc::gethostbyname() or libc:getaddrinfo()
  3. libc::gethostbyname_r()
  4. nss
  5. nsswitch/wins.c::_nss_wins_gethostbyname_r()
  6. lib/debug.c::setup_logging()
  7. lib/debug.c::debug_init()
  8. lib/debug.c::debug_add_class()
  9. SMB_REALLOC_ARRAY()
  10. libc::realloc()

なぜか lib/debug.c の debug_add_class() での realloc() が失敗しているみたいです。

カニズム

なので気になる lib/debug.c の内容をみてみます。

/* source/lib/debug.c: */

/******* snip snip snip *******/

static int debug_all_class_hack = 1;

int     *DEBUGLEVEL_CLASS = &debug_all_class_hack;

/******* snip snip snip *******/

int debug_add_class(const char *classname)
{
    void *new_ptr;

    /******* snip snip snip *******/

    new_ptr = DEBUGLEVEL_CLASS;
    if (DEBUGLEVEL_CLASS == &debug_all_class_hack) {
            /* Initial loading... */
            new_ptr = NULL;
    }
    new_ptr = SMB_REALLOC_ARRAY(new_ptr, int, debug_num_classes + 1);
    if (!new_ptr)
            return -1;
    DEBUGLEVEL_CLASS = (int *)new_ptr;

    /******* snip snip snip *******/
}

抜粋してみました。

まぁよくあるコードですね。あらかじめ変数にダミーを初期化値として設定しておいて,初期化されてなかったらうんぬん,みたいな。realloc() 時に元ポインタとして NULL を渡すと malloc() 相当になるというのがポイントといえばポイントですが。

で,いままで全然気づいてなかったんですが,これらの DEBUGLEVEL_CLASS というシンボルは libnss_wins.so や libsmbclient.so などで別個に定義されてます。

$ nm -D /usr/lib/libsmbclient.so.0.1 \
    | grep DEBUGLEVEL_CLASS

0020f080 D DEBUGLEVEL_CLASS
0020f084 D DEBUGLEVEL_CLASS_ISSET

$ nm -D /lib/libnss_wins.so.2 \
    | grep DEBUGLEVEL_CLASS

000dc720 D DEBUGLEVEL_CLASS
000dc724 D DEBUGLEVEL_CLASS_ISSET

いっぽう debug_all_class_hack は定義されてない。

$ nm -D /usr/lib/libsmbclient.so.0.1 \
    | grep debug_all_class_hack

static 変数なのであたりまえですけれど。

なんとなく libnss_wins.solibsmbclient.so を動的リンクしているものだと思っていましたけれど,ldd してみたら違いました。libsmb 以下のオブジェクトファイルを直接リンクしていたのでした。

なので,シンボルの衝突が発生していたみたいです。なにがおきるかというと,Binary Hacks の Hack#19「リンク時のシンボルの衝突に注意する」を参照してください。元ネタが リンクと同名のシンボル - bkブログ にあります。

具体的な挙動は,

  1. main が libsmbclient.so をロードする
  2. DEBUGLEVEL_CLASS を resolve in libsmbclient.so
  3. libc::gethostbyname() を呼び出す
  4. nss によって libnss_wins.so がロードされる
  5. DEBUGLEVEL_CLASS はすでに libsmbclient.so にマップされている
  6. libnss_wins::debug_add_class()DEBUGLEVEL_CLASS の値が初期化値と異なると誤認識
  7. realloc() しようとしてエラー

みたいになります。

シンプルに再現してみる

ちょっと実感としてわかりにくかったのでシンプルなモデルに落としてみました。ほぼ内容は元ネタと同じなんですが。

/* main.c */

#include <stdio.h>

static int debug_all_class_hack = 1;
int *DEBUGLEVEL_CLASS = &debug_all_class_hack;

int
main(int argc, char *argv[])
{
    fprintf(stderr, "DEBUGLEVEL_CLASS:      0x%08x\n"
                    "&debug_all_class_hack: 0x%08x\n"
            , (unsigned int) DEBUGLEVEL_CLASS
            , (unsigned int) &debug_all_class_hack);

    return 0;
}
/* external.c */

static int debug_all_class_hack = 1;
int *DEBUGLEVEL_CLASS = &debug_all_class_hack;

最初は external.c はリンクしないでやってみます。

# Makefile

all:    test

clean:
        rm -f test *.so

test:   main.so
        gcc -Wall -g -o $@ ./main.so

main.so:        main.c

external.so:    external.c

.c.so:
        gcc -Wall -g -fPIC -shared -c -o $@ $<

.SUFFIXES:      .c .so

ビルドして実行すると,

$ ./test

DEBUGLEVEL_CLASS:      0x08049648
&debug_all_class_hack: 0x08049648

同じ値になっている(正しい!)。


次に external.c もリンクするバージョンをやってみます。リンカ ld のオプションに --allow-multiple-definition をつけないとだめでした。

test:   main.so external.so
        gcc -Xlinker --allow-multiple-definition \
            -Wall -g -o $@ ./external.so ./main.so

先に external.so を追加していることに注意。

これでビルドして実行すると,

$ ./test

DEBUGLEVEL_CLASS:      0x08049648
&debug_all_class_hack: 0x08049650

おお,見事に異なりました。

このプログラムだと DEBUGLEVEL_CLASSexternal::DEBUGLEVEL_CLASS になってる。だから内容は &external::debug_all_class_hack なんだけど,&main::debug_all_class_hack を表示すると当然値は異なる,と。

ということで無事?再現しました。

What is the next action?

Ubuntu のせいじゃなくて upstream (Samba) のせいだと確定しました。なので Samba に報告しないといけないんですが……うーんこれをどう英語で記述して,どういう解決策を提示してあげるのがいいんだろう。

libnss_wins.so(や winbind 系や pam 系)を libsmbclient をリンクするようにすればいいんだと思うんですけどね。あるいは Hack#29「ライブラリの外に公開するシンボルを制限する」が使えるのかなぁ。