ねえママ, systemd が SEGV したらどうなるの?

Linux 4.5 から cgroup2 というやつが入りました. こいつはいままでの cgroup とは違って, cgroup の tree がシステム全体で唯1つになり, 様々なファイルの名前も変わっています.

そうすると, いろいろと cgroup を使っている systemd にも変更が必要…というわけでいまの systemd の git HEAD では様々開発されているような感じです.

とは言ってもせっかく Linux 4.5 なのだから systemd-229 でも cgroup2 使いたいというわけでがんばってみたら死んだという話. 基本的に必要になるのは次の patch

github.com


もともと cgroup2 の挙動は "__DEVEL__sane_behavior" の FS mount option で有効になっていたので, それを切り替えていく patch で, これだけでなんとなく動いている………のだけど

X のセッションからログアウトすると systemd が死ぬ

[  106.077141] systemd[1]: segfault at 0 ip           (null) sp 00007ffd67a25ea8 error 14 in systemd[559755a77000+16e000]

systemd が死んでしまうといろいろと大変で, 様々なサービスや socket が切れてしまうのでだいたいなにもできない. shutdown もまともに走らない.

でも, SEGV したわりには pid 1 はまだ見えていて, 同じみの attempt to kill init も出ていない.

というわけで, systemd が SEGV したらどうなるのかの話になる:

static void install_crash_handler(void) {
        static const struct sigaction sa = {
                .sa_handler = crash,
                .sa_flags = SA_NODEFER, /* So that we can raise the signal again from the signal handler */
        };
        int r;

        /* We ignore the return value here, since, we don't mind if we
         * cannot set up a crash handler */
        r = sigaction_many(&sa, SIGNALS_CRASH_HANDLER, -1);
        if (r < 0)
                log_debug_errno(r, "I had trouble setting up the crash handler, ignoring: %m");
}

PID 1として起動された場合, systemd は install_crash_handler() で crash handler をいれる. crash 関数はこんな感じ:

