新Linuxカーネル解読室 - カーネルモジュール

「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。
それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。
世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。

本稿では、カーネルモジュール機能実装の概要について説明します。

執筆者 : 高橋 浩和

※「新Linuxカーネル解読室」連載記事一覧はこちら ※ 本稿のサンプルコードは github にて公開しています。

はじめに

Linuxカーネルは一部機能を別オブジェクトのカーネルモジュールとして生成し、Linuxカーネル本体起動後に、必要に応じてそれらモジュールを組込むという方式を採用しています。このカーネルモジュール機能は、Linuxカーネルの開発が始まった初期から使われている古い機能です。

元々、カーネルモジュールを組込む処理の殆どはユーザランドinsmodコマンドの中で行われていました。現在のLinuxカーネル(kernel 6.8)では、それらの機能全てがカーネル機能として実現されています。

今回は、このカーネルモジュール機能を見て行きます。

1. 動かしてみよう

解説用の例題として簡単なカーネルモジュールを用意しました。github.comからダウンロードしてください。kmoduleディレクトリ配下に小さなカーネルモジュールkaidock用のCソースファイル1つと、ビルド用のMakefileが置いてあります。カーネルモジュールkaidockは、1秒毎にjiffies変数*1の値を表示します。

kaidockは非常に短かいプログラムです。kaidockモジュールを組込むとinit_module関数が呼びだされ、その中でタイマkaidock_timerがHZ時間後に満了時間を迎えるように登録しています(add_timer関数)。HZ時間経過後に呼び出されたkaidock_timerはkaidock_func関数を実行し、jiffiesの値をコンソールに表示します。 kaidock_func関数は、HZ時間後に再度タイマkaidock_timerが満了時間を迎えるように登録仕直します(mod_timer関数)。

タイマkaidock_timerの起動回数は、カーネルモジュール起動引数loopにて変更できます(module_param関数)。ライセンスはGPLとしました。

#include <linux/module.h>
#include <linux/kernel.h>

static int loop = INT_MAX;
module_param(loop, int, 0644);
MODULE_PARM_DESC(loop, "specify the loop count");

static void kaidock_func(struct timer_list *timer);

DEFINE_TIMER(kaidock_timer, kaidock_func);

static void kaidock_func(struct timer_list *timer)
{
        printk(KERN_ERR "%lx\n", jiffies);
        if (--loop > 0) {
                do {
                        mod_timer(timer, timer->expires + HZ);
                } while ((long)(timer->expires - jiffies) <= 0L);
        }
}

int init_module(void)
{
        kaidock_timer.expires = jiffies + HZ;
        add_timer(&kaidock_timer);
        return 0;
}

void cleanup_module(void) {
        del_timer_sync(&kaidock_timer);
}

MODULE_AUTHOR("taka");
MODULE_LICENSE("GPL");

1.1 ビルドする

Ubuntu 24.04 LTS環境での動作確認をしています。kaidockをダウンロードしたディレクトリにてビルドします。現在動作しているカーネル用のカーネルモジュールとしてコンパイルされます。*2

$ cd kmodule
$ make -C /lib/modules/`uname -r`/build M=`pwd` modules

1.2 組込む

kaidockカーネルモジュールを組込みます。前もって利用しているPCのセキュアブート機能を切っておくことが必要です。もしくは、kaidockカーネルモジュールに署名を付ける必要があります。署名手順は、「おまけ ~ カーネルモジュールへの署名」を参照してください。

カーネルモジュール起動引数loopにて、jiffiesの表示回数を指定することができます。コンソール画面の右上の方にjiffiesの値が緑色の文字で周期表示されます。

$ sudo insmod kaidock.ko loop=100

   jiffies:0x1113dac00

insmodを行なった端末に表示されない場合、どこかにあるコンソールに表示されています。下記のようにsyslogの内容を表示してください。

$ sudo tail -f /var/log/syslog 

組込まれているカーネルモジュールを表示します。kaidockカーネルモジュールが組込まれていることが確認できます。

