fedoraのsystem-upgradeが失敗して復旧させたメモ

この記事はLinuxのカレンダー | Advent Calendar 2022 - Qiitaの8日目の記事です。

会社にあるfedora 36のPCをfedora 37にアップグレードしようと思ってリモートからdnf system-upgradeでアップグレードしかけといたら何かしらの原因でアップグレードに失敗してて、それを復旧させるのに時間くらいかかったので防備録的に🥲

今回起きてた現象

root causeは何かしらの理由でdnf system-upgradeが失敗してたことなんだけど、その理由はもはやわからないのでこれによって発生してた現象のみを書くと次の3点でした。

  1. dnf・rpmコマンドでパッケージのインストールをしようとするときにsegvする(ファイルのdownloadなどは大丈夫だった)。
  2. fc36とfc37が中途半端に混在
  3. selinuxのラベルがおかしくなってた

復旧の流れ

  • リモートから接続できなかったのでまずは物理出社
  • 出社してPCを見てみたら電源が落ちてた
  • まずは電源オン
  • ログインできるし、ログイン画面もFedora 37のgnomeな感じだし、/etc/os-releaseとか/etc/fedora-releaseファイルもfedora 37になってる
  • ひとまずdistro-syncしてみる?と思って実行してみたらrpmファイルのインストールするときにsegvする
  • dnfのrepolistとかは使えるんだけど、インストール処理がだめっぽい
  • rpmはどうよ?と思ったけど、これも同様
  • この環境だけで復帰させるの無理じゃん😭
  • 会社にあるfedoraのライブCDを探したらfedora 32のCDがあったのでこれ使うかー
  • このPCはboot順番変更のメニュー出せるタイプなのかUEFI BIOSで起動順変える必要あるのかとか、どのキー押すのかまったく覚えてない😨
  • しょうがないので、ESC、F11、F12あたりを連打してUEFI BIOSに入れたのでDVDドライブから起動させる
  • ライブ環境が立ち上がったらディスク上にあるLVM領域の/を/mntにマウント
  • ライブCD(fedora 32)のdnfを使って/mntにマウントした環境をアップデートかけよう
  • dnfのオプションには--installroot=/mntと--releasever=37をつける
  • /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-37-x86_64がないと言われるけどfedora 32なんだからそれもそうだね
  • /mnt/etcのほうにもなかった
  • feodra 36のノートPCのほうにはこのファイルはあるけどどうやってファイルを受け渡すか?
  • ノートPCのほうで/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-37-x86_64を~/tmpとかにコピー
  • python -m http.server 8000で簡易的にhttpサーバを立ち上げてライブCDからcurlでファイルダウンロード
  • ダウンロードしたファイルは/etc/pki/rpm-gpg/と/mnt/etc/pki/rpm-gpgの両方に置いた
  • RPM-GPG-KEY-fedora-37-x86_64の問題は解決できたけど、一部のパッケージで検証に失敗する
  • dnfのオプションに--nogpgcheck追加
  • これで/mnt/環境にdnf updateをかけてみる
  • 成功したので/mntにchrootで入ってみる
  • docker-ce-stableとかの3rdパーティのリポジトリは無効にしてfedora.repoとfedora-updatesだけ有効にする
  • chrootで/mntに入ってdnf・rpmコマンドを試したらsegvしなくなってる!が、まだ問題はあった
  • repolistを確認するとリポジトリがfc36を見ている
  • releasever変数が36とされてるようだ
  • dnfのオプションで--releasever=37を付けても良かったんだけど、なんとなくsedでreleasever変数を37に置き換えた
  • distro-syncを試すとsystemdとかgnome-shellが依存関係の問題で削除されるとか言われてエラーになる
  • 元の環境であったdnf・rpmがsegvする問題はなくなったけど、system-upgradeが途中でエラーになったことでfc36とfc37が中途半端に混ざった状態っぽい
  • まあ、dnfは使えるようになったのでライブCD環境を終了して普通に起動してログイン
  • 起動したらyum.repo.dのほうは先ほどと同じ感じでfc37を見るように強制
  • rpm -qaでsystemdのパッケージを確認するとfc36とfc37の両方のパッケージが存在してる
  • dnfだと削除できないのでrpm -eでfc36・fc37が混在してるものはfc36のパッケージを削除
  • 全部消したところでdnfのdistro-sync
  • 成功したのでdnf update --refreshでさらに新しいパッケージへの更新もできた
  • yum.repo.dの3rdパーティのリポジトリも有効にしてupdateかけたり
  • カーネルはfc37版はインストール済みと言われるんだけど/bootにはfc36版しかない
  • カーネルもインストール状況が中途半端になってる感じだけど新しいバージョンがでればそれがインストールされて解決できるのでは?
  • という感じで、これはこのままにしておく
  • リモートからこのPCに入るためにはsshログインできる必要あるけど、ログインできないじゃん😭
  • クライアント側でこんなエラーメッセージが表示される
ssh client loop send disconnect

sshの-vオプションをつけるとさらにこんなメッセージが

openssh pledge filesystem full
  • dfでディスクの使用量見たけど全然余裕あるな
  • inode?と思ったけどこちらも問題なし
  • 検索するとアクセス権限の問題でもこのエラーメッセージは出るらしい
  • journalctlでログを見てみる
  • こんなログがでてるし、まさしくこれが原因でしょ
sshd_selinux_copy_context: setcon failed with permission denided
  • system-upgradeが途中で終わったことでselinuxのラベリングも中途半端になってるんじゃ
  • 今見えてる問題はsshdだけど他にもあるかもしれない
  • ラベリングし直すかということでファイルを作って再起動
touch /.autorelabel
  • ラベリングが終わってシステムが立ち上がったらsshログインしてみる
  • ログイン成功キタ━(゚∀゚)━!
  • virsh、dockerも動くか確認
  • これらもok
  • これでひとまず復旧完了として良いでしょということで復旧作業終了🥲

