The Man Who Fell From The Wrong Side Of The Sky:2007年2月8日分

2007/2/8(Thu)

どのようにしてlibcは後方互換を保つのか?(その3) 最終回

@ 共有ライブラリのSONAME変更による整合性問題

先日の「 共有ライブラリの後方互換性が壊れるとき」のエントリで
SONAMEの変更によって新旧のバージョンを共存する方法を説明しました。

しかしこの方法には大きな欠点があります。

  • libfoo.so.0(旧)をリンクするlibbar.so.0
  • libfoo.so.1(新)をリンクするlibbuz.so.0

が存在し、アプリケーションはlibbar.so.0をlibbuz.so.0をリンクしたい場合です。

どんな問題が発生するかサンプルコードで確認しましょう。

  • まずlibfoo.so.0を用意します。
    foo.h
    ------------------------------
    #ifndef _FOO_H_
    #define _FOO_H_
    
    void foo(const char *);
    
    #endif
    
    foo.c
    ------------------------------
    #include <stdio.h>
    #include <foo.h>
    void
    foo(const char *s)
    {
    	printf("%s\n", s);
    }
    
    $ gcc -shared -Wl,-soname=libfoo.so.0 -I. -o libfoo.so.0 foo.c
    $ ln -sf libfoo.so.0 libfoo.so
    
  • 次にこのlibfoo.so.0をリンクするlibbar.so.0を作ります。
    bar.h
    ------------------------------
    #ifndef _BAR_H_
    #define _BAR_H_
    
    void bar();
    
    #endif
    
    bar.c
    ------------------------------
    #include <foo.h>
    #include <bar.h>
    
    void
    bar()
    {
    	foo("i'm bar.");
    }
    
    $ gcc -shared -Wl,-rpath=.,-soname=libbar.so.0 -I. -L. -lfoo -o libbar.so.0 bar.c
    $ ln -sf libbar.so.0 libbar.so
    $ ldd libbar.so.0
    libbar.so.0:
    	-lfoo.0 => ./libfoo.so.0
    
  • foo.cに仕様変更を加え、SONAMEを変更しlibfoo.so.1を作ります。
    $ diff -uBw foo.h.orig foo.h
    --- foo.h.orig  2007-02-08 23:56:43.000000000 +0900
    +++ foo.h       2007-02-09 00:04:25.000000000 +0900
    @@ -1,6 +1,6 @@
     #ifndef _FOO_H_
     #define _FOO_H_
    
    -void foo(const char *);
    +void foo(const char *, size_t n);
    
     #endif
    
    $ diff -uBw foo.c.orig foo.c
    --- foo.c.orig  2007-02-09 00:09:36.000000000 +0900
    +++ foo.c       2007-02-09 00:09:50.000000000 +0900
    @@ -2,7 +2,7 @@
     #include <foo.h>
    
     void
    -foo(const char *s)
    +foo(const char *s, size_t n)
     {
    -	printf("%s\n", s);
    +	printf("%.*s\n", n, s);
     }
    
    $ gcc -shared -Wl,-soname,libfoo.so.1 -I. -o libfoo.so.1 foo.c
    $ ln -sf libfoo.so.1 libfoo.so
    $ ls -all libfoo.so
    lrwxr-xr-x  1 tnozaki  tnozaki  11 Feb  9 00:08 libfoo.so -> libfoo.so.1
    
  • 仕様変更後のlibfoo.so.1をリンクするlibbuz.so.0を作ります。
    buz.h
    ------------------------------
    #ifndef _BUZ_H_
    #define _BUZ_H_
    
    void buz();
    
    #endif
    
    buz.c
    ------------------------------
    #include <stddef.h>
    #include <foo.h>
    #include <buz.h>
    
    void
    buz()
    {
    	char s[8] = { 'i', '\'', 'm', ' ', 'b', 'u', 'z', '.' };
    	foo(s, sizeof(s));
    }
    
    $ gcc -shared -Wl,-rpath=.,-soname=libbuz.so.0 -I. -L. -lfoo -o libbuz.so.0 bar.c
    $ ln -sf libbuz.so.0 libbuz.so
    $ ldd libbuz.so.0
    libbuz.so.0:
    	-lfoo.1 => ./libfoo.so.1
    
  • 最後にこのlibbar.so.0とlibbuz.so.0をリンクするアプリケーションを用意します。
    main.c
    ------------------------------
    #include <bar.h>
    #include <buz.h>
    
    int
    main(void)
    {
    	bar();
    	buz();
    }
    
    $ gcc -Wl,-rpath=. -I. -L. -lbar -lbuz -o test main.c
    $ ldd test
    test:
    	-lfoo.0 => ./libfoo.so.0
    	-lbar.0 => ./libbar.so.0
    	-lfoo.1 => ./libfoo.so.1
    	-lbuz.0 => ./libbuz.so.0
            -lc.12 => /usr/lib/libc.so.12
    

