激動の2020年も気づけば残り3ヵ月、朝晩はめっきりと涼しくなって、ソースコードと戯れるのには絶好の季節になってきました。今回は、前回 に引き続いてglibc2のNSS回りの話題です。
転回
この問題をメンテナMLに報告してあれこれ議論しているうち、メンテナの加藤さんが「lsをstraceしたら、libnss_files.so.2を開いている」旨を報告してくれました。
strace は、対象のプログラムが動作する際に使うシステムコールや開こうとするファイルを追跡するコマンドです。前回紹介したgdbがプログラムの動作を追いかけるコマンドなのに対し、straceはプログラムとOSとのやりとりを調査するコマンドと言えるでしょう。straceのことはすっかり失念していたので、さっそく手元でも試してみました。
$ strace /sbin/installer/ls -l |& cat -n
1 execve("/sbin/installer/ls", ["/sbin/installer/ls", "-l"], 0x7ffcf614a268 /* 28 vars */) = 0
2 uname({sysname="Linux", nodename="2020_0822", ...}) = 0
3 brk(NULL) = 0x1b62000
4 brk(0x1b63200) = 0x1b63200
5 arch_prctl(ARCH_SET_FS, 0x1b628c0) = 0
...
80 openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 4
81 fstat(4, {st_mode=S_IFREG|0644, st_size=1686, ...}) = 0
82 lseek(4, 0, SEEK_SET) = 0
83 read(4, "root:x:0:0::/root:/bin/bash\nbin:"..., 4096) = 1686
84 --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xe5} ---
85 +++ killed by SIGSEGV +++
この結果を見ると、SIGSEGVで落ちているのはgdbで見た時と同じなものの、その直前に/etc/passwdを開き(80行目) 、そこからデータを読み取って(83行目)から、SIGSEGVが発生していることがわかります。
もう少し絞り込むために、openシステムコールを呼び出している部分だけ取り出してみます。
$ strace /sbin/installer/ls -l |& grep open | cat -n
....
9 openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
10 open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 4
11 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 4
12 open("/lib/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 4
13 open("/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 4
14 open("/lib64/ld-linux-x86-64.so.2", O_RDONLY|O_CLOEXEC) = 4
15 openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 4
最初の方は今回の問題とは関係ないロケール回りの処理なので省略しました。
この結果を見ると、本来はNSSの設定ファイル(nsswitch.conf)を読んで、その設定("passwd: files")に従って/etc/passwdを見に行くはずなのに、libnss_files.so.2を読み込んで(12行目) 、芋づる式にlibc.so.6やld-linux-x86-64.so.2を読みこんでしまっています。
この動作はどう見ても仕様のレベルだよなぁ…… と、改めてglibc-2.32に付属のconfigureスクリプトを眺めていると、static_nss という変数が存在することに気づきました。
「あれれ、static_nssって定義する必要があるのか?」と、これをキーワードにconfigureスクリプトを読み進めると、文字通り--enable-static-nss というオプションがありました。
$ cat -n configure | less
....
1436 --enable-stack-protector=[yes|no|all|strong]
1437 Use -fstack-protector[-all|-strong] to detect glibc
1438 buffer overflows
1439 --enable-static-nss build static NSS modules [default=no]
1440 --disable-force-install don't force installation of files from this package,
1441 even if they are older than the installed files
....
configureの説明は"build static NSS modules(スタティック版のNSSモジュールを作る)"というごく簡単なもので、これだけでは意味がよくわからないから、"--enable-static-nss"をキーワードにドキュメント類を調べ直したところ、glibc2のインストール方法を紹介する"INSTALL"というファイルにこんな記述がありました。
$ cat -n INSTALL | less
....
149 '--enable-static-nss'
150 Compile static versions of the NSS (Name Service Switch) libraries.
151 This is not recommended because it defeats the purpose of NSS; a
152 program linked statically with the NSS libraries cannot be
153 dynamically reconfigured to use a different name database.
154
....
どうやらこのオプションを指定しないとNSS回りのライブラリを静的リンク用にできないようです。ただし、この設定で作ったstatic linkなバイナリは、名前データベースを動的に切り替えれなくなるため、このオプションの指定は推奨しない、とのこと。確かにNSSの仕組みを考えると、この機能は動的に切り替えれた方が便利だとは思うものの、今回はこのあたりが原因のようなので、"--enable-static-nss"オプションを指定してglibc-2.32を再ビルドすることにしました。
インターミッション
UNIXの世界では、各ユーザはユーザID (uid)という数字で管理されています。しかしながら、人間にとっては数字だけでは扱いにくいので、uidに結びつけたユーザ名 を利用できるようになっています。このマッピングに使われるのが/etc/passwdファイルです。
uidとユーザ名以外にも、gidとグループ名、IPアドレスとホスト名など、数字と名称を結び付けるための設定ファイルは他にもいくつかあるものの、ここではそれらの代表例として/etc/passwdを使っています。
元々、/etc/passwdはそれぞれのコンピュータごとに用意していたものの、ユーザやコンピュータが増えて、さらにそれらがネットワークに接続されていくと、データの更新やコンピュータ間でのファイルの同期が大変になります。
そこで考案されたのが"YP (Yellow Pages)"と呼ばれる機能で、/etc/passwdファイルを1台のサーバ機で集中管理し、他のコンピュータは必要に応じてそのサーバに問い合わせよう、という仕組みです。
"Yellow Pages"は英米の「職業別電話帳」の謂( いい ) で、電話帳を使って電話番号を調べることになぞらえて名付けられたものの、"Yellow Pages"という語は商標登録されていたため、後に"NIS (Network Information Service)"と改称されました。
YP/NISはSun Microsystems社が開発し、業界標準としてUNIX系OSで広く採用されています。一方、Linuxが採用しているglibc2では、後発なこともあって単にNISの機能を再現するだけでは飽き足らず、「 名前解決」の対象範囲を広げるとともに、解決方法や適用順も自由に変更できるような実装にして、NSS (Name Service Switch)と呼ぶようになりました。
NSSの特徴のひとつが、名前解決の方法を共有ライブラリとして簡単に追加できる ことです。
たとえば、Plamo Linuxではネットワークプリンタ等を見つけるために、Avahi と呼ばれるマルチキャストDNS (mDNS)サーバを採用しています。
そして,このmDNSサービスを利用するためのNSSプラグインがnss_mdns というパッケージです。
このプラグインの本体はlibnss_mdns[4,6,_minimal].so.2という共有ライブラリで、このライブラリを適切なディレクトリに配置し、/etc/nsswitch.conf のhosts行にmDNSを参照するような指示を追加するだけでmDNSサービスが利用可能になります。
Plamo Linuxではnsswitch.confのhosts行はこのようにしています。
hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4
この設定で、ホスト名とIPアドレスの参照が必要になった場合、/etc/passwdを調べるfiles 、mDNSサーバAvahiに問い合わせるmdns4_minimal 、DNSサーバに問い合わせるdns 、それでも見つからなければmdns4 、という順でNSS用モジュールが適用されることになります。
なお、"mdns4_minimal [NOTFOUND=return]"という指定は、調べたいホスト名がmDNSの命名ルールに基づく"XXXX.local"だった場合にのみ適用し、[NOTFOUND=return]は、その名前がmDNSサーバ(Avahi)で解決できなければそのまま終了せよ、という指示です。この設定により、mDNS用の名前である"XXXX.local"をDNSサーバに問い合わせることを防いでいます。
glibc2では、このように名前解決の手段をプラグインで追加できることがウリのひとつとなっているため、"--enable-static"で明示的に指定しないと、static linkしたバイナリも"libnss_files.so"等の共有ライブラリを見に行くという仕様になっているようです。
解決
まずconfigureオプションに"--enable-static-nss"を追加してglibc-2.32をビルドし直し、指定せずにビルドした場合と比較したところ、作成されるライブラリの数や種類は同じものの、static link時に利用されるlibc.a とlibc_nonshared.a のサイズがだいぶ増加しました。
具体的なサイズを見てみると、"--enable-static-nss"を指定しなかった場合、libc.aが5.2MB、libc_nonshared.aは23KBでした。
$ ls -lh default_nss/usr/lib/libc*.a
-rw-r--r-- 1 kojima users 5.2M 8月 7日 08:06 default_nss/usr/lib/libc.a
-rw-r--r-- 1 kojima users 23K 8月 7日 08:06 default_nss/usr/lib/libc_nonshared.a
一方、"--enable-static-nss"を指定してビルドすると、libc.aが5.4MB、libc_nonshared.aが266KBで、それぞれ200KB ほど増加しました。
$ ls -lh static_nss/usr/lib/libc*.a
-rw-r--r-- 1 kojima users 5.4M 8月 16日 08:36 static_nss/usr/lib/libc.a
-rw-r--r-- 1 kojima users 266K 8月 16日 08:36 static_nss/usr/lib/libc_nonshared.a
実際にどのような機能が増えたのかをlibc.aに含まれるシンボル名 から調べてみたところ、今回問題となった"nss_files"に関連するシンボルは、前者では8 だけだったのに対し、後者では143 まで増加していました。
$ nm default_nss/usr/lib/libc.a |& grep nss_files | wc -l
8
$ nm static_nss/usr/lib/libc.a |& grep nss_files | wc -l
143
具体的に、どのような機能(=シンボル)が追加されたのかを確認してみると、"nss_files_fopen.o"というファイルの中に、実際にファイルを読む等の処理を行うgethostbyaddr やgetpwent といったシンボルが多数含まれていました。
$ nm static_nss/usr/lib/libc.a |& grep nss_files | cat -n
...
59 nss_files_fopen.o:
60 0000000000000000 T __nss_files_fopen
61 U __nss_files_fopen
62 0000000000000310 T _nss_files_endprotoent
63 0000000000000520 T _nss_files_getprotobyname_r
64 00000000000006d0 T _nss_files_getprotobynumber_r
65 0000000000000370 T _nss_files_getprotoent_r
66 0000000000000000 T _nss_files_parse_protoent
67 0000000000000280 T _nss_files_setprotoent
68 U __nss_files_fopen
69 0000000000000360 T _nss_files_endservent
70 0000000000000570 T _nss_files_getservbyname_r
....
141 0000000000000000 T _nss_files_initgroups_dyn
142 U _nss_files_parse_grent
143 0000000000000000 T _nss_files_init
一方、"--enable-static-nss"オプションを指定せずにビルドしたlibc.aでは、実際の処理を行うためのコードはほとんど見あたりません。
$ nm default_nss/usr/lib/libc.a |& grep nss_files | cat -n
1 0000000000000000 T _nss_files_parse_grent
2 0000000000000000 T _nss_files_parse_pwent
3 0000000000000000 T _nss_files_parse_spent
4 U _nss_files_parse_spent
5 0000000000000000 T _nss_files_parse_sgent
6 U _nss_files_parse_sgent
7 nss_files_fopen.o:
8 0000000000000000 T __nss_files_fopen
どうやらこのlibc.aを使えば共有ライブラリを呼び出さずにNSS機能が使えそうなので、さっそくcoreutils-8.32のlsをstatic linkでビルドし、組み込まれるシンボルを確認しました。
$ nm static-build/src/ls | grep nss | cat -n
1 000000000053a2a0 V __nss_aliases_database
2 0000000000472d40 T __nss_configure_lookup
...
68 0000000000473890 T _nss_files_getprotobyname_r
69 0000000000473a40 T _nss_files_getprotobynumber_r
70 00000000004736e0 T _nss_files_getprotoent_r
71 0000000000476bf0 T _nss_files_getpwent_r
72 0000000000476da0 T _nss_files_getpwnam_r
73 0000000000476f30 T _nss_files_getpwuid_r
...
101 0000000000477fb0 T _nss_netgroup_parseline
102 0000000000472490 t nss_parse_service_list
static linkしたlsに組み込まれているシンボルは102あり、_nss_files_getpwent_r 等、/etc/passwdを読むための機能も含まれているようです。
念のため、"--enable-static-nss"オプションを使わずに作成したglibc-2.30な環境(Plamo-7.2のデフォルト)で同じcoreutils-8.32をビルドして確認したところ、含まれるNSS関連のシンボルは26ほどで、"getpwent"等、/etc/passwdを実際に読み込む機能は共有ライブラリ(libnss_files.so)に任せているようです。
$ nm src/ls | grep nss | cat -n
1 000000000051d8e8 V __nss_aliases_database
2 0000000000479be0 T __nss_configure_lookup
...
19 000000000051d8a8 V __nss_passwd_database
20 000000000047a2c0 T __nss_passwd_lookup2
...
24 000000000051d888 V __nss_services_database
25 000000000051d880 V __nss_shadow_database
26 0000000000479330 t nss_parse_service_list
この"--enable-static-nss"なglibc2環境で作ったstatic linkのlsは、NSS回りの機能が自前で完結しているため、glibc-2.32以前の環境でも"Assertion failure"になったり、/lib/libnss_files.soを呼び出したりすることなく動作しました。
$ ls /lib/libc*
/lib/libc-2.30.so* /lib/libcap.so.2@ /lib/libcrack.so.2@ /lib/libcrypt-2.30.so*
/lib/libc.so.6@ /lib/libcap.so.2.27* /lib/libcrack.so.2.9.0* /lib/libcrypt.so.1@
$ strace ./ls -l |& grep openat | cat -n
...
5 openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 4
6 openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC) = 4
7 openat(AT_FDCWD, "/etc/group", O_RDONLY|O_CLOEXEC) = 4
8 openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 3
...
このように、glibc-2.32を"--enable-static-nss"オプションで作り直し、その環境でインストーラ用のstatic linkなバイナリファイルを作り直すことで、名前解決機能がglibc2のバージョンに依存する問題が解決できました。
2回に渡って、glibc-2.32の更新時に発生したトラブルを例に、nmやstraceを使ったバイナリデバッグの例を紹介してみた結果、リストや操作例が多くなってしまい、記事として読みづらくなったことをお詫びします。
Linuxに代表されるOSSの世界ではたいていのソースコードは公開されているので、printfデバッグやステップトレースで追いかけることも可能なものの、glibc2のような大規模なソフトウェアの場合、ソースコードをコツコツと調べるよりも、作成されたオブジェクトファイルを直接解析する方が手っ取り早いこともあります。
筆者自身、この分野にはまだまだ未熟なものの、機会があればこのような"binary hack"も紹介してみたいと思っています。