遅くなってしまいましたが、FreeBSD Advent Calendar 2020 11日目の記事です。
今日はFreeBSD-12.1 Releaseではディレクトリに対するread(2)がエラーとして扱われる挙動に変更されたという話をしようと思います。
背景
FreeBSD-12.1のリリースノートを眺めていると、以下のような記述を見つけました。どうやらデフォルトではread(2)によるディレクトリの読み取りが禁止となる挙動に変更されたようです。
The read(2) system call has been changed to disable read() calls on directories by default. A new sysctl(8) has been added, security.bsd.allow_read_dir, which when set to 1 will restore the previous behavior.
まずは試してみましょう。FreeBSD-12.1とFreeBSD-12.2のそれぞれの環境でディレクトリに対してcat(1)してみましょう。
FreeBSD-12.1の場合は、ディレクトリをcat(1)すると何やらデータが表示されます。
$ # FreeBSD-12.1の場合。
$ freebsd-version -ku
12.1-RELEASE
12.1-RELEASE
$
$ # ディレクトリがcatできる。
$ # (制御コードも表示されるのでstringsでフィルタしています)
$ cat /tmp | strings
.X11-unix
.XIM-unix
.ICE-unix
.font-unix
これに対し、FreeBSD-12.2ではディレクトリをcat(1)してもエラーとして処理されるという挙動になっています。
$ # FreeBSD-12.2の場合。
$ freebsd-version -ku
12.2-RELEASE
12.2-RELEASE
$
$ # エラーとして処理されています。
$ cat /tmp
cat: /tmp: Is a directory
ちなみに、Linuxの場合もディレクトリに対するread(2)はエラーとして処理されます。
$ cat /etc/redhat-release
CentOS Linux release 7.8.2003 (Core)
$
$ cat /tmp
cat: /tmp: ディレクトリです
もう少し踏み込んで調べてみる
コマンドでの振る舞いについては把握できましたが、どのような挙動でエラーになっているのか気になります。ktrace(1)を使用して、ディレクトリをcat(1)した時の内部的な振る舞いを見てみましょう。
$ ktrace cat /tmp
cat: /tmp: Is a directory
$
$ kdump -f ktrace.out
...中略...
1755 cat CALL openat(AT_FDCWD,0x7fffffffee04,0<O_RDONLY>)
1755 cat NAMI "/tmp"
1755 cat RET openat 3
...中略...
1755 cat CALL read(0x3,0x80064c000,0x1000)
1755 cat RET read -1 errno 21 Is a directory
1755 cat CALL write(0x2,0x7fffffffde60,0x5)
なるほど、openat(2)で指定したディレクトリをオープンしてファイルディスクリプタを取得、その後にread(2)でデータを読みだそうとした時に errno 21
が返されるという挙動になっています。
errno 21
が示すエラー内容を確認すると EISDIR
のマクロ定数となっていました。
$ find /usr/include/ -type f -name errno.h | xargs grep -w 21 | grep define
/usr/include/sys/errno.h:#define EISDIR 21 /* Is a directory */
つまりはFreeBSD-12.2では、ディレクトリに対するread(2)は EISDIR
を返すという挙動に変更されているという話のようです。
sysctlでディレクトリに対するread(2)の挙動を変更する
もう一度FreeBSD-12.1のリリースノートを見てみると以下の記述があります。どうやらsysctl(8)で security.bsd.allow_read_dir
というsysctl変数を 1
に設定することで、FreeBSD-12.1以前の挙動に戻すことができるようです。
A new sysctl(8) has been added, security.bsd.allow_read_dir, which when set to 1 will restore the previous behavior.
さっそく試してみましょう。まずは security.bsd.allow_read_dir
の説明を確認してみます。これまで確認してきたように、read(2)によるディレクトリの読み込みを有効・無効化するためのsysctl変数となっています。
$ sysctl -d security.bsd.allow_read_dir
security.bsd.allow_read_dir: Enable read(2) of directory for filesystems that support it
実際に security.bsd.allow_read_dir=1
を設定すると、FreeBSD-12.1以前の挙動(=ディレクトリがread(2)できるようになる)に戻ります。
$ sysctl security.bsd.allow_read_dir
security.bsd.allow_read_dir: 0
$ cat /tmp
cat: /tmp: Is a directory
$ sudo sysctl -w security.bsd.allow_read_dir=1
security.bsd.allow_read_dir: 0 -> 1
$ sysctl security.bsd.allow_read_dir
security.bsd.allow_read_dir: 1
$ cat /tmp | strings
.X11-unix
.XIM-unix
.ICE-unix
.font-unix
ソースコードの変更箇所を見てみる
より深く調べてみましょう。これら一連の変更はリビジョン 363016で行われたようです。
変更箇所はsys/kern/vfs_vnops.cとなっています。一連の変更箇所の中から、今回の挙動に関連する部分をピックアップして見てみます。
struct fileops.fo_read
にread(2)に対応する vn_io_fault()
という関数が設定されています。
106 struct fileops vnops = {
107 .fo_read = vn_io_fault,
108 .fo_write = vn_io_fault,
...
122 };
vfs_allow_read_dir
という変数がsysctl(8)で設定した security.bsd.allow_read_dir
に対応しています。
135 static int vfs_allow_read_dir = 0;
136 SYSCTL_INT(_security_bsd, OID_AUTO, allow_read_dir, CTLFLAG_RW,
137 &vfs_allow_read_dir, 0,
138 "Enable read(2) of directory for filesystems that support it");
vn_io_fault()
においては、1162行~1173行がリビジョン 363017で追加された内容となっています。修正内容としてはシンプルで、 vp->v_type == VDIR
ならディレクトリに対する読み込み処理であるため、変数 vfs_allow_read_dir
が 0
の場合に EISDIR
を返すという処理になっています。
1149 static int
1150 vn_io_fault(struct file *fp, struct uio *uio, struct ucred *active_cred,
1151 int flags, struct thread *td)
1152 {
...
1162 /*
1163 * The ability to read(2) on a directory has historically been
1164 * allowed for all users, but this can and has been the source of
1165 * at least one security issue in the past. As such, it is now hidden
1166 * away behind a sysctl for those that actually need it to use it.
1167 */
1168 if (vp->v_type == VDIR) {
1169 KASSERT(uio->uio_rw == UIO_READ,
1170 ("illegal write attempted on a directory"));
1171 if (!vfs_allow_read_dir)
1172 return (EISDIR);
1173 }
その他:コミットログに歴史的な経緯が記載されている
BSD系ではディレクトリがcat(1)できるがLinuxではそうではない、というのはある意味トリビア(?)的な情報となっていました。
(ちなみにSolaris10では cat /tmp
はエラーになるようです...)
このあたりの歴史的な経緯はリビジョン 363017のコミットログに詳しく記載されています。
まとめ
FreeBSD-12.1 Releaseにおいてread(2)によるディレクトリの読み込みがデフォルトでは禁止となったという話をしました。修正内容的には大きな修正ではありませんが、歴史的な経緯を踏まえると挙動を以前のものに戻せる仕組みを入れるといった、影響範囲を考慮した設計・実装が必要となるようです。