$ lsmod
Module                  Size  Used by
kaidock                12288  0
cpuid                  12288  0
tls                   151552  0
xt_conntrack           12288  1
nft_chain_nat          12288  3
xt_MASQUERADE          16384  1
nf_nat                 65536  2 nft_chain_nat,xt_MASQUERADE
nf_conntrack          200704  3 xt_conntrack,nf_nat,xt_MASQUERADE
nf_defrag_ipv6         24576  1 nf_conntrack
nf_defrag_ipv4         12288  1 nf_conntrack
       :
       :

1.3 停止させる

kaidockカーネルモジュールを停止させます。

$ sudo rmmod kaidock

2. 動作の概要

カーネルモジュールは、リロケータブルオブジェクト(再配置可能オブジェクト)です。xyz.cのコンパイル途中に生成される xyz.o ファイルと基本的に同じです。

  • 割り付けアドレスが未解決。モジュール内の関数や変数の割り付くアドレスが決定していない。
  • シンボルの外部参照が未解決。 カーネルモジュールの場合、カーネル本体や他のカーネルモジュールのシンボル(関数や変数)を参照する命令が、未解決状態になっている。

カーネルモジュールを組込むためには下記の操作が必要です。

  1. カーネルモジュールを組み込むためのカーネルメモリ領域を確保し、カーネルモジュールを読み込む
  2. カーネルと既に動作中のカーネルモジュールのシンボル情報取得(シンボル名とアドレスが組になった情報)
  3. 2.の情報を元に、カーネルモジュールの未解決アドレス、未解決シンボルを解決する
  4. 組み込んだカーネルモジュールを有効にする

現在のLinuxでは、これらの処理は全てカーネルが担っています。なんと、カーネルにリンカの機能まで組込まれています。

リンク処理の結果がどうなるか、リロケート前のカーネルモジュールオブジェクト(kaidock.ko)のコードとリンク後(カーネルへの組み込み後)のカーネルモジュールのコードを比較します。

リロケート前のオブジェクトkaidock.koのkaidoc_func関数は下記のようにアドレスが解決されていない状態にあります。

$ objdump -d kaidock.ko
        :
        :
   000000000000010 <kaidock_func>:
 (x)  10:   e8 00 00 00 00          call   15 <kaidock_func+0x5>
      15:   55                      push   %rbp
 (a)  16:   48 8b 35 00 00 00 00    mov    0x0(%rip),%rsi        # 1d <kaidock_func+0xd>
      1d:   48 89 e5                mov    %rsp,%rbp
      20:   53                      push   %rbx
      21:   48 89 fb                mov    %rdi,%rbx
      24:   48 c7 c7 00 00 00 00    mov    $0x0,%rdi
      2b:   e8 00 00 00 00          call   30 <kaidock_func+0x20>
 (b)  30:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 36 <kaidock_func+0x26>
      36:   83 e8 01                sub    $0x1,%eax
 (b)  39:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 3f <kaidock_func+0x2f>
      3f:   85 c0                   test   %eax,%eax
      41:   7e 29                   jle    6c <kaidock_func+0x5c>
      43:   48 8b 73 10             mov    0x10(%rbx),%rsi
      47:   48 81 c6 e8 03 00 00    add    $0x3e8,%rsi
      4e:   48 89 df                mov    %rbx,%rdi
 (c)  51:   e8 00 00 00 00          call   56 <kaidock_func+0x46>
      56:   48 8b 73 10             mov    0x10(%rbx),%rsi
 (a)  5a:   48 8b 15 00 00 00 00    mov    0x0(%rip),%rdx        # 61 <kaidock_func+0x51>
      61:   48 89 f0                mov    %rsi,%rax
      64:   48 29 d0                sub    %rdx,%rax
      67:   48 85 c0                test   %rax,%rax
      6a:   7e db                   jle    47 <kaidock_func+0x37>
      6c:   48 8b 5d f8             mov    -0x8(%rbp),%rbx
      70:   c9                      leave
      71:   31 c0                   xor    %eax,%eax
      73:   31 d2                   xor    %edx,%edx
      75:   31 f6                   xor    %esi,%esi
      77:   31 ff                   xor    %edi,%edi
 (d)  79:   e9 00 00 00 00          jmp    7e <kaidock_func+0x6e>
      7e:   66 90                   xchg   %ax,%ax

