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

[最新版] [一覧] [前月] [今月] [翌月]

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を変更します。
共有ライブラリのバージョン管理は


*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)マンセー、知らずに車輪の再発明をしてたのは内緒だ。


[ホームへ] [ページトップへ]