続・玩式草子 ―戯れせんとや生まれけん―

第24回君の名は(NSS:Name Service Switch)[その2]

激動の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ファイルです。

元々、/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を調べるfilesmDNSサーバAvahiに問い合わせるmdns4_minimalDNSサーバに問い合わせる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.alibc_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"というファイルの中に、実際にファイルを読む等の処理を行うgethostbyaddrgetpwentといったシンボルが多数含まれていました。

$ 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"も紹介してみたいと思っています。

おすすめ記事

記事・ニュース一覧