レッドハットの森若%仕事納めモード です。
猫は意外と胴が長いのですがこの記事とはあまり関係ありません。
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
無限に長くなる猫は好まれないという話でした。