Linux起動時のカーネルパニック(at ring_buffer_set_clock())修正めも

機種依存ですが、fedoraで5.4系のカーネルで起動時にカーネルパニックするようになり、自分もこのバグに当たったので修正してパッチをlkmlに投稿しました。 このパッチはLinux5.5.に取り込まれました🎉

github.com

ついでなのでどんな感じでデバッグしたかの記録です。10日前くらいの話だし、デバッグ時の思考の時系列は多少違う気もしますがだいたいこんな感じということで。

環境

こんな感じです。

バグの内容

自分がこのバグに当たったときにはfedoraのbugzillaにすでにバグ登録されてました

bugzilla.redhat.com

5.4系のカーネルだと起動時にカーネルパニックする☠という内容です。実際こんな感じでした。

f:id:masami256:20200127191203j:plain
oops

ただ、自分が普段使ってるデスクトップPCでも同じカーネルを使ってますがこちらでは同様の現象は起きてませんでした。ということで機種依存っぽい感じがします。そして、bugzillaを見てると共通点が見えてきました。 それは次の二点です。

  • Ryzenが載ってるThinkPadを使っている
  • Secure Bootが有効

暫定対策

上に貼り付けたoopsはカーネルコマンドラインオプションにquietが付いているので表示されていないですが、quietを外すと↓のprintk()の部分が表示されました。

 /* sched_clock_stable() is determined in late_initcall */
    if (!trace_boot_clock && !sched_clock_stable()) {
        printk(KERN_WARNING
               "Unstable clock detected, switching default tracing clock to \"global\"\n"
               "If you want to keep using the local clock, then add:\n"
               "  \"trace_clock=local\"\n"
               "on the kernel command line\n");
        tracing_set_clock(&global_trace, "global");
    }

ってことで、これを試すと起動成功したのでbugzillaにtrace_clock=locakを付けたら大丈夫だったよとコメントしました。また、他の人はSecure bootを無効にすることで対応したりもしてました。

原因を探す

NULL pointer dereference の原因箇所

ring_buffer_set_clock()はこんな関数です。

void ring_buffer_set_clock(struct ring_buffer *buffer,
               u64 (*clock)(void))
{
    buffer->clock = clock;
}

どこでエラーになったか一目瞭然ですね。

この関数に至る流れはtracing_set_default_clock()から始まり、 tracing_set_clock() -> ring_buffer_set_clock()となります。

初期化されていない状態でring_buffer_set_clock()が呼ばれてる感じですね。

ちなみにここで変更しようとしているclockは/sys/kernel/debug/tracing/trace_clockとして見えます。

masami@moon:~$ sudo cat  /sys/kernel/debug/tracing/trace_clock
[local] global counter uptime perf mono mono_raw boot x86-tsc

Unstable clock detected?

