ねえママ, 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 ももう入っててもっとよさそうだぞ.

systemd-cgtop の SEGV を直そう

systemd-cgtop という systemd 環境で CGroup ごとの CPU, Memory, Block IO usage を top のように見ることができるコマンドがあります. こんな感じ:

Control Group                                                     Tasks   %CPU   Memory  Input/s Output/s
/                                                                     -  123.4     6.3G        -        -
/system.slice                                                       102   82.9     1.9G        -        -
/system.slice/run-ree3377384dda4871ad4e305532d702d8.scope            50   79.7     1.5G        -        -
/user.slice                                                         578   40.2     3.7G        -        -
/system.slice/sddm.service                                            3    2.5   286.5M        -        -
/system.slice/NetworkManager.service                                  7    0.5     9.3M        -        -
/system.slice/dbus.service                                            1    0.1    26.8M        -        -
/system.slice/avahi-daemon.service                                    2    0.0     1.1M        -        -
/init.scope                                                           1      -     4.2M        -        -
/system.slice/acpid.service                                           1      -   264.0K        -        -
/system.slice/boot-efi.mount                                          -      -    12.0K        -        -
/system.slice/chronyd.service                                         1      -   616.0K        -        -
/system.slice/cronie.service                                          1      -     3.6M        -        -
/system.slice/cups.service                                            1      -     7.8M        -        -
/system.slice/dev-hugepages.mount                                     -      -    16.0K        -        -
/system.slice/dev-mqueue.mount                                        -      -     1.3M        -        -
/system.slice/dev-sda6.swap                                           -      -    20.0K        -        -
/system.slice/distccd.service                                         8      -     1.2M        -        -
/system.slice/geoclue.service                                         4      -     7.8M        -        -
/system.slice/nullmailer.service                                      1      -     1.9M        -        -
/system.slice/polkit.service                                          5      -     4.6M        -        -
/system.slice/rtkit-daemon.service                                    3      -   420.0K        -        -
/system.slice/sshd.service                                            1      -     1.3M        -        -
/system.slice/sys-kernel-config.mount                                 -      -    16.0K        -        -
/system.slice/system-getty.slice                                      1      -   208.0K        -        -
/system.slice/system-getty.slice/getty@tty2.service                   1      -        -        -        -
/system.slice/system-systemd\x2dbacklight.slice                       -      -    16.0K        -        -
/system.slice/system-systemd\x2dcoredump.slice                        -      -   416.0K        -        -
/system.slice/system-systemd\x2dfsck.slice                            -      -    48.0K        -        -
/system.slice/systemd-journald.service                                1      -    12.8M        -        -
/system.slice/systemd-logind.service                                  1      -     1.0M        -        -
/system.slice/systemd-udevd.service                                   1      -     6.0M        -        -
/system.slice/udisks2.service                                         5      -     2.6M        -        -
/system.slice/upower.service                                          3      -     2.5M        -        -
/system.slice/wpa_supplicant.service                                  1      -     1.6M        -        -
/user.slice/user-1000.slice                                         574      -        -        -        -
/user.slice/user-1000.slice/session-1.scope                         572      -        -        -        -
/user.slice/user-1000.slice/user@1000.service                         2      -        -        -        -
/user.slice/user-119.slice                                            4      -        -        -        -
/user.slice/user-119.slice/session-c1.scope                           2      -        -        -        -
/user.slice/user-119.slice/user@119.service                           2      -        -        -        -

ところが, これがうちの環境で SEGV していたので追っかけた話です.

naota ~ # gdb systemd-cgtop
(gdb) r
Starting program: /usr/bin/systemd-cgtop
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
refresh_one (controller=<optimized out>, path=0x5555557ac240 "", a=0x5555557ac270, b=0x5555557ac2a0, iteration=0, depth=0, ret=0x0) at /var/tmp/portage/sys-apps/systemd-229/work/systemd-229/src/cgtop/cgtop.c:407
407                         child &&
(gdb) bt
#0  refresh_one (controller=<optimized out>, path=0x5555557ac240 "", a=0x5555557ac270, b=0x5555557ac2a0, iteration=0, depth=0, ret=0x0)
    at /var/tmp/portage/sys-apps/systemd-229/work/systemd-229/src/cgtop/cgtop.c:407