所感

このレベルの壊れ具合でも復旧させることができるトラブルシューティング能力はあるんだなって思ったけども、めんどくさいから勘弁してほしい。あとはやっぱライブメディアはあると便利。というかなかったら復旧できなかった。今回は再インストール無しに復旧させたけど、fedora 37のライブメディアがあったらクリーンインストールして再セットアップしたかもしれない。そっちのほうが確実だし。家なら最新のfedoraライブメディアは常にあるけど、会社にあったのがfedora 32だったので32をインストールしてから34 -> 36 -> 37という感じにアップデートさせるのも面倒だったというのもある🥲

同じ状況になる人はまずいないと思うけど、個々の現象に関してはこんな対応方法もあるということで防備録でした。

家に帰ったら自分からのクリスマスプレゼントがamazonさんから届いてた🍪

stable kernelにコントリビュートする

この記事はLinux Advent Calendar 2021の18日目の記事です。

stable kernelへのコントリビュート

コントリビュートの方法としては次のような方法があると思います。

  • テストする

  • patchをレビューする

  • patchをバックポートする

  • その他

本記事ではpatchをバックポートする場合について書いてみたいと思います。基本的にはEverything you ever wanted to know about Linux -stable releases — The Linux Kernel documentationを読みましょうというところではありますが😑

patchをバックポートする理由

通常、mainlineに入ったバグ修正のpatchはstable treeに取り込まれますが、何かしらの理由で取り込まれていなかったり、patchが当たらずにエラーになったりして取り込まれないという場合があります。後者の場合は特にpatchの修正が必要になってきます。

patchをバックポートする

git-amを使ってpatchを当ててコンフリクトしたところを直してといった感じで通常通りに修正すればOKです。

stable kernel固有のお作法

バックポートしたpatchの先頭はこのような行が必要です。2行目のcommit hashの行は何パターンか書き方がある感じです。ここでは自分が書いてる書き方です。

From: <Author name><author email>

commit <commit hash>  upstream.

git-amでpatchを当てて、コミットメッセージを修正してcommit hashの行をつけてます。Fromの行についてはコミットメッセージには含めないで大丈夫です。これはgit-send-emailがつけてくれます。

適当なリポジトリを作って流れを試してみる

適当にリポジトリを作って流れを見てみましょう。branchはこんな感じになっています。signed-offに使うメールアドレスや名前は~/.gitconfigとリポジトリ内の.git/configを使い分けてやってます。

masami@moon:~/test-prg$ git branch 
* main
  test-1.y

ここでこんなコミットを作ったとします。この段階ではAuthorはmasami256です。

commit e8757ffa89dc4aeb1dedec7f811b3fecaf4de75a (HEAD -> main)
Author: masami256 <masami256+test@gmail.com>
Date:   Sat Dec 11 19:43:35 2021 +0900

    Add show() to display argv
    
    Add new function show() to display argv values.
    
    Signed-off-by: masami256 <masami256+test@gmail.com>

git-format-patchでtest-1.yブランチ向けにpatchを作ります。

masami@moon:~/test-prg$ git format-patch -1 e8757ffa89dc4aeb1dedec7f811b3fecaf4de75a test-1.y 
0001-Add-show-to-display-argv.patch

できたpatchはこのようになります。

masami@moon:~/test-prg$ cat 0001-Add-show-to-display-argv.patch 
From e8757ffa89dc4aeb1dedec7f811b3fecaf4de75a Mon Sep 17 00:00:00 2001
From: masami256 <masami256+test@gmail.com>
Date: Sat, 11 Dec 2021 19:43:35 +0900
Subject: [PATCH] Add show() to display argv

Add new function show() to display argv values.

Signed-off-by: masami256 <masami256+test@gmail.com>
---
 test.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/test.c b/test.c
index c0e2886..704995c 100644
--- a/test.c
+++ b/test.c
@@ -1,9 +1,13 @@
 #include <stdio.h>
 
-int main(int argc, char **argv)
+void show(char **av)
 {
-       while (*argv)
-               printf("%s\n", *argv++);
+       while (*av)
+               printf("%s\n", *av++);
+}
 
+int main(int argc, char **argv)
+{
+       show(argv);
        return 0;
 }
-- 
2.33.1

ここで、patchが素直に当たらないようにtest-1.yブランチのコードを適当に変えてみましょう。こんな感じにします。

diff --git a/test.c b/test.c
index c0e2886..b36a1ab 100644
--- a/test.c
+++ b/test.c
@@ -2,8 +2,8 @@
 
 int main(int argc, char **argv)
 {
-       while (*argv)
-               printf("%s\n", *argv++);
+       for (int i = 0; i < argc; i++)
+               printf("%s\n", argv[i]);
 
        return 0;
 }

そして、test-1.yブランチから適当なブランチ(ここではtest-1.y-work)を作ってそこでgit-amを実行すると当然patchが当たらないのでエラーになります。

masami@moon:~/test-prg$ git am --reject 0001-Add-show-to-display-argv.patch
Applying: Add show() to display argv
Checking patch test.c...
error: while searching for:
#include <stdio.h>

int main(int argc, char **argv)
{
        while (*argv)
                printf("%s\n", *argv++);

        return 0;
}

error: patch failed: test.c:1
Applying patch test.c with 1 reject...
Rejected hunk #1.
Patch failed at 0001 Add show() to display argv
hint: Use 'git am --show-current-patch=diff' to see the failed patch
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".

--rejectオプションをつけてるので.rejファイルもできてます。

masami@moon:~/test-prg$ cat test.c.rej 
diff a/test.c b/test.c  (rejected hunks)
@@ -1,9 +1,13 @@
 #include <stdio.h>
 
