home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/「デーモン君のソース探検」読書メモ/15, mktemp(3),mkstemp(3)

C言語系/「デーモン君のソース探検」読書メモ/15, mktemp(3),mkstemp(3)

C言語系 / 「デーモン君のソース探検」読書メモ / 15, mktemp(3),mkstemp(3)
id: 568 所有者: msakamoto-sf    作成日: 2010-01-30 12:22:47
カテゴリ: BSD C言語 

お題:mktemp(3)とmkstemp(3)の違いを調査せよ

※今回は軽めなので、「デーモン君のソース探検」書籍は軽く参考程度に留め、テストプログラムを組んだりしつつさくさくと進めていきたいと思います。


mktemp(3)とmkstemp(3)の違いとテストプログラム

まずmanページを確認してみる。

NAME
     mktemp, mkstemp, mkdtemp - make unique temporary file or directory name

LIBRARY
     Standard C Library (libc, -lc)

SYNOPSIS
     #include <stdlib.h>

     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:

#include <stdio.h>
#include <stdlib.h>
 
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:

#include <stdio.h>
#include <stdlib.h>
 
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の主要部分:

__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の主要部分:

#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の主要部分:

#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"の切り替えはプリプロセッサで調整されており、いずれにせよ最終的に以下の関数が呼ばれるようになっている:

#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ループにて、実際のファイル/ディレクトリ作成が行われ戻り値が返される。

  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コマンド呼び出し時のオプション群を把握する。

$ 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"を組み合わせ、表示内容をファイルに落として確認してみる。

$ 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"セクションは含まれていない。

$ 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によるリンク時に警告表示される流れが把握出来た。

今回のお題については、ここまで。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-01-30 12:25:58
md5:75bff76ebce3bb50cbe01c282ff820b2
sha1:e3ed59c1d84bf6688bbc59abe6de8d76246a9248
コメント
コメントを投稿するにはログインして下さい。