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

2007/2/5(Mon)

共有ライブラリの後方互換性が壊れる時

やっぱり月末は(以下略)ということで、遅くなりましたが続きです。
# いきなり第3回(最終回)のつもりでしたが、やっぱり一回間を挟みます。

@ 後方互換とは

共有ライブラリの後方互換性とは、古い共有ライブラリを新しい共有ライブラリで置き換えても
リンクし直す事無しに完全に動作することを保障するということです。
ではどのような変更を行うとこの後方互換が損なわれるのでしょうか?

  1. 関数や変数(=シンボル)が共有ライブラリから無くなる

    この記事の最後の例の通り、リンクローダが未定義シンボルを検出するとプログラムの実行を強制終了するので
    わりと発覚しやすい問題です、ただしこれまで無視されてた同名シンボルが昇格した場合は厄介です。 *1

  2. 関数の引数などのインタフェースが変わる場合

    Cでは言語仕様上、foo(int)とfoo(char *)は同時に宣言できないので
    foo(int)もfoo(char *)も同じfooシンボルです、よってリンクローダではこの変更を検出できません。 *2

    foo_print.c

    #include <stdio.h>
    #include <string.h>
    
    void
    foo_print(const char *s)
    {
    	printf("%s\n", s);
    }
    

    これをlibfoo.so.0としてコンパイルします。

    $ gcc -fstack-protector -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0 foo_print.c
    $ ln -sf libfoo.so.0 libfoo.so
    

    このfoo_print()を呼び出すアプリケーションを用意します。

    extern void foo_print(const char *);
    int
    main(void)
    {
            foo_print("hello, world.");
    
            return 0;
    }
    

    コンパイルして実行します。

    $ gcc -fstack-protector -Wl,-rpath,. -L. -lfoo -o test_foo main.c
    $ ./test_foo
    hello, world.
    

    次にfoo_print()の仕様を変更し、引数の数を増やします。

    --- foo_print.c.orig    2007-01-28 23:35:45.000000000 +0900
    +++ foo_print.c 2007-01-28 23:43:50.000000000 +0900
    @@ -2,7 +2,7 @@
     #include <string.h>
    
     void
    -foo_print(const char *s)
    +foo_print(int num, const char *s)
     {
    -	printf("%s\n", s);
    +	printf("%d: %s\n", num, s);
     }
    

    libfoo.so.0のみを作り直します。

    $ rm -f libfoo.so libfoo.so.0
    $ gcc -fstack-protector -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0 foo_print.c
    $ ln -sf libfoo.so.0 libfoo.so
    

    それではtest_fooを再コンパイルせずに実行してみましょう。

    $ ./test_foo
    134514549: @鄂
    
    当然ですが表示結果は予期しないものになります。
    場合によってはSegmentation faultなどのエラーも発生するでしょう。
  3. 定数値が変更された場合

    foo.h

    #ifndef _FOO_H_
    #define _FOO_H_
    
    #define FOO_LEN	32
    
    extern void foo_init(char *);
    #endif /*_FOO_H_*/
    
    foo_init.c
    #include <stdio.h>
    #include <string.h>
    #include "foo.h"
    
    void
    foo_init(char *s)
    {
    	printf("%s: FOO_LEN expected %d.\n",
    	    __func__, FOO_LEN);
    	memset((void *)s, 0, FOO_LEN);
    }
    

    これをlibfoo.so.0としてコンパイルします。

    $ gcc -fstack-protector -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0 foo_init.c
    $ ln -sf libfoo.so.0 libfoo.so
    

    このFOO_LENとfoo_init()を使うアプリケーションを用意しましょう。
    main.c

    #include <stdio.h>
    #include "foo.h"
    
    int
    main(void)
    {
    	char s[FOO_LEN];
    	printf("%s: FOO_LEN is %d.\n",
    	    __func__, FOO_LEN);
    	foo_init(s);
    
    	return 0;
    }
    

    コンパイルして実行します。

    $ gcc -fstack-protector -Wl,-rpath,. -L. -lfoo -o test_foo main.c
    $ ./test_foo
    main: FOO_LEN is 32.
    foo_init: FOO_LEN expected 32.
    
    当然ですが問題なく動作します。

    では仕様変更が発生し、FOO_LENを変更するとどんな結果になるでしょうか?

    --- foo.h.orig	2007-02-04 21:20:59.921875000 +0900
    +++ foo.h	2007-02-04 21:21:26.328125000 +0900
    @@ -1,7 +1,7 @@
     #ifndef _FOO_H_
     #define _FOO_H_
    
    -#define FOO_LEN	32
    +#define FOO_LEN	128
    
     extern void foo_init(char *);
     #endif /*_FOO_H_*/
    

    libfoo.so.0のみを作り直します。

    $ rm -f libfoo.so libfoo.so.0
    $ gcc -fstack-protector -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0 foo_init.c
    $ ln -sf libfoo.so.0 libfoo.so
    

    それではtestを実行してみましょう。

    $ ./test_foo
    
    [NetBSDの場合]
    main: FOO_LEN is 32.
    foo_init: FOO_LEN expected 128.
    Abort (core dumped)
    
    [Linuxの場合]
    main: FOO_LEN is 32.
    foo_init: FOO_LEN expected 128.
    *** stack smashing detected ***: ./test_foo terminated
    Aborted
    
    gccのスタック保護機能により強制終了されてしまいました。

    これは、main側ではFOO_LEN = 32byteスタック上にメモリを確保し、先頭ポインタをfoo_initに渡しますが
    コンパイルし直したfoo_init()側ではFOO_LEN = 128byteまでmemsetしようとして
    スタックオーバーランが発生したからです。

  4. 型、構造体、配列などのsizeofが変わるケース

    こちらもsizeof()が関数だと思っている人がやってしまいがちなミスです。
    以下のサンプルを3.と同様の手順で実行すると、同じくスタック破壊が発生します。
    foo.h

    #ifndef _FOO_H_
    #define _FOO_H_
    
    struct foo {
    	int foo_len;
    	char foo_buf[32];
    };
    extern void foo_init(struct foo *);
    #endif /*_FOO_H_*/
    
    foo_init.c
    #include <stdio.h>
    #include <string.h>
    #include "foo.h"
    
    void
    foo_init(struct foo *p)
    {
    	printf("%s: sizeof(*p) expected %d.\n",
    	    __func__, sizeof(*p));
    	memset((void *)p, 0, sizeof(*p));
    }
    
    main.c
    #include <stdio.h>
    #include "foo.h"
    
    int
    main(void)
    {
    	struct foo f;
    
    	printf("%s: sizeof(f) is %d.\n",
    	    __func__, sizeof(f));
    	foo_init(&f);
    
    	return 0;
    }
    

    仕様変更

    --- foo.h.orig  2007-01-25 00:32:24.000000000 +0900
    +++ foo.h       2007-01-25 00:56:22.000000000 +0900
    @@ -3,7 +3,7 @@
    
     struct foo {
     	int foo_len;
    -	char foo_buf[32];
    +	char foo_buf[128];
     };
    
    図にするとこんな感じ。

