一時ファイルが欲しいとき、Cのtmpnamで名前を作ってからfopenで開く方法は一見便利ですが、実は深刻な競合状態を生み、攻撃に悪用されます。
本記事ではなぜtmpnamが危険なのかを初心者の方にも分かりやすく解説し、安全な代替であるtmpfileとmkstempの使い方まで丁寧に紹介します。
tmpnamとは?
一時的なファイル名とは
開発中やデータの一時保管で、一時ファイルを使いたい場面はよくあります。
例えば計算結果を一時的に書き出したり、外部コマンドに渡すために一時的にファイルが必要になったりする場合です。
このとき必要になるのが他のファイルと衝突しない一時的なファイル名です。
tmpnamの動きと戻り値
tmpnamは一時的なファイル名の文字列を生成する標準Cの関数です。
- 宣言:
char *tmpnam(char *s); - 引数
sがNULLでない場合、そこに結果を書き込みます。NULLの場合は内部の静的領域に書かれ、そのポインタを返します。 - 返り値は生成されたパス文字列、失敗時は
NULLです。 - バッファ長は
L_tmpnamで十分なサイズが定義され、生成可能回数の上限はTMP_MAXです。
ここで重要なのはtmpnamは「名前」だけを作るという点です。
fopenなどで実際のファイルを作成するのは別の操作になります。
なぜ今は推奨されないのか
名前を作る操作と、ファイルを作成する操作が分離しているため、その「すき間」に競合状態が発生します。
別プロセスが先に同じ名前でファイルを作ったり、悪意あるリンクを仕掛けたりできるため、セキュリティ上の問題が起きやすくなります。
多くのプラットフォームの文書やリンタはtmpnamの使用を非推奨としています。
tmpnamが危険な理由:競合状態
名前生成→作成のすき間が危険
tmpnamは「存在しないはずの名前」を返しますが、それはあくまで呼び出した瞬間の話です。
tmpnamで名前を得てからfopenで作るまでのわずかな時間に、他プロセスが同じ名前でファイルを作成できてしまいます。
この典型的な不具合はTOCTTOU(Time Of Check To Time Of Use)と呼ばれます。
以下は敢えて競合を観察しやすくするため、生成後に待機してから開く不適切な例です。
実運用では待機の有無に関係なく原理的に危険です。
// insecure_tmpnam.c: tmpnamの危険な使い方の例(デモ用)
// 注意: 実際には使わないでください
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#define sleep_sec(n) Sleep((n)*1000)
#else
#include <unistd.h>
#define sleep_sec(n) sleep(n)
#endif
int main(void) {
char namebuf[L_tmpnam]; // 推奨サイズのバッファ
char *name = tmpnam(namebuf); // 一時ファイル名を生成(この時点では存在しないはず)
if (!name) {
fprintf(stderr, "tmpnam failed\n");
return 1;
}
printf("[victim] generated name: %s\n", name);
printf("[victim] waiting 5 seconds before fopen... (race window!)\n");
fflush(stdout);
sleep_sec(5); // デモのために隙間を作る
// 競合に弱い: すでに誰かが 'name' を使っているかもしれない
FILE *fp = fopen(name, "wb"); // 排他的ではない作成
if (!fp) {
fprintf(stderr, "fopen failed: %s\n", strerror(errno));
return 1;
}
const char *msg = "secret\n";
if (fwrite(msg, 1, strlen(msg), fp) != strlen(msg)) {
fprintf(stderr, "write failed\n");
} else {
printf("[victim] wrote to %s\n", name);
}
fclose(fp);
return 0;
}
実行結果(概念的な例):
[victim] generated name: /tmp/fileABCD
[victim] waiting 5 seconds before fopen... (race window!)
[victim] wrote to /tmp/fileABCD
この5秒の間に別プロセスが同名のファイルやシンボリックリンクを用意できてしまいます。
他プロセスとの衝突と上書きリスク
世界書き込み可能なディレクトリ(例: POSIXの/tmp)では、他プロセスが同じ名前でファイルを先に作ってしまうと、fopenのモードによっては既存ファイルを上書きしたり、意図しないファイルを開いたりします。
権限やリンクの組み合わせによっては特権昇格や情報漏えいに繋がることもあります。
予測可能な名前による悪用リスク
実装によってはtmpnamの名前パターンが予測可能で、攻撃者が先回りして大量に作成するだけで高確率で競合を誘発できます。
ディレクトリのパーミッション設定が甘いと被害が広がります。
初心者が陥りやすい誤解
- 「ユニークな名前なら安全」: いいえ。ユニークだったのは「今この瞬間」であり、後続の
fopenまで保証されません。 - 「
tmpnam_sなら安全」: いいえ。安全なバッファ操作をするだけで、名前生成とファイル作成の分離という本質的な問題は残ります。 - 「乱数で十分」: いいえ。乱数でも原子的に作成しない限り競合は防げません。
安全な代替1:tmpfileの基本と使い方
特徴
tmpfileは一時ファイルを作成して開いたFILE *を返す標準Cの関数です。
多くのPOSIX環境では作成直後にリンクが削除される(名前がない状態)ため、他プロセスから見えません。
クローズ時に自動で消えるので掃除のし忘れも防げます。
Windowsなど一部実装では内部挙動が異なりますが、APIとしては「自動削除される一時ファイルを開く」ものとして扱えます。
C11 Annex Kにはtmpfile_sもありますが、利用可否は処理系依存です。
使い方の流れ
FILE *fp = tmpfile();で開くfprintfやfwriteで書く- 読み戻したい場合は
rewindで先頭に戻してfgetsやfreadで読む fcloseで閉じると自動削除
// tmpfile_example.c: tmpfileの基本
#include <stdio.h>
#include <string.h>
int main(void) {
FILE *fp = tmpfile(); // 自動削除される一時ファイルを開く
if (!fp) {
perror("tmpfile");
return 1;
}
// 一時ファイルに書き込む
const char *msg = "hello tmpfile\n";
if (fputs(msg, fp) == EOF) {
perror("fputs");
fclose(fp);
return 1;
}
// 読み戻す(先頭へ)
rewind(fp);
char buf[64];
if (fgets(buf, sizeof buf, fp)) {
printf("read back: %s", buf); // 標準出力へ表示
} else {
printf("no data read\n");
}
// 閉じると自動的に破棄される
fclose(fp);
return 0;
}
read back: hello tmpfile
できること/できないこと
- できること: プロセス内で安全に読み書き、クローズと同時に削除。ファイル名を他者に見せない運用ができます。
- できないこと: 別プロセスと「名前で」共有することは想定していません。別プロセスに渡す必要があるなら
mkstempなどを検討します。
こんなときに選ぶ
- 一時的なワーク領域が必要で、他プロセスに見せる必要がない
- 自動削除されてほしい
- ISO Cだけで完結させたい
安全な代替2:mkstempの基本と使い方
特徴
mkstempはPOSIXの関数で、テンプレートから安全な一時ファイル名を生成し、その場で原子的にオープンします。
これにより競合状態が本質的に解消されます。
戻り値はファイルディスクリプタ(int)で、必要ならfdopenでFILE *に変換できます。
作られるファイルのパーミッションは通常0600です。
POSIX専用です。
Windowsでは代替として_mktemp_sがありますが、これはtmpnam同様に「名前だけ」なので同種の問題を避けられません。
Windowsでの厳密な代替はtmpfileか、WinAPIのCreateFileでFILE_ATTRIBUTE_TEMPORARYやFILE_FLAG_DELETE_ON_CLOSEを使います。
テンプレートの書き方
テンプレート文字列の末尾に6個のXを含める必要があります。
mkstempはこのX群を実際のランダムな文字に置換します。
文字列リテラルを直接渡してはいけません(書き換え不可のため)。
書き換え可能な配列にしてください。
char tmpl[] = "/tmp/myapp-XXXXXX";
使い方の流れ
- テンプレート配列を用意
int fd = mkstemp(tmpl);で原子的に作成・オープン- 必要なら
fdopenでFILE *化してfprintf等を使う - 自動削除にしたい場合、
unlink(tmpl)して名前を消す - 使い終わったら
fcloseまたはclose
// mkstemp_example.c: mkstempで安全な一時ファイルを作る
// POSIX専用(Linux, macOSなど)
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // mkstemp, unlink, close
#include <errno.h>
int main(void) {
char tmpl[] = "/tmp/myapp-XXXXXX"; // 末尾にXXXXXXが必要
int fd = mkstemp(tmpl); // 原子的に作成+オープン
if (fd == -1) {
perror("mkstemp");
return 1;
}
printf("created: %s\n", tmpl);
// FILE*に変換して高級I/Oを使う
FILE *fp = fdopen(fd, "w+"); // 以後はfpを閉じればfdも閉じられる
if (!fp) {
perror("fdopen");
close(fd); // 失敗時はfdを閉じる必要あり
unlink(tmpl); // ファイルも削除
return 1;
}
// 書き込み
fprintf(fp, "line 1: %d\n", 42);
fprintf(fp, "line 2: %s\n", "mkstemp is safe");
// 必要に応じて名前を消すと、クローズ時に自動で消える運用になる
if (unlink(tmpl) == -1) {
perror("unlink"); // 失敗しても致命的ではない
} else {
printf("unlinked name (file will be removed on close)\n");
}
// 読み戻し
rewind(fp);
char buf[128];
while (fgets(buf, sizeof buf, fp)) {
fputs(buf, stdout); // 標準出力へ
}
// 閉じる(自動で削除される)
fclose(fp);
return 0;
}
created: /tmp/myapp-2xY7QK
unlinked name (file will be removed on close)
line 1: 42
line 2: mkstemp is safe
注意点
- ポータビリティ:
mkstempはPOSIX専用です。WindowsではtmpfileやWinAPIの一時ファイル作成を検討してください。 - クリーンアップ:
fdopenに成功したらfcloseだけ、失敗したらcloseとunlinkが必要です。二重クローズに注意します。 - テンプレートの可変性: 文字列リテラルを渡すと未定義動作です。必ず配列にコピーしてから渡します。
- 自動削除にするか:
unlinkしてから使えばtmpfileに近い運用ができます。名前が必要ならunlinkしないで最後に削除します。
選び方の目安
- 名前が不要で、プロセス内だけで使う →
tmpfile - 別プロセスとファイルを共有したい、またはファイル名が必要 →
mkstemp - どうしてもWindowsで名前付きの安全な一時ファイルが必要 → WinAPIの
CreateFileでFILE_FLAG_DELETE_ON_CLOSE等を検討(C標準外)
参考: API比較早見表
| API | 何をするか | 競合対策の有無 | 戻り値 | ファイルの寿命 | ポータビリティ | 典型用途 |
|---|---|---|---|---|---|---|
tmpnam | 一時名を生成するだけ | なし(危険) | char* | 作成は別操作 | ISO C | 避けるべき |
tmpfile | 一時ファイルを作成して開く | あり(他から見えにくい) | FILE* | クローズで削除 | ISO C | プロセス内の一時作業 |
mkstemp | 名称生成と作成を原子的に行う | あり(原子性) | int fd | 任意(必要ならunlink) | POSIX | 名前が必要/共有したい |
空行
結論として、tmpnamは競合状態を避けられず危険です。
tmpfileかmkstempに乗り換えましょう。
まとめ
本記事では一時的なファイル名を生成するtmpnamがなぜ危険なのかを、競合状態(TOCTTOU)の観点から解説しました。
「名前生成」と「作成」が分離していること自体が根本原因であり、tmpnam_s等でも本質は解決しません。
代わりにISO Cで完結するtmpfile、またはPOSIXのmkstempを使えば、原子的な作成や自動削除によって安全性と利便性を確保できます。
用途に応じて、名前が不要ならtmpfile、名前が必要ならmkstempという指針を覚えておくとよいでしょう。
最後にもう一度だけ強調します。
tmpnamは使わない、これが安全なCプログラミングの第一歩です。
