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
うまくいきます。
このときの処理のフローは下記のようになっています。
- main
- libc::gethostbyname()
- libc::gethostbyname_r()
- nss
- nsswitch/wins.c::_nss_wins_gethostbyname_r()
これを,無駄に libsmbclient とリンクしてビルド・実行してみます。
$ gcc -o test test.c -lsmbclient
すると
*** glibc detected *** ./test: realloc(): invalid pointer: 0xb7f8d06c ***
みたいにいわれて落ちます。
このときのコールスタックは下記のような感じ。
- main
- libc::gethostbyname() or libc:getaddrinfo()
- libc::gethostbyname_r()
- nss
- nsswitch/wins.c::_nss_wins_gethostbyname_r()
- lib/debug.c::setup_logging()
- lib/debug.c::debug_init()
- lib/debug.c::debug_add_class()
- SMB_REALLOC_ARRAY()
- 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.so
は libsmbclient.so
を動的リンクしているものだと思っていましたけれど,ldd してみたら違いました。libsmb 以下のオブジェクトファイルを直接リンクしていたのでした。
なので,シンボルの衝突が発生していたみたいです。なにがおきるかというと,Binary Hacks の Hack#19「リンク時のシンボルの衝突に注意する」を参照してください。元ネタが リンクと同名のシンボル - bkブログ にあります。
具体的な挙動は,
- main が libsmbclient.so をロードする
DEBUGLEVEL_CLASS
を resolve in libsmbclient.solibc::gethostbyname()
を呼び出す- nss によって libnss_wins.so がロードされる
DEBUGLEVEL_CLASS
はすでに libsmbclient.so にマップされているlibnss_wins::debug_add_class()
がDEBUGLEVEL_CLASS
の値が初期化値と異なると誤認識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_CLASS
は external::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「ライブラリの外に公開するシンボルを制限する」が使えるのかなぁ。