#他にもobject formatの変更、特定のCPUに依存した最適化(gcc -mcpu=pentiumpro)などにより
#後方互換が失われるケースもありますがここでは触れません。

@ 非互換性を回避する

1.および2.の場合、そもそも消さずに互換性の為にそのまま残しておくという方法が一般的です。
*BSDでは、obsoleteな関数をリンクした時に警告が発せられる仕組みが用意されています。
この機能を試してみましょう。

例えばmktemp(3)はヘッダからこそまだ消されてませんが、安全性の問題からmkstemp(3)の使用が推奨されます。
NetBSDではmktemp(3)を呼び出すプログラムをコンパイルすると

cc -O2   -o hoge hoge.c
/var/tmp//ccy1ldHe.o: In function `main':
hoge.c:(.text+0x17): warning: warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp()

のように警告が発せられます。

この警告は__warn_referencesマクロによって実現されています。
cdefs_elf.h

73 #define __warn_references(sym,msg)						\
74     __asm(".section .gnu.warning." #sym "\n\t.ascii \"" msg "\"\n\t.text");
75

mktemp.c

59 __warn_references(mktemp,
60     "warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp()")
61
62 char *
63 mktemp(path)
64 	char *path;

インラインアセンブラをつかって警告メッセージを埋め込んでいます。
glibcの場合はlink_warningマクロです。
libc-symbol.h

242 # define link_warning(symbol, msg) \
243   __make_section_unallocated (".gnu.warning." #symbol) \
244   static const char __evoke_link_warning_##symbol[]     \
245     __attribute__ ((used, section (".gnu.warning." #symbol __sec_comment        ))) \
246     = msg;

3.および4.については非常に厄介です。
予防という意味では、3.のケースであればfoo_init()を
foo_init.c

#include <stddef.h>
#include <string.h>
void
foo_init(char *s, size_t n)
{
	printf("%s: n is %zd.\n",
	    __func__, n);
	memset((void *)s, 0, n);
}

main.c

#include <stdio.h>
#include "foo.h"

int
main(void)
{
	char s[FOO_LEN];
	printf("%s: FOO_LEN is %d.\n",
	    __func__, FOO_LEN);
	foo_init(s, sizeof(s));

	return 0;
}

のように定数を使わないようなI/F設計にする、あるいは4.のケースであれば
foo.h

#ifndef _FOO_H_
#define _FOO_H_

struct foo;
extern struct foo *foo_init(void);

foo_init.c

#include <stdio.h>
#include <string.h>
#include "foo.h"

struct foo {
	int foo_len;
	char foo_buf[32];
};
struct foo *
foo_init()
{
	struct foo *p;

	printf("%s: sizeof(*p) expected %d.\n",
	    __func__, sizeof(*p));
	p = malloc(sizeof(*p));
	if (p != NULL)
		memset((void *)p, 0, sizeof(*p));
	return p;
}

main.c

#include <stdio.h>
#include "foo.h"

int
main(void)
{
	struct foo *f;

	f = foo_init();

	return 0;
}

のように、malloc(3)を使って動的なメモリ確保を行うべきでしょう。

しかし定数やsizeofに依存したI/F設計になっている関数が既に存在している場合はどうでしょうか?
例えばC90AMD1で追加されたmbrtowc(3)やwcrtomb(3)の場合などです。

#include <locale.h>
#include <wchar.h>

int
main(void)
{
	mbstate_t st;
	const char *s = "あ";
	size_t n = strlen(s), ret;
	wchar_t wc;
	char buf[MB_LEN_MAX];

	setlocale(LC_CTYPE, "ja_JP.eucJP");

	memset(&st, 0, sizeof(st));
	ret = mbrtowc(&wc, s, n, &st);
	...

	memset(&st, 0, sizeof(st));
	ret = wcrtomb(buf, wc, &st);
	...
}

wcrtomb(3)はワイド文字wcをマルチバイトに変換してbufに書き込みますが
このbufの長さについては最低MB_LEN_MAXを呼出元が確保することになっています。

OpenBSDではMB_LEN_MAXはほとんどのarchでは1です。 *3
include/machine/limits.h

#define MB_LEN_MAX        1                /* no multibyte characters */

将来的にEUC-JPやUTF-8などのマルチバイト文字をサポートする為に
この値を増やそうとすれば3.のケースに当てはまり、後方互換性は失われます。

またCygwin/newlibではmbstate_tに4バイトまでしか解析中のマルチバイトを保持できません。
include/sys/_types.h

/* Conversion state information.  */
typedef struct
{
  int __count;
  union
  {
    wint_t __wch;
    unsigned char __wchb[4];
  } __value;		/* Value so far.  */
} _mbstate_t;

ですのでUTF-8の5〜6バイト文字は正しく扱えません。
newlib/libc/stdlib/mbtowc_r.c

198           /* five-byte sequence */
199           if (sizeof(wchar_t) < 4)
200             return -1; /* we can't store such a value */

よってmbstate_tのsizeofを変えようとするとこれまた4.のケースで後方互換が失われることでしょう。

このような場合、原則的には共有ライブラリのSONAMEを変更します。
共有ライブラリのバージョン管理は

  • real name
    この共有ライブラリのファイル名
  • sonameこの共有ライブラリを作成した際にld(1)の-sonameオプションで指定した名前。
    real name != sonameの場合、symlinkを貼ること。
    $ gcc -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0.0 foo.c
    $ ls -all libfoo.so.0.0
    -rwxr-xr-x  1 tnozaki  tnozaki  4467 Feb  5 21:30 libfoo.so.0.0
    
    $ ln -sf libfoo.so.0.0 libfoo.so.0
    $ ls -all libfoo.so.0
    lrwxr-xr-x  1 tnozaki  tnozaki  13 Feb  5 21:35 libfoo.so.0 -> libfoo.so.0.0
    
    $ objdump --all-headers libfoo.so.0.0 | grep SONAME
      SONAME      libfoo.so.0
    
  • linker name
    この共有ライブラリをリンクする為にld(1)の-lオプションに指定する名前
    linker name != sonameの場合、symlinkを貼ること。
    $ ln -sf libfoo.so.0 libfoo.so
    $ ls -all libfoo.so
    lrwxr-xr-x  1 tnozaki  tnozaki  13 Feb  5 21:36 libfoo.so -> libfoo.so.0.0
    
    $ gcc -Wl,-rpath=. -L. -lfoo -o test_foo test_foo.c
    $ ldd test_foo
    test_foo:
    	-lfoo.0 => ./libfoo.so.0
    	-lc.12 => /usr/lib/libc.so.12
    

で行います。

後方互換性を失った新しいlibfooのsonameをlibfoo.so.1に変更すれば
libfoo.so.0*とlibfoo.so.1*はファイルシステム上共存可能です。
再コンパイルする前の古いアプリケーションははlibfoo.so.0をそのまま参照し
新しくコンパイルしたバイナリはlibfoo.so.1をリンクするので干渉することはないはずです。

しかしGnomeやKDEなどの大量の、そして複雑な依存関係を持つ共有ライブラリの現代では
迂闊にSONAMEの変更するととんでもないトラブルが発生しかねないのです。

次回はそのトラブルと、それぞれのOSによって異なる回避策の予定です。

*1:リンク順ウィークシンボルなどによる。
*2:C++だとname manglingで引数情報も持っています。
*3:FreeBSDからの移植だと6だったり、NetBSDからの移植だと1だったり32だったり非常にテキトー。
まあNetBSDもなんだけどMB_LEN_MAXは別にMDじゃなくてMIで良いんじゃマイカ、FreeBSDは以前にMIに変更しましたね。

localedef(1)

ぼちぼち資料集め。
ISO/IEC TR14652
ポイントは

あたりかな。LC_*が増えてるのはどうでもいい。

とりあえずmklocale(1)捨ての為に、LC_CTYPEだけでも実装してみようかね。
<escseq2022>、<include>あたりはCitrus的に微妙だよな。

<escseq${encoding_module}> "param1";"param2";"escape sequence";

とでもして、他のstateful encodingの事も考慮しないとなぁ。。。

NetBSDのfparseln(3)マンセー、知らずに車輪の再発明をしてたのは内緒だ。

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抜けてエラーになってるっぽ。
週末にでも直すべや。

2007/2/10(Sat)

NetBSD

直近のTODO

2007/2/17(Sat)

Thinkpad

240のキーボードとs30のバッテリー(9セル)を新品に交換、まだまだ使うぜ。
...いや本音は新しいのに買い替えたいんだけど、去年すげー医療費使ったもんで当分無理。

NetBSD

citrus_csmapper.cだけcommitしておいた。
citrus_db.cは何の影響ないので仕様ということで放置しよ。

2007/2/19(Mon)

wchar_t != Unicode

http://docs.freebsd.org/cgi/mid.cgi?20070212070603.GA4107
Linuxのような__STDC_ISO10646__環境だとこれで動いちゃうから
文字コード変換にmbrtowc/wcrtombを使うもんだと思っている人、意外と多いんだよな。
iconv(3)使おうね。

2007/2/23(Fri)

localedef(1)

TR14652読んでて気付いた点

他にも色々あるんだけど、読めば読むほどmklocale(1)でいいじゃんって気になってきた。

あとSS2/SS3ってどうやって表現するの?も追加。

LS0(SO)		<include> "g0";"g0";
LS1(SI)		<include> "g1";"g0";
LS2		<include> "g2";"g0";
LS3		<include> "g3";"g0";
LS1R		<include> "g1";"g1";
LS2R		<include> "g2";"g1";
LS3R		<include> "g3";"g1";

SS2		<include> "g2";"g0";
SS3		<include> "g3";"g0";
SS2R		<include> "g2";"g1";
SS3R		<include> "g3";"g1";

つまりG[2-3]の場合、Locking-ShiftなのかSingle-Shiftなのか区別がつかないんだよね。
includeされる側の<escseq2022>で指定することもできるっちゃできるけど

LS2		<escseq2022> "g2";"g0";"\x1B\x6E"
SS2		<escseq2022> "g2";"g0";"\x8E"
あるいは
SS2		<escseq2022> "g2";"g0";"\x1B\x4E"

この場合includeされる側のCHARMAPにLS2とSS2の<escseq2022>を同時に定義できないことになってしまうがな。
ここで指定するバイト列は中間バッファへの指示用のエスケープだから駄目だわ、こりゃ。

2007/2/26(Mon)

Thinkpad

バッテリ買ったばっかなのにs30のマザー死亡、アヒャヒャヒャ。
去年の6月末に新品と交換して半年ちょっとしか持たないとわね、予定外。
どないすべ。

CNS11643

8面を使ってUnicodeのBMPと完全互換するようにしてたのね。
http://www.cns11643.gov.tw/web/seek_09.jsp?range=1
そのうち(いつだろうね)対応するか。

2007/2/27(Tue)

NetBSD

多分はてな経由で 不正なUTF-8文字列の話。
要するに RFC3629の10. Security Considerationsの話ですな。

嫌な予感がしたのでNetBSD iconvでちゃんとEILSEQを返すかのテスト。

○ GNU libiconv-1.10
○ glibc-2.5 iconv
○ Solaris8 iconv
× NetBSD 4.99.12 iconv

古い実装だしな、直すか。

同様にサロゲート"\xed\xa0\80"(0xd800)〜"\xed\xbf\xbf(0xdfff)がEILSEQになるかのテスト。

× GNU libiconv-1.10
× glibc-2.5 iconv
○ Solaris8 iconv
× NetBSD 4.99.12 iconv

ふーん。

つーわけでNetBSD用のpatch。
patch-citrus_utf8.c
patch-unicode.h
もうちょいテストしてcommitするべし。

まあ ISO-IR-165で書いたように、変換前にチェックをすることこそが危険なんだけどね。
他にも(昨日書いたけど)CNS11643の8面に basic latinがあったりするし。

ちょと話はズレるけど

エスケープ漏れといえば、某商用O-R mapping実装がIBM DB2のSQL方言(LIKEに全角_%が使える)に
対応してなくて(以下略)というのがあったなそういや、多分まだ直ってないんだろうな。