BPFプログラムからカーネル内のデータ構造にアクセスする方法

kprobeやtracepointなどにアタッチしたBPFプログラムは,BPF_FUNC_probe_read()関数でカーネル内のデータ構造にアクセスすることができます.これはprobe_kernel_read()のラッパーで,もし変な領域にアクセスしようした場合(page faultが発生した場合)は-EFAULTが返ります. また,BPF_FUNC_get_current_task()でcurrentのtask_structを得ることができます.

例として,/bin/catが実行されたときに,そのプロセスのvm_area_structの中身をダンプするプログラムを書いてみます.bccを利用して以下のように書けます.

#!/usr/bin/env python

import bcc

text = r"""
#include <linux/ptrace.h>
#include <linux/sched.h>

int probe(struct pt_regs *ctx) {
    int i;
    struct task_struct *t = (struct task_struct *)bpf_get_current_task();
    struct vm_area_struct *vma = t->mm->mmap;

    #pragma clang loop unroll(full)
    for (i = 0; i < 20; i++) {
        if (vma != NULL) {
            unsigned long inode = vma->vm_file->f_inode->i_ino;
            bpf_trace_printk("%lx-%lx %ld\n", vma->vm_start, vma->vm_end, inode);
            vma = vma->vm_next;
        }
    }

    return 0;
}
"""


def main():
    b = bcc.BPF(text=text, debug=0)
    b.attach_uprobe(name="/bin/cat", sym="main", fn_name="probe")
    b.trace_print()


if __name__ == "__main__":
    main()

ここで注意点としては,(古いカーネルだと)BPFプログラムはループを扱えないので*1,pragmaを利用して展開する必要があります.またbreak文を含むようなプログラムはverifierにおこられるので,ちょっと書き方に工夫がいります.

bccでは,ポインタ参照は原則bpf_probe_read()関数呼び出しになります*2.ここで,上記プログラムではポインタがNULLか確認していません*3.これで大丈夫?と思うかもしれませんが,実際のポインタ参照はbpf_probe_read()を利用していて,もし読み出しに失敗した場合はbccは事前にバッファをクリアするので得られる値は0になります.アドレス0に対するbpf_probe_read()は失敗するでしょうが,プログラムがクラッシュするといったことはないので,これで大丈夫です.結果としてはvma->vm_fileがNULLならinodeは0になります.

上記プログラムを実行しておいて*4,以下のようにcatを実行すると,

% cat /proc/self/maps
 55e4824bf000-55e4824c7000 r-xp 00000000 08:02 2621482                    /bin/cat
 55e4826c6000-55e4826c7000 r--p 00007000 08:02 2621482                    /bin/cat
 55e4826c7000-55e4826c8000 rw-p 00008000 08:02 2621482                    /bin/cat
 55e4835b0000-55e4835d1000 rw-p 00000000 00:00 0                          [heap]
 7f1b8bfcf000-7f1b8c1b6000 r-xp 00000000 08:02 6685444                    /lib/x86_64-linux-gnu/libc-2.27.so
 7f1b8c1b6000-7f1b8c3b6000 ---p 001e7000 08:02 6685444                    /lib/x86_64-linux-gnu/libc-2.27.so
 7f1b8c3b6000-7f1b8c3ba000 r--p 001e7000 08:02 6685444                    /lib/x86_64-linux-gnu/libc-2.27.so
 7f1b8c3ba000-7f1b8c3bc000 rw-p 001eb000 08:02 6685444                    /lib/x86_64-linux-gnu/libc-2.27.so
 7f1b8c3bc000-7f1b8c3c0000 rw-p 00000000 00:00 0
 7f1b8c3c0000-7f1b8c3e7000 r-xp 00000000 08:02 6685432                    /lib/x86_64-linux-gnu/ld-2.27.so
 7f1b8c41a000-7f1b8c43c000 rw-p 00000000 00:00 0
 7f1b8c43c000-7f1b8c5d7000 r--p 00000000 08:02 4326139                    /usr/lib/locale/locale-archive
 7f1b8c5d7000-7f1b8c5d9000 rw-p 00000000 00:00 0
 7f1b8c5e7000-7f1b8c5e8000 r--p 00027000 08:02 6685432                    /lib/x86_64-linux-gnu/ld-2.27.so
 7f1b8c5e8000-7f1b8c5e9000 rw-p 00028000 08:02 6685432                    /lib/x86_64-linux-gnu/ld-2.27.so
 7f1b8c5e9000-7f1b8c5ea000 rw-p 00000000 00:00 0
 7fffea906000-7fffea927000 rw-p 00000000 00:00 0                          [stack]
 7fffea9fc000-7fffea9ff000 r--p 00000000 00:00 0                          [vvar]
 7fffea9ff000-7fffeaa00000 r-xp 00000000 00:00 0                          [vdso]
 ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

