お手軽にcoredumpを圧縮して保存する方法 〜core_patternの話〜

# TL;DR

coredumpはサイズが大きくなりがちなので圧縮したいということはよくあると思う。
下記設定で、お手軽にgzip圧縮して保存できる、という小ネタ。

```text:/proc/sys/kernel/core_pattern
|/bin/sh -c /bin/gzip>/tmp/core.gz
```


# 環境
動作確認、および、引用しているLinuxのソースコードはv4.15.0。

# `core_pattern`とは？

coredumpが出力されるLinux環境では、デフォルトではプロセスの実行ディレクトリに`core`という名前で保存される。
ただこのままだと場所がバラけてしまうため管理しにくい。
また、同一ディレクトリで動いているプロセスにつき一つしかcoredumpを残せないのも問題だ。

そこでlinuxには`/proc/sys/kernel/core_pattern`で出力ファイル名を変更する仕組みが用意されている。


> man core(5)
> 
>   Naming of core dump files
>       By  default, a core dump file is named core, but the /proc/sys/kernel/core_pattern file (since Linux 2.6 and 2.4.21) can be set to define a template that is used to name core dump files.


上記のとおり、この機能は基本的にファイル名を指定するものだが、
システムに応じてより柔軟なcoredump出力ができるよう`|`(パイプ)機能をサポートしている。

> man core(5)
> 
>   Piping core dumps to a program
>        Since kernel 2.6.19, Linux supports an alternate syntax for the /proc/sys/kernel/core_pattern file.  If the first character of this file is a pipe symbol (|), then the remainder of  the  line
>        is interpreted as a program to be executed.  Instead of being written to a disk file, the core dump is given as standard input to the program.

たとえば下記のように、`|`(パイプ)の後ろにgzip圧縮するプログラムを指定すれば、coredumpをgzip圧縮して保存できる。

```text:/proc/sys/kernel/core_pattern
|/tmp/gzip.sh
```

```shell:/tmp/gzip.sh
#!/bin/sh
/bin/gzip > /tmp/core.gz
```

試してみる。

```shell-session
$ sleep 100 &
[1] 30428
$ kill -SEGV $!
[1]+  Segmentation fault      (core dumped) sleep 100
$ gunzip /tmp/core.gz
$ file /tmp/core
/tmp/core: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from 'sleep 100'
```

この`core_pattern`のパイプ機能でちょっと遊んでみた。というのが今回の記事である。


# やりたいこと
さて、上記のようにスクリプトを追加すれば、coredumpをgzip圧縮して保存できることはわかった。
ではなぜ、わざわざスクリプトを追加しないといけないのだろう？
追加スクリプトなしでgzip圧縮して保存する、というのが本記事でやりたいことだ。


# NG1. スクリプトの中身をそのまま`core_pattern`に書いてみる

```text:/proc/sys/kernel/core_pattern
|/bin/gzip > /tmp/core.gz
```

これはうまくいかない。
なぜなら、`core_pattern`文字列はシェルに渡されるのではなく、kernelで直接パースされるからだ。
オーバーヘッドを避けるためだと思われるが、もちろん機能はかなり限定的だ。

下記のように、`core_pattern`で指定したファイル名は`argv_split()`で`argv[]`に分割され、`call_usermodehelper_exec()`が実行される。

```c:fs/coredump.c
void do_coredump(const siginfo_t *siginfo)
{
...
		helper_argv = argv_split(GFP_KERNEL, cn.corename, NULL);
		if (!helper_argv) {
			printk(KERN_WARNING "%s failed to allocate memory\n",
			       __func__);
			goto fail_dropcount;
		}

		retval = -ENOMEM;
		sub_info = call_usermodehelper_setup(helper_argv[0],
						helper_argv, NULL, GFP_KERNEL,
						umh_pipe_setup, NULL, &cprm);
		if (sub_info)
			retval = call_usermodehelper_exec(sub_info,
							  UMH_WAIT_EXEC);

		argv_free(helper_argv);
		if (retval) {
			printk(KERN_INFO "Core dump to |%s pipe failed\n",
			       cn.corename);
			goto close_fail;
		}
...
}
```

```c:lib/argv_split.c
char **argv_split(gfp_t gfp, const char *str, int *argcp)
{
	char *argv_str;
	bool was_space;
	char **argv, **argv_ret;
	int argc;

	argv_str = kstrndup(str, KMALLOC_MAX_SIZE - 1, gfp);
	if (!argv_str)
		return NULL;

	argc = count_argc(argv_str);
	argv = kmalloc(sizeof(*argv) * (argc + 2), gfp);
	if (!argv) {
		kfree(argv_str);
		return NULL;
	}

	*argv = argv_str;
	argv_ret = ++argv;
	for (was_space = true; *argv_str; argv_str++) {
		if (isspace(*argv_str)) {
			was_space = true;
			*argv_str = 0;
		} else if (was_space) {
			was_space = false;
			*argv++ = argv_str;
		}
	}
	*argv = NULL;

	if (argcp)
		*argcp = argc;
	return argv_ret;
}
```

つまり、`>`(リダイレクト)なんて知らないので、そのままgzipコマンドに第1引数として渡され、うまく動かない。

- 起動されるコマンド: `/bin/gzip`
- 第一引数: `>`
- 第二引数: `/tmp/core.gz`



# NG2. リダイレクトをやめようとしてみる
であれば、gzipコマンドに渡す引数だけで出力ファイルを指定すれば良いじゃないか、ということで次のような方法を考える。

```text:/proc/sys/kernel/core_pattern
|/bin/gzip --output /tmp/core.gz
```

ただ、gzipコマンドには出力先を指定するオプションがない。
入力ファイル名にgz拡張子をつけて保存するか、stdoutに出力するか、の２択だ。

というわけで、この方法は取れない。


# NG3. リダイレクトするシェルを起動する

このあたりから怪しい匂いがしてくるが。。

リダイレクトしてくれないのがシェルを起動しないことが原因なら、起動すれば良いじゃないか、と。
スクリプトを追加しなくても、実行したいコマンドを指定することはできるぞ、と。

ということで下記のような設定を試してみる。

```text:/proc/sys/kernel/core_pattern
|/bin/sh -c "/bin/gzip > /tmp/core.gz"
```

が、これもうまくいかない。

理由はNG2と同じ。
kernelのパーサが`"`（ダブルクォート）を特別扱いしないからだ。
スペースだけを引数の区切り文字として、パイプ先のプロセスの引数を組み立てる。
今回の場合、下記のように解釈された結果、正しく動かない。

- 起動されるコマンド: `/bin/sh`
- 第一引数: `-c`
- 第二引数: `"/bin/gzip`
- 第三引数: `>`
- 第四引数: `/tmp/core.gz"`



# そして成功

ということで、冒頭の設定が出来上がる。


```text:/proc/sys/kernel/core_pattern
|/bin/sh -c /bin/gzip>/tmp/core.gz
```

どうだろうか、このそこはかとない背徳感。

一見シンプルに書けているが、実際には薄氷の上を歩くような実装で、知らずにメンテしたらハマる予感が半端ない。

実際に運用するときは世代管理など圧縮以外にもやることがあるだろうし、
トライアルはともかく本番はプログラムを分けといたほうが無難かな。（をい）

# 参考
[man core(5)](http://man7.org/linux/man-pages/man5/core.5.html)