#1  0x000055555555a4d3 in refresh (iteration=<optimized out>, b=0x5555557ac2a0, a=0x5555557ac270, root=0x5555557ac240 "") at /var/tmp/portage/sys-apps/systemd-229/work/systemd-229/src/cgtop/cgtop.c:442
#2  main (argc=<optimized out>, argv=<optimized out>) at /var/tmp/portage/sys-apps/systemd-229/work/systemd-229/src/cgtop/cgtop.c:936
(gdb) p child
$1 = (Group *) 0x7fff00000020
(gdb) list
402                     if (r < 0)
403                             return r;
404
405                     if (arg_recursive &&
406                         IN_SET(arg_count, COUNT_ALL_PROCESSES, COUNT_USERSPACE_PROCESSES) &&
407                         child &&
408                         child->n_tasks_valid &&
409                         streq(controller, SYSTEMD_CGROUP_CONTROLLER)) {
410
411                             /* Recursively sum up processes */

gdb で動かしてみるとやっぱり死ぬので backtrace とソース見たところ. child あたりがあやしいので見ていく

(gdb) p child
$2 = (Group *) 0x7fff00000020
(gdb) p child->n_tasks_valid
Cannot access memory at address 0x7fff00000020

ということで, child に変な値入ってるっぽい. これどうせ初期化忘れだろうなという気持ちで見ていく.

ちょっと上を見ると

r = refresh_one(controller, p, a, b, iteration, depth + 1, &child);

child はここで設定されてそう. refresh_one() を見ていく

static int refresh_one(
                const char *controller,
                const char *path,
                Hashmap *a,
                Hashmap *b,
                unsigned iteration,
                unsigned depth,
                Group **ret) {

        _cleanup_closedir_ DIR *d = NULL;
        Group *ours;
        int r;

        assert(controller);
        assert(path);
        assert(a);

        if (depth > arg_depth)
                return 0;

        r = process(controller, path, a, b, iteration, &ours);
        if (r < 0)
                return r;
…
        if (ret)
                *ret = ours;

        return 1;
}

"*ret = ours" されてて, "process(controller, path, a, b, iteration, &ours);" されてるけど, ours が初期化されていない. ただ "if (r < 0)" でエラー処理はされてて, その場合 ret が更新されないのが気になる.

process() を見てみると, どうもエラーが起きているのに 0 を返しているパスがある. この場合, ours は変更されない.

        } else if (streq(controller, "blkio") && cg_unified() <= 0) {
                _cleanup_fclose_ FILE *f = NULL;
                _cleanup_free_ char *p = NULL;
                uint64_t wr = 0, rd = 0;
                nsec_t timestamp;

                r = cg_get_path(controller, path, "blkio.io_service_bytes", &p);
                if (r < 0)
                        return r;

                f = fopen(p, "re");
                if (!f) {
                        if (errno == ENOENT)
                                return 0;
                        return -errno;
                }

ここで2つの論点がある. 1つはこの 0 を返しているのはどういうパスか, もう1つは ours をどこで NULL にするのが適切であるか.

前記のコードは "blkio.io_service_bytes" というファイルを開いている. これは cgroup の blkio tree で cgroup ごとの IO 量の統計を取得するファイルである. このファイルはIOスケジューラが cfq でなければ存在しない. そのため cfq をカーネルに組み込んでいないシステムでは ours が変更されずに process() から返っていくということになる.

  1. process() の先頭で NULL にする
  2. process() の return 部分で NULL にする
  3. refresh_one() で NULL 初期化する

systemd ではエラーの場合, "call-by-reference variables" を書きかえるな, という Coding Style なので*1, 1番はよろしくない.

2番は変更が面倒だけど, 正しい動作をする. 3番はシンプルだし, ここではうまく動くのだが全体的にはよくないところがある.

そもそも process() は refresh_one() 以外からも呼ばれうる. そして呼び出し側は refresh_one() が 0 を返してきた時, 返ってきたものが NULL か, 正しい Group を指していることを期待している. そうすると, 3番の修正だけでは不十分だということが言える.




って, 最後のはこれを書いてて気がついたんだけど, やっぱり面倒だしもう3番で PR 投げてしまったし, とりあえずね, 放置.

github.com

Software Design 2016年05月号: 仮想マシン用の準仮想化ドライバデバイスドライバのフレームワークvirtioドライバのしくみ

先月紹介記事を書いてから、他の記事も書こうかなー、wineのlocale話しでも書こうかなーと思っていたんですが、そのうちに次の雑誌が来ました。間に合わなかったよ…

今月号で50回目です. virtioドライバがvirtio-pciでdetectされ、読みこまれ、ringで通信する様子などを書いています.

その他にも, 今月のSoftware DesignにはなんとSIMカードもついていたので、後ほどよく読んでSIM登録手続きしちゃおかなという感じです. 

 

ソフトウェアデザイン 2016年 05 月号 [雑誌]
 

 

そのうちwineのlocale話しとか, いまsystemdがSEGVしている話しも書きたい.

Software Design 2016年04月号: Linux 4.1から4.4までのeBPFに関する開発

気がつけば、Linuxカーネル観光ガイドも第49回ということで、5年目に突入していました。びっくりですね。びっくりしたわりにいまさらですが、紹介記事を書いておきます。

5年目最初の記事は最近適用範囲がどんどん広がるeBPFについて見ています。特に最近はclangでCコードからeBPFバイトコードコンパイルしてkernelに投入などできちゃうので楽しいですよ。perfコマンドなら *.cファイルを指定すると、裏でclangでビルドして、eventのフィルタに使ってくれてさらにお得。

 というわけで、よろしくお願いします。

 

Linux 4.3のIO/FSまわりcommit

今日読んだ分をざっくりとtweetから持ってきた

 

Btrfsは独自にbioをsubmitしているので、blkio controllerにacountingされていなかったのを直したような感じ。複数disk対応にはまだ作業が必要とか

transactionが別のtransactionの完了を待っていたが、正常終了かどうかをcheckしていなかった。そのためcommitされていないerror transactionに依存する形でtransactionがcommitされてたという問題のfix

Btrfsではdfで空きスペースがあるのにfileが作れない消せないということがあって問題とされていた. 原因の1つとして、空になったchunkを回収してない -> 新しいchunkをallocできないというのがあった. いつのまにか空のchunkを回収するコードが入っていたらしい。

これはそのまま。evict inodeに時間がかかるほど大量のinodeを溜め込めるメモリがあるマシンじゃないといけないけど、そういうこともあるのかーと面白かった。

syncで無駄にCPUを使うことがなくなってよかったというはなし。ついついsyncしちゃう人に朗報?(謎

複数fileに同時にIO operation(しかもdisk上では並んでいる & fsyncはしてない)というのは実際どういうオペレーションになるのか少し不明だけどSSDでもこれだけパフォーマンス改善するのは面白い

KTSan面白そうなので触ってみたい

log部分のchecksumとかsizeとか、logのcancelが正しく動くかなどlogまわりに多くのfixが入っている

ディレクトリエントリ置換前のfiletypeと置換後のfiletypeをうっかり同じ変数に保存していたため, filetypeがおかしくなってたよというバグ. これは怖い…

portage tree を squashfs で sync しよう

portage-2.2.19以降では Portage tree を Squash FSでsyncできるようになったらしい。

下記メールにある通り、 dev-util/squashmergeをインストールして repos.conf を設定する。 emerge --sync すると最初は Squash FSの snapshot が、最初のsync以後はいまの snapshot に対する差分がDLされて portage tree として見えるようになる。

archives.gentoo.org

Portage tree に SquashFSを使うのは、 tree のサイズを小さくするよく知られたトリックである。 たとえばbtrfs+lzoで504MBのtreeがSquashFS+lzoで96MBにまで小さくできる。

http://dev.gentoo.org/~mgorny/articles/using-deltas-to-speed-up-squashfs-ebuild-repository-updates.pdf

しかし、Squashを毎回やり直すのが面倒であった。 今回のportageの変更で透過的にSquashFS上のportage treeの更新ができるようになる。

 

いまの自分のシステムは Git での syncにしているが、どうせportage treeの中で上書きなんてしないし、 SquashDelta sync に切り替えていこう。特にあまり容量のないサーバでinodeとディスクの節約になって嬉しい。

make.profile はディレクトリでいいらしい

Gentoo は選択なので当然 systemd をサポートしている。 systemd を使うには USE フラグを設定しておいたり、あるいは systemd とではうまく動かないパッケージがあるので使わないように設定しなければならない。これを簡単に設定できるように systemd の profile が作られている。

これまでは systemd の profile はdefault/linux/amd64/13.0/desktop/gnome/systemd か
default/linux/amd64/13.0/desktop/kde/systemd のようにデスクトップ環境の下に作られていた。 最近になってデスクトップ環境以外の systemd profile (default/linux/amd64/13.0/systemd)ができた。

archives.gentoo.org

これを聞いて、よかったべんりーと思っていたのだけれど、実は"/etc/portage/make.profile" は「/usr/portage/profiles下へのsymlink」でなくていいということを知った。

archives.gentoo.org

このメールによると /etc/portage/make.profile のディレクトリを作って、その下の parent ファイルに使いたい profile へのパスを書けば複数のprofile を "mix-in" できるようだ。

そうすると、 desktop+systemd (kdeでもgnomeでもない)とかdesktop+kde+systemd+developer とか no-multilib+systemd とかmusl+systemd とかも設定できる、ということなのだろう。すごい