アプリケーションはlibfoo.so.0とlibfoo.so.1の両方をリンクしてしまいました(当然ですが)。
このような場合「 リンカとリンクローダは共有ライブラリ間のグローバルシンボルの衝突をどのように扱うか?」で学んだように
libfoo.so.0とlibfoo.so.1で重複するシンボルfooは前者のものが優先されます。

アプリケーションを実行してみましょう。

$ ./test
i'm bar.
i'm buz.ろ真H  X躾躾推                                                        躾推

ヌル終端されていない文字列"i'm buz."を正しく扱うことが出来ず、オーバーランが発生してしまいました。

以上のことからわかる通り、libfoo.soのSONAMEをlibfoo.so.0 -> libfoo.so.1に変更した時点で
それをリンクしているlibbar.so.0も芋蔓でlibbar.so.1に変更する必要があるという事です。
当然bar.cのソース修正も必要になります。

このような不整合(通称 混ぜるな危険)をld(1)やGNU Autoconfといったツールは検出できません。
またRPMやpkg_installなどのバイナリパッケージの依存関係によって解決するにしても
何千とあるパッケージに何百人といるメンテナ、手間がかかり過ぎて現実的ではありません。 *1
なによりもlibbar.so.0がソース非公開、バイナリのみだったりすると手も足もでないことになります。

事がlibcの場合ほとんど全ての共有ライブラリからリンクされている訳ですから
SONAMEを変更してしまうと、上記の不整合を防ぐには再インストールに近い手間がかかることになります。

@ 実例でみるlibcのSONAME問題に対する各OSのアプローチの違い

それでは実際にC90→C99で発生した仕様変更を元にそれぞれのOSでの対策を見ていきましょう。
C90の標準関数localeconv(3)は、現在のロケール情報の数値(小数点、桁区切)及び通貨記号を含むオブジェクト
struct lconvのポインタを返す関数です。
POSIX環境ではより高機能なnl_langinfo(3)があるので使うことは稀でしょうが、printf(3)などは
内部でこのlocaleconv(3)を呼び出すことになっています。

このstruct lconvはC99の改定時にいくつかメンバが追加されています。

Warning:'STANDARDS' is reserved.
STANDARDS 
     The setlocale() and localeconv() functions conform to ANSI X3.159-1989
     (``ANSI C89'') and ISO/IEC 9899:1990 (``ISO C90'').

     The int_p_cs_precedes, int_n_cs_precedes, int_p_sep_by_space,
     int_n_sep_by_space, int_p_sign_posn and int_n_sign_posn members of struct
     lconv were introduced in ISO/IEC 9899:1999 (``ISO C99'').

struct lconvのsizeofが変わったので、 前回のケース4.に該当する可能性があります。

@ NetBSDの場合

この仕様変更は include/locale.hのrev1.12で取り込まれました。

