無限に長くなる猫は好まれない(?)話

レッドハットの森若%仕事納めモード です。

猫は意外と胴が長いのですがこの記事とはあまり関係ありません。

f:id:mrwk:20191227185721j:plain
ねこはのびます Photo by Timo Volz on Unsplash

RHEL7とRHEL8で動作が違うcat

突然ですが以下の2行をRHEL 7とRHEL 8で実行すると動作に違いがあります。どうなるでしょうか?

$ echo test > hoge
$ cat < hoge >> hoge

こたえはそれぞれ以下のようになります。

  • RHEL 7: 無限ループになって Ctrl-Cなどで停止させるまで止まらない。そしてファイルhogeにはどんどん'test\n' が追記されていく。
  • RHEL 8: cat: -: input file is output file とエラーが出力されてcatが停止、ファイルhogeは変化しない。

何が起きているのか? (shell編)

ここからはRHEL 8 の環境で cat < hoge >> hoge を実行したときに何が起こっているのかを詳しくみていきましょう。

観察するため、シェルのPIDを確認し、別のシェルでstraceを動かします。ファイルを準備している様子をみたいので、 ファイル関連のシステムコールだけを表示するように-e file オプションをつけてフィルタします。

[観察対象のsh]
$ echo $$
14340
[作業用のsh]
$ strace -p 14340 -e file -f

準備ができたので、観察対象のシェルでふたたび cat < hoge >> hoge を実行します。straceを仕込んだ方には以下の出力がでてきました。

strace: Process 14340 attached
strace: Process 14911 attached
[pid 14911] openat(AT_FDCWD, "hoge", O_RDONLY) = 4
[pid 14911] openat(AT_FDCWD, "hoge", O_WRONLY|O_CREAT|O_APPEND, 0666) = 4
[pid 14911] execve("/usr/bin/cat", ["cat"], 0x55c4fc2d96c0 /* 41 vars */) = 0
(以下略)

shがコマンドcatを実行するにあたり、まずforkでプロセス14911を作成し、ここでファイル "hoge" を2回open します。1回は標準入力(stdin)のために、読み込み専用(O_RDONLY)で、もう1つは標準出力(stdout)として、書き込み専用(O_WRONLY)かつ追記(O_APPEND)で開いています。 シェルがファイルを用意して環境がととのったのでexecveでcatが実行します。

ここまでの動作はRHEL 7でも同じです。

何が起きているのか? (cat編)

いよいよcatの実行がはじまります。今回は明確なエラーメッセージが出力されているのでソースの中でメッセージを探します。

RHEL8の coreutils 8.30内 cat.c より

    690 
    691       /* Don't copy a nonempty regular file to itself, as that would
    692          merely exhaust the output device.  It's better to catch this
    693          error earlier rather than later.  */
    694 
    695       if (out_isreg
    696           && stat_buf.st_dev == out_dev && stat_buf.st_ino == out_ino
    697           && lseek (input_desc, 0, SEEK_CUR) < stat_buf.st_size)
    698         {
    699           error (0, 0, _("%s: input file is output file"), quotef (infile));
    700           ok = false;
    701           goto contin;
    702         }
    703 

stat_bufは入力ファイルに対してstatを行った結果です。

  • 出力先のファイルがpipeやsocketではない通常のファイルで
  • 出力ファイルと比較してデバイスとinode番号が同じ(つまり同じファイル実体を指す)でかつ
  • 入力側の現在位置よりあとにデータが存在する

の3つが成立する場合、RHEL 7の時の動作として説明したように無限にファイルが伸びつづけてしまう(その結果リソースが枯渇する)のでこれをエラーとして早めに停止させるというコードが書かれています。

なるほど事故防止の意図はわかりました。RHEL 7のcatにはこのチェックは存在しなかったのでしょうか……? 該当する箇所をみてみましょう。

RHEL 7の coreutils 8.22内 cat.c より

    707       /* Compare the device and i-node numbers of this input file with
    708          the corresponding values of the (output file associated with)
    709          stdout, and skip this input file if they coincide.  Input
    710          files cannot be redirected to themselves.  */
    711 
    712       if (check_redirection
    713           && stat_buf.st_dev == out_dev && stat_buf.st_ino == out_ino
    714           && (input_desc != STDIN_FILENO))
    715         {
    716           error (0, 0, _("%s: input file is output file"), infile);
    717           ok = false;
    718           goto contin;
    719         }

おおむね同じような意図のコードがあるのですが、少しエラー検出の条件が甘く、入力ファイルが標準入力であれば見逃されてしまうようです。

確認

理解が正しいか確認するため、RHEL 7で以下のコマンドで正しくエラー検出がされることを確認します。

$ cat hoge >> hoge
cat: hoge: input file is output file

無限に長くなる猫は好まれないという話でした。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。