カーネル本体へ組み込み後のカーネルモジュールもcrashコマンドを用いて覗いてみましょう。カーネルモジュールのkaidck_func関数を逆アセンブルしてみます。組み込み前のカーネルモジュールのkaidck_func関数は0x10番地に割り付くようなコードになっていましたが、カーネルへの組み込み後は0xffffffffc0d4c010に割り付いていることが確認できます。

変数jiffies(a)(A)、loop(b)(B)、呼出し関数modtime(c)(C)のアドレスも解決されています。

関数からの復帰命令(d)(D)も解決されています。

kaidock_func関数の先頭にあるcall命令(x)はnopl命令(X)に置き換えられています。関数の先頭5バイトは、ftrace機能やkprobe機能などが効率的にフックを仕掛けられるように領域を予約しています*3。

$ sudo crash /usr/lib/debug/boot/vmlinux-`uname -r`

crash> dis kaidock_func
 (X) 0xffffffffc0d4c010 <kaidock_func>:      nopl   0x0(%rax,%rax,1) [FTRACE NOP]
     0xffffffffc0d4c015 <kaidock_func+5>:    push   %rbp
 (A) 0xffffffffc0d4c016 <kaidock_func+6>:    mov    -0xeb4465d(%rip),%rsi        # 0xffffffffb22079c0 <jiffies>
     0xffffffffc0d4c01d <kaidock_func+13>:   mov    %rsp,%rbp
     0xffffffffc0d4c020 <kaidock_func+16>:   push   %rbx
     0xffffffffc0d4c021 <kaidock_func+17>:   mov    %rdi,%rbx
     0xffffffffc0d4c024 <kaidock_func+20>:   mov    $0xffffffffc0d7b058,%rdi
     0xffffffffc0d4c02b <kaidock_func+27>:   call   0xffffffffaffb0040 <_printk>
 (B) 0xffffffffc0d4c030 <kaidock_func+32>:   mov    0x2012(%rip),%eax        # 0xffffffffc0d4e048 <loop>
     0xffffffffc0d4c036 <kaidock_func+38>:   sub    $0x1,%eax
 (B) 0xffffffffc0d4c039 <kaidock_func+41>:   mov    %eax,0x2009(%rip)        # 0xffffffffc0d4e048 <loop>
     0xffffffffc0d4c03f <kaidock_func+47>:   test   %eax,%eax
     0xffffffffc0d4c041 <kaidock_func+49>:   jle    0xffffffffc0d4c06c <kaidock_func+92>
     0xffffffffc0d4c043 <kaidock_func+51>:   mov    0x10(%rbx),%rsi
     0xffffffffc0d4c047 <kaidock_func+55>:   add    $0x3e8,%rsi
     0xffffffffc0d4c04e <kaidock_func+62>:   mov    %rbx,%rdi
 (C) 0xffffffffc0d4c051 <kaidock_func+65>:   call   0xffffffffb0004b60 <mod_timer>
     0xffffffffc0d4c056 <kaidock_func+70>:   mov    0x10(%rbx),%rsi
 (A) 0xffffffffc0d4c05a <kaidock_func+74>:   mov    -0xeb446a1(%rip),%rdx        # 0xffffffffb22079c0 <jiffies>
     0xffffffffc0d4c061 <kaidock_func+81>:   mov    %rsi,%rax
     0xffffffffc0d4c064 <kaidock_func+84>:   sub    %rdx,%rax
     0xffffffffc0d4c067 <kaidock_func+87>:   test   %rax,%rax
     0xffffffffc0d4c06a <kaidock_func+90>:   jle    0xffffffffc0d4c047 <kaidock_func+55>
     0xffffffffc0d4c06c <kaidock_func+92>:   mov    -0x8(%rbp),%rbx
     0xffffffffc0d4c070 <kaidock_func+96>:   leave
     0xffffffffc0d4c071 <kaidock_func+97>:   xor    %eax,%eax
     0xffffffffc0d4c073 <kaidock_func+99>:   xor    %edx,%edx
     0xffffffffc0d4c075 <kaidock_func+101>:  xor    %esi,%esi
     0xffffffffc0d4c077 <kaidock_func+103>:  xor    %edi,%edi
 (D) 0xffffffffc0d4c079 <kaidock_func+105>:  ret
     0xffffffffc0d4c07a <kaidock_func+106>:  int3
     0xffffffffc0d4c07b <kaidock_func+107>:  int3
     0xffffffffc0d4c07c <kaidock_func+108>:  int3
     0xffffffffc0d4c07d <kaidock_func+109>:  int3
     0xffffffffc0d4c07e <kaidock_func+110>:  xchg   %ax,%ax