-int main(int argc, char **argv)
+void show(char **av)
 {
-       while (*argv)
-               printf("%s\n", *argv++);
+       while (*av)
+               printf("%s\n", *av++);
+}
 
+int main(int argc, char **argv)
+{
+       show(argv);
        return 0;
 }

それはさておき、0001-Add-show-to-display-argv.patchを適用するようにコードを修正しましょう。。。

masami@moon:~/test-prg$ git am --continue
Applying: Add show() to display argv

コミットメッセージを修正します。

masami@moon:~/test-prg$ git commit --amend -s

こんな感じのコミットメッセージにしました。commit hashを足したのと、最下部のsigned-offは-sオプションでついたもの、その上のfix ~は自分で書いたものです。

commit 870abfd3042fb950e59c4f1ad9e156d0b68df259 (HEAD -> test-1.y-work)
Author: masami256 <masami256+test@gmail.com>
Date:   Sat Dec 11 19:43:35 2021 +0900

    Add show() to display argv
    
    commit e8757ffa89dc4aeb1dedec7f811b3fecaf4de75a upstream.
    
    Add new function show() to display argv values.
    
    Signed-off-by: masami256 <masami256+test@gmail.com>
    [fix conflict to use show()]
    Signed-off-by: Masami Ichikawa <masami256@gmail.com>

test-1.yブランチ向けにgit-format-patchでpatchを再作成します。

masami@moon:~/test-prg$ git format-patch -1 870abfd3042fb950e59c4f1ad9e156d0b68df259 test-1.y
0001-Add-show-to-display-argv.patch
masami@moon:~/test-prg$ cat 0001-Add-show-to-display-argv.patch
From 870abfd3042fb950e59c4f1ad9e156d0b68df259 Mon Sep 17 00:00:00 2001
From: masami256 <masami256+test@gmail.com>
Date: Sat, 11 Dec 2021 19:43:35 +0900
Subject: [PATCH] Add show() to display argv

commit e8757ffa89dc4aeb1dedec7f811b3fecaf4de75a upstream.

Add new function show() to display argv values.

Signed-off-by: masami256 <masami256+test@gmail.com>
[fix conflict to use show()]
Signed-off-by: Masami Ichikawa <masami256@gmail.com>
---
 test.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/test.c b/test.c
index 4f83ff9..704995c 100644
--- a/test.c
+++ b/test.c
@@ -1,9 +1,13 @@
 #include <stdio.h>
 
-int main(int argc, char **argv)
+void show(char **av)
 {
-       for (int i; i < argc; i++)
-               printf("%s\n", argv[i]);
+       while (*av)
+               printf("%s\n", *av++);
+}
 
+int main(int argc, char **argv)
+{
+       show(argv);
        return 0;
 }
-- 
2.33.1

これをgit-send-emailでメールします。

masami@moon:~/test-prg$ git send-email --to=masami256+test@gmail.com ./0001-Add-show-to-display-argv.patch
./0001-Add-show-to-display-argv.patch
(mbox) Adding cc: masami256 <masami256+test@gmail.com> from line 'From: masami256 <masami256+test@gmail.com>'
(body) Adding cc: masami256 <masami256+test@gmail.com> from line 'Signed-off-by: masami256 <masami256+test@gmail.com>'
(body) Adding cc: Masami Ichikawa <masami256@gmail.com> from line 'Signed-off-by: Masami Ichikawa <masami256@gmail.com>'

From: Masami Ichikawa <masami256@gmail.com>
To: masami256+test@gmail.com
Cc: Masami Ichikawa <masami256@gmail.com>
Subject: [PATCH] Add show() to display argv
Date: Sat, 11 Dec 2021 20:37:23 +0900
Message-Id: <20211211113723.79728-1-masami256@gmail.com>
X-Mailer: git-send-email 2.33.1
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

    The Cc list above has been expanded by additional
    addresses found in the patch commit message. By default
    send-email prompts before sending whenever this occurs.
    This behavior is controlled by the sendemail.confirm
    configuration setting.

    For additional information, run 'git send-email --help'.
    To retain the current behavior, but squelch this message,
    run 'git config --global sendemail.confirm auto'.

Send this email? ([y]es|[n]o|[e]dit|[q]uit|[a]ll): 

まず、e[dit]でメールのサブジェクトを変更します。

From 870abfd3042fb950e59c4f1ad9e156d0b68df259 Mon Sep 17 00:00:00 2001
From: masami256 <masami256+test@gmail.com>
Date: Sat, 11 Dec 2021 19:43:35 +0900
Subject: [PATCH for test-1.y] Add show() to display argv

commit e8757ffa89dc4aeb1dedec7f811b3fecaf4de75a upstream.

Add new function show() to display argv values.

Signed-off-by: masami256 <masami256+test@gmail.com>
[fix conflict to use show()]
Signed-off-by: Masami Ichikawa <masami256@gmail.com>

この時点では送信するpatchのコミットメッセージはオリジナルのままです。 変更したらエディタを終了してメニューに戻ります。そして、y[es]で送信しましょう。

(mbox) Adding cc: masami256 <masami256+test@gmail.com> from line 'From: masami256 <masami256+test@gmail.com>'
(body) Adding cc: masami256 <masami256+test@gmail.com> from line 'Signed-off-by: masami256 <masami256+test@gmail.com>'
(body) Adding cc: Masami Ichikawa <masami256@gmail.com> from line 'Signed-off-by: Masami Ichikawa <masami256@gmail.com>'

From: Masami Ichikawa <masami256@gmail.com>
To: masami256+test@gmail.com
Cc: Masami Ichikawa <masami256@gmail.com>
Subject: [PATCH for test-1.y] Add show() to display argv
Date: Sat, 11 Dec 2021 20:37:24 +0900
Message-Id: <20211211113723.79728-1-masami256@gmail.com>
X-Mailer: git-send-email 2.33.1
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