C99: add new parameters int_p_cs_precedes, int_n_cs_precedes,
int_p_sep_by_space, int_n_sep_by_space, int_p_sign_posn and
int_n_sign_posn to monetary locale information.

しかしlocaleconv(3)のインタフェースを考慮すると

  • 戻り値として返すstruct lconvは、libc側がメモリ確保(場所は静的領域)している

    そもそも非互換性の元となるスタック破壊は

    ・古いアプリケーションが仕様変更前のsizeofで確保したスタック領域に
    ・新しいlibcが仕様変更後のsizeofで書込を行ってしまう
    
    ことが原因なので、メモリの確保と書込はどちらもlibcが行うlocaleconv(3)は該当しません。
  • 仕様変更前より仕様変更後の方がstruct lconvのsizeofが大きい

    無理矢理以下のようなコードを実行したとしても

    #include <locale.h>
    
    main(void)
    {
    	struct lconv *l;
    
    	l = localeconv();
    	memset(l, 0, sizeof(*l));
    
    	return 0;
    }
    
    libc側が確保したメモリ領域を越えてmemset(3)が行われることはありません。
  • 仕様変更前と仕様変更後で構造体のメンバのoffsetが保たれている

    例えばこの新規追加メンバをstruct lconvの一番最初に追加してしまうと
    これまで先頭だったdecimal_pointのoffsetが変わってしまいます。

    test.c
    --------------------
    #include <stddef.h>
    #include <stdio.h>
    struct mylconv {
    	char *decimal_point;
    };
    int
    main(void)
    {
            printf("offsetof: %d\n", offsetof(struct mylconv, decimal_point));
    }
    CODE class=shell
    $ make test && ./test
    offsetof: 0
    
    メンバを追加します。
    test.c
    --------------------
    #include <stddef.h>
    #include <stdio.h>
    struct mylconv {
    	int int_p_cs_precedes; /* 新規追加 */
    	char * decimal_point;
    };
    int
    main(void)
    {
            printf("%d\n", offsetof(struct mylconv, decimal_point));
    }
    
    $ make test && ./test
    offsetof: 4
    
    アセンブラレベルでも構造体のメンバは全て先頭からのoffsetとして扱われるので
    並び順を変えてしまうと互換性が失われますが、今回はそのケースには該当しません。

よって何も特別な対策は行いませんでした。

もちろん特別な対策が必要な場合もあります。 前回のケース3.の実例として
MB_LEN_MAXが変わったことによりwcrtomb(3)でスタックオーバーランが発生する可能性を挙げました。
NetBSDではかつてlimits.hの定数MB_LEN_MAXを1から32に変更しています。
machine/limits.h

 #define	CHAR_BIT	8		/* number of bits in a char */
-#define	MB_LEN_MAX	1		/* no multibyte characters */
+#define	MB_LEN_MAX	32		/* no multibyte characters */
 

この時libcのSONAMEを変更したのでしょうか?
いいえ、実はあるトリックを使ってSONAMEの変更を回避しています。

サンプルコードです。

test_setlocale.c
----------------------------------------
#include <locale.h>
int
main(void)
{
        setlocale(LC_ALL, "");
}

setlocale(3)を呼び出すだけの簡単なサンプルを書きます。
プリプロセッサを通しても

$ gcc -E test_setlocale.c
# 1 "/usr/include/sys/cdefs_elf.h" 1 3 4
(中略)

# 2 "test.c" 2
int
main(void)
{
 setlocale(0, "");
 return 0;
}

特に何も変わってませんが、gas(1) assembler形式に落としてみるとどうでしょう?

$ gcc -S -o test_setlocale.s test_setlocale.c
test.s
----------
 1 	.file	"test.c"
...

 6 .globl main
 7 	.type	main, @function
 8 main:
 9 	leal	4(%esp), %ecx
...

