VagrantでRust製デバドラを動かす環境を作る話
CTF問題でもない限り好んでUse After Freeバグを作りたくはない.
その点Rust(ラストと読むと最近知った)はC/C++に比べてメモリ管理について優れていると言われている.
低レベル操作も充実していて速度もC/C++並に出るときもある.
Rustのチュートリアルを一通り読み終わったのでVagrant上でデバドラ開発環境を整えた.
やりたかったこと
よりバグを少なくデバドラ開発がしたい!
Goodbye, unsafe C/C++!
ヒャッハー,開発中のバグでホストがカーネルパニックしたくないから仮想環境の中に閉じ込めだぁ!
ステップ
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.box
にUbuntu 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 - tsgates/rust.ko: A minimal Linux kernel module written in rust.