Send this email? ([y]es|[n]o|[e]dit|[q]uit|[a]ll): y
OK. Log says:
Server: smtp.gmail.com
MAIL FROM:<masami256@gmail.com>
RCPT TO:<masami256+test@gmail.com>
RCPT TO:<masami256@gmail.com>
From: Masami Ichikawa <masami256@gmail.com>
To: masami256+test@gmail.com
Cc: Masami Ichikawa <masami256@gmail.com>
Subject: [PATCH for test-1.y] Add show() to display argv
Date: Sat, 11 Dec 2021 20:37:24 +0900
Message-Id: <20211211113723.79728-1-masami256@gmail.com>
X-Mailer: git-send-email 2.33.1
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

Result: 250 

届いたメールを確認するとこのようになっていて、Fromの行が先頭に追加され、ここはオリジナルのAuthorが設定されているのがわかります。

f:id:masami256:20211211203912p:plain
mail

ここまでの手順をstable kernelの場合も行うことになります。

メーリングリストにpatchを送る

git-format-patchでpatchが作れたらあとはメールを送るだけです。もし、バックポート対象のpatchがバックポートしたいブランチに当てるのに失敗していた場合はloreの該当メールにgit-send-emailでpatchを送る方法が書かれています。こんな感じで。なので指示の通りにメールを送りましょう。そうでない場合はmainlineにpatchを送るときと同様に必要な宛先を調べてto、ccをセットしましょう。

まとめ

この記事ではstable kernelにpatchをバックポートする流れを紹介してみました。どんなpatchをバックポートするかですが、自分は CIP Kernel Team: Helping CIP Sustain Industrial Grade Systemsのkernel teamの作業の一環として脆弱性の修正をバックポートする感じでやってます。

Linux 5.14.4のregressionがどんな感じだったのか調べる

Linux 5.14.4のリリースして同日にregressionが報告されて次の日には5.14.5がリリースされてたんですが、これがどんなバグだったのかなというのを調べたメモです。

バグ報告の内容としては5.14.4でNextcloudっていうphpのwwebアプリケーションを実行するとハングアップするということでした。このときにbisectも行われていて、[PATCH 5.14 011/334] posix-cpu-timers: Force next expiration recalc after itimer resetがbad commitというところまで判明してました。スレッドではpatchの作者さんによる修正patchを送ったよというメールもありますが、5.1.4.5では関連するpatchのrevertにて対応されています。

5.14.5でrevertされたのは2つのpatchでした。5.14.5はこのregressionの修正のみのリリースでした。

まずbad commitのほうでこれはposix-cpu-timers: Force next expiration recalc after itimer resetですね。こちらはこのようなpatchでした。

diff --git a/kernel/time/posix-cpu-timers.c b/kernel/time/posix-cpu-timers.c
index 517be7fd175e..a002685f688d 100644
--- a/kernel/time/posix-cpu-timers.c
+++ b/kernel/time/posix-cpu-timers.c
@@ -1346,8 +1346,6 @@ void set_process_cpu_timer(struct task_struct *tsk, unsigned int clkid,
            }
        }
 
-       if (!*newval)
-           return;
        *newval += now;
    }

もう一つはtime: Handle negative seconds correctly in timespec64_to_ns()です。こちらはこんな感じのpatchでした。

