拾い物のコンパス

まともに書いたメモ

VagrantでRust製デバドラを動かす環境を作る話

CTF問題でもない限り好んでUse After Freeバグを作りたくはない.
その点Rust(ラストと読むと最近知った)はC/C++に比べてメモリ管理について優れていると言われている.
低レベル操作も充実していて速度もC/C++並に出るときもある. Rustのチュートリアルを一通り読み終わったのでVagrant上でデバドラ開発環境を整えた.

やりたかったこと

よりバグを少なくデバドラ開発がしたい!
Goodbye, unsafe C/C++
ヒャッハー,開発中のバグでホストがカーネルパニックしたくないから仮想環境の中に閉じ込めだぁ!

ステップ

  1. Vagrantを立ち上げる

  2. C言語のHello, Worldなデバドラのコンパイル・実行

  3. Rust製のHello, Worldなデバドラのコンパイル・実行

C言語でも確認するのは必要なモジュールがインストールできているかの確認用.
Rust製のHello, Worldなデバドラはこれを使った.
Dockerで環境を作ることも考えたが,以下の点で向いていないと判断.

やるまえから嫌な予感はしつつ挑戦してみた.
「上記の問題解決を頑張るのが目的じゃないから違う方法を考えよう.」
そう気づいた頃にはDockerイメージ作成してから3時間経っていた.もっと早く気づけ.
今回はUbuntuのイメージで環境を作っていったが,Ubuntuの標準レポジトリからインストールできるlinuxソースのバージョンとホストOSのバージョンが違うと本当に面倒くさい.
レポジトリ追加とかやったら解決できるけどdmesgできない問題が口を開けて待っている.
良い子のみんなはフルエミュレーションな仮想環境が一番と心に刻んでほしい.

環境構築

Vagrantのインストールは探せばいくらでも出てくるから省略する.
$ sudo pacman -S vagrant
的なことをすれば良い.

$ vagrant --version
Vagrant 2.2.6

Vagrantを立ち上げる

以下をVagrantfileとして保存.

# frozen_string_literal: true

# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure('2') do |config|
  # The most common configuration options are documented and commented below.
  # For a complete reference, please see the online documentation at
  # https://docs.vagrantup.com.

  # Every Vagrant development environment requires a box. You can search for
  # boxes at https://vagrantcloud.com/search.
  config.vm.box = 'generic/ubuntu1810'

  # Disable automatic box update checking. If you disable this, then
  # boxes will only be checked for updates when the user runs
  # `vagrant box outdated`. This is not recommended.
  # config.vm.box_check_update = false

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine. In the example below,
  # accessing "localhost:8080" will access port 80 on the guest machine.
  # NOTE: This will enable public access to the opened port
  # config.vm.network "forwarded_port", guest: 80, host: 8080

  # Create a forwarded port mapping which allows access to a specific port
  # within the machine from a port on the host machine and only allow access
  # via 127.0.0.1 to disable public access
  # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

  # Create a private network, which allows host-only access to the machine
  # using a specific IP.
  # config.vm.network "private_network", ip: "192.168.33.10"

  # Create a public network, which generally matched to bridged network.
  # Bridged networks make the machine appear as another physical device on
  # your network.
  # config.vm.network "public_network"

  # Share an additional folder to the guest VM. The first argument is
  # the path on the host to the actual folder. The second argument is
  # the path on the guest to mount the folder. And the optional third
  # argument is a set of non-required options.
  config.vm.synced_folder './', '/vagrant_data'

  # Provider-specific configuration so you can fine-tune various
  # backing providers for Vagrant. These expose provider-specific options.
  # Example for VirtualBox:
  #
  # config.vm.provider "virtualbox" do |vb|
  #   # Display the VirtualBox GUI when booting the machine
  #   vb.gui = true
  #
  #   # Customize the amount of memory on the VM:
  #   vb.memory = "1024"
  # end
  #
  # View the documentation for the provider you are using for more
  # information on available options.

  # Enable provisioning with a shell script. Additional provisioners such as
  # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
  # documentation for more information about their specific syntax and use.
  # config.vm.provision 'shell', inline: <<-SHELL
  #  apt update
  #  apt install -y install git gcc curl make kmod linux-source linux-headers-$(uname -r) build-essential libelf-dev
  #  apt install llvm clang
  # SHELL