3. カーネルモジュール起動の動作フロー

Linuxカーネルは、カーネルモジュール組込みを要求するfinit_moduleシステムコールを用意しています。insmodコマンドはfinit_moduleシステムコールを呼び出すだけで、あとはfinit_moduleが全ての処理を行います。カーネルモジュールの組み込み処理は逐次処理で、アルゴリズム的に複雑な場所はありません。

int finit_module(int fd, const char *param_values, int flags);
引数 説明
fd カーネルモジュールのオブジェクトを示すファイルディスクリプタ。
param_values モジュール起動引数を渡す。今回の例では、"loop=100"などを渡す。
flags MODULE_INIT_IGNORE_MODVERSIONSとMODULE_INIT_IGNORE_VERMAGICは、バージョンの合わないモジュールを強制的に組込む場合に指定する。MODULE_INIT_COMPRESSED_FILE は、モジュールが圧縮されている場合に指定する。
  • カーネルモジュールイメージが圧縮されている場合、イメージを解凍します。 現在のLinuxカーネルは、圧縮形式として gzip/xz/zst のいずれか1つだけをカーネルのビルド時に選択できます。Ubuntu 24.04 LTSのカーネルはzstを採用しています。 (module_decompress関数)

  • 署名の確認を行います。PKCS#7で署名されていることを前提としています*4。 この署名はELFのセクションには置かれておらず、ELFファイルの後ろに添付する形になっています*5。 (module_sig_check関数)

  • カーネルモジュールリスト(modulesという名前の変数をヘッドとするリスト)に本カーネルモジュール(を管理するデータ構造)を登録します。カーネルモジュールの状態は初期化中(MODULE_STATE_UNFORMED)としておきます。(add_unformed_module関数)

  • カーネルモジュールを配置するためのカーネルメモリ領域をvmalloc系の関数を用いて確保します。 (layout_and_allocate関数)

  • 未解決シンボル情報を収集します。 組み込むカーネルモジュール内の未解決シンボルを、カーネル本体が公開しているシンボル、組み込み済みカーネルモジュールが公開しているシンボルの情報から見つけ出します。 組み込むカーネルモジュールがGPL以外のライセンスを持つ時、EXPORT_SYMBOL_GPL(シンボル名)で公開されているシンボルはリンクできないように制御しています*6。 (simplify_symbols関数、resolve_symbol関数)

  • 再配置可能セクションを解決します。 組込み前のカーネルモジュールのセクション情報を見るとRELAというタイプのセクションがあります*7。 これらのセクション中のアドレス未解決の命令を書き換えます(この情報はrelocationセクションにあります)。 これはアーキテクチャ依存の処理になります。 (apply_relocations関数)

Elfファイルのセクション情報とリロケーション情報を参照し、解決していきます。リロケーション情報には、命令中のアドレス情報が格納されている場所(Offset)、およびそのアドレス情報の型(Type)があります(R_X86_64_PLT32、R_X86_64_32Sなど)。このアドレス情報の部分(命令の一部のフィールド)を編集していきます。