5.3系では問題なかったので5.4系からのバグではあるのですが、まず以下の部分でUnstable clock detectedとなるのは5.3ではどうだったのか?というのを調べます。trace_boot_clockはtrace_clockオプションなので普段は設定してないので気にしません。

 if (!trace_boot_clock && !sched_clock_stable()) {

調べるといっても、5.3系カーネルの起動時にquietオプションを外すだけですが。そして結果はどうだったかと言うと5.3系でも5.4.7でも同じパスを通っていました。このPCはUnstable clock detectedと判断されるようですね。デスクトップPC(cpuはi7-9700K)のほうはこのパスは通ってませんでした。

upstraem / 他のディストリビューションの様子

とくに同じようなエラーが出てる感じはありませんでした。

ここまでのまとめ

以下の条件に当てはまる人がバグに当たっている感じです。

回避策はtrace_clock=localを設定するかSecure Bootを無効にする。

原因特定のためのデバッグ

Secure Boot環境で動くカーネルを作る方法を調べる

Secure Bootを無効にしちゃったら再現しないので自前でビルドしたカーネルへの署名方法を調べます。調べた結果は↓にまとめました。

kernhack.hatenablog.com

upstreamカーネルを試す

まずはバージョン同じものを使って、カーネルのコンフィグはfedoraの5.4.7のコンフィグを使います。ビルドしてこのカーネルから起動してみると無事に起動しました。ということで、次の手順に進みます。

fedora固有のパッチを調べる

調べると言うかパッチを一旦外せるだけ外してから足していくとかそんな感じですね。fedoraカーネルパッケージのソース一式はgitリポジトリにあるのでクローンしてきて、f31ブランチからブランチを切手作業していきます。この時点でf31ブランチは5.4.12カーネルになっていました。

src.fedoraproject.org

手順的にはspecファイルでパッチをコメントアウトして、fedpkgでsrpmを作ります。そして、必要最小限のパッケージだけ作りたかったので出来たsrpmをインストールしてrpmbuildでビルドするという方法を取りました。fedpkgで同じようなことができるんでしょうか? それはともかくrpmbuildはこんなオプションでやりました。

$ rpmbuild -bb --with baseonly --without debuginfo --target=$(uname -m) kernel.spec

arm64-Add-option-of-13-for-FORCE_MAX_ZONEORDER.patchのように外すと他に直さなきゃいけないところが出てくるパッチもあったりするのですが、まあこの辺は今回のバグに関係ないだろってことでこういうパッチは外さずに進めました。そして、efi-secureboot.patchを外せば問題ないというところまで判明しました。

efi-secureboot.patchの内容を調べる

efi-secureboot.patchLinux 5.4から入ったlockdownに関係するパッチです。

lockdownについてはこちらを参照してもらうとして。。

kernelnewbies.org

このパッチの内容は大きく分けると

最後のCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTの値に応じて処理してる部分は以下のifdefのところです。

diff --git a/arch/x86/kernel/setup.c b/arch/x86/kernel/setup.c
index 77ea96b794bd..a119e1bc9623 100644
--- a/arch/x86/kernel/setup.c
+++ b/arch/x86/kernel/setup.c
@@ -73,6 +73,7 @@
 #include <linux/jiffies.h>
 #include <linux/mem_encrypt.h>
 #include <linux/sizes.h>
+#include <linux/security.h>
 
 #include <linux/usb/xhci-dbgp.h>
 #include <video/edid.h>
@@ -1027,6 +1028,13 @@ void __init setup_arch(char **cmdline_p)
    if (efi_enabled(EFI_BOOT))
        efi_init();
 
+   efi_set_secure_boot(boot_params.secure_boot);
+
+#ifdef CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT
+   if (efi_enabled(EFI_SECURE_BOOT))
+       security_lock_kernel_down("EFI Secure Boot mode", LOCKDOWN_CONFIDENTIALITY_MAX);
+#endif
+
    dmi_setup();
 
    /*

上記のコードはカーネルuefiのsecure boot環境で起動されてたらlockdownしてます。

ここまでくるとCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTが疑わしい感じしてきます。そこでこのオプションは選択せずにカーネルをビルドします。オプションはkernel-x86_64-fedora.configにてこのように行われています。

CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT=y
# CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY is not set
# CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY is not set
CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE=y

オプションの設定はkernel-x86_64-fedora.configでCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTをコメントアウトしてsrpmを作ってからビルドしました。そしたら予想通り起動できたので、さらなるデバッグに入ります。

upstreamのソースを再確認

5.4.12のコードを見てたらこんなコードが入っていたんですね。これはregister_tracer()のコードです。

 if (security_locked_down(LOCKDOWN_TRACEFS)) {
        pr_warning("Can not register tracer %s due to lockdown\n",
               type->name);
        return -EPERM;
    }

他にも同じようなチェックがいくつかありました。それでlockdownされているとtracerの初期化しないんだなということかってわかり、初期化されていないのにtrace clockを変更しようとしたからヌルポになるということなんだなと理解しました。

upsteamで再現できるか確認

このときは5.4.7ではなくてfedoraカーネルパッケージのgitでは5.4.12だったのでstable treeの5.4.12を使いました。原因はほぼ掴めたというところなんですが、upstreamで再現するかどうか再度検証します。

まずlockdown機能に関するコンフィグを調べます。lockdownではCONFIG_SECURITY_LOCKDOWN_LSMが最重要の設定でY/Nの2択です。これでYを選べばlockdownの機能が使えます。Yを選択すると更にどのレベルでlockするかという選択があります。選択肢は3個でデフォルトはNoneです。fedoraカーネルもCONFIG_LOCK_DOWN_KERNEL_FORCE_NONEを選択していました。

  • CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE
  • CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY
  • CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY

fedoraの場合はCONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOTが4個目の選択肢ってところでしょうか。

オプションの内容ですがKconfigによるとCONFIG_LOCK_DOWN_KERNEL_FORCE_NONEは何もしません。CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITYは「Features that allow the kernel to be modified at runtime are disabled.」とあり、CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY「Features that allow the kernel to be modified at runtime or that permit userland code to read confidential material held inside the kernel are disabled」で一番制限が厳しくなっています。

最初にfedoraのコンフィグでmainlineカーネルをビルドしたときはCONFIG_LOCK_DOWN_KERNEL_FORCE_NONEが選択されてたので残りの2パターンを試したところCONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITYを設定したときに再現しましたキタ━━━━(゚∀゚)━━━━!!

修正する

lockdownが有効になっているときはtrace_clockを変更できないようにするのが良いだろうと思い、このようなパッチを書きました。

@@ -9420,6 +9420,11 @@ __init static int tracing_set_default_clock(void)
 {
        /* sched_clock_stable() is determined in late_initcall */
        if (!trace_boot_clock && !sched_clock_stable()) {
+               if (security_locked_down(LOCKDOWN_TRACEFS)) {
+                       pr_warn("Can not set tracing clock due to lockdown\n");
+                       return -EPERM;
+               }
+
                printk(KERN_WARNING
                       "Unstable clock detected, switching default tracing clock to \"global\"\n"
                       "If you want to keep using the local clock, then add:\n"

patchを送る

upstreamのカーネルでも再現できたのでupstreamにパッチを送ります。lkmlをみてたらtracing系はlkmlにccで良さげだったのでccにはlkml、toはscripts/get_maintainer.plで出てきたメンテナの人にしてgit format-patchでパッチを作りgit send-emailで送りました。

パッチを送ってしばらくしたらメンテナの人からmainlineへのpull rquestに自分のパッチが入っていたのでacceptされということがわかり、良かったって感じでしたね。5.5-rc8が出るのかと思ってたけどrc8は出ずに5.5が2020/01/27にでて、これに修正が含まれました。

lkml.org

mainlineに修正が取り込まれたのでそのうち5.4のstable treeにも修正が取り込まれるんじゃないかと思います。

まとめ

今回のバグは機種依存・カーネルの設定依存な面もありましたが、バグの原因はわかってしまえば簡単だったのと修正も単純な方法で済ますことができて良かったです😊 あとfedora向けの単純なsecure boot用の署名方法がわかったのは良かったかも。

secure bootが有効な環境で自前ビルドのカーネルに署名する方法(fedora向け)

fedoraなら多分一番手軽だと思う署名方法です。他のディストリビューションはわかりません😭

テスト環境

ディストリビューションは(もちろん) fedoraです。バージョンは31です。実機ではなくてqemuでテストしました。

準備

まずpesignとnss-toolsパッケージをインストールします。pesignパッケージをインストールするとpesignコマンドなどのバイナリの他に/etc/pki/pesign/と/etc/pki/pesign-rh-testにデータベースがインストールされます。今回は/etc/pki/pesign-rh-testにあるものを使います。

署名前の準備

まずcertificateファイルを取り出します。

$ sudo certutil -d /etc/pki/pesign-rh-test -L -n "Red Hat Test CA" -r > rhca.cer

次に取り出したcertファイルを登録します。このときにパスワードを聞かれるのでお好きなパスワードを入れてください。

$ sudo mokutil --import ./rhca.cer
input password: 
input password again:

インポートに成功すると次のように確認できます。

$ sudo mokutil --list-new | head -n 10
[key 1]
SHA1 Fingerprint: 65:37:ee:ed:f1:ea:fe:d0:a4:cd:d8:91:eb:f2:2b:45:45:41:cd:44
Certificate:
    Data:

そうしたらここで一旦rebootします。

enroll

再起動するとgrubなどは起動せずにuefiのアプリが起動します。

f:id:masami256:20200112150853p:plain

なにか適当にキーを押すとメニューが出るのでEnroll MOKを選択してエンターキーを押します。

f:id:masami256:20200112150941p:plain

View Key 0を選択してエンターキーを押すと鍵の内容が確認できます。

f:id:masami256:20200112151052p:plain

Continueを選ぶと鍵を登録するか聞かれるのでYesを選択してエンターキーを押しましょう。

f:id:masami256:20200112151206p:plain

パスワードを聞かれるので先程入力したパスワードを打ち込みます。

f:id:masami256:20200112151303p:plain

パスワードが正しければメニュー画面に戻ります。Rebootを選択すればシステムが再起動して通常のブートフローになります。

f:id:masami256:20200112151353p:plain

カーネルへの署名

参考文献の手順だとpesignのオプションでエラーになったんですが、fedoraカーネルパッケージをビルドしたときのログを見てたらコマンドラインオプションがわかりました。

↓がログです。

+ '[' -f arch/x86_64/boot/zImage.stub ']'
+ _pesign_nssdir=/etc/pki/pesign
+ '[' 'Red Hat Test Certificate' = 'Red Hat Test Certificate' ']'
+ _pesign_nssdir=/etc/pki/pesign-rh-test
+ '[' -x /usr/bin/pesign ']'
+ '[' x86_64 == x86_64 -o x86_64 == aarch64 ']'
+ '[' 0 -ge 7 -a -f /usr/bin/rpm-sign ']'
++ id -un
++ uname -m
+ '[' '%{vendor}' == 'Fedora Project' -a mockbuild == mockbuild -a x86_64 == x86_64 ']'
+ '[' -S /var/run/pesign/socket ']'
+ /usr/bin/pesign -c 'Red Hat Test Certificate' --certdir /etc/pki/pesign-rh-test -i arch/x86/boot/bzImage -o vmlinuz.signed -s
+ '[' '!' -s -o vmlinuz.signed ']'
+ '[' '!' -s vmlinuz.signed ']'

コマンドはこんな感じになります。-iオプションにbzImageのパスを渡してください。

$ pesign -c 'Red Hat Test Certificate' --certdir /etc/pki/pesign-rh-test -i ./bzImage -o vmlinuz.signed -s

自分の場合はカーネルのmake installまで済ませていて、カーネルはvmlinuz-5.4.8-testとして置いておいたのでvminuz.signedを/boot/vmlinuz-5.4.8-testとしてコピーして完了でした。 あとは再起動してこのカーネルで起動します。

自前のカーネルでブート

起動してカーネルのバージョン、secure bootが有効なことを確認してすべてOKって感じです🎉

$ uname -a
Linux secureboot-test 5.4.8-test #1 SMP Thu Jan 9 00:04:00 JST 2020 x86_64 x86_64 x86_64 GNU/Linux
$ dmesg | grep Secure
[    0.008666] Secure boot enabled

鍵を消したら

deleteオプションで鍵を消せます。再起動すると登録したときと同じようにShimのアプリが起動するのでそこで鍵を削除できます。

$ sudo mokutil --delete ./rhca.cer 
input password: 
input password again:

鍵を消すとその鍵で署名したカーネルでは起動できなくなります。

f:id:masami256:20200112152306p:plain

カーネルモジュール

自作モジュールもロードできます。

$ lsmod | grep test_mod
test_mod               16384  0

dmesgでも

[  104.658715] test_mod: loading out-of-tree module taints kernel.
[  104.658741] test_mod: module verification failed: signature and/or required key missing - tainting kernel
[  104.658958] test_mod: install test_mod : insmod

参考文献

次のブログを参考にしました。

Booting custom kernels in F18 with Secure Boot - pointless pontifications of a back seat driver — LiveJournal

killコマンドのあれこれ

(´-`).。oO(killコマンドを提供するプロジェクト多いなって

killコマンドを提供するプロジェクト

とりあえず気付いた範囲で4つ。

上のリストはmanページへのリンクになっているのでオプションの違いとかはそちらを参照してください。busyboxのkillは最小構成って感じなのでオプションは1つしかないですね。-sオプションはbusybox以外は実装してますね。で、-lオプションは全てに存在。まあ、これはシグナル名を表示するので無いと困りますね。

ディストリビューションのkillコマンド

じゃあ、ディストリビューションはどのkillコマンドを採用してるのか?ということで手元にあるディストリビューションを確認したところこんなふうになってました。

ディストリビューション killコマンドを提供するパッケージ
fedora 31 util-linux
centos 8 util-linux
arch linux(docker image) util-linux
ubuntu 18.04 procps-ng
alpine linux(docker image) busybox

killコマンドの実装

ソースを見てみます。調べるときに実際に動作させてなくてコードを読んだだけなのでもしかしたら間違ってる可能性もありますが。

util-linux

kill_verbose()がkill(2)でsignalをプロセスに投げているところのようです。

static int kill_verbose(const struct kill_control *ctl)
{
    int rc = 0;

    if (ctl->verbose)
        printf(_("sending signal %d to pid %d\n"), ctl->numsig, ctl->pid);
    if (ctl->do_pid) {
        printf("%ld\n", (long) ctl->pid);
        return 0;
    }
#ifdef HAVE_SIGQUEUE
    if (ctl->use_sigval)
        rc = sigqueue(ctl->pid, ctl->numsig, ctl->sigdata);
    else
#endif
        rc = kill(ctl->pid, ctl->numsig);

    if (rc < 0)
        warn(_("sending signal to %s failed"), ctl->arg);
    return rc;
}

if文でctl->do_pidをチェックしているところは-pオプションの処理です。util-linuxのkillはsigqueue(3)が利用できる場合はkill(2)ではなくてsigqueue(3)を使うんですね。sigqueue(3)の場合はデータを付加できるのでできるならこっちを使おうってことですね。これはmanにも書かれていますが-qオプションを使うとプロセスに送るデータを設定できます。

procps-ng

procps-ngの場合はkill_main()がkillを実現する関数っぽいです。 この関数の主要なところはここです。

 for (i = 0; i < argc; i++) {
        pid = strtol_or_err(argv[i], _("failed to parse argument"));
        if (!kill((pid_t) pid, signo))
            continue;
        error(0, errno, "(%ld)", pid);
        exitvalue = EXIT_FAILURE;
        continue;
    }

こちらはこれといって特殊なことはしてないですね。

coreutils

coreutisの場合はsend_signals()という関数がkill(2)を呼んでいます。

static int
send_signals (int signum, char *const *argv)
{
  int status = EXIT_SUCCESS;
  char const *arg = *argv;

  do
    {
      char *endp;
      intmax_t n = (errno = 0, strtoimax (arg, &endp, 10));
      pid_t pid = n;

      if (errno == ERANGE || pid != n || arg == endp || *endp)
        {
          error (0, 0, _("%s: invalid process id"), quote (arg));
          status = EXIT_FAILURE;
        }
      else if (kill (pid, signum) != 0)
        {
          error (0, errno, "%s", quote (arg));
          status = EXIT_FAILURE;
        }
    }
  while ((arg = *++argv));

  return status;
}

これも短い関数ですね。こちらもprocps-ng同様にkill(2)を呼ぶだけです。

busybox

busyboxの場合はkill_main()という関数でkillの処理を行っています。余談ですがbusyboxのkill.cはprocps/にあるし、procps-ngと同じ感じにしてるんですかね?

#if ENABLE_KILL
    /* Looks like they want to do a kill. Do that */
    while (arg) {
# if SH_KILL
        /*
        * We need to support shell's "hack formats" of
        * " -PRGP_ID" (yes, with a leading space)
        * and " PID1 PID2 PID3" (with degenerate case "")
        */
        while (*arg != '\0') {
            char *end;
            if (*arg == ' ')
                arg++;
            pid = bb_strtoi(arg, &end, 10);
            if (errno && (errno != EINVAL || *end != ' ')) {
                bb_error_msg("invalid number '%s'", arg);
                errors++;
                break;
            }
            if (kill(pid, signo) != 0) {
                bb_perror_msg("can't kill pid %d", (int)pid);
                errors++;
            }
            arg = end; /* can only point to ' ' or '\0' now */
        }
# else /* ENABLE_KILL but !SH_KILL */
        pid = bb_strtoi(arg, NULL, 10);
        if (errno) {
            bb_error_msg("invalid number '%s'", arg);
            errors++;
        } else if (kill(pid, signo) != 0) {
            bb_perror_msg("can't kill pid %d", (int)pid);
            errors++;
        }
# endif
        arg = *++argv;
    }
    return errors;
#endif

コードにはSH_KILLは通常定義されてるのかどうかわかりませんが、定義の有無にかかわらずkill(2)を使うだけですね。

実装の差異

util-linuxはsigqueue(3)が利用可能ならそれを使い、利用できなければkill(2)を呼ぶという実装になっていますが、それ以外はkill(2)を呼ぶ形でした。

まとめ

Linuxでは基本的とも言えるkillコマンドですが、このコマンドを提供するパッケージは複数あり、ディストリビューションによってどのパッケージのkillコマンドを使うか選択していました。 また、実装でも微妙な違いがあるということがわかりました( ´ー`)フゥー...

unameコマンドから始めるデバッグ&カーネルハック入門

この記事はLinux Advent Calendar 2019の1日目の記事です。

はじめに

本記事ではLinuxサーバのホスト名、Linuxカーネルのバージョン、cpuアーキテクチャなどのシステム情報を表示するuname(1)を利用してLinux環境でのデバッグカーネルハックについて説明していきます。本記事ではコマンドやツールの使い方の説明ではなくて、それらを使ってどのようにデバッグするのかというところを説明します。

環境

ディストリビューションにはFedora 31(x86_64)を利用します。動作環境はQEMUlibvirtOracle VM VirtualBoxなどの仮想環境でも良いですし、物理マシンにインストールしても構いませんが仮想環境を利用したほうが手軽に環境構築できるのでおすすめです。ユーザーはsudoを使えるようにしておいてください。インストール時もしくはインストール後に以下のグループをインストールすると後で楽かもしれません。

  • C Development Tools and Libraries
  • Development Tools
  • RPM Development Tools
  • System tools
  • Guest Agents(仮想環境にインストールする場合)

パッケージグループのインストールは以下のコマンドで行えます。

[masami@unamebook ~]$ sudo dnf group install -y "C Development Tools and Libraries" "Development Tools" "RPM Development Tools" "System tools" "Guest Agents"

ファイルの編集をするのにエディタが必要です。これは好きなエディタを使ってください。

おことわり

本記事で行うツールの利用方法などはディストリビューションに依存しないことが多いと思いますが、コマンドの出力結果やソースコードについてはディストリビューション固有のpatchが当たっている場合もあり、fedora 31以外のディストリビューション(場合によってはfedora 31でもパッケージのバージョン違い)と違うことがあります。

uname(1)

uname(1)*1を実行したことのある方は多いと思いますが、まずは普通にuname(1)を実行しましょう。このコマンドはcoreutilsパッケージに含まれています。

[masami@unamebook ~]$ uname -a
Linux unamebook 5.3.11-300.fc31.x86_64 #1 SMP Tue Nov 12 19:08:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

unameコマンドに渡した-aオプションはすべての情報を表示させるオプションです。上記の出力結果を分解したものが以下の「表1 unameの出力内容」です。

出力内容 該当するunameコマンドのオプション 意味
Linux -s カーネル
unamebook -n ホスト名
5.3.11-300.fc31.x86_64 -r カーネルリリース
#1 SMP Tue Nov 12 19:08:07 UTC 2019 -v カーネルバージョン*2
x86_64 -m マシンのハードウェア名
x86_64 -p プロセッサ名
x86_64 -i ハードウェアプラットフォーム
GNU/Linux -o オペレーティングシステム

表1 unameの出力内容

uname(1)が出力する内容

uname(1)を実行してシステム情報を見ることができましたがこの情報はどこから取得したのでしょうか?この情報はLinuxカーネルが持っていて、uname(1)はシステムコールを通じてカーネルに問い合わせを行い、カーネルから受け取った結果を表示しています。このときに利用するシステムコールuname(2)*3です。uname(2)のプロトタイプは次のようになっています。カーネルは引数で渡されたbuf変数にシステム情報をセットします。

int uname(struct utsname *buf)

uname(2)のインターフェース

uname(2)の詳細はmanページで確認してください。

[masami@unamebook ~]$ sudo dnf install -y man-pages

strace(1)

strace(1)は、uname(1)の実行時にuname(2)が呼ばれていることを確認しましょう。ここではデバッガは使わずにstrace(1)を利用します。strace(1)は簡単に言うと指定したコマンド・プロセスによるシステムコール呼び出しをトレースするコマンドです。まずはstraceパッケージをインストールします。

[masami@unamebook ~]$ sudo dnf install -y strace

straceパッケージをインストールしたら実行してみましょう。オプションの細かい内容は説明しませんが、ここではuname(2)に絞って表示させるように実行しました。

[masami@unamebook ~]$ strace -v -s 1024 -e trace=uname -C  uname -a
uname({sysname="Linux", nodename="unamebook", release="5.3.11-300.fc31.x86_64", version="#1 SMP Tue Nov 12 19:08:07 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="unamebook", release="5.3.11-300.fc31.x86_64", version="#1 SMP Tue Nov 12 19:08:07 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="unamebook", release="5.3.11-300.fc31.x86_64", version="#1 SMP Tue Nov 12 19:08:07 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
Linux unamebook 5.3.11-300.fc31.x86_64 #1 SMP Tue Nov 12 19:08:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
+++ exited with 0 +++
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000007           2         3           uname
------ ----------- ----------- --------- --------- ----------------
100.00    0.000007                     3           total

uname(2)の呼び出しが3回ありますね。なぜ3回uname(2)を実行しているのでしょうか?これはソースコードを確認しないとわかりません。unameコマンドを提供しているのはcoreutilsパッケージなのでcoreutilsパッケージのソースコードをダウンロードしましょう。利用しているのはfedoraにインストールされているcoreutilsパッケージなのでupstreamではなくfedoraソースコードを取得します。

[masami@unamebook ~]$ dnf download --source coreutils

ソースパッケージ(.src.rpm)をダウンロードしたらインストールします。

まず展開先のディレクトリをセットアップします。

[masami@unamebook ~]$ rpmdev-setuptree

そしてrpmコマンドでインストールします。

[masami@unamebook ~]$ rpm -i coreutils-8.31-6.fc31.src.rpm

インストール時に次のようなwarningが出ますが無視して大丈夫です。

warning: user mockbuild does not exist - using root
warning: user mockbuild does not exist - using root

これで~/rpmbuildにソースパッケージがインストールされましたが、ソースコードはまだ展開されていないでソースコードの展開&patchを当てましょう。と、その前にいくつか必要なパッケージをインストールします。

[masami@unamebook ~]$ sudo dnf builddep -y coreutils

これで大丈夫です。ではソースコードの準備をしましょう。

[masami@unamebook ~]$ cd rpmbuild/SPECS/
[masami@unamebook SPECS]$ rpmbuild -bp coreutils.spec

ソースコードを展開しpatchが適用されたソースコードは~/rpmbuild/BUILDにあります。

[masami@unamebook SPECS]$ cd ~/rpmbuild/BUILD/coreutils-8.31/
[masami@unamebook coreutils-8.31]$ ls
ABOUT-NLS   autom4te.cache  build-aux  configure     DIR_COLORS               dist-check.mk  GNUmakefile  lib       Makefile.am  NEWS    src     thanks-gen             THANKStt.in
aclocal.m4  bootstrap       cfg.mk     configure.ac  DIR_COLORS.256color      doc            init.cfg     m4        Makefile.in  po      tests   THANKS.in              TODO
AUTHORS     bootstrap.conf  ChangeLog  COPYING       DIR_COLORS.lightbgcolor  gnulib-tests   INSTALL      maint.mk  man          README  THANKS  THANKS-to-translators

uname(1)のソースコードはsrc/uname.cです。grepuname(2)の呼び出し箇所を調べると3箇所あることがわかります。

[masami@unamebook coreutils-8.31]$ grep -n "uname *(" src/uname.c
286:      if (uname (&name) == -1)
313:    uname(&u);
364:    uname(&u);

286行目付近を見るとこのようになっています。toprint変数はuname(1)の引数に応じてビットが立ちます。オプションに-aを渡した場合はこのif文は真になるので286行目のuname(2)が実行されます。

    280   if (toprint
    281        & (PRINT_KERNEL_NAME | PRINT_NODENAME | PRINT_KERNEL_RELEASE
    282           | PRINT_KERNEL_VERSION | PRINT_MACHINE))
    283     {
    284       struct utsname name;
    285 
    286       if (uname (&name) == -1)
    287         die (EXIT_FAILURE, errno, _("cannot get system name"));

313行目はどうかというと、ここは-pオプションの処理部分です。マクロのelseブロックが実行されるようです。

    304 #if HAVE_SYSINFO && defined SI_ARCHITECTURE
    305       {
    306         static char processor[257];
    307         if (0 <= sysinfo (SI_ARCHITECTURE, processor, sizeof processor))
    308           element = processor;
    309       }
    310 #else
    311       {
    312         static struct utsname u;
    313         uname(&u);
    314         element = u.machine;
    315       }

残りの364行目も確認しましょう。こちらは-iオプションのハードウェアプラットフォーム取得部分です。こちらもマクロのelseブロックが呼ばれているようです。

    354 #if HAVE_SYSINFO && defined SI_PLATFORM
    355       {
    356         static char hardware_platform[257];
    357         if (0 <= sysinfo (SI_PLATFORM,
    358                           hardware_platform, sizeof hardware_platform))
    359           element = hardware_platform;
    360       }
    361 #else
    362       {
    363         static struct utsname u;
    364         uname(&u);
    365         element = u.machine;
    366         if(strlen(element)==4 && element[0]=='i' && element[2]=='8' && element[3]=='6')
    367                 element[1]='3';
    368       }
    369 #endif

この3箇所のuname(2)の呼び出しですが、286行目はupstreamのコードにもあります。しかし、残りの2箇所はupstreamのコードには存在しません。この2箇所はfedoracoreutilsパッケージが独自当てているpatchです。このpatchは~/rpmbuild/SOURCES/coreutils-8.2-uname-processortype.patchです。興味のある方は確認してみてください。

gdb(1)

ソースコードを読んでuname(2)の呼び出し箇所が3箇所あることはわかったのですが、一応実際の動作を見てみましょう。ここではgdbを利用します。gdbを使ってデバッグをする場合はデバッグ情報があると便利です。通常のパッケージに含まれるバイナリファイルはデバッグ情報が存在しないため、デバッグが不便です。fedoraの場合はデバッグ情報はdebuginfoパッケージとして存在しているのでこれをインストールします。glibcのdebuginfoパッケージも合わせてインストールします。

と、その前にdebuginfo-installコマンドが必要なのでパッケージをインストールしましょう。

[masami@unamebook ~]$ sudo dnf install -y dnf-utils

dnf-utilsパッケージをインストールしたらdebuginfo-installコマンドが利用できますので、これでdebuginfoのパッケージをインストールします。

[masami@unamebook ~]$ sudo debuginfo-install -y coreutils glibc

debuginfoパッケージをインストールしたらgdbを起動しましょう。gdbは自動的にデバッグ情報を読み込んでくれます。

[masami@unamebook ~]$ gdb -q /usr/bin/uname
Reading symbols from /usr/bin/uname...
Reading symbols from /usr/lib/debug/usr/bin/uname-8.31-6.fc31.x86_64.debug...
(gdb) 

gdbが立ち上がったらとりあえず280行目のif文あたりにブレークポイントを張りましょう。listコマンドで280行目付近を表示します。

(gdb) list 280
275       toprint = decode_switches (argc, argv);
276
277       if (toprint == 0)
278         toprint = PRINT_KERNEL_NAME;
279
280       if (toprint
281            & (PRINT_KERNEL_NAME | PRINT_NODENAME | PRINT_KERNEL_RELEASE
282               | PRINT_KERNEL_VERSION | PRINT_MACHINE))
283         {
284           struct utsname name; 

breakコマンド(略性はb)で280行目にブレークポイントをセットします。

(gdb) b 280
Breakpoint 1 at 0x27b4: /usr/src/debug/coreutils-8.31-6.fc31.x86_64/separate/../src/uname.c:280. (2 locations)

runコマンド(略称はr)でunameコマンドを実行します。uname(1)のオプションには-aを渡しています。unameコマンドを実行すると先程設定したブレークポイントで止まるので、あとはnでステップ実行していけばuname(2)の呼び出しが3回行われるのがわかります。

(gdb) r -a
Starting program: /usr/bin/uname -a

Breakpoint 1, main (argc=2, argv=0x7fffffffe448) at ../src/uname.c:280
280       if (toprint
(gdb) n
286           if (uname (&name) == -1)
(gdb)  n
~略~
301       if (toprint & PRINT_PROCESSOR)
(gdb) 
313             uname(&u);
(gdb) n
~略~
(gdb) n
364             uname(&u);
(gdb) c
Continuing.
Linux unamebook 5.3.11-300.fc31.x86_64 #1 SMP Tue Nov 12 19:08:07 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
[Inferior 1 (process 6522) exited normally]

今回はプログラムが単純で短いのでステップ実行で済ませています。 ここまででstrace(1)、ソースコードリーディング、gdb(1)を使いuname -aの実行時にuname(2)が3回呼ばれる謎を調べることに成功しました。

bpftrace

bpftraceはLinuxのeBPFを使用したトレースツールです。eBFPとはextended Berkeley Packet Filterの略で元来はパケットフィルタの機能ですがLinuxではこのパケットフィルタ機能が進化し、パケットフィルタだけでは収まらなくなった機能です。Linuxではbpf(2)があり、c言語でeBPFの機能を使ったプログラムを作成できますが、これは大変なので普通はBCCを使うことが多いのではないでしょうか。BCCはBPF Compiler Collectionの略でPythonc言語を使ってeBFPを利用するプログラムを作ることが出来ます。そしてbpftraceですがBCCをバックエンドとして使った高レベルのトレース用言語と説明されています。bpftraceではカーネルレベルのトレースを取ることが出来ます。Fedoraにはパッケージがあるのでインストールしましょう。

[masami@unamebook ~]$ sudo dnf install -y bpftrace

インストールが完了したらuname(2)のトレースを行ってみましょう。まずはシステムコールの呼び出しをトレースします。 この他に、カーネルのソースも読みたいのでこちらもインストールしましょう。今回はdebuginfo-installコマンドを使用してデバッグ情報とソースをインストールします。coreutilsのときは単にソースを取得したいだけだったのでsrpmパッケージをインストールしましたが、debuginfoパッケージの場合はデバッグ情報とソースコードをインストールできます。gdb(1)の章でcoreutilsglibcのdebuginfoパッケージをインストールしましたが、このときにソースコードもインストールされいます。じゃあ、この2つの使い分けは?という疑問があると思いますが、ソースを変更して再ビルドする用途にはsrpmパッケージを、デバッグ情報のみが必要な場合はdebuginfoパッケージを使用すれば良いと思います。では、カーネルのdebuginfoパッケージをインストールします。

[masami@unamebook ~]$ sudo debuginfo-install -y kernel-debuginfo-common-x86_64

debuginfoパッケージに含まれるソースコードは/usr/src/debug以下にインストールされます。

[masami@unamebook ~]$ ls /usr/src/debug/
coreutils-8.31-6.fc31.x86_64  glibc-2.30-13-g919af705ee  kernel-5.3.fc31

インストールできたらカーネル側のuname(2)を処理する関数を探します。システムコールはinclude/linux/syscalls.hで探すことが出来ます。カーネルソースコードは以下の場所にあります。

[masami@unamebook ~]$ cd /usr/src/debug/kernel-5.3.fc31/linux-5.3.11-300.fc31.x86_64/

まずはunamegrepをかけましょう。

[masami@unamebook linux-5.3.11-300.fc31.x86_64]$ grep uname include/linux/syscalls.h
asmlinkage long sys_newuname(struct new_utsname __user *name);
asmlinkage long sys_memfd_create(const char __user *uname_ptr, unsigned int flags);
asmlinkage long sys_uname(struct old_utsname __user *);
asmlinkage long sys_olduname(struct oldold_utsname __user *);

いくつか見つかりますね。sys_のprefixはシステムコールを表しています。名前からsys_newuname、sys_unameのどちらかだろうと想像できますね。ファイルを実際に読んで確認しましょう。まずはそのままな名前のsys_unameを見てみます。

/* obsolete: kernel/sys.c */
asmlinkage long sys_gethostname(char __user *name, int len);
asmlinkage long sys_uname(struct old_utsname __user *);
asmlinkage long sys_olduname(struct oldold_utsname __user *);

obsoleteってコメントがありますね。こちらではなさそうです。ではsys_newunameはどうでしょうか。こちらは特にコメントもありません。uname(2)の実行時はsys_newuname()が実行されるようですね。

asmlinkage long sys_setpriority(int which, int who, int niceval);
~略~
asmlinkage long sys_newuname(struct new_utsname __user *name);

コメントにkernel/sys.cとあるので実装はこのファイルにあるようです。では、このファイルも見てみましょう。SYSCALL_DEFINE1というのはシステムコールの関数に使われるマクロです。SYSCALL_DEFINE1の1は引数を一つ受け取るという意味です。実装は比較的単純ですね。

SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
{
        struct new_utsname tmp;

        down_read(&uts_sem);
        memcpy(&tmp, utsname(), sizeof(tmp));
        up_read(&uts_sem);
        if (copy_to_user(name, &tmp, sizeof(tmp)))
                return -EFAULT;

        if (override_release(name->release, sizeof(name->release)))
                return -EFAULT;
        if (override_architecture(name))
                return -EFAULT;
        return 0;
}

ここまででユーザー空間でuname(2)を実行するとカーネル空間のsys_newuname()が実行されることがわかりました。前置きが長くなりましたが実際にbfptrace(1)を使って見ましょう。まずは単純にsys_newutsname()の実行をトレースします。この関数が実行されたときに呼び出し元のコマンド名を表示させるのが以下のコマンドラインです。

[masami@unamebook ~]$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_newuname { printf("%s\n", comm); }'

コマンドを入力してちょっとすると次のようなメッセージが表示されます。終了したい場合はCtrl-Cで終了できます。

Attaching 1 probe...

メッセージが表示されたら別の端末からuname -aを実行すると以下のようにunameが3回表示されます。uname -aを実行するとuname(2)が3回実行されるというのはstrace(1)の章で調べましたね。

[masami@unamebook ~]$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_newuname { printf("%s\n", comm); }'
Attaching 1 probe...
uname
uname
uname
^C

systemtap

systemtapはbpftrace同様にカーネルのトレーシングを行うことができるツールです。systemtapを応用するとトレース以上のこともできます。まずはインストールしましょう。

[masami@unamebook ~]$ sudo dnf install -y systemtap

systemtapはstap(1)がコマンドラインのプログラムとなります。systemtapは専用のスクリプト言語があり、c言語のコードを埋め込むこともできます。本章ではsystemtapを使ってsys_newutsname()の挙動を変更してみましょう。uname(1)で表示するノード名などはutsname()から取得しています。utsname()の戻り値をtmp変数に一旦保存し、それをcopy_to_user()でユーザー空間のプログラムから渡されたnameにコピーします。

SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
{
        struct new_utsname tmp;

        down_read(&uts_sem);
        memcpy(&tmp, utsname(), sizeof(tmp));
        up_read(&uts_sem);
        if (copy_to_user(name, &tmp, sizeof(tmp)))
                return -EFAULT;

        if (override_release(name->release, sizeof(name->release)))
                return -EFAULT;
        if (override_architecture(name))
                return -EFAULT;
        return 0;
}

では、systemtapを使ってプログラムにnodenameを書き換えてみましょう。systemtapを利用したライブパッチ機能の実装という感じです。systemtapのプログラムは次のようになります。

#!/usr/bin/env stap

%{
#include <linux/string.h>
#include <linux/uaccess.h>
#include <uapi/linux/utsname.h>
%}

function set_dummy_uname:long(name:long)
%{
        struct new_utsname u = { 0 };
           
        if (copy_from_user(&u, (struct new_utsname *) STAP_ARG_name, sizeof(u)))
        STAP_RETURN(-EFAULT);
    
        strcpy(u.nodename, "livepatched");

        if (copy_to_user((struct new_utsname *) STAP_ARG_name, &u, sizeof(u)))
                STAP_RETURN(-EFAULT);
    
        STAP_RETURN(0);

%}

probe begin {
        printf("start livepatch code. Ctrl-C to stop\n")
}

probe kernel.function("__do_sys_newuname").return {
    // Only set dummy nodename to test program
    if (execname() == "uname-test") {
        // Use @entry() to get name parameter in return probe.
        set_dummy_uname(@entry($name))
    }
}

probe end {
        printf("Done\n")
}

このプログラムではシステムコールのreturn時に処理を入れています。まずシステムコールを呼び出したプログラム名を調べ、テストプログラムでなければ特に変更は行わないで終了します。nodenameを設定しているのはset_dummy_uname()で呼び出し時にシステムコールのパラメータとして渡されたname変数をset_dummy_uname()の引数として渡しています。システムコールを呼び出したのがテストプログラムならnodenameをlivepatchedに変更します。systamtapスクリプトの特徴として、自分で実装した関数の引数にアクセスする場合はSTAP_ARG_変数名でアクセスします。このスクリプトではnameという変数名で引数を受け取っているためSTAP_ARG_nameと書いています。

ここで使用するテストプログラムですが以下のような簡単なものです。

#include <stdio.h>
#include <sys/utsname.h>

int main(int argc, char **argv)
{
    struct utsname name = { 0 };
    
    if (uname(&name)) {
        perror("uname");
        return -1;
    }

    printf("nodename: %s\n", name.nodename);
    return 0;
}

このcコードをコンパイルしましょう。

[masami@unamebook ~]$ gcc uname-test.c -o uname-test

では、このsystemtapスクリプトを実行しましょう。ここでも端末を2つ使います。1つはuname-testプログラムを実行します。もう1つは次のようにsystemtapスクリプトを実行します。start livepatch ~と出たら準備完了です。

[masami@unamebook ~]$ sudo stap -g uname_livepatch.stp 
start livepatch code. Ctrl-C to stop

別の端末でテストします。

[masami@unamebook ~]$ uname -n
unamebook
[masami@unamebook ~]$ ./uname-test 
nodename: livepatched
[masami@unamebook ~]$ uname -n
unamebook
[masami@unamebook ~]$ 

次にsystemtapのプログラムをCtrl-Cで止めてuname-testを実行してみます。systemtapスクリプトを終了したことでsys_newuname()の挙動がもとに戻ったことでuname(2)の戻り値がもとに戻りましたね。

[masami@unamebook ~]$ ./uname-test 
nodename: unamebook

Kernel Hack

前章のsystemtapではsystemtapを利用してカーネルの挙動を変えてみました。本章ではカーネルソースコードを変更し、前章と同様の挙動に変えてみます。カーネルのソースパッケージにパッチを当てるにはgit-am(1)で当てることができるパッチを作る必要があります。パッチの作り方としてメインラインのカーネルを変更してパッチをつくるかFedoraカーネルソースを変更するの2パターンがあります。FedoraカーネルにはFedoraプロジェクトが適用したパッチも含まれているため、メインラインのカーネルを変更した場合、変更内容によってはFedoraカーネルソースとコンフリクトが発生する可能性もあります。そこで本章ではFedoraカーネルソースを変更してパッチを作ります。まずはカーネルのソースパッケージをダウンロードしてインストールします。以前coreutilsのソースパッケージをインストールしているので綺麗なrpmbuildディレクトリを作りたいところです。よって、既存のrpmbuildディレクトリはリネームして置いておき、新たにrpmbuildディレクトリを作成します。そしてソースパッケージをダウンロードしてインストールします。

[masami@unamebook ~]$ dnf download --source kernel
[masami@unamebook ~]$ rpm -i kernel-5.3.11-300.fc31.src.rpm 

カーネルパッケージのビルドに必要な依存パッケージのインストールを行います。

[masami@unamebook ~]$ sudo dnf builddep -y kernel
[masami@unamebook ~]$ sudo dnf install -y pesign

これでカーネルソースパッケージをビルドする準備は整ったのでですが、specファイルにあるbuildidを設定してFedorarpmと区別できるようにします。お好みのエディタでkernel.specを開き# define buildid .localの下に%define buildid .unametestを追加します。

# define buildid .local
%define buildid .unametest

この段階ではまだ~/rpmbuild/SOURCESにカーネルのコードやパッチが置かれているだけなのでソースコードを展開します。

[masami@unamebook SPECS]$ rpmbuild -bp kernel.spec

展開に成功すると~/rpmbuild/BUILD/kernel-5.3.fc31/linux-5.3.11-300.unametest.fc31.x86_64/にパッチが当てられたカーネルのコードが置かれます。また通常のカーネルソースツリーとは違いconfigsディレクトリが存在します。このディレクトリにはfedoraの各アーキテクチャ向けのconfigファイルが存在します。rpmbuildではなく、ローカル環境でビルドを試す際はこのディレクトリにあるconfigを使用してmake oldconfigを行うか、使用しているカーネルのコンフィグファイルが/bootにあるのでそれをコピーして使いましょう。 では、ここからはカーネルソースコードを弄っていきます。まずは~/rpmbuild/BUILD/kernel-5.3.fc31に移動し、 linux-5.3.11-300.unametest.fc31.x86_64をコピーして作業ディレクトリを作ります。コピー先の名前は任意です。ここでは linux-5.3.11-300.unametest.fc31.x86_64.hackとしました。

[masami@unamebook kernel-5.3.fc31]$ cp -a linux-5.3.11-300.unametest.fc31.x86_64 linux-5.3.11-300.unametest.fc31.x86_64.hack

一旦ソースをいじっていない状態でビルドができることを確認しましょう。この段階でビルドに成功することを確認しておけば、ソースの変更後にビルドが通らなくなった場合の問題の切り分けに役立ちます。この段階でgitのブランチも作っておきます。 linux-5.3.11-300.unametest.fc31.x86_64には.gitディレクトリがあり、gitがすでに利用できるようになっています。

[masami@unamebook kernel-5.3.fc31]$ cd linux-5.3.11-300.unametest.fc31.x86_64.hack/
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git checkout -b uname-hack
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ cp configs/kernel-5.3.11-x86_64.config .config
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make oldconfig
scripts/kconfig/conf  --oldconfig Kconfig
#
# configuration written to .config
#

make oldconfigは特に問題なく完了すると思います。では、カーネルカーネルモジュールをビルドします。bzImageで圧縮されたカーネルのイメージをビルドし、modulesでカーネルモジュールをビルドしています。-j$(nproc)は並列ビルドのオプションです。ログインしている環境で利用可能なcpu数をもとにビルドを並列実行します。カーネルのビルドは最後にarch/x86/boot/bzImage is readyというメッセージが出れば成功です。rpmのソースパッケージを使ってるのでrpmbuildでビルドしても良いのですが、それだと時間がかかるのでまずは手軽にmakeでビルドしています。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make -j$(nproc) bzImage
~略~
BUILD   arch/x86/boot/bzImage
Setup is 17692 bytes (padded to 17920 bytes).
System is 9085 kB
CRC dde74ef3
Kernel: arch/x86/boot/bzImage is ready  (#1)

モジュールのビルドも実行します。モジュールのビルドは成功時にこれといったログはありません。次のようにしれっとプロンプトが表示されていたら成功しています。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make -j$(nproc) modules
~略~
  LD [M]  sound/x86/snd-hdmi-lpe-audio.ko
  LD [M]  virt/lib/irqbypass.ko

さて、カーネルとモジュールのビルドに成功したら準備は完了です。実際にカーネルのソースに手を加えましょう。変更するのはkernel/sys.cです。カレントプロセス名を調べてプロセス名がuname-testならnodenameを変更します。やっていることはsystemtapの場合と同様です。変更内容は次のようになります。

diff --git a/kernel/sys.c b/kernel/sys.c
index 2969304..908ce7f 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -1243,6 +1243,10 @@ SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
        down_read(&uts_sem);
        memcpy(&tmp, utsname(), sizeof(tmp));
        up_read(&uts_sem);
+
+       if (!strcmp(current->comm, "uname-test"))
+               strcpy(tmp.nodename, "livepatched");
+
        if (copy_to_user(name, &tmp, sizeof(tmp)))
                return -EFAULT;
 

systemtapの場合は関数の途中にコードを入れていないので関数から返るところに処理を入れ込みましたが、今回は直接カーネルのコードを弄っているので素直なコードの変更になってますね。変更したらカーネルがビルドできるか確認しましょう。ビルド方法は先ほどと同じです。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ make -j$(nproc) bzImage
~略~
  BUILD   arch/x86/boot/bzImage
Setup is 17692 bytes (padded to 17920 bytes).
System is 9089 kB
CRC 27223b7c
Kernel: arch/x86/boot/bzImage is ready  (#2)

今回も成功したらarch/x86/boot/bzImage is readyというメッセージが出ます。このメッセージの最後にある#2というのはこのカーネルをビルドしたのが2回目という意味です。ビルドに成功したのでパッチを作成しましょう。まず最初にgitのメールアドレスと名前を変更します。これはFedoraカーネルの場合、デフォルトではFedoraカーネルチームのメールアドレスが使われるので上書きして自分のメールアドレスと名前に変更します。そうしたら変更をコミットし、git-format-patch(1)でパッチを作ります。出来たパッチはカレントディレクトリに置かれます。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git config --local --add user.email <your email>
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git config --local --add user.name "your name"
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git add kernel/sys.c
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git commit -m "uname: add test code"
[uname-hack 9ea2b59] uname: add test code
 1 file changed, 4 insertions(+)
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ git format-patch -s master
0001-uname-add-test-code.patch
[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$

ここで出来たパッチは次のようになります。コミットメッセージ、変更内容、メール送信用のサブジェクトなどが設定されています。最後の2.23.0というのはgitのバージョンです。

From 9ea2b595c1c246a7ff94e6c4c721896ffcf9f5a6 Mon Sep 17 00:00:00 2001
From: Your Name <your email>
Date: Mon, 25 Nov 2019 23:26:46 +0900
Subject: [PATCH] uname: add test code

Signed-off-by:Your Name <your email>
---
 kernel/sys.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/kernel/sys.c b/kernel/sys.c
index 2969304..908ce7f 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -1243,6 +1243,10 @@ SYSCALL_DEFINE1(newuname, struct new_utsname __user *, name)
        down_read(&uts_sem);
        memcpy(&tmp, utsname(), sizeof(tmp));
        up_read(&uts_sem);
+
+       if (!strcmp(current->comm, "uname-test"))
+               strcpy(tmp.nodename, "livepatched");
+
        if (copy_to_user(name, &tmp, sizeof(tmp)))
                return -EFAULT;
 
-- 
2.23.0

では、このパッチをFedoraカーネルソースパッケージに追加してrpmbuild(8)でビルドしましょう。パッチを~/rpmbuild/SOURCESディレクトリにコピーします。

[masami@unamebook linux-5.3.11-300.unametest.fc31.x86_64.hack]$ cp 0001-uname-add-test-code.patch ../../../SOURCES/.

そして~/rpmbuild/SPECSに移動します。kernel.specファイルをエディタで開き、# END OF PATCH DEFINITIONSの行まで移動します。PatchXXX(Xは数字)の行がありますが、この数字は任意です。ここでは999としました。次のように# END OF PATCH DEFINITIONSの上に2行を追加します。

# uname hack patch
Patch999: 0001-uname-add-test-code.patch

# END OF PATCH DEFINITIONS

そうしたらビルドしましょう。ここでは必要最小限のカーネルパッケージだけ作成します。次のようなコマンドライン引数でビルドを行います。

[masami@unamebook SPECS]$ rpmbuild -bb --with baseonly --without debuginfo --without debug --target=$(uname -m) kernel.spec
~略~
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.0BA8HX
+ umask 022
+ cd /home/masami/rpmbuild/BUILD
+ cd kernel-5.3.fc31
+ /usr/bin/rm -rf /home/masami/rpmbuild/BUILDROOT/kernel-5.3.11-300.unametest.fc31.x86_64
+ RPM_EC=0
++ jobs -p
+ exit 0

出来上がったrpmファイルは~ rpmbuild/RPMS/x86_64/に置かれます。

[masami@unamebook x86_64]$ ls
kernel-5.3.11-300.unametest.fc31.x86_64.rpm       kernel-devel-5.3.11-300.unametest.fc31.x86_64.rpm    kernel-modules-extra-5.3.11-300.unametest.fc31.x86_64.rpm
kernel-core-5.3.11-300.unametest.fc31.x86_64.rpm  kernel-modules-5.3.11-300.unametest.fc31.x86_64.rpm

出来上がったrpmをインストールします。

[masami@unamebook x86_64]$ sudo dnf localinstall -y ./*.rpm

インストールしたらカーネルの優先順を確認しましょう。通常はインストールしたカーネルで起動できると思いますが、出来なかった場合はここで調べたindexを使ってgrub2-rebootコマンドで起動するカーネルを設定してリブートします。

[masami@unamebook ~]$ sudo grubby --info=ALL
index=0
kernel="/boot/vmlinuz-5.3.11-300.unametest.fc31.x86_64"
args="ro resume=UUID=c98315dc-7144-427b-a6a0-2aad53d8ebc5 rhgb quiet"
root="UUID=0b584087-de7b-42f8-8dea-d3b679d22613"
initrd="/boot/initramfs-5.3.11-300.unametest.fc31.x86_64.img"
title="Fedora (5.3.11-300.unametest.fc31.x86_64) 31 (Thirty One)"
id="24b7d4741c7c4982889ac4d4acb27ae1-5.3.11-300.unametest.fc31.x86_64"

再起動したらuname(1)でカーネルを確認しましょう。unametestが入っているのでビルドしたカーネルで正しく起動したかわかりますね。

[masami@unamebook ~]$ uname -a
Linux unamebook 5.3.11-300.unametest.fc31.x86_64 #1 SMP Mon Nov 25 23:32:51 JST 2019 x86_64 x86_64 x86_64 GNU/Linux

そして、systemtapのときにも使ったuname-testを実行してみましょう。

[masami@unamebook ~]$ ./uname-test 
nodename: livepatched

nodenameが変わりましたね。ここでuname-testをuname-test2としてコピーして実行すると、オリジナルのnodenameがでましたね。ということでカーネルに手を入れてuname(2)の挙動を変更することにも成功しました🎉

[masami@unamebook ~]$ cp uname-test uname-test2
[masami@unamebook ~]$ ./uname-test2
nodename: unamebook

Livepatch

systemtapの章ではsystemtapを利用してカーネルの挙動を変えてみましたが、LinuxカーネルにはLivepatchという機能があり、実行中のカーネルの挙動を再起動なしに変更する(patchを当てる)ことができる機能があります。本章ではこの機能でunameシステムコールの挙動を変更してみます。ただしFedoraカーネルではLivepatch機能は有効になっていないためカーネルのソースパッケージよりLivepatchを有効にしたカーネルを作る必要があります。カーネルのソースは前章でダウンロードしてきてあるのでこれに対してコードを弄っていきましょう。ここで本当はunameの挙動を変更するのがベターなところですが、unameの実装をLivepatchするのはちょっと手間なので単純に弄ることができるgetpid(2)を使います。

まず~/rpmbuild/SOURCESに移動し、Livepatchの機能を有効にする設定を追加します。

[masami@unamebook ~]$ cd rpmbuild/SOURCES/
[masami@unamebook SOURCES]$ echo "CONFIG_LIVEPATCH=y" >> kernel-local
[masami@unamebook SOURCES]$ echo "CONFIG_TEST_LIVEPATCH=m" >> kernel-local

kernel-localファイルはこのようになります。

[masami@unamebook SOURCES]$ cat kernel-local 
# This file is intentionally left empty in the stock kernel. Its a nicety
# added for those wanting to do custom rebuilds with altered config opts.
CONFIG_LIVEPATCH=y
CONFIG_TEST_LIVEPATCH=m

Fedoraではkernel-localファイルにカーネルコンフィギュレーションを設定することでベースとなる設定ファイル(x86_64ならkernel-x86_64.config)に設定を追加できます。 設定を追加したら~/rpmbuild/SPECに移動し依存パッケージのインストールを行います。kernel-localファイルを作ったらあとはビルドするだけです。

[masami@unamebook SOURCES]$ cd ~/rpmbuild/SPECS/

先程はunametestという名称をカーネルパッケージに追加しましたが、今度はgetpidtestという名称を付けましょう。kernel.specを以下のように変更します。

# define buildid .local
%define buildid .getpidtest

specファイルを変更したらカーネルをビルドします。

[masami@unamebook SPECS]$  rpmbuild -bb --with baseonly --without debuginfo --without debug --target=$(uname -m) kernel.spec
~略~
+ cd kernel-5.3.fc31
+ /usr/bin/rm -rf /home/masami/rpmbuild/BUILDROOT/kernel-5.3.11-300.getpidtest.fc31.x86_64
+ RPM_EC=0
++ jobs -p
+ exit 0

ビルドが終了したら~/rpmbuild/RPMS/x86_64に移動してlsするとgetpidtestと名前の付いたrpmが出来ているのが確認できます。

[masami@unamebook x86_64]$ ls
kernel-5.3.11-300.getpidtest.fc31.x86_64.rpm       kernel-core-5.3.11-300.unametest.fc31.x86_64.rpm    kernel-modules-5.3.11-300.getpidtest.fc31.x86_64.rpm        kernel-modules-extra-5.3.11-300.unametest.fc31.x86_64.rpm
kernel-5.3.11-300.unametest.fc31.x86_64.rpm        kernel-devel-5.3.11-300.getpidtest.fc31.x86_64.rpm  kernel-modules-5.3.11-300.unametest.fc31.x86_64.rpm
kernel-core-5.3.11-300.getpidtest.fc31.x86_64.rpm  kernel-devel-5.3.11-300.unametest.fc31.x86_64.rpm   kernel-modules-extra-5.3.11-300.getpidtest.fc31.x86_64.rpm

新しくビルドしたrpmをインストールします。

[masami@unamebook x86_64]$ sudo dnf localinstall -y  kernel-*.getpidtest.*

次にgrubのエントリを確認します。

[masami@unamebook x86_64]$ sudo grubby --info=ALL | grep index -A1
index=0
kernel="/boot/vmlinuz-5.3.11-300.unametest.fc31.x86_64"
--
index=1
kernel="/boot/vmlinuz-5.3.11-300.getpidtest.fc31.x86_64"
--
index=2
kernel="/boot/vmlinuz-5.3.11-300.fc31.x86_64"
--
index=3
kernel="/vmlinuz-0-rescue-24b7d4741c7c4982889ac4d4acb27ae1"

getpidtestのカーネルはindexが1となっているのでこのカーネルで起動するようにします。

[masami@unamebook x86_64]$ sudo grub2-reboot 1

これで準備完了なので再起動します。再起動したらもちろんuname(1)で正しいカーネルで起動したか確認します😄

[masami@unamebook ~]$ uname -a
Linux unamebook 5.3.11-300.getpidtest.fc31.x86_64 #1 SMP Tue Nov 26 23:10:42 JST 2019 x86_64 x86_64 x86_64 GNU/Linux

カーネルビルド時にライブパッチ機能が有効になっていたか確認します。fedoraは/bootにカーネルのコンフィグが置かれるのでこれで確認できます。

[masami@unamebook ~]$ grep CONFIG_LIVEPATCH /boot/config-5.3.11-300.getpidtest.fc31.x86_64 
CONFIG_LIVEPATCH=y

ではライブパッチを作りましょう。カーネルのライブパッチはカーネルモジュールとして作成します。まず作業ディレクトリを作ります。

[masami@unamebook ~]$ mkdir getpid-livepatch-module

そして、getpid-livepatch.cを以下の内容で作成します。your nameのところは適当に変えてください。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/livepatch.h>
#include <linux/string.h>
#include <linux/sched.h>

MODULE_DESCRIPTION("getpid livepatch test module");
MODULE_AUTHOR("your name");
MODULE_LICENSE("GPL");
MODULE_INFO(livepatch, "Y");

asmlinkage long livepatch_getpid(void)
{
        pr_info("%s\n", __func__);
        
        if (unlikely(!strcmp(current->comm, "getpid-test")))
                return 1;

        return  task_tgid_vnr(current);
}

static struct klp_func funcs[] = {
        {
                .old_name = "__x64_sys_getpid",
                .new_func = livepatch_getpid,
        }, {}
};

static struct klp_object objs[] = {
        {
                .funcs = funcs,
        }, {}
};

static struct klp_patch patch = {
        .mod = THIS_MODULE,
        .objs = objs,
};

static int getpid_livepatch_init(void)
{
        int ret = 0;

        ret = klp_enable_patch(&patch);
        if (ret) {
                pr_info("failed to enable getpid live patch\n");
                return ret;
        }

        pr_info("getpid livepatch test module is enabled\n");
        return ret;
}

static void getpid_livepatch_cleanup(void)
{
}

module_init(getpid_livepatch_init);
module_exit(getpid_livepatch_cleanup);

このライブパッチではgetpid(2)を実行したプログラム名がgetpid-testの場合に1を返し、それ以外の場合は普通にpidを返しています。このファイルをビルドするのにMakefileが必要ですのでこれも作成します。Makefileのインデントはtabなんで気をつけてください。

KERNDIR := /lib/modules/`uname -r`/build
BUILD_DIR := $(shell pwd)
VERBOSE = 0

obj-m := getpid-livepatch.o
smallmod-objs := getpid-livepatch.o

all:
        make -C $(KERNDIR) M=$(BUILD_DIR) KBUILD_VERBOSE=$(VERBOSE) modules

clean:
        rm -f *.o
        rm -f *.ko
        rm -f *.mod.c
        rm -f *~

ビルドします。ビルドに成功するとgetpid-livepatch.koというファイルができます。

[masami@unamebook getpid-livepatch-module]$ make
make -C /lib/modules/`uname -r`/build M=/home/masami/getpid-livepatch-module KBUILD_VERBOSE=0 modules
make[1]: Entering directory '/usr/src/kernels/5.3.11-300.getpidtest.fc31.x86_64'
  CC [M]  /home/masami/getpid-livepatch-module/getpid-livepatch.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/masami/getpid-livepatch-module/getpid-livepatch.mod.o
  LD [M]  /home/masami/getpid-livepatch-module/getpid-livepatch.ko
make[1]: Leaving directory '/usr/src/kernels/5.3.11-300.getpidtest.fc31.x86_64'

次にテストプログラムを用意しましょう。getpid-test.cという名前でファイルを作り、以下の内容で保存します。

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
{
        printf("pid is %d\n", getpid());
        return 0;
}

ビルドします。

[masami@unamebook getpid-livepatch-module]$ gcc getpid-test.c -o getpid-test

実行すると以下のようにpidが表示されます。

[masami@unamebook getpid-livepatch-module]$ ./getpid-test 
pid is 1435

では、ライブパッチのモジュールをロードします。

[masami@unamebook getpid-livepatch-module]$ sudo insmod ./getpid-livepatch.ko

dmesgコマンドでカーネルのログを見ると以下のような行があるのが確認できます。

[  959.659727] getpid_livepatch: loading out-of-tree module taints kernel.                                                                                                                                                                    
[  959.659729] getpid_livepatch: tainting kernel with TAINT_LIVEPATCH                                                                                                                                                                         
[  959.659787] getpid_livepatch: module verification failed: signature and/or required key missing - tainting kernel                                                                                                                          
[  959.668116] livepatch: enabling patch 'getpid_livepatch'                                                                                                                                                                                   
[  959.670655] livepatch: 'getpid_livepatch': starting patching transition                                                                                                                                                                    
[  959.670802] getpid_livepatch: getpid livepatch test module is enabled      

ただ、livepatch_getpid()の最初にpr_info()で関数名を表示させているので以下のようなログが大量に出てると思いますが。

[ 1042.927756] getpid_livepatch: livepatch_getpid

それはさておきgetpid-testを再度実行するとpidとして1が返ってきます。ということでカーネルのライブパッチ機能を使ったライブパッチも成功です🎉

[masami@unamebook getpid-livepatch-module]$ ./getpid-test 
pid is 1

プログラム名が違えばもちろんpidは普通に返ってきます。

[masami@unamebook getpid-livepatch-module]$ cp getpid-test getpid-test2
[masami@unamebook getpid-livepatch-module]$ ./getpid-test2
pid is 1455

おまけ

FedoraをupstreamとしているRHELCentOSuname(1)の挙動も紹介します。

RHEL 8.1

バージョンは8.1です。これでuname(1)を実行するとuname(2)が3回呼ばれているのがわかりますね。

[masami@rhel8 ~]$ cat /etc/os-release  | grep VERSION_ID
VERSION_ID="8.1"
[masami@rhel8 ~]$ strace -v -s 1024 -e trace=uname -C  uname -a
uname({sysname="Linux", nodename="rhel8", release="4.18.0-147.0.3.el8_1.x86_64", version="#1 SMP Mon Nov 11 12:58:36 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="rhel8", release="4.18.0-147.0.3.el8_1.x86_64", version="#1 SMP Mon Nov 11 12:58:36 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="rhel8", release="4.18.0-147.0.3.el8_1.x86_64", version="#1 SMP Mon Nov 11 12:58:36 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
Linux rhel8 4.18.0-147.0.3.el8_1.x86_64 #1 SMP Mon Nov 11 12:58:36 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
+++ exited with 0 +++
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000068          22         3           uname
------ ----------- ----------- --------- --------- ----------------
100.00    0.000068      

coreutilsのバージョンは8.30です。

[masami@rhel8 ~]$ rpm -qi coreutils | grep -e Version -e Release
Version     : 8.30
Release     : 6.el8

CentOS 8

CentOSのバージョンは8です。こちらもuname(2)が3回呼ばれてますね。

masami@centos8 ~]$ cat /etc/os-release  | grep VERSION_ID
VERSION_ID="8"
[masami@centos8 ~]$ strace -v -s 1024 -e trace=uname -C  uname -a
uname({sysname="Linux", nodename="centos8", release="4.18.0-80.11.2.el8_0.x86_64", version="#1 SMP Tue Sep 24 11:32:19 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="centos8", release="4.18.0-80.11.2.el8_0.x86_64", version="#1 SMP Tue Sep 24 11:32:19 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
uname({sysname="Linux", nodename="centos8", release="4.18.0-80.11.2.el8_0.x86_64", version="#1 SMP Tue Sep 24 11:32:19 UTC 2019", machine="x86_64", domainname="(none)"}) = 0
Linux centos8 4.18.0-80.11.2.el8_0.x86_64 #1 SMP Tue Sep 24 11:32:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
+++ exited with 0 +++
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000013           4         3           uname
------ ----------- ----------- --------- --------- ----------------
100.00    0.000013                     3           total

coreutilsのバージョンはこちらも8.30です。

[masami@centos8 ~]$ rpm -qi coreutils | grep -e Version -e Release
Version     : 8.30
Release     : 6.el8

まとめ

最後はgetpid(2)を使いましたが、主にuname(1)とuname(2)を使ってユーザーランドでのデバッグからカーネルを弄るところまでを行いました(_´Д`)ノ~~オツカレー

Raspberry Piで学ぶコンピュータアーキテクチャ (Make:PROJECTS)

Raspberry Piで学ぶコンピュータアーキテクチャ (Make:PROJECTS)

*1:1というのはmanセクションの番号です。1は通常のコマンドです。

*2:カーネルバージョンにある#1というのはビルド環境にてビルドした回数です。

*3:システムコールのmanセクションは2です

Fedora Kernel test week参加記録

FedoraではkernelのテストイベントとかGnome 3.34 Test DayI18N Test Day などのテストイベントがちょくちょくあります。 今回はKernel 5.3 Test Weekの備忘録です。ちなみに、この手のイベントはfedora MAGAZINEで紹介されることがあります。

今回の参加結果の最終成果はこちらです。

github.com

参加ε≡≡ヘ( ´Д`)ノ

今回のtest weekは開催時期が9月末から10月の最初の週でちょうどfedora 31のリリースが近かったころです。当時のfedora 29と30はカーネルは5.2系が使われていて、fedora 31が5.3系って感じでした。メインラインは5.4のrc1が出てた辺りです。それでfedora 31もそろそろ出るし、ベータとかrcのテストも参加してなかったし、メインラインのカーネルも最近使ってないなーなんてことでテストに参加しとくかーって感じで参加したわけです。

バグ発見/(^o^)\

テストに使うカーネルrpmをデスクトップ環境のfedora 30マシンにインストールして再起動すると、本来GDMのログイン画面がでるところでスクリーンがブリンクし続ける状態でした。一応メインラインのカーネル(5.4-rc1)も試してみたけど結果は同じでした。ここまでで5.3〜で症状が出るらしいってことは確定。というわけでここまでの内容で一旦bugzillaにバグレポートしました。

1758831 – GDM login screen doesn't show up on kernel 5.3.2

その後ですが、じゃあ、fedora 31ならどうよ?と思ってテスト用にビルドされたライブデスクトップのisoイメージを使ってみたところこちらも同様の現象。ただブートメニューからTroubleshooting" -> "Start kernel in basic graphic mode"を選ぶと問題なし。間違いなくGPU関連のバグですねって感じです。この情報もbugzillaに追記しました。

debug🐝

一応ここまででバグを見つけた人として一応のことはしたのですが、自分のPCで新しいカーネルが使えないのは最高に困るのでここからはbisectして原因を探りました。バグに当たっている人がテスト時点では自分くらいしかいなかったし、他のディストリビューションを見てもArch Linuxで同じような現象の人が1人くらいいたかなーって感じでした。そうするとバグは機種依存っぽいし自分でデバッグしたほうが良さげだったんですね。

ということでbisectを開始したわけですがバグが実機じゃないと再現できないのでダラダラやりつつ土曜日の朝から夕方まで使った気がします。そして、first bad commitは36a0f92020dc8794d3aa69b7fb4c5d2bf99b0099というのがわかりました。ちなみにこのpatchは一連のシリーズの中の一つなのと、このコミット以降でリファクタリングされていてファイルのパスやヘルパー関数が追加されています。さて、差分ですがpatchだけ見るとわかりにくいので自分の環境で動かなくなった原因のsanitize_aux_ch()より主要部分のbefore/afterを示すとこうなります。

before

       const struct ddi_vbt_port_info *info =
        &dev_priv->vbt.ddi_port_info[port];

    for (p = PORT_A; p < I915_MAX_PORTS; p++) {
        struct ddi_vbt_port_info *i = &dev_priv->vbt.ddi_port_info[p];

        if (p == port || !i->present ||
            info->alternate_aux_channel != i->alternate_aux_channel)
            continue;

        i->supports_dp = false;
        i->alternate_aux_channel = 0;
    }

after

        struct ddi_vbt_port_info *info = &dev_priv->vbt.ddi_port_info[port];

    for (p = PORT_A; p < I915_MAX_PORTS; p++) {
        struct ddi_vbt_port_info *i = &dev_priv->vbt.ddi_port_info[p];

        if (p == port || !i->present ||
            info->alternate_aux_channel != i->alternate_aux_channel)
            continue;

        info->supports_dp = false;
        info->alternate_aux_channel = 0;
    }

大きな違いはfor文中のif文でcontinueしなかった場合に使用している変数です。変更前は配列のp番目の要素をiにセットしてその変数の構造体のデータに値を設定していますが、変更後の方は最初にinfoに設定した値をそのまま使っていてp番目の要素に対する変更は行っていません。そんなわけで、変更前と同様の挙動にしたらどう?と思って作ったのが以下の修正です。

diff --git a/drivers/gpu/drm/i915/display/intel_bios.c b/drivers/gpu/drm/i915/display/intel_bios.c
index efb39f350b19..c886dae82aa7 100644
--- a/drivers/gpu/drm/i915/display/intel_bios.c
+++ b/drivers/gpu/drm/i915/display/intel_bios.c
@@ -1313,6 +1313,8 @@ static void sanitize_aux_ch(struct drm_i915_private *dev_priv,
 
        p = get_port_by_aux_ch(dev_priv, info->alternate_aux_channel);
        if (p != PORT_NONE) {
+               info = &dev_priv->vbt.ddi_port_info[p];
+
                DRM_DEBUG_KMS("port %c trying to use the same AUX CH (0x%x) as port %c, "
                              "disabling port %c DP support\n",
                              port_name(port), info->alternate_aux_channel,
@@ -1330,6 +1332,7 @@ static void sanitize_aux_ch(struct drm_i915_private *dev_priv,
                info->supports_dp = false;
                info->alternate_aux_channel = 0;
        }
+
 }
 
 static const u8 cnp_ddc_pin_map[] = {

このpatchをstableの5.3.2に当てたところちゃんと動くようになりましたワーイヽ(゚∀゚)メ(゚∀゚)メ(゚∀゚)ノワーイ これでとりあえず自分の環境では新しいカーネルでもなんとか動くようになったわけですが、ローカルパッチを当て続けるとかあり得ないですね。なので次はupstreamへの報告となります。

36a0f92020dc8794d3aa69b7fb4c5d2bf99b0099を見るとsanitize_ddc_pin()も同じような変更が入ってるのですが、こちらは自分の環境では影響出てなかったです。GPUわからん😨

freedesktop.orgのbugzillaに苦戦😭

upstreamにバグ報告と言っても報告先として考えられるのはlkml、サブシステムのメーリングリストkernelのbugzilla、またはそれ以外とあります。GPU(i915)のバグ報告先をさがしたところ↓が見つかりました。ここにレポートの仕方が書かれていたのでまずはそれに従います。drm-tipツリーをcloneしてきてこのツリーの最新コードを試したり。。

How to report bugs | 01.org

バグのレポートはfreedesktop.orgのbugzillaを使うということだったのでアカウントを作りました。そしていざレポートってところで色々書いてsubmitボタンを押すと空白ページになるだけでバグが登録できません😨 たまたまバグったのかな?と思ってもう一度試してもダメ、じゃあ時間を置いてと思って次の日に試してもダメ。。。バグ報告したいのにバグ報告できないというバグにハマったわけです\(^o^)/オワタ こんなのわからんよと思ってbugzillaについて聞けるところはないかな?と探したところircで聞けるというのがわかったのでircクライアントをインストールし、freenodeにある#freedesktopに入って状況説明したところ中の人がログを確認したりデバッグを手伝ってくれました。このときのバグレポートはfedoraと同じ感じで書いていたのでタイトルにloginという単語が入っていたのですが、これがバグの原因というのがわかりました。だいたい2時間位色々と試したんじゃないかな。。 ちなみにLoginだと大丈夫で小文字のloginという文字列がバグレポートの新規作成時に入っているとダメという状況でした。既存のバグレポートの編集時だと大丈夫という謎。 それはともかく、手伝ってくれた方には感謝しかありません👃

upstreamへの報告

なんだかんだで無事に報告できました。タイトルについてるbisectedというのはbisect結果を貼ったあとに変更されました。ちゃんと見てくれてますね( ̄ー ̄)bグッ!

111966 – [bisected] GDM Login screen doesn't show up on kernel 5.3 or later

まず普通にバグの報告したあとでbisectの結果やquick hackなパッチをレポートしました。その後、同じような現象の人が現れ、この方も自分が作ったパッチでバグが直ったという報告をしてくれました👃 その後のちゃんとしたパッチの作成までは結構速く進み、コミット36a0f92020dc8794d3aa69b7fb4c5d2bf99b0099の設定方法だとダメな機種があるということでコメント等適切に直したパッチが作成されました。自分ともう一人の方はこのパッチをテストして動くのを確認しました。

drm/i915: Favor last VBT child device with conflicting AUX ch/DDC pin · torvalds/linux@0336ab5 · GitHub

修正方法は自分がやった方法と同じなので自分のパッチも暫定修正とはいえ正しい修正方法にたどり着けててエライって感じです😃

サブシステムへの取り込み -> mainlineへの取り込み -> stable treeへの取り込み待ち

修正パッチがサブシステムのメーリングリストに投げられたのであとは次の3個の待ちとなります。とくに自分がすることはありません。

  1. サブシステムのtreeへの取り込み
  2. mainlineへの取り込み
  3. stable treeへの取り込み

3のときに Patch "drm/i915: Favor last VBT child device with conflicting AUX ch/DDC pin" has been added to the 5.3-stable tree ってメールが届いて5.3.8(5.3.7はすでにリリースされてたので次は5.3.8のときだった)にパッチが入るんだなーってのがわかりました。

freedesktop.orgのbugzillaは1か2のタイミングでcloseされたと思います。

fedoraへの取り込み待ち

新しいstable releaseが出れば fedoraカーネルが最新のstable releaseに追従したときにfedoraカーネルでもバグが直ります。なのでこちらも待ちです(-_-)zzz fedoraのパッケージは最初updates-testingというリポジトリにアップロードされます。そこで人間がテストするか一定期間経てばupdatesリポジトリにパッケージが入ります。で、fedoraカーネル5.3.8のパッケージがアップロードされたので早速テストして結果を書きました。

https://bodhi.fedoraproject.org/updates/FEDORA-2019-0e85bbd15b

あと、fedoraのbugzillaのほうにも5.3.8のカーネルパッケージで直ったという報告を書いておきました。で、こちらもクローズとなりました。

終わり

というわけでkernel test weekへの参加とバグ発見、bisectとローカルパッチの作成、upstreamへの報告とupstreamへの協力、fedoraでの修正確認ということを最近しました( ´ー`)フゥー... 今回一番難しかったのはbugzillaでバグ報告できない問題でしたね。自分の環境で動かせられないし/(^o^)\

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~

雑文:Linuxとか低レイヤが好きで趣味で色々やってたら趣味が仕事になったのと、知識が足りないとこを勉強しようと思って学生にもなった件

このブロクで使ってるカテゴリは↓のようになっていて、こいつLinuxとかカーネル好きだなって感じなんですが、前まではLinux自体は使うけどそれくらいでガッツリとLinuxに絡んだりとかはしてませんでした。

f:id:masami256:20190905215910p:plain

前職はこんな感じのところでnodejsなんかのコード書いてわけですね。 f:id:masami256:20140629161407j:plain

web系っぽい記事もたまに書いてましたよw

qiita.com

とまあ、前職はweb系だったわけですが今はLinuxがっつりな仕事になってます。転職したのは去年の12月ですけどね。Linuxが好きでいろいろやってきて、そこでやったことをblogに書いたりしたわけですが、それも今の仕事で役立つことがあったりとまあ何が役立つかわからないものですね。メモリ管理に興味を持ったり自作OSに興味を持ったり、CTFだったりと低レイヤ周辺をウロウロしてたけど基本的な部分はブレなかったのが良かった気がします。 継続は力なりなんて格言もあるし、続けるってのは大事っすね。とゆーか、ものすごく運が良かったとは思ってます。すごい人達とも知り合うことができたし。

もともとソフトウェアエンジニアになったのも趣味を仕事にしたら一石二鳥じゃね?ってところから仕事にした感じですね。今流行りの未経験からエンジニアになった系ですw もうかなり前だけどw で、そんなことはどうでもよくて、趣味&独学でやってきたので自分の好きな分野はまあわかるけど情報理論とか理論系よくわからん😭という状況だったりというのもありました。じゃあ、ちゃんと勉強しちゃうか?ってことで2019年4月から放送大学の情報コースに全科履修生で入学したりしました。まあ、この辺の決断はかなり軽くて昼に町に出てどのごはん屋さんに行くかを決めるよりは軽い感じで決めてます。会社員兼学生という感じなので履修ペースは遅いけどそこはしょうがない。。。今のところ1学期に4科目くらいのペースですね。

2019度第1学期にとったこれは自分のわかる分野だったので単位認定試験で一番良い評価とれました😃

f:id:masami256:20190905222227j:plain

だがしかし、こっちはギリギリ単位取れた。。。数学苦手😨

f:id:masami256:20190905222242j:plain

ちなみに、コンピュータの動作と管理を見てたらこんなことが😄

\177ELF on Twitter: "@uchan_nos @d_kami 放送大学の情報コースの教科書に見慣れたお名前を発見😇… "

さて、2019度第2学期も無事に単位を取ることができるのか💀

Joel on Software

Joel on Software

UEFIのSecure boot + kdump・kexec

UEFIのSecure boot + kdump・kexecの動作確認

uefiのSecure bootが有効な環境でkdump・kexecは動作するのか?ってところの確認です。

Secure bootとは?というところはこちらのドキュメントを参照してください。 access.redhat.com

テスト環境

テスト環境にはlibvirtを使います。ホストはFedora 30 x86_64、ゲストもFedora 30 x86_64です。

テスト環境セットアップ

まずはSecure bootを有効にした環境を作ります。まずは普通にlibvirtを使ってOSをインストールします。 このときに、UEFIを使うように設定します。そのため、edk2-ovmfパッケージをインストールしておく必要があります。

f:id:masami256:20190830235150p:plain

インストール段階だとセキュアブート用のファームウェアは指定できないのでとりあえずはセキュアブートなしでインストールします。

kdumpの設定

インストールが終わったら普通に設定しましょう。

access.redhat.com

この時点で動作確認もしてkdumpが正常動作することを確認しておきます。

Secure bootの設定

virsh editでxmlファイルを編集します。VMは一旦shutdownしておきましょう。

まず、OVMF_VARS.secboot.fdをコピーします。コピー先の名前はなんでも良いです。ここではvm名と合わせてます。

$ sudo cp /usr/share/edk2/ovmf/OVMF_VARS.secboot.fd /var/lib/libvirt/qemu/nvram/secureboot-test_VARS_secboot.fd

そしたらvirsh editで編集します。以下の部分を探します。

  <os>
    <type arch='x86_64' machine='pc-q35-3.1'>hvm</type>
    <loader readonly='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.fd</loader>
    <nvram>/var/lib/libvirt/qemu/nvram/secureboot-test_VARS.fd</nvram>
    <boot dev='hd'/>
  </os>

そして、以下のように編集します。変更したのはloaderのところでOVMF_CODE.secboot.fdを指定したのと、nvramには先ほどコピーしたファイルを指定しました。

  <os>
    <type arch='x86_64' machine='pc-q35-3.1'>hvm</type>
    <loader readonly='yes' type='pflash'>/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd</loader>
    <nvram>/var/lib/libvirt/qemu/nvram/secureboot-test_VARS_secboot.fd</nvram>
    <boot dev='hd'/>
  </os>

セキュアブート有効で起動

普通にvmを起動すればセキュアブートが有効な状態で立ち上がります。 dmesgで確認するとこんなふうになります。

[masami@secureboot-test ~]$ dmesg | grep secureboot
[    0.000000] secureboot: Secure boot enabled
[    0.928493] systemd[1]: Set hostname to <secureboot-test>.
[    3.770079] systemd[1]: Set hostname to <secureboot-test>.
[masami@secureboot-test ~]$ 

これでvmのセキュアブートが有効になりました。

kdumpのテスト

セキュアブートが有効な状態でkdumpをスタートすると失敗します。エラーメッセージはこのようになってます。

[masami@secureboot-test ~]$ sudo systemctl status kdump
● kdump.service - Crash recovery kernel arming
   Loaded: loaded (/usr/lib/systemd/system/kdump.service; enabled; vendor preset: disabled)
   Active: failed (Result: exit-code) since Fri 2019-08-30 22:29:52 JST; 12s ago
  Process: 1316 ExecStart=/usr/bin/kdumpctl start (code=exited, status=1/FAILURE)
 Main PID: 1316 (code=exited, status=1/FAILURE)

Aug 30 22:29:51 secureboot-test systemd[1]: Starting Crash recovery kernel arming...
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: Secure Boot is enabled. Using kexec file based syscall.
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: kexec_file_load failed: Operation not permitted
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: kexec: failed to load kdump kernel
Aug 30 22:29:52 secureboot-test kdumpctl[1316]: Starting kdump: [FAILED]
Aug 30 22:29:52 secureboot-test systemd[1]: kdump.service: Main process exited, code=exited, status=1/FAILURE
Aug 30 22:29:52 secureboot-test systemd[1]: kdump.service: Failed with result 'exit-code'.
Aug 30 22:29:52 secureboot-test systemd[1]: Failed to start Crash recovery kernel arming.

dmesgでカーネルのエラーを確認するとこのようなメッセージを発見できます。

[  173.215036] Lockdown: kexec: /proc/kcore is restricted; see man kernel_lockdown.7

カーネルのコンフィグを確認するとLockdown機能が有効になってますね。

[masami@secureboot-test ~]$ grep LOCK_DOWN /boot/config-5.2.9-200.fc30.x86_64 
CONFIG_LOCK_DOWN_KERNEL=y
# CONFIG_LOCK_DOWN_KERNEL_FORCE is not set
CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT=y

Lockdownの解除

手元にmanがなかったので検索するとこちらが見つかりました。

lwn.net

manによると物理的に接続されたキーボードからのsysrq + xでlockdownが解除できるとあります。というわけで、virsh send-keyでsysrq + xを送ります。

$ sudo virsh send-key secureboot-test KEY_LEFTALT KEY_SYSRQ KEY_X

そうすると以下のようにlockdownを解除したよってメッセージがでます。

[masami@secureboot-test ~]$ dmesg
[ 2236.328389] sysrq: Disabling Secure Boot restrictions
[ 2236.328934] Lifting lockdown

kdumpのテスト

まずセキュアブートが有効か確認します。

[masami@secureboot-test ~]$ sudo mokutil --sb-state
SecureBoot enabled
[masami@secureboot-test ~]$ 

で、kdumpを起動

[masami@secureboot-test ~]$ sudo systemctl start kdump

エラーが出なければOKなので、クラッシュさせてみると /var/crash/に該当時刻のディレクトリが出来ていてコアダンプが取得できてるのが確認できます😃

[masami@secureboot-test ~]$ sudo sh -c "echo c > /proc/sysrq-trigger"
client_loop: send disconnect: Broken pipe
masami@moon:~$ ssh secureboot-fedora 
Web console: https://secureboot-test:9090/ or https://192.168.122.191:9090/

Last login: Fri Aug 30 22:58:25 2019 from 192.168.122.1
[masami@secureboot-test ~]$ ls /var/crash/
127.0.0.1-2019-08-30-01:05:24  127.0.0.1-2019-08-30-23:38:24
[masami@secureboot-test ~]$ date
Fri 30 Aug 2019 11:38:53 PM JST
[masami@secureboot-test ~]$ 

まとめ

セキュアブートが有効な環境でもkdump・kexecは使えますが、カーネルのLockdown機能が有効な場合は使えません。もしkdump・kexecを使用したい場合はLockdownを止める必要があるということでした。

追記1 2019/08/31

kexecとセキュアブート

セキュアブートが有効な場合と無効な場合ではkexec実行時のオプションがちょっと違います。では具体的に何が違うのか?というとセキュアブートが有効な場合は-sオプションを使います。 Fedoraの場合はkdumpctlコマンドがこの辺の処理をしていて、以下のようなコードとなってます。

    
    # For secureboot enabled machines, use new kexec file based syscall.
    # Old syscall will always fail as it does not have capability to
    # to kernel signature verification.
    if is_secure_boot_enforced; then
        echo "Secure Boot is enabled. Using kexec file based syscall."
        KEXEC_ARGS="$KEXEC_ARGS -s"
    fi
 
    $KEXEC $KEXEC_ARGS $standard_kexec_args \
        --command-line="$KDUMP_COMMANDLINE" \
        --initrd=$TARGET_INITRD $kdump_kernel
    if [ $? == 0 ]; then
        echo "kexec: loaded kdump kernel"
        return 0
    else
        echo "kexec: failed to load kdump kernel" >&2
        return 1
    fi

セキュアブートが有効な場合は-sオプション付けて使用するシステムコールを変える必要があります。

f:id:masami256:20190831104347p:plain

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~

超例解Linuxカーネルプログラミング~最先端Linuxカーネルの修正コードから学ぶソフトウェア品質~