拾い物のコンパス

まともに書いたメモ

Micro Hardeningv2に参加した話(2022/5/21)

参加して,ぼこぼこにされた話.

 きっかけ

 上司命令でconnpassのイベントに参加してきて腕を磨いて来いと言われた.
 昔トラウマになったCTF Attack&Defenderに似たMicro Hardeningが楽しそうだったから.

 競技概要

 こんなマニアックなブログを見ているのは競技を知っている人だろうから触りだけ.
 各チーム(最高6名)にサーバが割り当てられ,その上で動いているサービス(ショッピング,ブログ,サイト)の運用監視し,脆弱性攻撃やらを防ぐ.

 チームとしてやったこと

私は主にログ監視していた.multitailとaliasのコンビが非常に秀逸.

  • 脆弱なパスワード変更
  • ファイルバックアップ
    ...etc

 トラウマ

  • 気づいたらHP書き換えられている
  • 気づいたら売り上げ落ちている
  • 不正ログインのオンパレード
  • ログが静かだと思ったら上記全てが終わっていた・・・

 反省

  • パスワード変更対象が多すぎて漏れていた
  • 最低限のアクセス制限とユーザ自体の完全遮断のバランスが難しい(悪いユーザも善行をすることがある)

 次回やりたいこと

  • SQLi/XSS対策
  • ユーザ削除系のシステム化(チェック自動化)
  • 脆弱性対策
    ・・・俺たちの戦いはこれからだ!

 所感

 攻撃が来たらヒャッハーって叫びながらログをつなぎ合わせて攻撃者と内容を見るのが楽しかった.
 競技の最後にあった大規模障害の対応方法や障害対応にとって大切なことは非常に参考になった.
 slack/discord,Guacamoleと知らないものも触れられてよかった.
 SSH接続のみやってたのでGUI画面も次回は触ってみたい.

 主催者の川口さんやその後のまったり運営されていた方々,ありがとうございました!
 反省を繰り返しながら9回ほど参加すればほぼ満点取れるという伝説もあるようなのでまた出たい.

 (参考)結果

 team3メンバーがサーバに強い人達だったので初参加にしては結構良い得点だった.

スコア

 参考文献

team member: Micro Hardening v2に参加した話

connpss: Micro Hardening - connpass

qemu上でLinuxカーネルを動かしたらエラーメッセージが止まらない問題への調査結果と対処法

qemu上にて自分でビルドしたカーネルを動かしたらエラーメッセージが止まらない.
これに対処するために原因調査,対処方法を探した話.
結果的に対処法は見つかったが,根本的な原因追求はできなかった.
知っている人がいれば情報募集中.

対象者

この記事はqemuLinuxカーネルを動かしたときに下記のようなメッセージが何度も出続ける現象に苦しむ人を対象とする.

process '-/bin/sh' (pid 15) exited. Scheduling for restart.
can't open /dev/tty3: No such file or directory

TL;DR

  • エラーメッセージの原因はinitrd(initramfs)内にあることまで突き止めた
  • vagrantubuntu(恐らくServer版)のinitrdを使えば問題を回避できた
    (initrdを替えられない事情がある人には無用な記事かも)

何があった?

カーネルを自分でビルドして,qemu上で起動したら下記エラーメッセージが止まらない.
メッセージが出すぎてまともにコマンド入力できない.

process '-/bin/sh' (pid 15) exited. Scheduling for restart.
process '-/bin/sh' (pid 16) exited. Scheduling for restart.
process '-/bin/sh' (pid 17) exited. Scheduling for restart.
can't open /dev/tty3: No such file or directory
can't open /dev/tty4: No such file or directory
can't open /dev/tty2: No such file or directory

ググってみると下記コマンドで改善するという情報があった.
$ ln -sf /dev/console /dev/tty3
しかしメッセージが多すぎてコマンドが打てないため,他の方法を探す必要があった.
他の記事を見てもOSアップデートで改善したとかしか出ず,原因まで書いたものを見つけられなかった.
だから自分で調べることにした.

環境構築

まずは今回の現象が再現する環境構築方法を記し,その後回避策を説明する.
手っ取り早く解決したい人はinitrdの用意まで読み飛ばすこと.
エラーメッセージにたどり着くためにまずは起動用のカーネルイメージ(bzImage)をビルドする必要がある.

bzImageのビルド

