signal送信元を知る

# はじめに
プロセス間の非同期通信としてよく使われるsignalですが、
送信元を知る必要があり方法を調べてみたので備忘メモとして残しておきます。

試した環境は以下のとおりです。

```
$ lsb_release -d
Description:	Ubuntu 18.04.4 LTS
$ uname -r
5.3.0-61-generic
$ trace-cmd --version | grep version
trace-cmd version 2.6.1
$ stap --version | head -1
Systemtap translator/driver (version 4.3/0.170, commit release-4.3-0-gc9c23c987d81)
```

# 方法１. sigaction(2)で知る

signalを受け取るプロセスで送信元の情報を取得するアプローチです。

signalを受け取るプロセスを変更することができて、
かつプロセスでハンドリングできるsignalが調査対象の場合に使える方法です。
SIGKILLのようにプロセスでハンドリングできないsignalには使えません。

sigaction(2)は、`sa_flags`に`SA_SIGINFO`を指定することで、
シグナルハンドラでsignalに関する詳細な情報`siginfo_t`を受け取ることができるようになります。
`si_pid`で送信元のPIDを、`si_uid`で送信元の実ユーザIDを知ることができます。

```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

static int g_sig;
static siginfo_t g_siginfo;

static void
my_sigaction (int sig, siginfo_t *info, void *ucontext)
{
  g_sig = sig;
  // memcpy(3) is a async-signal-safe function
  // according to man signal-safety(7)
  memcpy (&g_siginfo, info, sizeof(g_siginfo));
}

static char*
code2str (int code)
{
  switch (code)
    {
    case SI_USER: return "SI_USER";
    case SI_KERNEL: return "SI_KERNEL";
    case SI_QUEUE: return "SI_QUEUE";
    case SI_TIMER: return "SI_TIMER";
    case SI_MESGQ: return "SI_MESGQ";
    case SI_ASYNCIO: return "SI_ASYNCIO";
    case SI_SIGIO: return "SI_SIGIO";
    case SI_TKILL: return "SI_TKILL";
    default: return "unknown";
    }
}

int
main (void)
{
  struct sigaction act;
  act.sa_flags = SA_SIGINFO;
  act.sa_sigaction = my_sigaction;
  int ret = sigaction(SIGTERM, &act, NULL);
  if (ret < 0)
    {
      perror ("sigaction");
      exit (EXIT_FAILURE);
    }
  printf ("pid: %d\n", getpid());

  sleep (10000);

  fprintf (stderr, "sig: %d, si_pid: %d, si_uid: %d, si_code: %s\n",
           g_sig,
           g_siginfo.si_pid,
           g_siginfo.si_uid,
           code2str(g_siginfo.si_code));

  return 0;
}

```

# 方法２．trace-cmd (=ftrace)で知る

kernelは誰が誰にsignalを送ったか知っているので、kernelのイベントをトレースすることで調べるアプローチです。
kernelのイベントトレースには、ftraceを使います。

ftraceで`signal_generate`のイベントをトレースすることで送信元を特定することができます。
debugfsからftraceを直接操作してもいいですが、ここでは`trace-cmd`を使ってトレースする方法を紹介します。

```
[トレース開始]
$ sudo trace-cmd start -e signal_generate -f 'sig==9 && pid == 6985'

[トレース終了]
$ sudo trace-cmd stop

[トレース結果の確認]
$ sudo trace-cmd show
# tracer: nop
#
# entries-in-buffer/entries-written: 1/1   #P:8
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
     test_sender-6993  [001] d...  1660.447368: signal_generate: sig=9 errno=0 code=0 comm=test_receiver pid=6985 grp=1 res=0
```

`pid=6985`のプロセスに`sig=9`を送ったのは、test_senderというPIDが6993のプロセスであることがわかります。

ここで`signal_generate`イベントは大量に発生するので、上記のように`-f`オプションを使い適切にフィルタをかけたほうが良いです。
上記では、signal番号とsignalを受け取るpidでフィルタしています。

フィルタ関数で使えるfield名は、`trace-cmd list`コマンドで確認できます。

```shell-session
$ trace-cmd list -F -e signal_generate
system: signal
name: signal_generate
ID: 194
format:
	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
	field:int common_pid;	offset:4;	size:4;	signed:1;

	field:int sig;	offset:8;	size:4;	signed:1;
	field:int errno;	offset:12;	size:4;	signed:1;
	field:int code;	offset:16;	size:4;	signed:1;
	field:char comm[16];	offset:20;	size:16;	signed:1;
	field:pid_t pid;	offset:36;	size:4;	signed:1;
	field:int group;	offset:40;	size:4;	signed:1;
	field:int result;	offset:44;	size:4;	signed:1;
```

# 方法３．systemtapで知る

方法２と同じく、kernelのイベントをトレースすることで調べるアプローチです。
kernelのイベントトレースにsystemtapを使います。

下記のようにワンライナーでモニタできます。

```shell-session
$ stap -e 'probe signal.send { if (sig==9 && sig_pid==8722) printf("%s: %s(%d) -> %s(%d)\n", sig_name, execname(), pid(), pid_name, sig_pid) }'
SIGKILL: test_sender(9191) -> test_receiver(8722)
```

スクリプトにして、ついでに引数取れるようにすると以下のようになります。
（qiitaにsystemtapモードがないようなので、c言語モードにしています）

```c:signal_sender.stp
#!/usr/bin/env stap
probe signal.send
{
  if (sig==strtol(@1,10) && sig_pid==strtol(@2,10))
    printf("%s: %s(%d) -> %s(%d)\n", sig_name, execname(), pid(), pid_name, sig_pid)
}
```

systemtapはPCだとこのようにスマートにできるのですが、
組み込みだとコンパイル環境と実行環境が別になるため手順が少し煩雑になるのが欠点です。

# まとめ

他にもあると思いますが、選択肢として３つ紹介しました。
調べた中にはSELinuxのaudit.logを利用するなんて方法もありました(ご興味があれば[こちら](https://access.redhat.com/ja/solutions/385753)をご覧ください)。
個人的には、使いやすさと環境のハードルのバランスがいいのは、ftrace(trace-cmd)かなと思ってます。
ちょっと力尽きてしまいましたが、eBPFでもできるでしょうからeBPF版もそのうち追記したいです。

# 参考
[Man page of SIGACTION](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/sigaction.2.html)
[trace-cmd(1) - Linux man page](https://linux.die.net/man/1/trace-cmd)
[stap(1): systemtap script translator/driver - Linux man page](https://linux.die.net/man/1/stap)
[trace-cmdコマンドの使い方 - Qiita](https://qiita.com/hana_shin/items/359240688a53c849593b)
[プロセスへの SIGKILL の送信元を追跡する方法 - Red Hat Customer Portal](https://access.redhat.com/ja/solutions/385753)
[takeoverjp/test_signal: signal simple test program](https://github.com/takeoverjp/test_signal)