noreturn static void crash(int sig) {
        struct sigaction sa;
        pid_t pid;
…
                pid = raw_clone(SIGCHLD, NULL);
                if (pid < 0)
                        log_emergency_errno(errno, "Caught <%s>, cannot fork for core dump: %m", signal_to_string(sig));
                else if (pid == 0) {
                        /* Enable default signal handler for core dump */

                        sa = (struct sigaction) {
                                .sa_handler = SIG_DFL,
                        };
                        (void) sigaction(sig, &sa, NULL);

                        /* Don't limit the coredump size */
                        (void) setrlimit(RLIMIT_CORE, &RLIMIT_MAKE_CONST(RLIM_INFINITY));

                        /* Just to be sure... */
                        (void) chdir("/");

                        /* Raise the signal again */
                        pid = raw_getpid();
                        (void) kill(pid, sig); /* raise() would kill the parent */

                        assert_not_reached("We shouldn't be here...");
                        _exit(EXIT_FAILURE);
                } else {
                        siginfo_t status;
                        int r;

                        /* Order things nicely. */
                        r = wait_for_terminate(pid, &status);
                        if (r < 0)
                                log_emergency_errno(r, "Caught <%s>, waitpid() failed: %m", signal_to_string(sig));
                        else if (status.si_code != CLD_DUMPED)
                                log_emergency("Caught <%s>, core dump failed (child "PID_FMT", code=%s, status=%i/%s).",
                                              signal_to_string(sig),
                                              pid, sigchld_code_to_string(status.si_code),
                                              status.si_status,
                                              strna(status.si_code == CLD_EXITED
                                                    ? exit_status_to_string(status.si_status, EXIT_STATUS_FULL)
                                                    : signal_to_string(status.si_status)));
                        else
                                log_emergency("Caught <%s>, dumped core as pid "PID_FMT".", signal_to_string(sig), pid);
                }

crash handler の中身として core dump まわりでは

  • fork する
  • 子プロセスは
    • rlimit で core dump サイズを無制限にする
    • cd / する
    • kill(getpid()) で自殺
  • 親プロセスは
    • 子プロセスが自殺するのを待って
    • core dump したよーとログに書く

ということになる.

なんとなくうまく動きそうだけれど, systemd には罠がある.

"/usr/lib/sysctl.d/50-coredump.conf" の設定によって systemctl の "kernel.core_pattern" が書きかえられ, コアダンプは "/usr/lib/systemd/systemd-coredump" にパイプされるようになる. このプログラムは "coredumpctl" コマンド管理下にコアを保存して, コアを圧縮してくれたり, スタックトレースをとってくれたりとなかなかべんりにコアを扱えるコマンドになっている.

           UID: 119 (sddm)
           GID: 984 (sddm)
        Signal: 11 (SEGV)
     Timestamp: Fri 2016-04-29 07:53:50 JST (15h ago)
  Command Line: /usr/bin/sddm-greeter --socket /tmp/sddm-:0-uKcNEp --theme /usr/share/sddm/themes/breeze
    Executable: /usr/bin/sddm-greeter
 Control Group: /user.slice/user-119.slice/session-c2.scope
          Unit: session-c2.scope
         Slice: user-119.slice
       Session: c2
     Owner UID: 119 (sddm)
       Boot ID: 9790002963754beba034ca2ef60a1ae9
    Machine ID: fa62c26247d1db1dc2be16db53adb1e7
      Hostname: ako
      Coredump: /var/lib/systemd/coredump/core.sddm-greeter.119.9790002963754beba034ca2ef60a1ae9.13748.1461884030000000000000.lz4
       Message: Process 13748 (sddm-greeter) of user 119 dumped core.

                Stack trace of thread 13748:
                #0  0x00007fc7199bb1b0 _ZN7QSGNode7setFlagENS_4FlagEb (libQt5Quick.so.5)
                #1  0x00007fc719a14a10 n/a (libQt5Quick.so.5)
                #2  0x00007fc719a150aa _ZN19QQuickWindowPrivate15updateDirtyNodeEP10QQuickItem (libQt5Quick.so.5)
                #3  0x00007fc719a15b7b _ZN19QQuickWindowPrivate16updateDirtyNodesEv (libQt5Quick.so.5)
                #4  0x00007fc719a16c00 _ZN19QQuickWindowPrivate14syncSceneGraphEv (libQt5Quick.so.5)
                #5  0x00007fc7199e4ff8 n/a (libQt5Quick.so.5)
                #6  0x00007fc7199e66f8 n/a (libQt5Quick.so.5)
                #7  0x00007fc718db4af5 _ZN7QWindow5eventEP6QEvent (libQt5Gui.so.5)
                #8  0x00007fc719a201f5 _ZN12QQuickWindow5eventEP6QEvent (libQt5Quick.so.5)
                #9  0x00007fc718a6cd5a n/a (libQt5Core.so.5)
                #10 0x00007fc718a6ce8a _ZN16QCoreApplication15notifyInternal2EP7QObjectP6QEvent (libQt5Core.so.5)
                #11 0x00007fc718daabcd _ZN22QGuiApplicationPrivate18processExposeEventEPN29QWindowSystemInterfacePrivate11ExposeEventE (libQt5Gui.so.5)
                #12 0x00007fc718dab80d _ZN22QGuiApplicationPrivate24processWindowSystemEventEPN29QWindowSystemInterfacePrivate17WindowSystemEventE (libQt5Gui.so.5)
                #13 0x00007fc718d89f0b _ZN22QWindowSystemInterface22sendWindowSystemEventsE6QFlagsIN10QEventLoop17ProcessEventsFlagEE (libQt5Gui.so.5)
                #14 0x00007fc70f1472c0 n/a (libQt5XcbQpa.so.5)
                #15 0x00007fc714b09dfd g_main_dispatch (libglib-2.0.so.0)
                #16 0x00007fc714b0a0e0 g_main_context_iterate (libglib-2.0.so.0)
                #17 0x00007fc714b0a18c g_main_context_iteration (libglib-2.0.so.0)
                #18 0x00007fc718abbeef _ZN20QEventDispatcherGlib13processEventsE6QFlagsIN10QEventLoop17ProcessEventsFlagEE (libQt5Core.so.5)
                #19 0x00007fc718a6bd1a _ZN10QEventLoop4execE6QFlagsINS_17ProcessEventsFlagEE (libQt5Core.so.5)
                #20 0x00007fc718a7383c _ZN16QCoreApplication4execEv (libQt5Core.so.5)
                #21 0x0000000000414b04 main (sddm-greeter)
                #22 0x00007fc717f107c0 __libc_start_main (libc.so.6)
                #23 0x0000000000414bc9 _start (sddm-greeter)

                Stack trace of thread 13750:
                #0  0x00007fc717fcf78d __poll (libc.so.6)
                #1  0x00007fc719e87ac2 _xcb_conn_wait (libxcb.so.1)

実は systemd-229 ではこの coredumpctl のコア保存部分が rework されている. 以前は "systemd-coredump" がコアの解析から保存までなにからなにまで自分一人でやっていたが, systemd-229 からは "systemd-coredump" は最小限のメタデータ収集などの仕事だけをやって, 得られたデータの保存は "systemd-coredump@.service"というサービスを随時立ち上げて行なうことになった.

これ自体はコアダンプの保存に nice とか IO優先度とかかけられるようになって嬉しい, ことなのだけれど, systemd の SEGV とあわさると事態が最悪になる. systemd が SEGV する -> systemd の crash handler が fork して自殺 -> カーネルにより systemd-coredump が呼びだされる -> systemd-coredump は PID 1につなぎにいこうとするが… -> つながらないのでコアは保存しません!!!!

っということになってしまうのだ. というわけでみんなも systemd が SEGV してるなー?おかしいなー?なんか変だなー?と思った時は "/proc/sys/kernel/core_pattern" をちゃんと書きかえようね, という話になりますね



それと Gentoo では "/usr/lib/sysctl.d/50-coredump.conf.disabled" となっていて, core_pattern の設定が systemd-coredump を指すようになっていない. (これは journal にコアを保存するのはよくないね, などという話があったため) その一方で systemd-229 は問答無用で PID 1 起動時に /proc/sys/kernel/core_pattern に "|/bin/false" を書くようになっているので, Gentoo のように無効化しているとコアが全く出なくなったね, ということになるので気をつけて.

じゃあ systemd-229 はなぜそんなことを…ということなのだけれど, これは systemd-coredump が rlimit 考慮してくれるからデフォルトで rlimit 無制限にしとこう -> でもこれだと systemd-coredump が core_pattern に設定されるまではやばいから "|/bin/false" 書いておこう, ということで一応まあ筋は通っている.




で, その SEGV の原因は? というのも気になることだろうし解説しておこう. cgroup1 ではゾンビ化したプロセスは即座に root group につなぎ変えられたが, cgroup2 では死亡のタイミングではまだ元のグループに残ることになっている. 一方で systemd は「グループが空っぽになったよ」というイベントを待ちうけていて空のグループの削除を行なってもいる. そうすると, ざっくりとは なんかのプロセスがゾンビ化 -> ゾンビのグループを取得・グループが空になったから削除 -> 存在しないグループのハンドラ(NULL)を呼びだすぞーとなって SEGV する. 以下の patch で直ってるよ

github.com

ということで, systemd に patch を当ててどんどん cgroup2 を楽しんでいこうな. systemd git HEAD だと昨日のぼくの patch ももう入っててもっとよさそうだぞ.