LoginSignup
6
1

More than 3 years have passed since last update.

FreeBSD-12.2ではディレクトリに対するread(2)がエラーとして扱われるようになった話

Last updated at Posted at 2020-12-15

遅くなってしまいましたが、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_readread(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_dir0 の場合に 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)によるディレクトリの読み込みがデフォルトでは禁止となったという話をしました。修正内容的には大きな修正ではありませんが、歴史的な経緯を踏まえると挙動を以前のものに戻せる仕組みを入れるといった、影響範囲を考慮した設計・実装が必要となるようです。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1