以下の出力が得られます*5.BPFプログラム側からvma_structの情報が取得できていることが分かります.

 <...>-8164  [001] .... 512025.107366: 0: 55e4824bf000-55e4824c7000 2621482
 <...>-8164  [001] .... 512025.107381: 0: 55e4826c6000-55e4826c7000 2621482
 <...>-8164  [001] .... 512025.107382: 0: 55e4826c7000-55e4826c8000 2621482
 <...>-8164  [001] .... 512025.107383: 0: 7f1b8bfcf000-7f1b8c1b6000 6685444
 <...>-8164  [001] .... 512025.107384: 0: 7f1b8c1b6000-7f1b8c3b6000 6685444
 <...>-8164  [001] .... 512025.107384: 0: 7f1b8c3b6000-7f1b8c3ba000 6685444
 <...>-8164  [001] .... 512025.107385: 0: 7f1b8c3ba000-7f1b8c3bc000 6685444
 <...>-8164  [001] .... 512025.107390: 0: 7f1b8c3bc000-7f1b8c3c0000 0
 <...>-8164  [001] .... 512025.107391: 0: 7f1b8c3c0000-7f1b8c3e7000 6685432
 <...>-8164  [001] .... 512025.107394: 0: 7f1b8c5d7000-7f1b8c5d9000 0
 <...>-8164  [001] .... 512025.107395: 0: 7f1b8c5e7000-7f1b8c5e8000 6685432
 <...>-8164  [001] .... 512025.107396: 0: 7f1b8c5e8000-7f1b8c5e9000 6685432
 <...>-8164  [001] .... 512025.107399: 0: 7f1b8c5e9000-7f1b8c5ea000 0
 <...>-8164  [001] .... 512025.107402: 0: 7fffea906000-7fffea927000 0
 <...>-8164  [001] .... 512025.107405: 0: 7fffea9fc000-7fffea9ff000 0
 <...>-8164  [001] .... 512025.107408: 0: 7fffea9ff000-7fffeaa00000 0

またbpftraceなら以下のように書けます.

#include <linux/sched.h>
#include <linux/fs.h>

uprobe:/bin/cat:main {
  $vma = (struct vm_area_struct*)curtask->mm->mmap;

  unroll(20) {
    if ($vma != 0) {
      $inode = $vma->vm_file->f_inode->i_ino;
      printf("%lx-%lx %ld\n", $vma->vm_start, $vma->vm_end, $inode);
      $vma = $vma->vm_next;
    }
  }
}

(ハイライトが欲しい...)

使い所が限られますが,あの情報ここにあったっけ?という時にカーネルモジュールを書かなくても確認できます.

*1:Linux5.3からBounded Loopに対応しました

*2:BPFプログラムに与えられる引数(ctx)に対するアクセスはload命令になります

*3:例えば,vma->vm_fileはmmapされた領域でなければNULLになります

*4:attach_uprobeする際はデバッグ情報をみてアドレス解決するので,デバッグシンボル (ubuntuならcoreutils-dbgsym)をインストールしておく必要があります.

*5:[heap]などの情報が表示されていないのはこれはカーネル側で表示している情報であるためです