$ readelf -S kaidock.ko
There are 43 section headers, starting at offset 0x494c0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.gnu.bu[...] NOTE             0000000000000000  00000040
       0000000000000024  0000000000000000   A       0     0     4
  [ 2] .note.Linux       NOTE             0000000000000000  00000064
       0000000000000030  0000000000000000   A       0     0     4
  [ 3] .text             PROGBITS         0000000000000000  000000a0
       000000000000010f  0000000000000000  AX       0     0     16
  [ 4] .rela.text        RELA             0000000000000000  00027978
       0000000000000210  0000000000000018   I      40     3     8
              :
              :
$ readelf -r kaidock.ko
Relocation section '.rela.text' at offset 0x27950 contains 19 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000011  003100000004 R_X86_64_PLT32    0000000000000000 __fentry__ - 4
000000000019  003900000002 R_X86_64_PC32     0000000000000000 jiffies - 4
000000000027  00040000000b R_X86_64_32S      0000000000000000 .rodata.str1.8 + 0
00000000002c  003300000004 R_X86_64_PLT32    0000000000000000 _printk - 4
000000000032  000d00000002 R_X86_64_PC32     0000000000000000 .data + 24
00000000003b  000d00000002 R_X86_64_PC32     0000000000000000 .data + 24
000000000052  003600000004 R_X86_64_PLT32    0000000000000000 mod_timer - 4
00000000005d  003900000002 R_X86_64_PC32     0000000000000000 jiffies - 4
00000000007a  003800000004 R_X86_64_PLT32    0000000000000000 __x86_return_thunk - 4
000000000091  003100000004 R_X86_64_PLT32    0000000000000000 __fentry__ - 4
000000000099  003900000002 R_X86_64_PC32     0000000000000000 jiffies - 4
0000000000a0  003a0000000b R_X86_64_32S      0000000000000000 kaidock_timer + 0
0000000000b0  003a00000002 R_X86_64_PC32     0000000000000000 kaidock_timer + c
0000000000b5  003500000004 R_X86_64_PLT32    0000000000000000 add_timer - 4
0000000000bf  003800000004 R_X86_64_PLT32    0000000000000000 __x86_return_thunk - 4
0000000000e1  003100000004 R_X86_64_PLT32    0000000000000000 __fentry__ - 4
0000000000e9  003a0000000b R_X86_64_32S      0000000000000000 kaidock_timer + 0
0000000000f1  003000000004 R_X86_64_PLT32    0000000000000000 timer_delete_sync - 4
0000000000fb  003800000004 R_X86_64_PLT32    0000000000000000 __x86_return_thunk - 4
               :
               :
  • 組み込んだカーネルモジュールのシンボル(EXPORT_SYMBOL、EXPORT_SYMBOL_GPLなどで指定したもの)を公開します。(add_kallsyms関数)

  • CPUモデルに合せた命令への書き換えも行います。(module_finalize関数)

  • 関数の先頭にあるcall命令をnopl命令に置き換えます。(ftrace_module_init関数)

  • sysファイル(/sys/module/kaidock)を作成します。(mod_sysfs_setup関数)

  • モジュールがinit_module関数を呼び出す準備が整ったことを示すMODULE_STATE_COMING状態にします。(complete_formation関数)

  • カーネルモジュールに定義したinit_module()を呼び出し、その後でカーネルモジュールの状態を実行中(MODULE_STATE_LIVE)にします。(do_init_module関数)

カーネルモジュール起動(insmod)処理はこれで終わりです。後はカーネル本体機能と区別なく動作を続けます。

4. カーネルモジュールの削除

delete_moduleシステムコールにて実現されています。rmmodコマンドはdelete_moduleを呼び出すだけです。

削除対象のカーネルモジュールが他モジュールから参照されている場合にエラーとしてはじくだけで、その後は削除処理を始めます。削除するカーネルモジュールのcleanup_module関数を呼び出した後、finit_moduleで確保した資源を解放します。(free_module関数)

最後に

カーネルモジュールは、ライブパッチ(Live Patch)機能の実現のためにも利用されています。問題のある関数の修正版をカーネルモジュールとしてカーネル本体に組み込んだ後、ftraceの仕組みを使って問題のある関数の先頭に修正版の関数の先頭にジャンプする命令を埋め込むことが基本的戦略です。