--- a/include/linux/time64.h
+++ b/include/linux/time64.h
@@ -25,7 +25,9 @@ struct itimerspec64 {
 #define TIME64_MIN            (-TIME64_MAX - 1)
 
 #define KTIME_MAX         ((s64)~((u64)1 << 63))
+#define KTIME_MIN          (-KTIME_MAX - 1)
 #define KTIME_SEC_MAX         (KTIME_MAX / NSEC_PER_SEC)
+#define KTIME_SEC_MIN          (KTIME_MIN / NSEC_PER_SEC)
 
 /*
  * Limits for settimeofday():
@@ -124,10 +126,13 @@ static inline bool timespec64_valid_sett
  */
 static inline s64 timespec64_to_ns(const struct timespec64 *ts)
 {
-   /* Prevent multiplication overflow */
-   if ((unsigned long long)ts->tv_sec >= KTIME_SEC_MAX)
+   /* Prevent multiplication overflow / underflow */
+   if (ts->tv_sec >= KTIME_SEC_MAX)
        return KTIME_MAX;
 
+   if (ts->tv_sec <= KTIME_SEC_MIN)
+       return KTIME_MIN;
+
    return ((s64) ts->tv_sec * NSEC_PER_SEC) + ts->tv_nsec;
 }

patchだけだとわかりにくいので全体を見てみましょう。。。まずはset_process_cpu_timer()から。revert後の5.14.5ではこんな関数です。

void set_process_cpu_timer(struct task_struct *tsk, unsigned int clkid,
               u64 *newval, u64 *oldval)
{
    u64 now, *nextevt;

    if (WARN_ON_ONCE(clkid >= CPUCLOCK_SCHED))
        return;

    nextevt = &tsk->signal->posix_cputimers.bases[clkid].nextevt;
    now = cpu_clock_sample_group(clkid, tsk, true);

    if (oldval) {
        /*
        * We are setting itimer. The *oldval is absolute and we update
        * it to be relative, *newval argument is relative and we update
        * it to be absolute.
        */
        if (*oldval) {
            if (*oldval <= now) {
                /* Just about to fire. */
                *oldval = TICK_NSEC;
            } else {
                *oldval -= now;
            }
        }

        if (!*newval)
            return;
        *newval += now;
    }

    /*
    * Update expiration cache if this is the earliest timer. CPUCLOCK_PROF
    * expiry cache is also used by RLIMIT_CPU!.
    */
    if (*newval < *nextevt)
        *nextevt = *newval;

    tick_dep_set_signal(tsk, TICK_DEP_BIT_POSIX_TIMER);
}

↓の部分がpatchが変更する箇所ですね。

     if (!*newval)
            return;
        *newval += now;

patchではif文のところを消して常にnewvalが指す値にnowの値を足すようになってます。元のコードではnewvalの指す値が0なら何もしないでreturnするんですが、patchではif文を消してるので常にその先が実行されるようになりますね。そうすると、今までは呼ばれることがなかったtick_dep_set_signal()が実行されるようになって本来送る必要のないシグナルを送ってしまうと修正patchのコミットメッセージにも書かれています。これがregressionとして現れてたんですかねぇ。

もう一つのrevertはtime: Handle negative seconds correctly in timespec64_to_ns()です。こちらの変更箇所はtimespec64_to_ns()です。これもrevert後のコードを見るとこんな感じになってます。

static inline s64 timespec64_to_ns(const struct timespec64 *ts)
{
    /* Prevent multiplication overflow */
    if ((unsigned long long)ts->tv_sec >= KTIME_SEC_MAX)
        return KTIME_MAX;

    return ((s64) ts->tv_sec * NSEC_PER_SEC) + ts->tv_nsec;
}

patchでの変更は次のようになっています。

-    /* Prevent multiplication overflow */
-   if ((unsigned long long)ts->tv_sec >= KTIME_SEC_MAX)
+   /* Prevent multiplication overflow / underflow */
+   if (ts->tv_sec >= KTIME_SEC_MAX)
        return KTIME_MAX;
 
+   if (ts->tv_sec <= KTIME_SEC_MIN)
+       return KTIME_MIN;
+

timespec64構造体のtv_secはtime64_tでこれは__s64のtypedefとなってます。ということでtv_secは符号ありの64bit整数値です。ということで、コミットメッセージにも書いてますが、もともとはs64の値をunsigned long long(64bitの符号なし整数ですね)にキャストしてチェックしているので負の整数の場合にKTIME_MAXを返すことになりますと。で、patchでは符号ありの整数としてKTIME_SEC_MAXを超えたり、KTIME_SEC_MIN以下になる場合には上限値や下限値を返す感じにしています。ここだけ見るとそうだなと思うんですが、今までと返る値が変わることによる影響もあったんでしょうか。。。

いずれにせよ、5.14.5では2つのpatchのrevertが行われてregressionが修正されたということでした。

Raspberry Piのdtoverlay・dtparam、dtbそしてブートプロセスのメモ

 はじめに

この記事はRaspberry Pi 3B+の実際の挙動と公式のドキュメントから大体こんな感じだろうというところで書いてるので正確さは期待しないでください。

下図のような構成でLinux kernel(Raspberry Pi向けのカーネルじゃなくて、mainlineとかstable treeのカーネル)を使うときにdtoverlay・dtparamを使う方法を調べたメモです。なので、自分でビルドしたカーネルを使う必要がなければRaspberry Pi OSとかmeta-raspberrypiを使うのが良いかと思います。

-> Raspberry Piのブートローダー  
  -> u-boot 
    -> Linux kernel

Linux kernel source とdtbファイル

mainlineのカーネルにはbcm2837-rpi-3-b-plus.dts等のdtsファイルがあるのでこれをビルドして使えばmainlineのカーネルでもRaspberry Piで動きます。だがしかし、config.txtでdtoverlay・dtparamを使うのはできませんでした。なんでかというと、Raspberry Piカーネルとmainlineのカーネルにあるdtsファイルを見比べるとRaspberry Piカーネルのほうにはoverridesがあって設定をoverrideできるようになってるんですね。例えばarm/boot/dts/bcm2710-rpi-3-b-plus.dtsだとこんな感じで設定があります。

/ {
    __overrides__ {
        act_led_gpio = <&act_led>,"gpios:4";
        act_led_activelow = <&act_led>,"gpios:8";
        act_led_trigger = <&act_led>,"linux,default-trigger";

        pwr_led_gpio = <&pwr_led>,"gpios:4";
        pwr_led_activelow = <&pwr_led>,"gpios:8";
        pwr_led_trigger = <&pwr_led>,"linux,default-trigger";

        eee = <&eth_phy>,"microchip,eee-enabled?";
        tx_lpi_timer = <&eth_phy>,"microchip,tx-lpi-timer:0";
        eth_led0 = <&eth_phy>,"microchip,led-modes:0";
        eth_led1 = <&eth_phy>,"microchip,led-modes:4";
        eth_downshift_after = <&eth_phy>,"microchip,downshift-after:0";
        eth_max_speed = <&eth_phy>,"max-speed:0";
    };
};

上記のブロックはmainlineのほうにはありません。そんなわけでdtoverlay・dtparamを手軽に使うならdtbファイルはブートローダーなんかと一緒に配布されてるdtbファイル(firmwareのリポジトリ)を使いましょうという感じです。

dtoverlay・dtparamはどのように処理されてるか

Raspberry Piブートローダーが自身のデバイスに合う適切なdtbファイルを読んでくれます(公式ドキュメントのどこかにそんなことが書いてありました)。そして、config.txtに記載されてるdtoverlay・dtparamの記述に沿ってメモリ上に読み込まれてるdtbファイルのデータを更新します。そしてそのメモリ上で更新したdtbファイルを使うという流れです。

u-bootをブートローダーとして使いたい場合

Raspberry Piブートローダーはすでにdtbファイルを読み込んでdtoverlay・dtparamの設定もメモリ上で反映させてます。そのため、u-bootがfatloadとかして自分でdtbファイルを読んじゃうと意味がありません。なので、config.txtでdtbファイルを読み込むアドレスを指定し、u-bootのほうはそのアドレスからdtbファイルを読む感じにします。例えばconfig.txtで下記のように読み込むアドレスを指定しておきます。

device_tree_address=0x02600000

u-bootのほうはこんな感じでfdtコマンドを使ってconfig.txtで指定したメモリアドレスを利用します。

setenv fdt_addr_r 0x02600000
fdt addr ${fdt_addr_r}

こんな感じにすればRaspberry Piブートローダーが設定したdtbファイルをu-bootから読めて、それを更にカーネルの起動に利用することができます。

fanotify(7)めも

fapolicydがどのようにアプリケーションの実行を禁止しているのだろうか?と思って調べためもです。

fapolicydがアプリケーションの実行を禁止する仕組み

仕組みとしてはfanotifyの仕組みを使ってファイルが実行のために開かれた場合に通知を受け取り、設定されたルールを調べて実行可能かどうかを返すということをしてます。

fanotify

fanotifyの機能を使うには2つの関数があります。fanotify_init(2)fanotify_mark(2)の2つです。fanotify_init(2)で初期化をしてファイルディスクリプタを得ます。次にfanotify_mark(2)で通知を受け取りたいイベントなどを設定します。イベントの受信はpoll(2)を使います。pollfd構造体の配列のうち1つはfanotify_init(2)によって初期化したfdを設定します。

実行の許可

fanotify_response構造体のresponse変数にFAN_ALLOWもしくはFAN_DENYの値を設定することでその後の処理の継続を許可するか拒否するか設定します。レスポンスはfdに対して書き込みます。

サンプル

fanotify(7)にあるサンプルコードベースに作ってみました。実行するアプリケーションが/tmp/lsの場合だけ許可しません。

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fanotify.h>
#include <fcntl.h>
#include <poll.h>
#include <errno.h>
#include <limits.h>
#include <unistd.h>

static void handle_events(int fd)
{
    struct fanotify_event_metadata *metadata;
    struct fanotify_event_metadata buf[256];
    struct fanotify_response response;
    ssize_t len;

    while (1) {
        len = read(fd, buf, sizeof(buf));
        if (len == -1 && errno != EAGAIN) {
            perror("read()");
            exit(-1);
        }

        if (len <= 0)
            break;

        metadata = buf;

        while (FAN_EVENT_OK(metadata, len)) {
            if (metadata->vers != FANOTIFY_METADATA_VERSION) {
                fprintf(stderr, "fanotify version mismatch\n");
                exit(-1);
            }

            if (metadata->fd >= 0) {
                if (metadata->mask & FAN_OPEN_EXEC_PERM) {
                    char *proc_path, path[PATH_MAX] = { '\0' };
                    ssize_t path_len;
                    
                    asprintf(&proc_path, "/proc/self/fd/%d", metadata->fd);
                    path_len = readlink(proc_path, path, sizeof(path) - 1);
                    if (path_len == 1) {
                        perror("readlink()");
                        exit(-1);
                    }

                    if (!strcmp(path, "/tmp/ls")) {
                        printf("[+]Deny execute application %s (pid:%d)\n", path, metadata->pid);
                        response.response = FAN_DENY;
                    } else
                        response.response = FAN_ALLOW;

                    response.fd = metadata->fd;
                    write(fd, &response, sizeof(response));

                    free(proc_path);
                }
            }

            close(metadata->fd);
            metadata = FAN_EVENT_NEXT(metadata, len);
        }
    }
}

int main(int argc, char **argv)
{
    int fd;
    int poll_num;
    struct pollfd fds[2];
    
    fd = fanotify_init(FAN_CLOEXEC | FAN_NONBLOCK | FAN_CLASS_CONTENT,
        O_RDONLY | O_LARGEFILE | O_CLOEXEC | O_NOATIME);
    
    if (fd == -1) {
        perror("fanotify_init()");
        exit(-1);
    }

    if (fanotify_mark(fd, FAN_MARK_ADD | FAN_MARK_MOUNT,
        FAN_OPEN_EXEC_PERM, -1, "/tmp") == -1 ) {
        perror("fanotify_mark()");
        exit(-1);
    }

    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;
    fds[1].fd = fd;
    fds[1].events = POLLIN;

    printf("[+]Press ctrl-c to quit program\n");

    while (1) {
        poll_num = poll(fds, 2, -1);
        if (poll_num == -1) {
            if (errno == EINTR)
                continue;
            perror("poll()");
            exit(-1);
        }

        if (poll_num > 0) {
            if (fds[1].revents & POLLIN)
                handle_events(fd);
        }
    }

    return 0;
}

このコードを動かすとこんな感じです。

masami@kerntest:~/prevent_exec$ sudo ./a.out
[+]Press ctrl-c to quit program
[+]Deny execute application /tmp/ls (pid:3058)

実行を止められる側はこんな感じ。

masami@kerntest:~/prevent_exec$ /tmp/ls2
a.out  prevent_exec.c
masami@kerntest:~/prevent_exec$ /tmp/ls
-bash: /tmp/ls: Operation not permitted

Linuxプログラミングインタフェース

Linuxプログラミングインタフェース

マイペースなOSSコントリビュート生活 fedora編

この記事はLinux Advent Calendar 2020の17日目の記事です。

OSSのコントリビュート方法はpatchを書いたりドキュメントを翻訳したり、バグレポートしたりと色々な方法がありますね。fedoraでテストというとリリース前のベータ版のテスト、パッケージがアップデートされた際のテスト(QA:Updates Testing - Fedora Project Wiki)、テストイベントのTest Days(QA/Test Days - Fedora Project Wiki)などがあります。

updates-testing

このなかで手軽なものとしてはupdates testingがあります。fedoranはパッケージがアップデートされる場合、最初にupdates-testingリポジトリに更新されたパッケージが入ります。その後、基本的には以下のルールに則ってパッケージがupdateリポジトリに入るか更新がリジェクトされるかのいずれかになります。

By default, test updates with karma of 3 are automatically sent out as full official updates, while test updates with karma of -3 are automatically withdrawn from the testing repository. 

テストした人が実際にパッケージを使ってみて問題なかった・修正されたと書かれているバグが直ってたなんてことを確認したりしてみて結果をフィードバックします。これはコマンドラインのアプリでも出来ますし、bodhiと呼ばれるWebインターフェースもあります。

bodhi.fedoraproject.org

bodhiのパッケージの画面ではこのような感じでチェック項目があります。これはkernel-5.9.14-200.fc33のときのものです。バグフィックスに関しては自分の環境だと再現できないとかあると思うのでそういう場合はグレーの👍を選択します。問題が合ったときは👎を選択し、特に問題なかったら緑の👍を選択する感じです。

f:id:masami256:20201227232230p:plain

テストの内容はカーネルの例のように指定されている場合もありますし、無い場合もあります。基本的には指定されたテストケース以外のことについては実際に使ってみてOKかどうかってところでみんなやってると思います。全部のアップデートをテストしようとか気張らずに自分がよく使ってるものだけでも簡単にやろうくらい気軽な感じでできます😊 セキュリティアップデートもupdates-testingリポジトリに最初に入るので更新して使ってみた結果のフィードバックをするのって割と大事かななんて思います。

Test Days(Test Week)

こちらはカーネルGnomeなんかの主要なパッケージのメジャーバージョンが上がるような時に行われることが多いです。カーネルですとfedoraはリリース後にもカーネルのメジャーバージョンは上がりますのでイベントに参加して自分の環境で問題ないか確認しておくのは良いかもしれません。Test Daysではiosイメージ、リリースしているfedoraのバージョン用のrpmファイルが用意されますのでお好きな方法を選択できます。カーネルだとテスト内容はupdates-testingと一緒です。こちらも割と手軽ですが通常のupdates-testingではなく専用のイベントが用意されるのでシステム的にインパクトの大きな変更が入るということなので場合によっては自分の環境だと問題があるってこともあると思います。まあ、そういうのを見つけるためのイベントなわけですが😃

カーネルのTest Weekで自分が実際にバグに当たってupstreamに報告とパッチを投げたり・テストしたり、fedoraで修正されたことをupdates-testingで確認したときのログが↓ですので興味がある人は読んでみてください🙋 kernhack.hatenablog.com


fedoraに限らず使ってみて結果をフィードバック(特にバグレポート)するというのは重要な貢献だと思いますので、OSSに貢献したいけど何をすれば良いか🤔と考えている人は新しいバージョンを使ってみて結果をフィードバックするというのも検討してみてはいかがでしょうか👍

OSSライセンスの教科書

OSSライセンスの教科書

1999年発行(日本語翻訳版)のLinuxカーネル解説本を見返してみる

この記事はLinux Advent Calendar 2020の24日目の記事です。

自分がLinuxを使いだした頃、OSはどうやってブートしてんるんだろう🤔とか色々知りたくて買ったのが↓の本です。

f:id:masami256:20201225194100j:plain
Linuxカーネルインターナル

買った当時は知識がなくて読むのが大変だったけど今はそれなりに読めるようになり、さすがに成長したなとも思ってみたり😃 初版が1999年6月25日と書いてあるので2000年台の始めの頃に買ったんでしょう。それにしても翻訳版の初版が発行された時点からでも21年前の本ということか。

この本が対象としているのはLinux 2.0です。目次は次のようになってます。メモリ管理、ファイルシステム、モジュール、ネットワーク等々ありますね。基本的な機能は変わってないので現在のカーネルでこの目次の内容に沿ってコードを読んでみるというのは面白いかもしれません。この記事を書いている時点での最新のLinuxのバージョンは5.10.2です。本で説明されてるこの構造体は今でもあるのかな?と思ったときは5.10.2で調べてます。本書は付属としてLinux 2.0のソースコード入りCDが付いてます。

序文

本書の序文はLinusさんが書いています。「OSの開発は今も昔もエキサイティングなプロジェクトだ。でも、カーネルの内部を詳しく知ろうとするとドキュメントが足りないのが難点で、ハックしようと思ったらコードを読むしかなかった。それは良いことなんだけど、本書のようにLinuxの使用とカーネルにつて説明する本書のようなドキュメントが出たのは良いね」といった感じのことが書かれてます。

はじめに

本書は著者さんによる同名の著書の第2版で、Linux2.0をカバーしていると書いてあるので第1版は1.xの解説をしてたということなんでしょう。本書の方針として上げている「Linux はオープンなOS。隠された秘密は何もない。問題に遭遇したらソースコードに当たれば良い」ってのが個人的に好きです。

第1章 Linuxオペレーティングシステム

Linuxがどんなものかの説明です。機能や、Linuxディストリビューションとは?といったことの説明が書かれています。

第2章 カーネルコンパイル

カーネルハックをする上でカーネルコンパイルは必須ですからね。最近のカーネルに関しては@progrunner17さんによるLinuxカーネルビルド大全が詳しいです。本書ではカーネルのコードの場所は/usr/src/linuxを使用しています。最近は任意なところにgitでコードをクローンしてビルドできますが当時は/usr/src/linuxが標準だったんですね。この章ではこのディレクトリにはこの機能のコードがあるよなんてのも軽く解説しています。本章ではカーネルコンパイル方法、i386以外のarchを使いたい場合のMakefileの変更方法なんかも説明されてます。

第3章 カーネルの概要

章の流れとしてはカーネルとはなにか?といったところの説明、プロセスとタスク、システムコール、割り込みルーチン、プロセスとスレッドなどの用語の説明カーネルのデータ構造、シグナル、割り込み、カーネルの最初のc関数、スケジューラ、システムコールの実装(仕組み、fork()などいくつかのシステムコールの説明)など盛りだくさんです。この章で説明されている構造体のtask_struct、mm_struct、fs_struct、inode、fileなどは今でも重要な立ち位置ですね。それとは逆にプロセスを登録するプロセステーブルは今ではもうありませんね。メモリの確保・解放だと__get_free_pages()、kmalloc()、kfree()なんかも今でも使われてますね。

第4章 メモリ管理

Linux 2.0の時点で複数のアーキテクチャに対応しているのでまずはアーキテクチャ非依存のモデルについて説明しています(仮想アドレス空間など)。次はi386のページテーブルの説明がきて、LinuxのPGD・PMD・PTEの説明になります。そしてvm_area_struct構造体(これも今もありますね)、brkシステムコールmmapシステムコールの説明ときて、カーネルが使うセグメント(x86のセグメント)の説明があります。つぎにkmallo()・kfree()やvmalloc()・vfree()の説明です。この次にバッファキャッシュの説明があります。ここで説明されているbuffer_head構造体もいまでもあります。bdflushの説明もあります。バッファキャッシュの管理・利用方法の説明もここで行っています。あとはswap処理、空きページの管理(今でもページの管理に使うpage構造体はこの当時からあります)。最後にページフォルトの処理を説明して本章は終わりです。

第5章 プロセス間通信

まずはカーネル内の処理で同期を取る方法の説明があります。キューを使って寝て待って、処理が終わったら起こしてもらったり、セマフォを使うなどの説明です。fcntl(2)によるファイルのロックの処理についても説明されてます。あとはpipeの実装、ptrace(2)の仕様、System V IPC、共有メモリ、Unixドメインソケットを使ったプロセス間通信などです。この辺りの機能は今でも使われますね。

第6章 Linuxファイルシステム

VFSの説明、カーネルでのファイルシステムの表現、マウント処理、スーパーブロックの処理(super_operations構造体に含まれる各種関数の説明)、inodeの処理(inode_operations構造体に含まれる各種関数の説明)、file構造体の説明、ファイル操作(file_operations構造体に含まれる各種関数の説明)、ファイルを開くときの処理、procファイルシステムext2ファイルシステムのデータ構造、ブロックへの配置アルゴリズムなどの説明があります。ジャーナリングファイルシステムについての説明はさすがにないですが今だと当たり前にある機能とも言えるので現在カーネルの解説本を書くなら必須項目になるんでしょうね。

第7章 Linuxデバイスドライバ

ブロックデバイス・キャラクターデバイス、メジャー・マイナー番号の説明があり、割り込み・ポーリングの説明、ハードウェアの検出などの説明があります。ドライバの実装の説明としてはPC内蔵スピーカーを例にしたりしてます。デバドラの実装の説明では初期化、open・read/write・ioctlの実装方法など説明してます。また、DMAの操作も説明してます。

第8章 ネットワークの実装

ネットワークの実装ではsk_buff構造体の説明、proto構造体の説明など構造体の説明の他に、ネットワークデバイスの実装の説明があります。プロトコルの説明としてはarp、ipの説明があります。ルーティングやパケットフィルタについても説明されています。そして、tcpの説明ももちろんあります。

第9章 モジュールとデバッグ

カーネルモジュールの説明です。モジュールがどのようにロードされるか、モジュールのタイプ別にどのような関数を実装するべき(ファイルシステムならregister_filesystem()と言った感じで)かといった説明などあります。 モジュールの例としてはPCMCIAのモジュールを使って説明してます。デバッグでは今でも使われるprintkデバッグgdbを使ったデバッグ方法について触れられています。printkデバッグでは最良のデバッガ:printk()と書かれています。この当時だとカーネルデバッグというとprintk()というのが主だったんでしょうね。カーネルのビルド・再起動に時間はかかるけど手軽さは確かにありますが今ならftrace、perf、eBPFなどがお薦めの手段でしょうか。

第10章 マルチプロセッシング

この章ではSMPの実装におけるカーネルの初期化、cpu間の通信、割り込み処理についての説明があります。smp対応のカーネルコンパイルする場合はMakefileSMP = 1の部分のコメントを削除してねって説明がありますが、時代を感じるところですね。

Appendix A システムコール

この章は色々なシステムコールについての説明です。例えばfork()ならカーネルのどのファイルにこのシステムコールの実装が合って、プロトタイプはこうでシステムコールの仕様はこんなんだよといった感じの説明があります。カーネルよりなmanページと言った感じでしょうか。システムコールはプロセス管理系、ファイルシステム系、プロセス間通信系、メモリ管理系というような感じでカテゴライズされています。

Appendix B カーネル関連のコマンド

manのセクション8というかpsだったりfreeだったりといったコマンドの説明があります。また、pid1のinitプロセスについても説明があります。/etc/inittabの説明もあります。ほかにはstrace、shutdown、ifconfig、traceroute、mountコマンドのなどの説明があります。

Appendix C Procファイルシステム

/procにあるファイルの説明です。14ページほどあるので結構なボリュームですよね。

Appendix D ブートプロセス

BIOSから始まるブートプロセスの説明です。ブートローダーのLILOについての説明もあります。当時はブートローダーといえばLILOでしたね。カーネルの再構築後にLILOのコマンド忘れて起動できないとかよくやったもんです😭 LILOの説明もファイルの書式、オプション、エラーメッセージの意味、起動時の表示メッセージの意味などの説明がありかなり親切です。LIで止まっている場合の原因はXXXだよみたいな説明があってホント親切だと思います。

Appendix E 重要なカーネル関数

カーネルのコードを書く時に使う関数の説明ですmanのセクション9に近いでしょうか。


というわけで1999年発行のLinuxカーネル解説本を見返してみたわけですが、基本的な機能は変わってないのでというかメモリ管理とかファイルシステムは今でも重要な機能ですが実装についてはだいぶ変わっているのでその辺の差分を見ていくとカーネルの進化の歴史を辿れますね。

この目次の内容でLinux 5.10版も見てみたいですね👍