end

$ vagrant initで生成されるファイルほぼそのまま.
いじったのはconfig.vm.boxUbuntu 18.10を選択したこととホストとの共有フォルダとしてVagrantfileがあるディレクトリを/vagrant_dataとしてマウントする設定を加えた.
SHELLは無効化中のため割愛.

Vagrant環境内に開発環境を持ち込むと設定がまた時間がかかるから共有フォルダ経由で編集はホスト,コンパイル・検証は仮想環境内に切り分けた.
起動してログインする.

$ vagrant up
$ vagrant ssh
vagrant@ubuntu1810:/$

環境とバージョンは以下の通り.

$ uname -a
Linux ubuntu1810.localdomain 4.18.0-25-generic #26-Ubuntu SMP Mon Jun 24 09:32:08 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

まずは必要なパッケージをインストール.

$ apt-get update
$ apt-get install -y install git gcc curl make kmod linux-source linux-headers-$(uname -r) build-essential libelf-dev
$ ls /lib/modules/`uname -r`/build # 必要なものがある確認
arch   crypto         firmware  init    Kconfig  Makefile        net      security  tools   virt
block  Documentation  fs        ipc     kernel   mm              samples  sound     ubuntu  zfs
certs  drivers        include   Kbuild  lib      Module.symvers  scripts  spl       usr

$ gcc --version
gcc (Ubuntu 8.3.0-6ubuntu1~18.10.1) 8.3.0
$ make --version
GNU Make 4.2.1

作業用ディレクトリ(共有フォルダ)に移動.

$ cd /vagrant_data
$ ls
Vagrantfile

これで最低限の環境は整った.

C言語のHello, Worldなデバドラのコンパイル・実行

C言語でのデバイスドライバコンパイル・実行できるかを確認する.
ソースコードMakefileを用意する.

// hello.c
#include <linux/module.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("poppycompss <t0g0v31dk@gmail.com>");
MODULE_DESCRIPTION("Hello world kernel module");

static int km_init(void)
{
    printk(KERN_INFO "Hello my module\n");
    return 0;
}
static void km_exit(void)
{
    printk(KERN_NOTICE "Goodbye my module\n");
}

module_init(km_init);
module_exit(km_exit);

起動時にHello my module, 終了時にGoodbye my moduleと出力するだけのデバドラ.
続いてMakefile

# Makefile
.PHONY: all clean
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD = $(shell pwd)
all:
        make -C  ${KDIR} M=$(PWD) modules
clean:
        make -C  ${KDIR} M=$(PWD) clean

コンパイルに必要なファイルを作業用ディレクトリにコピー.

$ cp /lib/modules/`uname -r`/build/.config ./
$ cp /lib/modules/`uname -r`/build/Module.symvers ./

準備はここまで.コンパイルする.

$ make
make -C  /lib/modules/4.18.0-25-generic/build M=/vagrant_data modules
make[1]: Entering directory '/usr/src/linux-headers-4.18.0-25-generic'
  CC [M]  /vagrant_data/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  LD [M]  /vagrant_data/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-4.18.0-25-generic'

hello.koが生成されているはず.

$ sudo insmod hello.ko
$ sudo rmmod hello
$ dmesg | tail
[21061.045821] Hello my module
[21061.054198] Goodbye my module

これでC言語のデバドラが動いていることが確認できた.
次からが本題.

Rust製のHello, Worldなデバドラのコンパイル・実行

必要なパッケージのインストール

$ sudo apt install -y llvm clang
$ clang -v
clang version 7.0.0-3 (tags/RELEASE_700/final)
Target: x86_64-pc-linux-gnu
...(snip)...

Rustをローカルにインストールする.

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

もし上記がうまく行かなければ

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rust-installer.sh
$ chmod +x ./rust-installer.sh
$ ./rust-intsaller.sh

プロンプトで「どれをインストールする?」って聞かれたら'1'を入れた.
インストールが終わったらパスを通す

$ source $HOME/.cargo/env

これでrustupコマンドが使えるようになる.
今回動かそうとしているデバドラにはRustのnightlyを使う必要があるからインストールして切り替える.
切り替えはデバドラの作業ディレクトリで実施した.