18 	pushl	$0
19 	call	__setlocale_mb_len_max_32
20 	addl	$16, %esp
...

callするシンボルがsetlocaleでなく__setlocale_mb_len_max_32に変わっています。

このシンボルのすり替えは、__RENAMEマクロというものを使って実現しています。
sys/cdefs.h

247 #if !defined(_STANDALONE) && !defined(_KERNEL)
248 #ifdef __GNUC__
249 #define __RENAME(x)	___RENAME(x)
250 #else
...

sys/cdefs_elf.h

41 #if __STDC__
42 #define ___RENAME(x)    __asm(___STRING(_C_LABEL(x)))
43 #else
...

locale.h

80 #ifdef __SETLOCALE_SOURCE__
81 char		*setlocale(int, const char *);
82 char		*__setlocale_mb_len_max_32(int, const char *);
83 char		*__setlocale(int, const char *);
84 #else /* !__SETLOCALE_SOURCE__ */
85 char		*setlocale(int, const char *)	__RENAME(__setlocale_mb_len_max_32);
86 #endif /* !__SETLOCALE_SOURCE__ */

以上のように#include locale.hすることでcall setlocaleを
問答無用でcall __setlocale_mb_len_max_32に書き換えてくれるのが__RENAMEマクロです。
再コンパイルを行って新しいlibcにリンクすれば__setlocale_mb_len_max_32を使うようになります。
古いアプリケーションはこれまで通りsetlocaleを参照したままです。

setlocaleと__setlocale_mb_len_max_32の処理の違いを見てみましょう。

39 __warn_references(setlocale,
40     "warning: reference to compatibility setlocale(); include <locale.h> for correct reference")
...
46 char *
47 setlocale(category, locale)
...
53
54 	__mb_len_max_runtime = 1;
55 	return __setlocale(category, locale);
...
39 char *
40 __setlocale_mb_len_max_32(category, locale)
...
47 	__mb_len_max_runtime = 32;
48 	return __setlocale(category, locale);

__mb_len_max_runtimeに1あるいは32をセットします。
wcrtomb(3)はこの__mb_len_max_runtimeの値を元に、引数で渡されたbufferに最大何byte書込できるかを知るので
スタックオーバランは回避可能です。 *2

その他/usr/lib以下の共有ライブラリにはlibcをリンクしないことで
アプリケーションがリンクするlibc以外は使わないようにしているのも対策のひとつのような気もします。

$ ldd /usr/lib/lib*.so | grep "\-lc\." | wc -l
0

@ glibcの場合

こちらは同じ仕様変更を include/locale/locale.hのrev1.13で取り込んでいます。

(struct lconv): Add new elements from ISO C99.

このような後方互換性が失われる可能性のある場合、Solarisに習い"ELF Symbol versioning" *3の機能を使い
同一のlibc内に複数のlocaleconvシンボルを共存させています。

$ nm /lib/libc.so.6 | grep localeconv
00021340 t __localeconv
00021340 t __localeconv20
00021340 T localeconv@@GLIBC_2.2
00021340 T localeconv@GLIBC_2.0

nm(1)の結果には、シンボルのバージョン情報は

シンボル名 + @@ + バージョン	デフォルトバージョン
シンボル名 + @ + バージョン	互換性の為のバージョン

として表示されます。

libcのリンク時、ld(1)はlocaleconvの解決にデフォルト(GLIBC_2.2)を使います。

test.c
--------------------
#include <locale.h>

int
main(void)
{
        struct lconv *l;
        l = localeconv();
	return 0;
}
$ make test
$ nm a | grep localeconv
	U localeconv@@GLIBC_2.2

リンクローダはアプリケーションに埋め込まれたバージョンのlocaleconvを探してシンボル解決をします。
これによって__RENAMEマクロよりもスマートな方法で *4SONAMEの変更無しに後方互換を実現しています。

ソースコードを見てみましょう。
localeconv.c

