こんにちは、末廣です。
これは TECHSCORE Advent Calendar 2015 の 18日目の記事です。
ある日のこと
とあるアプリケーションでログファイルのローテーションが失敗して、削除されたファイルに書き込み続けるということが起こりました。
後輩「ログファイルのローテーションに失敗して、削除されたファイルに書き込み続けてるんですけど、救い出す方法ないですかね。」
私「どれどれ、PID は 26778 なのね……」
| 1 2 3 4 5 6 7 8 | $ ls -l /proc/26778/fd 合計 0 lrwx------ 1 suehiro suehiro 64 12月 17 09:27 0 -> /dev/pts/2 lrwx------ 1 suehiro suehiro 64 12月 17 09:27 1 -> /dev/pts/2 lrwx------ 1 suehiro suehiro 64 12月 17 09:27 2 -> /dev/pts/2 lr-x------ 1 suehiro suehiro 64 12月 17 09:27 3 -> pipe:[31933163] l-wx------ 1 suehiro suehiro 64 12月 17 09:27 4 -> pipe:[31933163] l-wx------ 1 suehiro suehiro 64 12月 17 09:27 5 -> /home/suehiro/TECHSCORE/app.log (deleted) | 
私「あー、app.log はもう削除されてるのね。アプリが掴んだままだからファイルの実体は残ってるけど、アクセスする手段がないなー」
後輩「/proc/26778/fd/5 を cat したらどうっすか?」
私「それって、シンボリックリンクみたいなもんやからアカンやろ。シンボリックリンクは参照するファイル名が書かれてるだけやし。」
| 1 2 3 4 5 6 7 8 9 | $ stat /proc/26778/fd/5   File: `/proc/26778/fd/5' -> `/home/suehiro/TECHSCORE/app.log (deleted)'   Size: 64              Blocks: 0          IO Block: 1024   シンボリックリンク Device: 4h/4d   Inode: 31936416    Links: 1 Access: (0300/l-wx------)  Uid: ( 1000/ suehiro)   Gid: ( 1000/ suehiro) Access: 2015-12-17 09:27:41.568052368 +0900 Modify: 2015-12-17 09:27:41.568052368 +0900 Change: 2015-12-17 09:27:41.568052368 +0900  Birth: - | 
私「でもまあ、一応試してみるか。」
| 1 2 3 4 5 | # cat /proc/26778/fd/5 2015-12-17 09:26:38 hoge 2015-12-17 09:26:39 piyo 2015-12-17 09:26:40 fuga ..... | 
私「Σ(゚д゚;) ヌオォ!? こいつ、読み出せるのかー」
後輩「これで救い出せますね!」
対象の /proc/<pid>/fd/<fd> を読み出すことで、削除されたファイルの内容を救い出せることが分かりました。
更に調べてみる
stat に "-L" オプションを付けて /proc/26778/fd/5 の参照先の情報を表示してみます。
| 1 2 3 4 5 6 7 8 9 | $ stat -L /proc/26778/fd/5   File: `/proc/26778/fd/5'   Size: 7570            Blocks: 16         IO Block: 4096   通常ファイル Device: 807h/2055d      Inode: 976385      Links: 0 Access: (0644/-rw-r--r--)  Uid: ( 1000/ suehiro)   Gid: ( 1000/ suehiro) Access: 2015-12-17 09:29:28.826721123 +0900 Modify: 2015-12-17 09:30:53.153674495 +0900 Change: 2015-12-17 09:30:53.153674495 +0900  Birth: - | 
きちんと参照先の情報が取得できているようです。通常と異なるのは Link カウントが 0 になっている点です。これはファイル名がなくなっているからですね。
ファイル名を付与できないか?
参照先の情報(inode)が取得できるのなら、"ln -L" でファイル名を付与することができそうな気がします。"-L" オプションは「リンクを作成する対象のシンボリックリンクをたどる」ことを示します。
| 1 2 3 4 5 | SYNOPSIS        ln [OPTION]... [-T] TARGET LINK_NAME   (1st form) ...        -L, --logical               dereference TARGETs that are symbolic links | 
では、試してみます。
| 1 2 | $ ln -L /proc/26778/fd/5 new.log ln: `new.log' から `/proc/26778/fd/5' へのハードリンクの作成に失敗しました: そのようなファイルやディレクトリはありません | 
残念、失敗です。対象の inode 番号を知ることはできるのに、ハードリンクは作らせてくれません。
ln コマンド内部で呼び出されているシステムコールを調べたところ、linkat システムコールが使われていることが分かりました。
| 1 2 | $ strace ln -L /proc/26778/fd/5 new.log  2>&1 | grep link linkat(AT_FDCWD, "/proc/26778/fd/5", AT_FDCWD, "new.log", AT_SYMLINK_FOLLOW) = -1 ENOENT (No such file or directory) | 
linkat(2) の man ページによると、linkat などの *at 系のシステムコールは Linux 2.6.16 で追加され、glibc 2.4 でサポートされたそうです。
| 1 2 3 4 5 6 | SYNOPSIS        #include <fcntl.h>           /* Definition of AT_* constants */        #include <unistd.h>        int linkat(int olddirfd, const char *oldpath,                   int newdirfd, const char *newpath, int flags); | 
link システムコールは oldpath と newpath を指定するだけでしたが、linkat システムコールはそれぞれの親ディレクトリをファイルディスクリプターで指定することができます。
flags に指定できるのは次の 2つです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |        AT_EMPTY_PATH (since Linux 2.6.39)               If oldpath is an empty string, create a link to the file  refer‐              enced  by  olddirfd  (which  may  have  been  obtained using the              open(2) O_PATH flag).  In this case, olddirfd can refer  to  any              type  of  file,  not  just a directory.  This will generally not              work if the file has a link count of zero  (files  created  with              O_TMPFILE and without O_EXCL are an exception).  The caller must              have the CAP_DAC_READ_SEARCH capability in  order  to  use  this              flag.  This flag is Linux-specific; define _GNU_SOURCE to obtain              its definition.        AT_SYMLINK_FOLLOW (since Linux 2.6.18)               By default, linkat(), does not dereference oldpath if  it  is  a               symbolic  link (like link()).  The flag AT_SYMLINK_FOLLOW can be               specified in flags to cause oldpath to be dereferenced if it  is               a  symbolic  link.  If procfs is mounted, this can be used as an               alternative to AT_EMPTY_PATH, like this:                   linkat(AT_FDCWD, "/proc/self/fd/<fd>", newdirfd,                          newname, AT_SYMLINK_FOLLOW); | 
"link -L" は AT_SYMLINK_FOLLOW を指定していたんですね。
最後のあがき
ダメ元で AT_EMPTY_PATH の方も試してみます。これは ln コマンドではできないので、C で簡単なプログラムを書いてみました。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #include <stdio.h> int main() {     int fd;     int rc;     struct stat buf;     fd = open("/proc/26778/fd/5", O_PATH);     fstat(fd, &buf);     printf("inode: %d\n", buf.st_ino);     rc = linkat(fd, "", AT_FDCWD, "new.log", AT_EMPTY_PATH);     if (rc < 0)         perror("linkat");     close(fd);     return rc; } | 
12行目の open では O_PATH フラグを指定しています。こうすると linkat などの *at 系のシステムコールや close、fstat、dup などでの利用に制限されたファイルディスクリプターを得ることができます。ファイル自体はオープンしていないので、read や write などのオペレーションをすることはできません。
では、コンパイルして実行してみます。AT_EMPTY_PATH は Linux 独自の拡張なので _GNU_SOURCE を define してやる必要があります。
| 1 2 3 4 5 | $ cc -D _GNU_SOURCE -o linkat linkat.c $ ./linkat inode: 976385 linkat: No such file or directory | 
削除されたファイルをきちんと指定できていることは inode 番号から確認できます。がしかし、ハードリンクの作成はできませんでした。
linkat(2) の AT_EMPTY_PATH の項をよく読むと、これは O_TMPFILE フラグを指定して作成した名無しのファイルに名前を与えるときに使うのを意図しているようです。それ以外の名無しの場合は ENOENT エラーとなるので、どだい無理な話でした。
O_TMPFILE フラグはいくつか便利な用途があるようなので、機会があれば記事にしてみたいと思います。
まとめ
書き込み中に削除されたファイルは、対応する /proc/<pid>/fd/<fd> を読み出すことで内容を救出することができました。削除されたファイルの inode まで到達できるものの、ファイル名を付与することはできなさそうです。
Linux のファイル操作まわりは、open 系システムコールの新たなフラグや、*at 系のシステムコールなど、まだまだ新しい機能が追加されていて面白そうです。

 
						