この記事は「渡部 Advent Calendar 2025」の5日目の記事です。
ファイルシステムに関わるプログラムを書いていると、access() システムコールを見かける機会は意外と多いと思います。しかし、この access() は “知っておくと便利” というより、「正体を理解して積極的に使うものではない」種類のシステムコールということを当時のエンジニアの先輩に教わりました。特に、セキュリティや SRE、運用観点でファイルアクセスを扱う際には、access() の特性を誤解すると危険な判断につながることがあります。
この記事では、access() の基本から内部動作、なぜ避けるべきとされるのかあたりについて書いていきます。
access() システムコールとは?
access() は 「このプロセスが、指定したファイルに対してパーミッションを持っているか?」 をカーネルに問い合わせるためのシステムコールです。
#include <unistd.h> int access(const char *pathname, int mode);
- R_OK … 読み取り可能か?
- W_OK … 書き込み可能か?
- X_OK … 実行可能か?
- F_OK … 存在しているか?
返り値
- 0 → 要求されたアクセスが可能
- -1 → 不可能(errno で理由がわかる:EACCES, ENOENT など)
access() と 通常のパーミッションチェック は違う
UNIX のプロセスには以下があります:
- 実ユーザID(real UID)
- 実効ユーザID(effective UID) ← アクセス権の基準
- 保存セットUID
access() は 実効 UID/GID を使ってアクセス可能かどうか判断 します。
sudo で実際に起きる例
sudo -u www-data ./app
この場合:
access() は www-data の権限で判定する
しかし open() は setuid プログラムなら root 権限で実行される可能性がある。つまり access() が通らないのに open() は成功することがあり得ます。
なぜ access() を使うべきでないと言われるのか?
TOCTTOU (Time-of-check vs Time-of-use) 脆弱性
典型例
if (access(path, W_OK) == 0) { fd = open(path, O_WRONLY); }
access() と open() の間に パスの指し示すファイルが変更される可能性 がある。例えば攻撃者がaccess() の直後にファイルを symlink 別のファイルへ付け替えるた状態のopen()は別のファイルを開いてしまう可能性があります。これが classic TOCTTOU attackです。
カーネル内部ではどう動くのか?
- sys_access() が呼ばれる
- パス名の解決(VFS → inode へ)
- inode_permission() でパーミッションチェック
- 実効UID/GID を使って所有者・group・others の判定
- 必要なら capabilities を評価(CAP_DAC_OVERRIDE など)
open() と違って実際にファイルディスクリプタは作られないです。この辺はVFSのレイヤーで実装されているので追うならnfsdあたりが読みやすいかなと思います。
代わりに何を使うべきか?
ファイルを開きたい:open() を直接使う
int fd = open(path, O_WRONLY); if (fd < 0) { // ここで errno を見る }
ファイルの属性だけ知りたい:stat()
struct stat st; stat(path, &st);
実ユーザ権限での判定が必要:faccessat()
より安全で柔軟な *at() ファミリーを使う。
まとめ
- access() は 「実効ユーザIDでアクセス可能か?」 の問い合わせ
- TOCTTOU の危険が大きい
- 実際のファイルアクセス権は open() の結果で判断すべき
- setuid プログラムなど特殊用途以外では基本的に使わない
明日は渡部さんによる6日目の記事です。楽しみですね。