$ rustup install nightly
$ git clone https://github.com/fishinabarrel/linux-kernel-module-rust
$ cd linux-kernel-module-rust/hello-world
$ rustup toolchain list # 現在使っているバージョンを確認
stable-x86_64-unknown-linux-gnu (default)
nightly-x86_64-unknown-linux-gnu
$ rustup override set nightly # nightlyに切り替え
$ rustup component add rust-src rustfmt
$ rustup --version
rustc 1.41.0 (5e1a79984 2020-01-27)

ようやくmakeできると思いきやいろんなエラーにぶつかる (ぶつからなかった人はそのまま次に進んでください)

$ make
// エラーのみ抜粋
error[E0308]: mismatched types
  --> /vagrant_data/linux-kernel-module-rust/src/allocator.rs:13:41
   |
13 |         bindings::krealloc(ptr::null(), layout.size(), bindings::GFP_KERNEL) as *mut u8
   |                                         ^^^^^^^^^^^^^ expected `u64`, found `usize`
   |
help: you can convert an `usize` to `u64` and panic if the converted value wouldn't fit
   |
13 |         bindings::krealloc(ptr::null(), layout.size().try_into().unwrap(), bindings::GFP_KERNEL) as *mut u8
   |                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0308]: mismatched types
   --> /vagrant_data/linux-kernel-module-rust/src/file_operations.rs:193:28
    |
193 |         self.0.read = Some(read_callback::<T>);
    |                            ^^^^^^^^^^^^^^^^^^ expected `u64`, found `usize`
    |
    = note: expected fn pointer `unsafe extern "C" fn(_, _, u64, _) -> i64`
                  found fn item `unsafe extern "C" fn(_, _, usize, _) -> isize {file_operations::read_callback::<T>}`

error[E0308]: mismatched types
   --> /vagrant_data/linux-kernel-module-rust/src/file_operations.rs:200:29
    |
200 |         self.0.write = Some(write_callback::<T>);
    |                             ^^^^^^^^^^^^^^^^^^^ expected `u64`, found `usize`
    |
    = note: expected fn pointer `unsafe extern "C" fn(_, _, u64, _) -> i64`
                  found fn item `unsafe extern "C" fn(_, _, usize, _) -> isize {file_operations::write_callback::<T>}`

error[E0308]: mismatched types
   --> /vagrant_data/linux-kernel-module-rust/src/sysctl.rs:136:36
    |
136 |                 proc_handler: Some(proc_handler::<T>),
    |                                    ^^^^^^^^^^^^^^^^^ expected `u64`, found `usize`
    |
    = note: expected fn pointer `unsafe extern "C" fn(_, _, _, *mut u64, _) -> _`
                  found fn item `unsafe extern "C" fn(_, _, _, *mut usize, _) -> _ {sysctl::proc_handler::<T>}`

error: aborting due to 4 previous errors

For more information about this error, try `rustc --explain E0308`.
The following warnings were emitted during compilation:
error: could not compile `linux-kernel-module`.

bindgenのバージョンによる変更の影響らしい. Cargo.tomlを修正する.

[-] bindgen = "*"
[+] bindgen = "0.51.0"
$ make

コンパイルが成功したら読み込んでみる.

$ sudo insmod helloworld.ko
$ sudo rmmod helloworld
$ dmesg | tail
...(snip)...
[40960.460642] Hello kernel module!
[40960.468459] My message is on the heap!
[40960.468460] Goodbye kernel module!
...(snip)...

動いた.

所感

dockerはデバドラ開発には向いてない(確信)

実はRust製デバドラとしてはよりシンプルなものもあるがこっちもエラーにぶつかった. 今後の開発を考えると今回使ったやつのほうが実装されている部分が多くて参考になりそうだったからデバッグはしてない.
開発は慣れるまでCで書いて動作確認しつつRustに書き直す作業になりそうだ.

参考

「Rust」言語はCよりも遅いのか、研究者がベンチマーク結果を解説:モダンCPUでは性能低下は軽微 - @IT

GitHub - fishinabarrel/linux-kernel-module-rust: Framework for writing Linux kernel modules in safe Rust

GitHub - tsgates/rust.ko: A minimal Linux kernel module written in rust.

centos7 - Running dmesg on Docker results in "dmesg: read kernel buffer failed: Permission denied" - Server Fault