21 #include <shlib-compat.h>
22
23 /* Return monetary and numeric information about the current locale.  */
24 struct lconv *
25 __localeconv (void)
26 {
27   static struct lconv result;
...

71 versioned_symbol (libc, __localeconv, localeconv, GLIBC_2_2);
72 #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_2)
73 strong_alias (__localeconv, __localeconv20)
74 compat_symbol (libc, __localeconv20, localeconv, GLIBC_2_0);
75 #endif

最新バージョンの宣言はversioned_symbolマクロで行います(71行目)。
localeconv@@GLIBC_2_2は、実際には__localeconv(25行目)のエイリアスとしています。

過去のバージョンの宣言はcompat_symbolマクロで行います(74行目)。
localeconv@GLIBC_2_0の実体は__localeconv20です。

# ただしNetBSDで説明した通り、実際には何もしなくても後方互換は保たれていますので
# __localeconvと__localeconv20で違う処理をする必要はありません。
# ですのでstrong_aliasマクロを使い、__localeconv20を__localeconvのエイリアスにしています(73行目)。

@ OpenBSDの場合

struct lconvの仕様変更はまだ取り込まれていません。
その時がやってきても、NetBSDのような__RENAMEマクロを持っていませんし、ELF symbol versioningの機能も使わないでしょう。
それにOpenBSDのld.soはパラノイアックで、シンボルのsizeofが変わると再リンクを促す警告を発します。 resolve.c

421 	if (ref_sym != NULL && ref_sym->st_size != 0 &&
422 		(ref_sym->st_size != (*this)->st_size)  &&
423 		(ELF_ST_TYPE((*this)->st_info) != STT_FUNC) ) {
424 			_dl_printf("%s:%s: %s : WARNING: "
425 			"symbol(%s) size mismatch, relink your program\n",
426 			_dl_progname, req_obj->load_name,
427 			object->load_name, name);
428 	}

NetBSDのように「何もしない」という道すら自ら断っています。

libcのSONAMEが変えないなんて努力はしません、ガンガン変えます。
かつて3.7 → 3.8のわずかな期間にlibc.so.35 → libc.so.38と3回も変わった時もあります。

というわけでOpenBSDは

  • リリース版を使う
  • 自分でportsから作らずに、コンパイル済packagesをインストールする
  • バージョンアップの時は再インストール、portsを含め古いバイナリは一切残さない
  • 漢と書いて(以下略)

くらいの覚悟を持って使うべきかと思います。

@ 最後に

まああとは実際のlibcのソースを読んでみてください。

*1:RPMの依存関係は地獄だぜ、フゥハハー(AA略)といっても別にRPMが悪い訳じゃないってことです。
*2:実際はMB_LEN_MAX < MB_CUR_MAXなロケールでsetlocale(3)が成功したら変なので
ロケール情報の読み込み時点で蹴ってます、ですのでwcrtomb(3)自身はノーチェックです。

*3:Linux Standard Base Core Specification 3.2-20060415
11.7. Symbol Versioning
Linker and Libraries Guide
Chapter 5 Application Binary Interfaces and Versioning

*4:__RENAMEマクロの場合、ヘッダをインクルードしないと有効にならないという欠点があります。
__warn_referencesマクロで適切なヘッダをインクルードするように警告を出しますが無視されたらお終い。
NetBSDでELF Symbol Versioningマダー?(AA略)

NetBSD

つか俺がcitrus_csmapper.cをちゃんと嫁ということだ orz はーなさけねー。
とりあえず暫定対処だけしといてpullup request投げて今日はおしまい。

続NetBSD

やっぱこれcitrus_csmapper.cのfind_best_pivot_pvdb()がバグってるっぽ。
db_lookup_by_s()からの戻りをクリアせずにgoto quit4してるから
charmap.pivotにある組合せを全部試さずにloop抜けてエラーになってるっぽ。
週末にでも直すべや。