カーネルは全てビルドすると20GB以上のストレージと8GB以上のメモリ,数時間は必要となり自分の手持ちPCではできない.
ビルド中は数時間掛けてじわじわ使用メモリが増えていき,気づいたときはメモリ切れで強制終了,数時間が水の泡となるのは心に来る(経験談).
しかし考えてみればUbuntuなどでは起動に必要なvmlinuzは20GBも必要ない.
最小構成であればそこまで容量も必要ないのではないかと考えた.
bzImageがあれば最低限起動できるようだったため,シンプルにビルドする方法を選んだ.
カーネルのビルド方法は下記サイトを参考にした.
minimalなlinuxを作りたい、と思ったんだ - livaの雑記帳
Linux を(わりと)シンプルな構成でビルドして Qemu で起動する - Qiita
ビルドしたLinuxカーネルをブートできる最低限の環境を用意する(with Busybox & qemu) - 豆腐の豆腐和え

ビルド手順

このサイトからソースを持ってきた.
使ったバージョンは5.9.1で今は公開されていないが,5.9系ならそこまで違いは無いかと思う.
そこからはポチポチコマンドを実行していく.
makeするために必要なパッケージがあるかもしれないから,それはエラーメッセージを参照しながら補完してほしい.

$ tar xvf linux-5.9.1.tar.xz
$ cd linux-5.9.1
$ make allnoconfig
$ make menuconfig
    [*] 64-bit kernel

    -> General setup
    [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
    -> General setup
      -> Configure standard kernel features
    [*] Enable support for printk

    -> Executable file formats / Emulations
    [*] Kernel support for ELF binaries

    -> Device Drivers
      -> Character devices
    [*] Enable TTY

    -> Device Drivers
      -> Character devices
        -> Serial drivers
    [*] 8250/16550 and compatible serial support
    [*]   Console on 8250/16550 and compatible serial port
$ make -j4 bzImage
$ find ./ -name bzImage
./arch/i386/boot/bzImage
./arch/x86_64/boot/bzImage
./arch/x86/boot/bzImage
$ mkdir work
$ cp ./arch/x86_64/boot/bzImage work
$ cd work

これでbzImageの作成は完了.
参考サイトでは起動できるところまでだったり(カーネルが起動し,panicするところまで),busyboxの用意があったりでややこしい.

ちゃんとしたinitrdを用意してやれば小さなLinuxとして使えるようだ.
というわけで今回使ったinitramfs-linux.imgは普段使っているLinux(Arch Linux, Kernel5.9.11-arch2-1)から抜き出した.

$ cp /boot/initramfs-linux.img work/initramfs-linux.img
$ qemu-system-x86_64 -kernel ./bzImage -initrd ./initramfs-linux.img -nographic -append "console=ttyS0"

実行後ブートメッセージが出力されて起動する.
panicも起こらない.
やった!悲劇はここからだ!!
エラーメッセージが止まらない・・・

例のエラーメッセージ

process '-/bin/sh' (pid 18) exited. Scheduling for restart.
process '-/bin/sh' (pid 19) exited. Scheduling for restart.
process '-/bin/sh' (pid 20) exited. Scheduling for restart.
can't open /dev/tty2: No such file or directory
can't open /dev/tty3: No such file or directory
can't open /dev/tty4: No such file or directory

調査する前にこの状態だとまともにコマンド入力もできないから,ホスト側から他のシェルを使ってqemuを停止.

$ kill -KILL `ps aux | grep qemu | awk -F" " '{print $2}' | head -n1`

調査

メッセージ内容からすると/dev/tty(2,3,4)が無いからメッセージが出力されているように見られる. まず調べるべきはttyデバイスが作成されるタイミング.
怪しいのは

辺りかと思い,詳細を調べた.
補足するとinitrdは起動時に読み込まれるファイルシステムinitrdの中にある最初に実行されるプログラムがinittab/systemdとなる.
昔はinittabだったようだが,今はsystemdがこの役割を担っているようだ.
/sbin/initsystemdへのリンクになっていたのを知ったときは訳がわからなかった.(感想)

しかしここで行き詰まった.
ttyデバイスはシェルで接続した瞬間作成されるようだ. よって問題のメッセージを読み解くと

  1. 自動で実行されるプロセスが/bin/shを実行
  2. 異常終了
  3. 再実行がスケジュール

/dev/ttyがなくて/bin/shが異常終了するのか,他の要因があるのかはわからない.
このプロセスがスケジューリングされるのはinitrd内にあるinittab/systemdで自動的に時実行されるプロセス(inittabならrc.S,systemdならtarget)と推察される.
つまり問題となるプロセスを登録していないinitrdがあれば問題のメッセージは出力されないのではないか.
というわけでvagrantubuntu(Server)からinitramfs.imgを取り出して試してみる.
ubuntuを選んだ理由はCUI起動のみという点でビルドしたカーネルイメージと環境が近いと考えたから.

initrdの用意(回避策はここから)

まずは初期設定.

$ vagrant init ubuntu/trusty64

作成されたVagrantfileにファイル取り出し用の共有フォルダを設定する.
config.vm.synced_folderの箇所を編集する.
ルールはconfig.vm.synced_folder <ホストのディレクトリパス> <ゲスト内でのディレクトリパス>
パスは各自の環境を入力.

SHELLの箇所に編集されたところがあるが,今回とは関係ないからコメントアウトのままでOK.

# -*- 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 = "ubuntu/trusty64"

  # 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 "/home/<user>/kernel-5.9.1/work/", "/shared"

  # 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
  # Ansible, Chef, Docker, Puppet and Salt are also available. Please see the
  # documentation for more information about their specific syntax and use.
  # config.vm.provision "shell", inline: <<-SHELL
  #   yes | apt install autoconf bison libtool automake autopoint pkg-config
  # SHELL
end

編集が終わったら起動する.

$ vagrant up
$ vagrant ssh
(vagrant)$ uname -a
Linux vagrant-ubuntu-trusty-64 3.13.0-170-generic #220-Ubuntu SMP Thu May 9 12:40:49 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
(vagrant)$ cp /boot/initrd.img-3.13.0-170-generic /shared

これで指定した共有フォルダ内にimgファイルが保存される.
保存したimgを使ってカーネルイメージを再度実行してみる.
オリジナルのままだと名前が長いから改名した.

$ mv ./initrd.img-3.13.0-170-generic initrd_ubuntu.img
$ qemu-system-x86_64 -kernel ./bzImage -initrd ./initrd_ubuntu.img -nographic -append "console=ttyS0"

メッセージは出なくなった!

結論

問題のエラーメッセージはinitrd内(inittab/systemdが有力)に登録されたプロセスが原因と推測される.
問題となるプロセスが登録されていないinitrdがあればメッセージを回避できる.

所感

カーネルをビルドして使うなら用途に応じてinitrdも作り込まないといけないってことか.

参考

linux - can't open /dev/tty(2,3,4). No such file or directory - Stack Overflow

minimalなlinuxを作りたい、と思ったんだ - livaの雑記帳

Linux を(わりと)シンプルな構成でビルドして Qemu で起動する - Qiita

ビルドしたLinuxカーネルをブートできる最低限の環境を用意する(with Busybox & qemu) - 豆腐の豆腐和え

The Linux Kernel Archives

PCのBluetoothバージョンを確認するコマンド(Arch Linuxを例に)

Bluetooth 5.0は速いと聞く.
でもPC・イヤホンなどのBluetooth機器が両方ハード・ソフト的に対応していないと使えないらしい.
ソフトはアップグレードできるから,とりあえずPCのハードはどのバージョンなのかを確かめたメモ.

コマンド郡

コマンドの流れは以下の通り.
1. bluetooth関連のパッケージ(bluez, bluez-utils)をインストール
2. buetoothデーモンを実行(modprobe, systemctl)
3. bluetoothバイスを有効化(対話式コマンドbluetoothctlで操作)
4. 3.で起動したbluetoothバイスの詳細情報からバージョンを確認
恥ずかしいからMACアドレス?にしてある.

  # 1.bluetooth関連のパッケージ(`bluez`, `bluez-utils`)をインストール
  $ sudo pacman -S bluez bluez-utils

  # 2. buetoothデーモンを実行(`modprobe`, `systemctl`)  
  $ sudo modprobe btusb
  $ sudo systemctl start bluetooth # デーモン実行
  $ sudo systemctl enable bluetooth # サービスの有効化

  # 3. bluetoothデバイスを有効化(対話式コマンド`bluetoothctl`で操作)  
  $ which bluetoothctl # コマンド有無
  $ bluetoothctl help # コマンドの確認
  $ bluetoothctl # 対話式
  Agent registered
  [CHG] Controller ??:??:??:??:??:?? Pairable: yes
  [bluetooth]# agent KeyboardOnly
  [bluetooth]# default-agent
  [bluetooth]# power on
    [CHG] Controller ??:??:??:??:??:?? Class: 0x000c010c
    Changing power on succeeded
  
    [CHG] Controller ??:??:??:??:??:?? Powered: yes
  [bluetooth]# scan on
  [bluetooth]# devices # 近くにあるデバイス一覧表示(何らかのデバイスが出てくればOK)
  [bluetooth]# quit

  # 4. 3.で起動したbluetoothデバイスの詳細情報からバージョンを確認  
  $ hciconfig -a
  hci0:   Type: Primary  Bus: USB
        BD Address: ??:??:??:??:??:??  ACL MTU: 1021:4  SCO MTU: 96:6
        UP RUNNING PSCAN ISCAN
        RX bytes:15896 acl:0 sco:0 events:2476 errors:0
        TX bytes:596459 acl:0 sco:0 commands:2466 errors:0
        Features: 0xff 0xfe 0x0f 0xfe 0xdb 0xff 0x7b 0x87
        Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3
        Link policy: RSWITCH HOLD SNIFF
        Link mode: SLAVE ACCEPT
        Name: '????????????'
        Class: 0x0c010c
        Service Classes: Rendering, Capturing
        Device Class: Computer, Laptop
        HCI Version: 4.2 (0x8)  Revision: 0x100
        LMP Version: 4.2 (0x8)  Subversion: 0x100
        Manufacturer: Intel Corp. (2)

最後のコマンド結果にあるLMP Version: 4.2...というところがBluetoothのバージョンに当たる.
LMP (Link Manager Protocol) のバージョンとBluetoothの対応付は以下の通り.

LMP (Link Manager Protocol) バージョン Bluetooth コア仕様
LMP 0 Bluetooth コア仕様 1.0b (使用中止)
LMP 1 Bluetooth コア仕様 1.1 (使用中止)
LMP 2 Bluetooth コア仕様 1.2 (使用中止)
LMP 3 Bluetooth コア仕様 2.0 + EDR (使用中止)
LMP 4 Bluetooth コア仕様 2.1 + EDR (非推奨、使用中止予定)
LMP 5 Bluetooth コア仕様 3.0 + HS (非推奨、使用中止予定)
LMP 6 Bluetooth コア仕様 4.0
LMP 7 Bluetooth コア仕様 4.1
LMP 8 Bluetooth コア仕様 4.2
LMP 9 Bluetooth コア仕様 5.0
LMP 10 Bluetooth コア仕様 5.1
LMP 11 Bluetooth コア仕様 5.2tel Corp. (2)

今回自分のPCは4.2.つまりBluetooth コア仕様 2.1 + EDR (非推奨、使用中止予定)
最新機種のイヤホンなどはほぼその性能を活かせない.ウソみたいだろ?

所感

Bluetooth機器を買う夢は始まる前から終わっていた.
LMPの対応表を見つけるまではLMP=Bluetoothでなんだ4.2対応機種を探せばいいのかーくらいに軽く考えていた.

もしかして最新のAirPodsが接続できなかったのはこれが原因だろうか・・・.
調べた感じだとプロファイルがきっちりしていれば接続自体はできるとあったが,それでも高速伝送などの面で劣ることに変わらない.
次の手段を考えるかー.
まだできることはあるはずだ.

参考

Bluetooth - ArchWiki

最新のBluetooth規格とアップグレード方法 | ゲーミングPC辞典

https://support.microsoft.com/ja-jp/help/4524769/windows-10-what-bluetooth-version-is-on-my-device

情報系カンファレンス格付けサイトCOREを知った話

カンファレンスの格付けが素早く調べられるCOREを知った話.

背景

研究の流行やState Of The Art(SOTA)を知りたいときはトップカンファレンスの論文を探すことから始めるようにしている.
限られた時間で質の良い論文をすばやく探すのは重要だ.

課題

論文を探す上で以下の課題に悩まされていた.

  • 無数にあるカンファレンスの中でどれがトップなのか悩む
  • 効率が悪い(検索アルゴリズムはbrute force, O(n)?)
    (とりあえず知っているACM CCSの論文で参照されている会議の論文をあたる
    ACMの公式サイトにあるカンファレンスを総当り)

インパクトファクターもあるけど生物系の評価基準でコンピュータ・サイエンスには使いにくい気がする(個人の意見です).

格付けサイトを使う利点

今回紹介するCORE(に限らず)格付けサイトを使えば以下の利点がある.

  • 無数にあるカンファレンスの中でどれがトップなのか悩む
    → トップカンファレンス(格付けサイト内で評価が高い)の把握ができる
    質の良い論文が揃っているカンファレンスを素早く把握できる
  • 効率が悪い(検索アルゴリズムはbrute force)
    → 調べる対象を減らすことができる(O(n)的なのは変わらないかも・・・)

上記に加えて自分が知らない領域でも読むべきカンファレンスの絞り込みができる.

Computing Research & Education(CORE)

本題.
Computing Research & Education(以降CORE)というサイトを紹介.
オーストラリアとニュージーランドの大学により運営されている.
コンピュータ・サイエンスに特化しており,分野を限って調べたいときは都合が良い.
ただし,Unrankedなカンファレンスも多いため参考にできないこともある.
Documentも2020年版が掲載されているため,情報も新しそうだ.

格付け基準

カンファレンスの格付けは*A/A/B/C/unranked.
詳細はここに記載.

迷ったら*A/Aを当たれば良さげ.
内部ではJournal Citation ReportsとScopusの評価を参考にしているようだ.

使い方

ここに会議名を入れるだけ.
dimvaってトップカンファレンスと思っていたけどC評価だった.
ええ・・・?

所感

格付けサイトの回し者みたいな記事になってしまった・・・.
調べはじめの方針決めに使うのが良さげ.
実際は他の分野も総括して調べられるScopus(使ったことはない)を使うのが良いのかな.

余談になるが,会計処理も公開していた.
2019年は$45,234.21, 日本円に換算すると480万円.
サイト運営も楽じゃないようだ.

参考

Computing Research & Education

Filesystem Hierarchy Standardを読む

Linuxに出会ってから10年ほど経つが標準文書を読んだことはなかった.
まずはファイルシステムについて規定されたFilesystem Hierarchy Standardを理解がてらまとめる.
適宜省略しているため必要に応じて原文を読んでください.

Linuxの標準について

Linuxの規定はLinux Standard Base(以後LSB)に記載されている.
目的: LinuxディストリLSBの目標は、Linuxディストリビューション間での互換性を向上させ、準拠システム上でのアプリケーションの動作を保証するよう標準規格を策定・振興することである。さらに、ソフトウェアベンダーがLinux向けに製品を移植したり開発する際の調整努力を助ける。(Wikipediaより)

最新は5(2020/05/17現在)Coreドキュメントは964ページある.
基本的なライブラリlibc関係とかよくしらなかったがそんな疑問もこれを読めばとっかかりとなりそう.

Filesystem Hierarchy Standard

ようやく本題.3.0(50 page)を読み進めていく.
Linuxの必要なファイル・ディレクトリ配置を記載した文書.
目的はアプリケーション,管理ツールなどの互換性を図るもの.

文書の目的(Chapter1 Introduction Purpose)

ユーザ・ソフトがインストールされているファイルやディレクトリの場所を予想できるようにすること.

Root Filesystem(Chapter3)

root filesystemはboot/restore/recover/repairのために必要なものを格納する.

必要なディレクトリ一覧

ディレクトリ名 詳細
bin 必須コマンドバイナリ置き場
boot ブートローダ用の静的ファイル
dev バイスファイル
etc ホスト特有の設定ファイル
lib 必須共有ライブラリとカーネルモジュール
media removable meidaのマウントポイント
mnt 一時的なファイルシステムのマウントポイント
opt Add-onアプリ置き場
run running processesのデータ関連
sbin 必須システムバイナリ
srv システムから提供されるサービスのデータ
tmp 一時ファイル
usr Secondary Hierarchy
var Variable data
以下,オプション
home ユーザ用ホームディレクト
lib 必須な共有ライブラリの代替
root root用のホームディレクト

bootableなLinuxを作ろうと思ったら,最低限上記のディレクトリを揃える必要があるということか.

ここでは省略するが,各ディレクトリの詳細も記載されている.
/usr/share/colorとか初耳なディレクトリもある.

/var Hierarchy(Chapter5)

可変データファイル置き場. スプールディレクトリ,ファイル,管理ログも含まれる.

必要なディレクトリ一覧

ディレクトリ名 詳細
cache キャッシュデータ
lib 状態情報
local /usr/localの情報
lock lock files
log ログファイル関連
opt /opt用の情報
run running processes関連のデータ
spool アプリのspool data(syorimati data)
tmp 一時ファイル
以下,オプション
account process accounting logs
crach system crash dumps
games variable game data
mail user mailbox files
yp Network Information Service database files

/var/lockは複数のアプリで共有されているデバイスやリソースを管理するために使われる.

所感

文書によっては2770ページという頭が真っ白になりそうな量もあるが,調べたい領域を絞れば数十ページで済む.
読み始めてしまえばわかりやすい英語でスラスラ読めた.

今回読んだFHSには各ディレクトリ全てについて何に使うために存在するかが書かれている.
何度も繰り返し読みながら理解を進めていきたい.
spoolは処理待ちの一時ファイルを格納する場所初めて知ることもあってまだまだ勉強不足だと思い知った.
kernel開発者になる道は遠い. 一歩々々進んでいきたい.

参考

Linux Standard Baseとは

FHS本文

規定文書リンクサイト

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

Linux環境でのインストールスクリプト確認用にdocker-composeを整備する

開発したプログラムが複数の環境上で動くかどうかの確認をしたいときがある.
例えばUbuntu, CentOSはバージョン・ディストリの特徴によって環境が僅かに違うが,新しいOSをインストールした際の設定自動化スクリプトはどんな場所でも動いてほしい.
例えば自分が作成したプログラムのコンパイル・インストールをどんな環境でも動くスクリプト1つ実行する形にまで仕上げたいとき
しかしこれを全環境の仮想マシンを用意してチェックしていくのは面倒だ.
というわけでdocker-composeを使って一斉に各ディストリで動くかどうかをチェックする環境を作った話.

やりたかったこと

最初に書いた理由も再掲.

  1. 作ったプログラムが各ディストリ・バージョン上で動くかどうか簡単に確かめたい
  2. 開発環境で動くようになったがまっさらな環境で動くのか確認したい
  3. 上記を一斉に行いたい

とうわけでコンパイル・インストールを確認する環境を作り,自分の環境構築用スクリプトが動くかどうかを確かめた.

何を使って実現するか

他の人がどうやっているのか知らないがいくつか案を練った.

  1. Virtualboxを複数立てて(vagrantを使う?)SSHで同時接続
  2. コンテナ(docker-compose, Kubernetes)を使う
  3. ラズパイをたくさん買ってPCを複数接続できるキーボードを用意して頑張る(ド鬼畜)

3.は財布的・手間的にない.1.は理想的だが手間がかかる.
2.が現実的かな.
docker-composeKubernetesを比較すると後者のほうが大規模の構成ができそうだ.
が,今回の用途だとそこまで複雑な実装は不要そう.
zozoのブログが非常に刺激になり,現実的な実装を思いつけた.

考えたアーキテクチャ

考慮した結果こうなった.

f:id:poppycompass:20200205213220p:plain
イメージ
開発者は端末から最初コマンドを実行すればあとはコンテナを立ち上げ,各コンテナ内での標準入出力をログとしてホスト上に保存する.
ファイル構成はこんな感じ.

|--dists # 各ディストリ設定を置く
|  |--ubuntu
|  |  |--Dockerfile
|  |  |--personal-settings.yml
|--docker-compose.yml # 共通設定
|--.env # 環境変数
|--logs # 各コンテナからのログの保存先
|  |--ubuntu.log # ubuntuコンテナからのログ(dists/???は???.logというファイルを作る)
|--run_env_test.sh # テスト実行

コンテナの追加はシンプルにしたかった.
考えた結果,追加はdists以下にコンテナ用の設定ディレクトリを追加するだけで良いようにした.
$ cp dists/ubuntu dists/centosとかして,中のファイルをいじれば良い.

各ファイルの中身

dists/<ディストリ>/Dockerfile

例はubuntuの場合で説明.

FROM ubuntu # 好きなコンテナを指定.他の設定もお好みで追加.

dists/<ディストリ>/personal-settings.yml

ubuntuの場合.
各コンテナ固有の設定を書く. ホスト上のディレクトリをマウントして共有している.
commandのところに実行したいコマンドを入力する.
今入力されているのは環境構築用のスクリプト
最後のsleepコマンドはエラーが合った時にデバッグするためにbashを継続させる.

version: "3.7"
services:
  ubuntu:
    image: ubuntu
    tty: true
    container_name: 'ubuntu'
    environment:
      APP_ENV: ubuntu
    volumes:
      - ./dists/ubuntu/Dockerfile:/mnt/host/Dockerfile:ro
      - ./dists/ubuntu/personal-settings.yml:/mnt/host/personal-settings.yml:ro
      - ./logs:/mnt/host/logs
    command: bash -c 'apt -y update > /mnt/host/logs/ubuntu.log &&
                      apt -y install git >> /mnt/host/logs/ubuntu.log &&
                      git clone https://github.com/poppycompass/dotfiles >> /mnt/host/logs/ubuntu.log &&
                      cd dotfiles >> /mnt/host/logs/ubuntu.log &&
                      ./start.sh >> /mnt/host/logs/ubuntu.log &&
                      sleep 600'

volumes:
  ubuntu:
    driver_opts:
      type: none
      # 絶対パスで指定する
      device: /home/user/logs
      o: bind

.env

環境設定用ファイル.

UBUNTU=ubuntu

run_env_test.sh

テストを実行するために使うスクリプト
コンテナの完全初期化と実行というシンプルな内容.

#!/bin/sh
# installation test for each Linux distributions
TOPDIR="dists"
COMMON="docker-compose.yml"

readonly DEBUG=false
if "${DEBUG}"; then
  DISTS="ubuntu" # 1つずつの環境で実行したいとき
else
  DISTS=`ls ${TOPDIR}`
fi

# Run your installation process
function run () {
  for dist in ${DISTS}
  do
    # sudo docker ps
    # sudo docker exec -it <hash> bash
    sudo docker-compose -f ${COMMON} -f ${TOPDIR}/${dist}/personal-settings.yml -p ${dist} up -d
  done;
}

# Initialize all docker containers
function initialize () {
  sudo docker stop $(sudo docker ps -aq)
  sudo docker rm $(sudo docker ps -aq)
  sudo docker network prune -f
  sudo docker rmi -f $(sudo docker images --filter dangling=true -qa)
  sudo docker volume rm $(sudo docker volume ls --filter dangling=true -q)
  sudo docker rmi -f $(sudo docker images -qa)
}

# Show help
function show_help () {
  echo "Usage: ./run_env_test.sh [options]"
  echo "[options]: "
  echo "  run : run your installation process"
  echo "  init: initialize all container"
}

# Main routines
if [ "$1" == "run" ]; then
  run
elif [ "$1" == "init" ]; then
  initialize
else
  show_help
fi

実行

$ ./run_env_test.sh run
これで各コンテナが実行される.
$ tail -f logs/ubuntu.log
とかすればコンテナ内で何が行われているか把握できる.

初期化

コンテナをまっさらな状態にしたいときは
$ ./run_env_test.sh init

評価

やりたかったこと(再掲)

  1. 作ったプログラムが各ディストリ・バージョン上で動くかどうか簡単に確かめたい
    → 各ディストリ・バージョン環境の簡単な追加,コマンド実行ができるようになった!
  2. 開発環境で動くようになったがまっさらな環境で動くのか確認したい
    → コンテナを初期化することで毎回まっさらな環境で試せるようになった!
  3. 上記を一斉に行いたい
    → 一斉に起動可能!

最初に設定した要件は満たした.

改善が必要なところ

  • dockerは基本的にrootのため,user環境での実験作成
    add userからやるのであれば問題ないか?
  • プログラムを修正してからの再度実行に手間がかかる(Dockerの初期化・再起動させる必要がある.デバッグ時の細かい試し実行がしにくい)
    → 対策考え中

所感

しばらくはこれで運用しようと思う.
Kubernetesは勉強中のためこっちのほうが良さそうならまた同じような環境を作ろうと思う. もっと冴えたやり方を知っている方は是非教えてください.

参考

自動テストの実行環境をDockerでお気軽引っ越し - ZOZO Technologies TECH BLOG

Docker入門(第六回)〜Docker Compose〜 | さくらのナレッジ

複数のDockerコンテナを自動で立ち上げる構成管理ツール「Docker Compose」(Dockerの最新機能を使ってみよう:第7回) | さくらのナレッジ

Dockerfileでうまくマウントできないのでdocker-compose.yml使ってマウントする(o*。_。)o - Qiita

Docker Composeのトップレベルvolumesでホストのディレクトリをマウントする - ニューなんとなく書くブログ

docker-compose volumeマウント - Qiita