また、最近「LinuxカーネルがRust対応した」という話を聞いたことがあると思いますが、これによりカーネルモジュールの記述言語をRustにすることができるようになりました。insmod処理からすると、カーネルモジュールが*.ko形式のリロケータブルオブジェクトであれば、元の記述言語がCであろうがRustであろうが関係ありません。一方、カーネルモジュール開発者にとっては、Rustを使うことでメモリ操作関連のバグが入りにくくなるというメリットがあります。

現時点での課題は、カーネル機能を利用するためのRust向けAPIがまだ揃っていない*8ことと、C言語記述のカーネルモジュールより少しオーバヘッド(Rust言語仕様上からくるもの)が大きくなるため、性能が求められるカーネルモジュールでは使いにくいことでしょうか。このあたりの詳細については、藤田さんの記事を参照されると良いと思います。Rust愛が伝わってきます。

おまけ ~ カーネルモジュール組み込み機構の歴史

古いinsmodコマンドは下記に示す流れでカーネルモジュールを組み込んでいました。

  1. query_moduleシステムコールでカーネルのシンボル情報取得
  2. create_moduleシステムコールでメモリ領域確保
  3. カーネルモジュールのアドレス解決
  4. init_moduleシステムコールでカーネルモジュール組み込み

カーネルv2.6の時代には既にquery_module、create_moduleシステムコールは廃止されており、init_moduleシステムコールがモジュール組み込みのすべての処理を行なうように変更されています。

その後カーネルv3.8にて、カーネルモジュールイメージを直接渡すinit_moduleシステムコールに代わり、カーネルモジュールリロケータブルオブジェクト(*.ko)のファイルディスクリプタを渡すfinit_moduleシステムコールが追加されました。ファイルシステム自体の機能もカーネルモジュールの正当性の確認に役立てようということです。2つのシステムコールはカーネルモジュールの渡し方が異なるのみで、カーネル内では同じ動きをします。

おまけ ~ カーネルモジュールへの署名

/var/lib/shim-signed/mok/配下に、下記のファイルがある時は既に署名用の鍵が用意されています。

$ ls /var/lib/shim-signed/mok/
MOK.der  MOK.priv

署名用の鍵がある場合

kmodsignコマンドを用い、カーネルモジュールに署名を添付します。 ハッシュアルゴリズムは、Ubuntu 24.04 LTSの他のモジュールと同じsha512にしておきます。

$ sudo kmodsign sha512 /var/lib/shim-signed/mok/MOK.priv /var/lib/shim-signed/mok/MOK.der kaidock.ko

これでカーネルモジュールへの署名は完了です。念のためカーネルモジュールの署名を確認しておきましょう。

$ modinfo kaidock.ko
filename:       /home/riscv/kaidoku/kmodule/kaidock.ko
license:        GPL
author:         taka
srcversion:     FCA95EBA12E11A29AD336FF
depends:
retpoline:      Y
name:           kaidock
vermagic:       6.8.0-40-generic SMP preempt mod_unload modversions
sig_id:         PKCS#7
signer:         new-linux-kaidokusitu-pj Secure Boot Module Signature key
sig_key:        3A:AB:5A:1E:A0:B8:B8:E7:86:68:E7:21:58:2C:39:1B:65:0B:BD:F5
sig_hashalgo:   sha512
signature:      EF:A8:73:6D:28:96:27:A5:63:74:09:F6:03:7B:D1:53:F6:3F:08:02:
        DF:0B:86:FB:14:1A:AC:21:CF:61:87:CB:15:BB:F2:8A:29:33:BD:8B:
        6A:34:5F:5A:88:67:BC:9F:11:52:9E:76:48:64:BA:9B:4C:D7:73:4C:
        D7:B0:B4:E1:5B:84:49:58:98:36:C5:D6:03:F3:D8:8F:1E:F6:B6:56:
        7F:07:D3:00:D8:FC:E5:57:5E:46:CF:06:92:72:5A:CC:C5:3D:B5:F3:
        32:1B:02:01:80:69:35:64:75:1C:80:A1:7D:34:FF:74:32:04:55:0A:
        42:90:6B:59:44:78:7C:FA:26:2C:B6:ED:5E:73:D4:62:D4:B2:1E:54:
        8F:FC:5B:D3:95:24:AC:F8:96:E2:53:02:17:82:67:1C:81:B1:F5:80:
        BA:94:6B:07:4C:09:DE:A6:E7:8F:F5:07:38:FD:56:3B:B2:FD:A5:05:
        8B:10:6D:94:6F:E5:A2:D8:24:11:57:91:FA:FB:79:EB:70:48:37:EF:
        69:73:1E:25:18:CE:34:B1:1F:B6:6F:5E:D8:D6:64:D1:C6:2B:78:A9:
        27:59:A0:A2:0E:EC:82:74:A8:AC:8F:57:40:E5:5F:2F:86:DE:5E:15:
        17:A5:B3:D6:C9:B2:27:4D:68:35:EF:D7:3F:92:80:68
