kpatchの使い方

#1 kpatchとは?
ライブパッチ機能です。
カーネルを再起動することなく、パッチを適用することができます。

#2 検証環境
VMware Workstation 14 Player上の仮想マシンを使いました。
仮想マシンは、「最小限のインストール」->「開発ツール」を選択して作成しました。

##2.1 カーネル版数

```console:版数
[root@server ~]# cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core)

[root@server ~]# uname -r
3.10.0-957.el7.x86_64
```

##2.2 ディスク容量
15Gの空き領域が必要です。
空き領域は、~/.kpatchに作成するキャッシュで使用するようです。


#3 コマンドのインストール
kpatch-buildとkpatchをインストールします。
・kpatch-build：パッチからモジュールを作成するコマンド。
・kpatch：モジュールのロード／アンロードをするコマンド。
　　　　　モジュールをロードすることで、パッチがカーネルに適用されます。

##3.1 パッケージのインストール
[kpatch: dynamic kernel patching](https://github.com/dynup/kpatch)のCentOS 7に記載されている手順にしたがって実行しました。

```console:kpatchコマンドに必要なパッケージのインストール
[root@server ~]# UNAME=$(uname -r)
[root@server ~]# yum -y install gcc kernel-devel-${UNAME%.*} elfutils elfutils-devel
```

```console:kpatch-buildコマンドに必要なパッケージのインストール
[root@server ~]# yum -y install pesign yum-utils zlib-devel binutils-devel newt-devel python-devel perl-ExtUtils-Embed audit-libs audit-libs-devel numactl-devel pciutils-devel bison
[root@server ~]# yum-config-manager --enable debug

[root@server ~]# yum-builddep kernel-${UNAME%.*}
[root@server ~]# debuginfo-install kernel-${UNAME%.*}

[root@server ~]# yum -y install epel-release
[root@server ~]# yum -y install ccache
[root@server ~]# ccache --max-size=5G
Set cache size limit to 5.0 GB

[root@server ~]# yum -y install patchutils
```


##3.2 コマンドのインストール

```console:リポジトリのクローン
[root@server ~]# git clone https://github.com/dynup/kpatch.git
Cloning into 'kpatch'...
remote: Enumerating objects: 41, done.
remote: Counting objects: 100% (41/41), done.
remote: Compressing objects: 100% (31/31), done.
remote: Total 6609 (delta 13), reused 28 (delta 10), pack-reused 6568
Receiving objects: 100% (6609/6609), 1.84 MiB | 267.00 KiB/s, done.
Resolving deltas: 100% (3932/3932), done.
```

```console:コマンドのコンパイル
[root@server ~]# cd kpatch/
[root@server kpatch]# make
```

make installを実行すると、kpatchコマンドとkpatch-buildコマンドがインストールされます。

```console:コマンドのインストール
[root@server kpatch]# make install
[root@server kpatch]# kpatch version
0.6.3
```

#4 パッチの作成
kernel-debuginfoパッケージをインストールすると、
下記ディレクトリにカーネルソースが展開されます。

```console:ディレクトリの確認
[root@server kernel-3.10.0-957.el7]# pwd
/usr/src/debug/kernel-3.10.0-957.el7

[root@server kernel-3.10.0-957.el7]# ls -F
linux-3.10.0-957.el7.x86_64/
```

ディレクトリをコピーします。

```console:ディレクトリのコピー
[root@server kernel-3.10.0-957.el7]# cp -pr linux-3.10.0-957.el7.x86_64 linux-3.10.0-957.el7.x86_64.new
```

コピー先ディレクトリ(newが付いたディレクトリ)のソースに対して修正を実施します。

```console:ディレクトリの確認
[root@server kernel-3.10.0-957.el7]# ls -F
linux-3.10.0-957.el7.x86_64/  linux-3.10.0-957.el7.x86_64.new/
```

net/ipv4/ping.cのping_init_sock関数にprintkを1行(下記★)追加します。

```console:ping_init_sockの抜粋
static int ping_init_sock(struct sock *sk)
{
        struct net *net = sock_net(sk);
        kgid_t group = current_egid();
        struct group_info *group_info;
        int i, j, count;
        kgid_t low, high;
        int ret = 0;

     ★ printk(KERN_ERR "TEST:ping_init_sock(1)");

        inet_get_ping_group_range_net(net, &low, &high);
        if (gid_lte(low, group) && gid_lte(group, high))
                return 0;
-以下、略-
```


修正前後のping.cの差分を抽出します。抽出した差分がパッチとなります。
パッチは、test.patchというファイル名で保存します。

```console:パッチの作成
[root@server kernel-3.10.0-957.el7]# diff -Nur linux-3.10.0-957.el7.x86_64 linux-3.10.0-957.el7.x86_64.new > /root/test.patch
```

作成したパッチの中身を確認します。

```console:パッチの確認
[root@server kernel-3.10.0-957.el7]# cat /root/test.patch
diff -Nur linux-3.10.0-957.el7.x86_64/net/ipv4/ping.c linux-3.10.0-957.el7.x86_64.new/net/ipv4/ping.c
--- linux-3.10.0-957.el7.x86_64/net/ipv4/ping.c 2018-10-05 05:18:19.000000000 +0900
+++ linux-3.10.0-957.el7.x86_64.new/net/ipv4/ping.c     2019-05-05 10:32:32.825126680 +0900
@@ -211,6 +211,8 @@
        kgid_t low, high;
        int ret = 0;

+        printk(KERN_ERR "TEST:ping_init_sock(1)");
+
        inet_get_ping_group_range_net(net, &low, &high);
        if (gid_lte(low, group) && gid_lte(group, high))
                return 0;
```


#5 モジュールの作成
kpatch-buildコマンドを使って、パッチからモジュールを作成します。
kpatch-buildコマンドの初回実行時は、結構時間(40分くらい)がかかりました。


```console:モジュールの作成
[root@server ~]# ls
anaconda-ks.cfg  kpatch  test.patch

[root@server ~]# kpatch-build -t vmlinux test.patch
Fedora/Red Hat distribution detected
Downloading kernel source for 3.10.0-957.el7.x86_64
Unpacking kernel source
Testing patch file(s)
Reading special section data
Building original source
Building patched source
Extracting new and modified ELF sections
ping.o: changed function: ping_init_sock
Patched objects: vmlinux
Building patch module: livepatch-test.ko
SUCCESS
```

モジュール(livepatch-test.ko)が作成されたことがわかります。

```console:モジュールの確認
[root@server ~]# ls -F
anaconda-ks.cfg  kpatch/  livepatch-test.ko  test.patch
```

#6 パッチの適用
モジュールをロードして、動作中のカーネルに対してパッチを適用してみます。
まだ何もモジュールがロードされていないことがわかります。

```console:モジュールの確認
[root@server ~]# kpatch list
Loaded patch modules:

Installed patch modules:
```

モジュール(livepatch_test)をロードします。

```console:モジュールのロード
[root@server ~]# kpatch load livepatch-test.ko
loading patch module: livepatch-test.ko
waiting (up to 15 seconds) for patch transition to complete...
transition complete (2 seconds)
```

モジュール(livepatch_test)がロードできたことがわかります。

```console:モジュールの確認
[root@server ~]# kpatch list
Loaded patch modules:
livepatch_test [enabled]

Installed patch modules:
```

#7 実験結果
適用したパッチが動作するかどうか確認してみます。
printkの出力結果をジャーナルで確認するため、journalctlコマンドを実行します。
journalctlコマンドの使い方は、[ここ(journalctl コマンドの使い方)](https://qiita.com/hana_shin/items/96095571b7bf1b721255)を参照してください。

```console:ジャーナルの確認
[root@server ~]# journalctl -f
```

もう1つターミナルを開きます。
デフォルトGWに対してpingを1回実行してみます。
pingコマンドの使い方は、[ここ(pingコマンドの使い方)](https://qiita.com/hana_shin/items/c31b0d05a91244c4db83)を参照してください。


```console:pingの実行
[root@server ~]# ping -c 1 192.168.3.1
```

私の環境では、pingを1回実行しても、ジャーナルが出力されませんでした。

```console:ジャーナルの確認
[root@server ~]# journalctl -f
 5月 05 11:25:37 server kernel: TEST:ping_init_sock(1)
 5月 05 11:25:37 server kernel: TEST:ping_init_sock(1)
```
1回実行した後、さらに、もう1回実行すると、2回分のジャーナルが出力されました。
何故そのような挙動になるのか理解できませんが、とりあえず、
動作中のカーネルに対して、再起動なしでパッチが適用できることは確認できました。

systemtapで確認すると、pingを1回実行すると、ping_init_sock関数が1回
呼ばれていました。


#8 その他
##8.1 モジュールのアンロード方法
モジュールのアンロードは、以下のようにコマンドを実行します。

```console:モジュールの確認
[root@server ~]# kpatch list
Loaded patch modules:
livepatch_test [enabled]

Installed patch modules:
```

```console:モジュールのアンロード
[root@server ~]# kpatch unload livepatch_test
disabling patch module: livepatch_test
waiting (up to 15 seconds) for patch transition to complete...
transition complete (1 seconds)
unloading patch module: livepatch_test
```

```console:モジュールの確認
[root@server ~]# kpatch list
Loaded patch modules:

Installed patch modules:
```

##8.2 古いCentOSで実行した場合の挙動
今現在(2019/5/6)、CentOS7の最新はCentOS7.6です。
CentOS7.4でkpatchを試したのですが、下記エラーがでて、モジュールの作成ができませんでした。

```
[root@server ~]# kpatch-build --skip-gcc-check -t vmlinux test.patch
WARNING: Skipping gcc version matching check (not recommended)
Fedora/Red Hat distribution detected
Downloading kernel source for 3.10.0-693.el7.x86_64
ERROR: kpatch build failed. Check /root/.kpatch/build.log for more details.
```

上記エラーは、下記★行のdie関数で出力しています。
シェルスクリプトの一部を以下に引用しました。

```
[root@server ~]# less /usr/local/bin/kpatch-build

                if [[ -z "$SRCRPM" ]]; then
                        if [[ "$DISTRO" = fedora ]]; then
                                wget -P "$TEMPDIR" "http://kojipkgs.fedoraproject.org/packages/kernel/$KVER/$KREL/src/kernel-$KVER-$KREL.src.rpm" 2>&1 | logger || die
                        else
                                command -v yumdownloader &>/dev/null || die "yumdownloader (yum-utils or dnf-utils) not installed"
                            ★  yumdownloader --source --destdir "$TEMPDIR" "kernel$ALT-$KVER-$KREL" 2>&1 | logger || die
                        fi
                        SRCRPM="$TEMPDIR/kernel$ALT-$KVER-$KREL.src.rpm"
                fi
```

以下は、★印の行を抜き出したものです。

```
yumdownloader --source --destdir "$TEMPDIR" "kernel$ALT-$KVER-$KREL" 2>&1 | logger || die
```

sourceは、バイナリRPMではなく、ソースRPMをダウンロードするオプションです。
destdirは、ダウンロードするRPMの保存場所(/root/.kpatch/tmp)を指定するオプションです。
上記1行は、以下のように展開されます。

```
yumdownloader --source --destdir /root/.kpatch/tmp kernel-3.10.0-693.el7 2>&1 | logger || die
```

yumdownloaderを単独で実行すると、以下のように「ソースRPMがない」という結果になりました。

```
[root@server ~]# yumdownloader --source --destdir /root/.kpatch/tmp kernel-3.10.0-693.el7
読み込んだプラグイン:fastestmirror, langpacks
Enabling epel-source repository
Loading mirror speeds from cached hostfile
 * base: ftp.riken.jp
 * epel: ftp.riken.jp
 * epel-source: ftp.riken.jp
 * extras: ftp.riken.jp
 * updates: ftp.riken.jp
No source RPM found for kernel-3.10.0-693.el7.x86_64
Nothing to download
```

一方、最新(CentOS7.6)のソースRPMをダウンロードしてみると、成功しました。
つまり、yumdownloaderが参照するリポジトリには、最新のRPMしか存在しない、
ということのようです。

```
[root@server ~]# yumdownloader --source --destdir /tmp kernel-3.10.0-957.el7
読み込んだプラグイン:fastestmirror, langpacks
Enabling epel-source repository
Loading mirror speeds from cached hostfile
 * base: ftp.riken.jp
 * epel: ftp.riken.jp
 * epel-source: ftp.riken.jp
 * extras: ftp.riken.jp
 * updates: ftp.riken.jp
No Presto metadata available for base-source
kernel-3.10.0-957.el7.src.rpm                                                                                                         |  96 MB  00:06:05

[root@server ~]# ls -l /tmp/kernel-3.10.0-957.el7.src.rpm
-rw-r--r--. 1 root root 101032927 11月 13 00:31 /tmp/kernel-3.10.0-957.el7.src.rpm
```

そこで、リポジトリの定義を参照してみました。
ソースRPMのリポジトリは、`/etc/yum.repos.d/CentOS-Sources.repo`に定義されています。
baseurlの部分だけを抜き出して、`$releasever`を7に置き換えると、以下のようになります。
ブラウザでbaseurlを参照してみたのですが、CentOS7.4のソースRPMはありませんでした。

```
[base-source]
baseurl=http://vault.centos.org/centos/7/os/Source/
[updates-source]
baseurl=http://vault.centos.org/centos/7/updates/Source/
[extras-source]
baseurl=http://vault.centos.org/centos/7/extras/Source/
[centosplus-source]
baseurl=http://vault.centos.org/centos/7/centosplus/Source/
```

結論として、下記エラーがでて、モジュールの作成ができないのは、
kpatch-buildのバグではないかと思います。

```
ERROR: kpatch build failed. Check /root/.kpatch/build.log for more details.
```
kpatch-buildを実行しているCentOSの版数と同じ版数のソースRPMを
ダウンロードできるようにしなくてはいけないと思います。
`$releasever`の部分を`7`ではなく、`7.4.1708`に置き換えることができれば
上手くいくと思うのだけど。う～ん、どうしよう。。。


#Z 参考情報
[Red Hat Enterprise Linux 7.2 以降における kpatch のガイド](https://access.redhat.com/ja/articles/3132221)
[Linux live kernel patching with kpatch on CentOS 7](http://jensd.be/651/linux/linux-live-kernel-patching-with-kpatch-on-centos-7)
[kpatch - live kernel patching](https://github.com/dynup/kpatch)