#navi_header|C言語系| お題:mktemp(3)とmkstemp(3)の違いを調査せよ ※今回は軽めなので、「デーモン君のソース探検」書籍は軽く参考程度に留め、テストプログラムを組んだりしつつさくさくと進めていきたいと思います。 #more|| #outline|| ---- * mktemp(3)とmkstemp(3)の違いとテストプログラム まずmanページを確認してみる。 #pre||> NAME mktemp, mkstemp, mkdtemp - make unique temporary file or directory name LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include char * mktemp(char *template); int mkstemp(char *template); ||< ここで"template"という引数が一時ファイル名を指定するところで、連続する"X"の部分がプロセスIDと適当な英字の組み合わせに置換される。 mktemp()の場合は置換後のファイル名が、mkstemp()の場合は実際にファイルを作成しそのファイル記述子が返される。 mkstemp()の場合に置換後のファイル名をどうやって取り出すか、だが、template引数にconstが付いていない。つまり、template引数の連続する"X"の部分が直接上書きされることになる。 mktemp()とmkstemp()の差異は、mktemp()はファイル名を決定するだけでファイルは作成しない。一方mkstemp()の場合は実際にファイルを作成し、ファイル記述子まで返す。 manページの"SECURITY CONSIDERATION"にある通り、mktemp()の場合はファイル名生成と、その後のファイル作成で間が空いてしまうので競合する可能性が出てきてしまう。mkstemp()/mkdtemp()はそうならないようになっているので、そちらを推奨とのこと。 実際にテストプログラムを組んでみる。 ** mktemp(3)のテストプログラム mktemp.c: #code|c|> #include #include int main() { char template[] = "/tmp/mktemp_test.XXXX"; char *res = NULL; printf("template(before) = %s\n", template); res = mktemp(template); if (NULL == res) { perror("mktemp"); exit(1); } printf("template(after) = %s\n", template); printf("res = %s\n", res); return 0; } ||< $ cc -Wall -c -o mktemp.o mktemp.c $ cc -Wall -o mktemp mktemp.o mktemp.o: In function `main': mktemp.o(.text+0x40): warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp() $ ./mktemp template(before) = /tmp/mktemp_test.XXXX template(after) = /tmp/mktemp_test.308a res = /tmp/mktemp_test.308a 実行ファイルのリンク時にwarningが表示されている。これについては後ほど追跡する。 ファイル名までは生成されるが、実際のファイルは作成されていない: $ ls -a /tmp ./ ../ ** mkstemp(3)のテストプログラム mkstemp.c: #code|c|> #include #include int main() { char template[] = "/tmp/mkstemp_test.XXXX"; int fd = -1; printf("template(before) = %s\n", template); fd = mkstemp(template); if (-1 == fd) { perror("mkstemp"); exit(1); } printf("template(after) = %s\n", template); printf("fd = %d\n", fd); return 0; } ||< $ cc -Wall -c -o mkstemp.o mkstemp.c $ cc -Wall -o mkstemp mkstemp.o $ ./mkstemp template(before) = /tmp/mkstemp_test.XXXX template(after) = /tmp/mkstemp_test.328a fd = 3 実行してみると、実際にファイル作成まで行われている: $ ls -a /tmp ./ ../ mkstemp_test.328a * mkstemp(3)とmktemp(3)の中身 mktemp/mkstemp/mkdtempのソースコードはそれぞれ以下の場所にあった。 /usr/src/lib/libc/stdio/ mkdtemp.c mkstemp.c mktemp.3 mktemp.c まずmktemp.cの主要部分: #pre||> __warn_references(mktemp, "warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp()") char * mktemp(path) char *path; { _DIAGASSERT(path != NULL); return (__gettemp(path, (int *)NULL, 0) ? path : (char *)NULL); } ||< 続いてmkstemp.cの主要部分: #pre||> #if HAVE_CONFIG_H #define GETTEMP gettemp #else /* ... */ #define GETTEMP __gettemp #endif int mkstemp(path) char *path; { int fd; _DIAGASSERT(path != NULL); return (GETTEMP(path, &fd, 0) ? fd : -1); } ||< 最後にmkdtemp.cの主要部分: #pre||> #if HAVE_CONFIG_H #define GETTEMP gettemp #else /* ... */ #define GETTEMP __gettemp #endif char * mkdtemp(path) char *path; { _DIAGASSERT(path != NULL); return (GETTEMP(path, (int *)NULL, 1) ? path : (char *)NULL); } ||< 共通しているのは、いずれも"gettemp"or"__gettemp"なる関数へ処理を委譲している点である。 "gettemp"のソースコードは、同じディレクトリ内の次のファイル: /usr/src/lib/libc/stdio/gettemp.c "gettemp"/"__gettemp"の切り替えはプリプロセッサで調整されており、いずれにせよ最終的に以下の関数が呼ばれるようになっている: #pre||> #if HAVE_CONFIG_H #define GETTEMP gettemp #else #include "local.h" #define GETTEMP __gettemp #endif int GETTEMP(path, doopen, domkdir) char *path; int *doopen; int domkdir; { ||< 引数の変数名でほぼ使い方が分かるようになっている。ここでもう一度mk({s|d}?)tempでの呼び出しをまとめ直すと・・・ int GETTEMP(char *path, int *doopen, int domkdir): mktemp : __gettemp(path, (int *)NULL, 0) mkstemp : GETTEMP(path, &fd, 0) mkdtemp : GETTEMP(path, (int *)NULL, 1) 見たままで、解説は不要。doopenがファイル作成指示&ファイル記述子格納先、domkdirがディレクトリ作成フラグになっている。 本体のソースコードの前半は、getpid()で取得されてたプロセスIDと、GETTEMP()内でのstaticな変数xtraを使ってなるべくユニークになるような文字列を生成している。 後半の無限forループにて、実際のファイル/ディレクトリ作成が行われ戻り値が返される。 #pre||> for (;;) { if (doopen) { /* mkstemp()の場合、O_CREAT + O_EXCL:排他作成でopen(2)する */ if ((*doopen = open(path, O_CREAT|O_EXCL|O_RDWR, 0600)) >= 0) return (1); if (errno != EEXIST) return (0); } else if (domkdir) { /* mkdtemp()の場合、mkdir()を実行 */ if (mkdir(path, 0700) >= 0) return (1); if (errno != EEXIST) return (0); } else if (lstat(path, &sbuf)) /* mktemp()の場合、lstat()が-1でerrnoがENOENT:未存在なら真を返す */ return (errno == ENOENT ? 1 : 0); ||< 以上でmktemp/mkstemp/mkdtempの中身は把握出来た。 * "__warn_references" → ".gnu.warning"セクションについて 最後に、mktemp.cで出てきた __warn_references(mktemp, "warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp()") について調べてみたい。 Cソースの、関数以外の部分で出現しているので何らかのマクロであることが予想される。"/usr/include"でgrepしてみる。 $ cd /usr/include $ grep -r "warn_reference" * sys/cdefs_aout.h:#define __warn_references(sym,msg) \ sys/cdefs_aout.h:#define __warn_references(sym,msg) \ sys/cdefs_aout.h:#define __warn_references(sym,msg) sys/cdefs_aout.h:#undef __warn_references(sym,msg) sys/cdefs_aout.h:#define __warn_references(sym,msg) sys/cdefs_elf.h:#define __warn_references(sym,msg) \ sys/cdefs_elf.h:#define __warn_references(sym,msg) \ 実行ファイルはELF形式になるので、"sys/cdefs_elf.h"の方を見てみる。 #define __warn_references(sym,msg) \ __asm__(".section .gnu.warning." #sym " ; .ascii \"" msg "\" ; .text"); 組み込みアセンブラに展開されることが分かる。"cc -E"でプリプロセッサを展開してみると、次のように展開された。 $ cd /usr/src/lib/libc/stdio $ cc -E mktemp.c | more ... __asm__(".section .gnu.warning." "mktemp" " ; .ascii \"" "warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp()" "\" ; .text"); ... ".section" でセクションが ".gnu.warning.mktemp" に変更されている。これの影響でmktemp()を使ったプログラムをリンクする時にwarningメッセージが出ている事は確実である。 コンパイル時の流れをもう少し細かく追う為、まずccで"-v"オプションをつけ、ldコマンド呼び出し時のオプション群を把握する。 #pre||> $ cc -Wall -v -o mktemp mktemp.o Using builtin specs. gcc version 2.95.3 20010315 (release) (NetBSD nb3) ld -m elf_i386 -dc -dp \ -e __start \ -dynamic-linker /usr/libexec/ld.elf_so \ -o mktemp \ /usr/lib/crt0.o /usr/lib/crtbegin.o \ mktemp.o \ -lgcc -lc -lgcc /usr/lib/crtend.o mktemp.o: In function `main': mktemp.o(.text+0x40): warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp() ||< ldコマンドのオプションを把握出来たので、これに実行時詳細を表示する"--verbose"を組み合わせ、表示内容をファイルに落として確認してみる。 #pre||> $ ld -m elf_i386 --verbose ... > link.log mktemp.o: In function `main': mktemp.o(.text+0x40): warning: mktemp() possibly used unsafely, use mkstemp() or mkdtemp() $ more link.log GNU ld version 2.11.2 (with BFD 2.11.2nb1) Supported emulations: elf_i386 i386nbsd using internal linker script: ================================================== OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386") OUTPUT_ARCH(i386) ENTRY(_start) SEARCH_DIR(/usr/lib); /* Do we need any of these for elf? __DYNAMIC = 0; */ ... .text : { *(.text) *(.text.*) *(.stub) /* .gnu.warning sections are handled specially by elf32.em. */ *(.gnu.warning) *(.gnu.linkonce.t.*) } =0x9090 ||< ようやく手がかりが見つかった。 /* .gnu.warning sections are handled specially by elf32.em. */ "elf32.em"がファイル名か、何らかの機能の略称か当初は不明だったので、あちこちをgrepしてしまった。 結論としてはファイル名なので、locateコマンドで辿ることができた。 $ locate elf32.em /usr/src/gnu/dist/ld/emultempl/elf32.em /usr/src/gnu/dist/toolchain/ld/emultempl/elf32.em 2つ見つかってしまったが、".gnu.warning"の警告表示機能についてはどちらも同様だったので、ここでは"gnu/dist/ld/"以下のelf32.emを見てみる。検索すると直ぐに見つかった。 /* Look for any sections named .gnu.warning. As a GNU extensions, we treat such sections as containing warning messages. We print out the warning message, and then zero out the section size so that it does not get copied into the output file. */ { ... コードは省略するが、ここで".gnu.warning"で始まるセクション名について警告表示を行っている(正確には警告用のcallback関数を読んでいる)。 また、同ファイル中で".gnu.warning"で始まるセクションは実行ファイルから除去する処理も組み込まれていた。このため、mktemp.oおよびmktempファイルには".gnu.warning"セクションは含まれていない。 #pre||> $ readelf -S mktemp.o There are 10 section headers, starting at offset 0x184: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 0000a4 00 AX 0 0 4 [ 2] .rel.text REL 00000000 00041c 000058 08 8 1 4 [ 3] .data PROGBITS 00000000 0000d8 000000 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 0000d8 000000 00 WA 0 0 4 [ 5] .note NOTE 00000000 0000d8 000014 00 0 0 1 [ 6] .rodata PROGBITS 00000000 0000ec 000054 00 A 0 0 1 [ 7] .shstrtab STRTAB 00000000 000140 000044 00 0 0 1 [ 8] .symtab SYMTAB 00000000 000314 0000d0 10 9 8 4 [ 9] .strtab STRTAB 00000000 0003e4 000038 00 0 0 1 ... $ readelf -S mktemp There are 27 section headers, starting at offset 0xe30: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 080480f4 0000f4 000017 00 A 0 0 1 [ 2] .note.netbsd.iden NOTE 0804810c 00010c 000018 00 A 0 0 4 [ 3] .hash HASH 08048124 000124 0000b4 04 A 4 0 4 [ 4] .dynsym DYNSYM 080481d8 0001d8 0001a0 10 A 5 1 4 [ 5] .dynstr STRTAB 08048378 000378 00010f 00 A 0 0 1 [ 6] .rel.got REL 08048488 000488 000048 08 A 4 12 4 [ 7] .rel.plt REL 080484d0 0004d0 000050 08 A 4 9 4 [ 8] .init PROGBITS 08048520 000520 0000e1 00 AX 0 0 16 [ 9] .plt PROGBITS 08048604 000604 0000b0 04 AX 0 0 4 [10] .text PROGBITS 080486b4 0006b4 000354 00 AX 0 0 4 [11] .fini PROGBITS 08048a10 000a10 000081 00 AX 0 0 16 [12] .rodata PROGBITS 08048aa0 000aa0 000134 00 A 0 0 32 [13] .data PROGBITS 08049bd4 000bd4 000010 00 WA 0 0 4 [14] .jcr PROGBITS 08049be4 000be4 000004 00 WA 0 0 4 [15] .eh_frame PROGBITS 08049be8 000be8 000004 00 WA 0 0 4 [16] .ctors PROGBITS 08049bec 000bec 000008 00 WA 0 0 4 [17] .dtors PROGBITS 08049bf4 000bf4 000008 00 WA 0 0 4 [18] .got PROGBITS 08049bfc 000bfc 000058 04 WA 0 0 4 [19] .dynamic DYNAMIC 08049c54 000c54 000088 08 WA 5 0 4 [20] .sbss PROGBITS 08049cdc 000ce0 000000 00 W 0 0 1 [21] .bss NOBITS 08049ce0 000ce0 000028 00 WA 0 0 32 [22] .note NOTE 00000000 000ce0 000050 00 0 0 1 [23] .ident PROGBITS 00000000 000d30 000039 00 0 0 1 [24] .shstrtab STRTAB 00000000 000d69 0000c6 00 0 0 1 [25] .symtab SYMTAB 00000000 001268 000430 10 26 1f 4 [26] .strtab STRTAB 00000000 001698 000197 00 0 0 1 ||< 以上で"__warn_refereces"マクロにより、".gnu.warning"セクションが作成され、ldによるリンク時に警告表示される流れが把握出来た。 今回のお題については、ここまで。 #navi_footer|C言語系|