parm:           loop:specify the loop count (int)

署名用の鍵がない場合

まず、署名用のパッケージをインストールします。

$ sudo apt install shim-singned

鍵の生成をします。

$ sudo update-secureboot-policy --new-key
Generating a new Secure Boot signing key:
Can't load /var/lib/shim-signed/mok/.rnd into RNG
40779766D3700000:error:12000079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:106:Filename=/var/lib/shim-signed/mok/.rnd
.+..........+...............+.....+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.........+.......+...+...........+.+.........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.....+....+.........+........+...+...............+....+.....+.......+........+............+.+.....+...+..........+..+.......+.................+...+...+...+.+......+.....+....+...+.....+.+...........+....+.........+........+.........+.+............+..+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
........+.+..+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*....+....+......+..+.......+........+.+...........+.+..+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.......+..+.+......+...........+.......+...+..+.+.....+....+..+.........+................+........+......+....+..+....+..+.......+..+....+.........+..+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-----

鍵が生成されました。

$ ls /var/lib/shim-signed/mok/
MOK.der  MOK.priv

次に、この鍵をUEFIセキュアブートの鍵データベースに登録します。下記コマンドは、鍵登録を依頼するコマンドです。次のステップで利用するパスワード設定が求められます。

$ sudo mokutil --import /var/lib/shim-signed/mok/MOK.der

システムを再起動します。

  1. Shim UEFI key managerが起動します。「Press any key to perform MOK management」と表示されるので、任意のキーを押してMOK管理を開始します。
  2. 画面に表示される「Enroll MOK」を選択します。
  3. 「View key 0」 選択すると先ほど生成した鍵の情報が表示されます。
  4. 任意のキーを押すと「Enroll MOK」のメニューに戻ります。
  5. Continueを選択すると、「Enroll the key(s)?」と聞かれるので、Yesを選択します。
  6. パスワード入力が求められるので、先ほどの鍵生成の時に設定したパスワードを入力します。
  7. 「Reboot」 を選択し、システムを再起動します。

システムが再起動したら、署名用の鍵がある場合に進みます。

*1:jiffiesは、カーネル起動時からの経過時間を表す変数です。

*2:カーネルをコンパイルした時のgccとインストールされているgccのバージョンが異なる時はビルド処理がエラーとなります。その場合、バージョン番号付きのgccを指定してmakeしてください。Ubuntu 24.04の場合は、$ make -C /lib/modules/`uname -r`/build M=`pwd` modules MAKEFLAGS="CC=gcc-13" となります。/boot/config-`uname -r`ファイルを参照するとカーネルバージョンとgccのバージョンの対応が分かります。

*3:こういう領域を予約するためのコンパイラオプションがあります

*4:カーネルヘッダを見ると署名方式としてOpenPGP、X.509、PKCS#7が選択できそうに見えますが、実際にはPKCS#7用以外のコードは実装されておらず、エラーになります。

*5:ツールによっては、壊れたelfファイルと認識するかも

*6:OSSであってもGPL以外のライセンス(MITやApacheなど)を持つカーネルモジュールは、実装しにくくなっています。

*7:アーキテクチャの種類によってはSHT_REL

*8:Rustのコードから、C関数を直接呼び出すことは御法度とされている。