$30 off During Our Annual Pro Sale. View Details »

CloudNative Days Winter 2025: 一週間で作る低レイヤコンテナランタイム

Avatar for ternbusty ternbusty
November 19, 2025

CloudNative Days Winter 2025: 一週間で作る低レイヤコンテナランタイム

Avatar for ternbusty

ternbusty

November 19, 2025
Tweet

More Decks by ternbusty

Other Decks in Programming

Transcript

  1. © LY Corporation 2 Ayako Hayasaka LINEヤフー株式会社 Software Engineer 2023

    年度⼊社 SWAT として Web バックエンド領域で全社横断的な 技術⽀援を実施 メインは Spring Boot + Java / Kotlin • RAG 技術を利⽤した業務効率化ツール SeekAI 開発 • Yahoo!知恵袋の AI 回答機能「みんなの知恵袋」開発 • ⽣成 AI を利⽤した QA 領域の⽣産性向上ツール開発 • Yahoo!きっずフィルタリング機能 AI 導⼊ © LY Corporation 2
  2. © LY Corporation Agenda • そもそもコンテナとは? • 低レイヤコンテナランタイムって何をするの? • Kotlin/Native

    の概要と選定理由 • ⾃作コンテナランタイムの実装紹介 (create, start を中⼼に) • 各種 namespace 分離をどう実現するか • namespace 分離後の流れ 6
  3. © LY Corporation そもそもコンテナとは? 図は徳永航平, イラストでわかる Docker と Kubernetes, 技術評論社

    (2020), p6 を元に作成 ホストから隔離された、独⽴した実⾏環境 • プロセスとして実⾏されている • カーネルはホストのものを共有する ハードウェア OS プロ セス プロ セス 7
  4. © LY Corporation 「隔離」ってどういうこと? • ホストから取り扱えるリソースと、コンテナから取り扱えるリソースを分けて、 別の名前空間で管理する • Linux の

    namespace 機能が⽤いられる • 例: PID namespace (プロセス ID の namespace) そのコンテナ内で動作しているプロセスのみが 表⽰され、ホストのプロセスは⾒えない ホストからコンテナのプロセスは⾒えるが Pid は異なる 10
  5. © LY Corporation Linux に存在する namespace namespace 意義 PID namespace

    プロセス群の隔離 UTS namespace ホスト名などの隔離 (ホストと異なるホスト名をつけられるようになる) IPC namespace プロセス間通信に関するリソースの隔離 network namespace ネットワーク関連のリソースの隔離 mount namespace マウントポイントリストの隔離 user namespace ユーザ・グループや権限などの隔離 (プレゼン後半で詳述) cgroup namespace プロセスから⾒える cgroup の階層を隔離 time namespace システム起動からの経過時間を隔離 11
  6. © LY Corporation 「異なるルートファイルシステムを持つ」って どういうこと? • コンテナ内からホストのファイルシステム全体 が⾒えたり、操作ができたりしたらまずい • Linux

    では、pivot_root(2) を⾏うと、現在の プロセスのルートディレクトリを変更できる • コンテナ内部から⾒ると、そのディレクトリが ルートディレクトリに⾒えるようになり、それ より上にはアクセスできない ホスト上のファイルシステム • /hoge/fuga/piyo/test.txt • /hoge/fuga/example.sh • /hoge/sample.py /hoge/fuga/ をルートディレクトリに 変更したプロセスからはこう⾒える • /piyo/test.txt • /example.sh 13
  7. © LY Corporation 「異なるルートファイルシステムを持つ」って どういうこと? • コンテナ内からホストのファイルシステム全体 が⾒えたり、操作ができたりしたらまずい • Linux

    では、pivot_root(2) を⾏うと、現在の プロセスのルートディレクトリを変更できる • コンテナ内部から⾒ると、そのディレクトリが ルートディレクトリに⾒えるようになり、それ より上にはアクセスできない ホスト上のファイルシステム • /hoge/fuga/piyo/test.txt • /hoge/fuga/example.sh • /hoge/sample.py /hoge/fuga/ をルートディレクトリに 変更したプロセスからはこう⾒える • /piyo/test.txt • /example.sh 14
  8. © LY Corporation コンテナ内部からファイルシステムを確認してみよう • コンテナ内部からルートディレクトリを⾒ると ← のような感じ • 同じものがホスト上のファイルシステムからも

    ⾒える。この merged 以下をコンテナの ルートディレクトリに指定している形↓ この /var/lib/docker/.../merged ディレクトリはどうやって作られたのか? 15
  9. © LY Corporation コンテナのルートファイルシステムが作られる流れ コンテナのファイルシステムが含まれている (※) コンテナイメージ コンテナランタイムが展開してホストのファイルシステムにマウント /var/lib/docker/overlay2/.../merged/... このディレクトリ以下を

    コンテナのルートディレクトリとして指定 ※ コンテナイメージにファイルシステムがそのまま含まれるわけではなく、実際はコンテナイメージに含まれる イメージレイヤとコンテナレイヤを overlay ファイルシステムで重ね合わせたものがマウントされています 16
  10. © LY Corporation cgroup について知ろう! • プロセスが利⽤可能なリソースについて制限を設定できる機能 • CPU の制限、メモリ使⽤量の制限、プロセス数の制限など

    • コンテナはプロセスなので、cgroup を利⽤して制限をかけられる • k8s における resources.limits.memory などで間接的に利⽤したことの ある⼈は多そう • /sys/fs/cgroup に存在する擬似ファイルシステムを通じて設定が管理されて いる 20
  11. © LY Corporation cgroup による制限はどのように設定できるの? [Ubuntu2404-template:/sys/fs/cgroup] $ tree . ├──

    test │ ├── cgroup.procs │ ├── cgroup.controllers │ ├── cgroup.subtree_control │ ├── cpu.max │ ├── memory.max │ $ cat cgroup.controllers cpuset cpu io memory hugetlb pids rdma misc このグループで制限可能なリソースの種類の⼀覧 $ cat cgroup.procs 3200627 3201166 制限を適⽤させる対象のプロセス ID の羅列 この下のサブディレクトリで制御を有効にする リソースを指定するためのファイル (ここでは機能していない) $ cat cpu.max 50000 100000 $ cat memory.max 536870912 Process 2300627、3201166 およびその⼦プロセス全部の合計について CPU 500m, memory 512MB 制限を適⽤している 21
  12. © LY Corporation 例えばコンテナに memory の制限を かけたいときはどうするの? • ホストは、/sys/fs/cgroup/cgroup.subtree_control に

    “+memory” を書き込む • その後、/sys/fs/cgroup/ に test など適当な名前でディレクトリを切る • /sys/fs/cgroup/test/cgroup.procs にコンテナのプロセス ID を記⼊ • /sys/fs/cgroup/test/memory.max に 512MB (536870912) を書き込む • これらのファイル操作が完了すれば、制限は有効な状態! • コンテナ内部から⾃⾝にかけられているリソース制限を知るには? • cgroup namespace を利⽤するように指定されていればそれを分離するか、 あるいは/sys/fs/cgroup/test の内容をコンテナ内の/sys/fs/cgroup に bind mount する必要がある 22
  13. © LY Corporation • コンテナ内で、ホストのリソースを 枯渇させるような操作が⾏われると困る • めちゃくちゃ⼤量のメモリを使うとか • コンテナ内で、ホストに影響を与えるような

    操作が⾏われると困る • reboot 操作とか cgroup を使えば 制限をかけられる capability の制限, seccomp, AppArmor, SELinux などの 仕組みが存在する 本プレゼンの後半でこのうち seccomp に focus する予定 コンテナが悪さをすると困る! 23
  14. © LY Corporation ここまでのまとめ • コンテナはプロセスだが、普通のプロセスとは⼀味違う • ホストと隔離された実⾏環境を持つ: Linux の

    namespace 機能を⽤いて実現 • ホストと異なるルートファイルシステムを持つ: pivot_root をすれば コンテナから⾒えるルートディテクトリを変えられる • リソースの使いすぎを予防できる: cgroup の機能を⽤いて実現 • 各種セキュリティの適⽤: コンテナに実⾏されたくない操作を制限できる 24
  15. © LY Corporation 低レイヤコンテナランタイムって何? ⾼レイヤランタイム (CRI ランタイム) 低レイヤランタイム (OCI ランタイム)

    指⽰ (コンテナの create, start, kill, delete, state) コンテナ・イメージ・ネットワーク等の 全般的な管理を⾏う。containerd など ユーザ・kubelet などによる指⽰ 徳永航平, イラストでわかる Docker と Kubernetes, 技術評論社 (2020), p97 の図を参考に作成 ホストから隔離された実⾏環境を コンテナとして作り出し、操作可能にする runc, youki など 26
  16. © LY Corporation 低レイヤコンテナランタイムって何? ⾼レイヤランタイム (CRI ランタイム) 低レイヤランタイム (OCI ランタイム)

    指⽰ (コンテナの create, start, kill, delete, state) コンテナ・イメージ・ネットワーク等の 全般的な管理を⾏う。containerd など ユーザ・kubelet などによる指⽰ 徳永航平, イラストでわかる Docker と Kubernetes, 技術評論社 (2020), p97 の図を参考に作成 ホストから隔離された実⾏環境を コンテナとして作り出し、操作可能にする runc, youki など 27
  17. © LY Corporation 低レイヤランタイムはどのような指⽰を受ける? • ⾼レイヤランタイムは、低レイヤランタイムのバイナリを実⾏することにより コンテナ操作の指⽰を出す • この interface

    仕様は、Open Container Initiative により Open Container Interface (OCI) Runtime Specification として定められている • create, start, kill, delete, state の 5 種類の操作とその ⼊出⼒仕様を定義 • 低レイヤランタイムはこの仕様に準拠した interface を提供する必要があるの で、OCI Runtime とも呼ばれる https://github.com/opencontainers/runtime-spec 28
  18. © LY Corporation 低レイヤランタイムはどのような指⽰を受ける? • コンテナ作成時の実⾏⽅法を詳しく⾒てみよう $ sudo runc create

    --bundle test-bundle test-container 低レイヤランタイムの バイナリ 実⾏したい操作 作りたいコンテナ名 ディレクトリを 指定 • Bundle には、以下のものが含まれる • コンテナのルートファイルシステム • コンテナの設定が⼊った config.json ファイル 29
  19. © LY Corporation 低レイヤランタイムはどのような指⽰を受ける? • コンテナ作成時の実⾏⽅法を詳しく⾒てみよう $ sudo runc create

    --bundle test-bundle test-container 低レイヤランタイムの バイナリ 実⾏したい操作 作りたいコンテナ名 ディレクトリを 指定 • Bundle には、以下のものが含まれる • コンテナのルートファイルシステム • コンテナの設定が⼊った config.json ファイル
  20. © LY Corporation 低レイヤランタイムはどのような指⽰を受ける? • コンテナ作成時の実⾏⽅法を詳しく⾒てみよう $ sudo runc create

    --bundle test-bundle test-container 低レイヤランタイムの バイナリ 実⾏したい操作 作りたいコンテナ名 ディレクトリを 指定 • Bundle には、以下のものが含まれる • コンテナのルートファイルシステム • コンテナの設定が⼊った config.json ファイル 31
  21. © LY Corporation config.json の中⾝ってどんなの? https://github.com/opencontainers/runtime-spec/blob/main/schema/test/config/good/spec-example.json • コンテナの設定全般が含まれる。この仕様も OCI Runtime

    Spec に定義されている どの namespace を 切るかを指定 ルートファイルシステムが 存在する path 起動時にどんなコマンドを 実⾏するか 32
  22. © LY Corporation create した後に start が必要! $ sudo runc

    start test-container 低レイヤランタイムの バイナリ 実⾏したい操作 起動したいコンテナ名 • ただ create しただけでは、コンテナになるプロセスの準備ができただけで、 まだコンテナは実⾏されていない • コンテナ起動時に実⾏するコマンドをそのプロセス内で実⾏することで、初めて コンテナとして起動する • それを指⽰するのが start コマンド 33
  23. © LY Corporation コンテナが create & start される流れ 1. ユーザ・kubelet

    などによる指⽰ 2. コンテナイメージを展開して ルートファイルシステムの準備・ config.json の作成 3. bundle を指定して runc create を実⾏ 4. config.json に従って namespace 分離や pivot_root などを ⾏いコンテナプロセスを作成 5. ネットワークの設定などを 実施 6. runc start を実⾏ 7. 作成されていたコンテナプロセスで 起動時のコマンドを実⾏ -> コンテナが⽴ち上がる ⾼レイヤランタイム (CRI ランタイム) 低レイヤランタイム (OCI ランタイム) (create コマンド実⾏が成功) 34
  24. © LY Corporation Kotlin/Native はどう動いているのか .kt Frontend Kotlin IR Backend

    LLVM IR LLVM .kexe • LLVM をバックエンドとして利⽤し、ネイティブバイナリ (つまり JVM がなくても動く!) を⽣成する 39
  25. © LY Corporation Kotlin/Native はどう動いているのか .kt Frontend Kotlin IR Backend

    LLVM IR LLVM .kexe • LLVM をバックエンドとして利⽤し、ネイティブバイナリ (つまり JVM がなくても動く!) を⽣成する • ランタイムの性質 • バイナリが実⾏されると、メインスレッドだけでなく GC ⽤スレッドも ⽴ち上がり、マルチスレッドで動作する • Go と同じような仕組み 40
  26. © LY Corporation 41 C Interoperability の様⼦ import kotlinx.cinterop.ExperimentalForeignApi import

    platform.linux.__NR_getpid import platform.posix.getpid import platform.posix.syscall @OptIn(ExperimentalForeignApi::class) fun basicExample() { // Using POSIX getpid() println(getpid()) // Using syscall to invoke getpid() val pid = syscall(__NR_getpid.toLong()) println("pid = $pid") } libc の関数が普通に呼べる syscall 直呼びもできる https://github.com/ternbusty/kotlin-native-playground/blob/main/src/nativeMain/kotlin/examples/CInteropExample.kt
  27. © LY Corporation 42 memScoped の利⽤ ここで C の関数に ポインタを渡している

    @OptIn(ExperimentalForeignApi::class) fun memScopedFileWriteExample() { memScoped { val path = "/tmp/memscoped_example.txt" val content = "Hello from Kotlin/Native!" val fd = open(path, O_WRONLY or O_CREAT or O_TRUNC, (S_IRUSR or S_IWUSR).toUInt()) if (fd < 0) { perror("open") return } val cContent = content.cstr.getPointer(this) val written = write(fd, cContent, content.length.toULong()) if (written < 0) { // 後略 例えば Kotlin の String を C には そのまま渡せない C とやり取りする際に⼀時的なネイティブ メモリ領域を確保したいときに使う このブロックを抜けた時点で、確保した メモリは解放される
  28. © LY Corporation 43 libc にないものも呼べる Kotlin コード内から そのライブラリの関数を 呼び出せるようになる

    headers = seccomp.h headerFilter = seccomp.h package = libseccomp compilerOpts = -I/usr/include -I/usr/include/x86_64-linux- gnu linkerOpts = -L/usr/lib/x86_64-linux-gnu -lseccomp libseccomp.def import libseccomp.* // 中略 val ctx = seccomp_init(defaultAction) ?: run { https://github.com/ternbusty/kontainer-runtime/blob/main/src/nativeMain/kotlin/seccomp/Seccomp.kt 利⽤したい C ライブラリの Header 等を .def ファイルに記載する
  29. © LY Corporation ⾃作コンテナランタイムの紹介 • Kotlin/Native 製⾃作コンテナランタイム kontainer-runtime • https://github.com/ternbusty/kontainer-runtime

    • x86_64 の Linux でしか動きません • 現時点で、OCI Runtime Spec 完全準拠ではありません • 注意 • cgroup namespace, time namespace 未対応 • Rootless 実⾏できません (systemd 連携なし) • コンテナへのファイル / ディレクトリマウント未対応 • Hooks 未対応 • cgroup: cpu, memory の制限のみ & cgroup path が階層的になると cgroup が正しく機能しない • AppArmor, SELinux 未対応 45
  30. © LY Corporation ⾼レイヤコンテナランタイムと組み合わせたデモ containred はデフォルトでは低レイヤランタイムとして runc を利⽤するので、 ⾃作コンテナランタイムを利⽤させるため runc

    binary path の指定が必要 ctr は containerd を 呼び出す⽤の CLI ⾃作コンテナランタイムの バイナリを指定 うまくいけば alpine の 情報が出⼒されるはず 47
  31. © LY Corporation Namespace 分離ってどうやるの? unshare --user --mount --pid --ipc

    ‒uts ... こんな感じでやれば上⼿くいきそう! 50
  32. © LY Corporation Namespace 分離ってどうやるの? unshare --user --mount --pid --ipc

    ‒uts ... こんな感じでやれば上⼿くいきそう! コトはそう単純ではない! 51
  33. © LY Corporation • 前提: 先述の通り、Kotlin/Native は GC スレッドも含めたマルチスレッドで起動する •

    名前空間分離のため unshare (2) を叩くとどうなる……? • 例えば user namespace を分離しようとするとエラーになる 52 Kotlin/Native のマルチスレッド性による問題 (1) 普通には unshare(2) ができない! https://man7.org/linux/man-pages/man2/unshare.2.html
  34. © LY Corporation • 前提: 先述の通り、Kotlin/Native は GC スレッドも含めたマルチスレッドで起動する •

    名前空間分離のため unshare (2) を叩くとどうなる……? 53 Kotlin/Native のマルチスレッド性による問題 (1) https://github.com/ternbusty/kotlin-native-playground • エラーにならないまでも、呼び出した スレッドのみが新しい namespace に⼊り、 他のスレッドは元の namespace に残った ままというおかしな状態になるケースがある • 右図: network namespace 分離後に 各スレッドの namespace を表⽰したもの このスレッドだけ 違う namespace に 移動している 普通には unshare(2) ができない!
  35. © LY Corporation • 前提: pid namespace は、単に unshare しただけでは

    namespace 移動は発⽣せず、その 後に fork した後のプロセスが新しい namespace に⼊る。つまり fork が必須 • Kotlin/Native 上で fork & そのタイミングで運悪く GC が発⽣するとどうなる……? • fork 時の挙動 • 親プロセスのメモリは全てコピー • 親プロセスがマルチスレッドだった場合は、呼び出し元スレッドのみがコピー • つまり、⼦プロセスからすると、GC の時に取られたロック状態はコピーされるが、 そのロックを解除するはずの GC スレッドはもはや存在しない状態になってしまう • 結果的に、永久にロックを解除できずに hang してしまう 54 Kotlin/Native のマルチスレッド性による問題 (2) fork すると hang してしまう可能性がある! https://github.com/ternbusty/kotlin-native-playground
  36. © LY Corporation • namespace 分離および fork 部分は Kotlin/Native でやるのを潔く諦めよう!

    • Kotlin ランタイムが起動する前に、C のプログラムが起動するように設定しておく • __attribute__((constructor)) を付与した C の関数は、 elf バイナリの .init_array というセクションに配置され、プログラムの メイン関数を呼び出す前に実⾏される • つまり、シングルスレッド状態で実⾏することが可能 • まず初回は普通に Kotlin ランタイムを起動したのち、⾃分⾃⾝を指定して再実⾏させるこ とにより C のプログラムが動くように⼯夫した 55 Kotlin/Native のマルチスレッド性による問題 Tricky な解決策
  37. © LY Corporation • 実は Go で書かれた runc でも同じ問題に苦しんでいる •

    Go もデフォルトでマルチスレッドで動作するため • 同様に⼀部を C で実装することによりマルチスレッド問題を回避している 56 Kotlin/Native のマルチスレッド性による問題 実は runc でも同じ問題に苦しんでいる https://github.com/opencontainers/runc/blob/f73814296c013f82f72252d6e4e2d917090028f2/libcontainer/nsenter/nsexec.c
  38. © LY Corporation Create の流れ (概略版、途中まで) Main Process Stage 1

    Stage 2 Config のパースなど clone して ⾃分⾃⾝を再実⾏ 各種 namespace の分離 pid namespace 分離後 もう⼀度 clone ホスト側で 実⾏すべきあれこれ コンテナプロセス側で 実⾏すべきあれこれ Start シグナル待ち Stage1 は C で書かれている Parse 結果は環境変数に 書き込んでおく 57
  39. © LY Corporation Create の流れ (概略版、途中まで) Main Process Stage 1

    Stage 2 Config のパースなど clone して ⾃分⾃⾝を再実⾏ 各種 namespace の分離 pid namespace 分離後 もう⼀度 clone ホスト側で 実⾏すべきあれこれ コンテナプロセス側で 実⾏すべきあれこれ Start シグナル待ち Stage1 は C で書かれている これらに関わってくる User Namespace について ⼀般論から深掘りしていきます Parse 結果は環境変数に 書き込んでおく 58
  40. © LY Corporation • 例えば普通に nginx を⽴ち上げた時、コンテナ内のプロセスを実⾏しているユーザは誰? 59 コンテナを実⾏しているユーザって誰……? User

    Namespace はなぜ必要か $ docker run -d --name nginx-test nginx $ docker top nginx-test UID PID CMD root 3151279 nginx: master process nginx -g daemon off; • ホスト側から⾒て root で実⾏されてしまっている! • namespace 隔離があるので通常は問題にならないが、脆弱性を突いてコンテナエスケープ された場合、そのプロセスはホストのリソースを root 権限で操作できてしまう権限を 持つことになり、危ない!
  41. © LY Corporation • コンテナのプロセスを root ではなく、⼀般ユーザの id を指定して実⾏させればいいのでは? 60

    じゃあ⼀般ユーザを利⽤したらいいのでは? • Root 実⾏が前提のイメージなので、今度はコンテナ内で権限が⾜りず、⽴ち上げに失敗 😭 • こんな仕組みがあったらいいのに…… • ホスト側から⾒ると、そのプロセスの uid は⼀般ユーザ (ホスト側での特権なし) • コンテナ内では、そのプロセスは root として振る舞える (コンテナ内での特権あり) $ docker run --rm -u 10000 nginx # 中略 nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied) User Namespace はなぜ必要か
  42. © LY Corporation • プロセスがユーザおよびグループ ID の独⾃のビューを持てるようにする仕組み • 例えば、コンテナ内の root

    ユーザ (ID 0) を、ホスト上の⾮ root ユーザ (例えば ID 100000) に mapping すると • コンテナから⾒ると uid 0、ホストから⾒えると uid 100000 • つまり、コンテナ内では root として振る舞い、だがホストから⾒ると⼀般ユーザ、 という状態を実現できる! User Namespace を導⼊してみる 61
  43. © LY Corporation • mapping 設定は /proc/<pid>/uid_map に存在し、ホストの root 権限で編集できる

    例: コンテナ上の 0 ‒ 65535 を ホスト上の 100000 ‒ 165535 に mapping する設定 62 User Namespace を導⼊してみる どうやって設定するの? 0 100000 65536 ⼦プロセスから⾒た mapping する最⼩ ID ホスト上で mapping されるべき最⼩ ID mapping される ID の数 • 新規 User Namespace を切ったのち、上記ファイルを書き込むことにより mapping が 設定される じゃあ、user namespace の unshare をしたあと ファイル書き込みをすればいいんだね!
  44. © LY Corporation • 新しい User Namespace に⼊ったプロセスが⾃⾝の uid/gid mapping

    を 書き込む事はできない。そのためホストに書いてもらう必要がある • が、stage1 プロセスが user namespace の分離を⾏うより前のタイミングで ホストが mapping を記⼊しようとすると、エラーになる • 以下の順序を守らせるには Main と Stage 1 で協調をするしかない 1. (Main) clone して Stage 1 プロセスを作成 2. (Stage 1) user namespace 分離 3. (Main) uid/gid mapping 書き込み 63 User Namespace を導⼊してみる いつ誰がその mapping を書き込むのか?
  45. © LY Corporation Create の流れ (概略版、途中まで) Main Process Stage 1

    Stage 2 Config のパースなど clone して ⾃分⾃⾝を再実⾏ その他 namespace の分離 pid namespace 分離後 もう⼀度 clone uid/gid mapping を 書き込む コンテナプロセス側で 実⾏すべきあれこれ Start シグナル待ち User User namespace の分離 uid/gid mapping request を送信 mapping 完了通知 cgroup の設定など 64
  46. © LY Corporation 65 namespace 分離を確認してみよう ホスト側から⾒ると、uid 100000, PID は

    2238190 になっている コンテナ内で ps aux uidmap “0:100000:65536” を設定しコンテナを起動 コンテナ内からみると USER は root, PID は 1 uid pid
  47. © LY Corporation Create, Start の流れ (途中から最後まで) Main Process Stage

    1 Stage 2 その他 namespace の分離 pid namespace 分離後 もう⼀度 clone コンテナプロセス側で 実⾏すべきあれこれ pivot_root, 権限の放棄など Start シグナル待ち Stage 2 pid を通知 seccompNotifyRequest seccompNotifyDone Init Ready コンテナの状態を json に書き出す execve Start シグナル 以降は コンテナのアプリケーションに 置き換わる 67
  48. © LY Corporation 68 コンテナプロセス側でやるべきことは? kontainer-runtime で実装済みの処理のみ時系列順に列挙しました (抜粋) 処理 説明

    ハマったポイント pivot_root ルートファイルシステムの切り替え マウント伝播の変更が必須だった sethostname ホスト名を変更する CAP_SYS_ADMIN を drop する前 にやらないと失敗する no new privilege 設定 setsid 経由等での権限昇格禁⽌ これをやらないと後の seccomp で 失敗 capability: bounding set プロセスが今後絶対に保持しない capability を drop setuid より前に実⾏する必要あり setuid uid が指定されている場合のみ実施 特権を喪失するので順序に注意 capability: bounding set 以外 保持する capability を設定 PR_SET_KEEPCAPS のありがたみ を感じた seccomp filter 適⽤ 実⾏できる syscall Λ੍ݶ root の状態から権限を捨て続ける 正しい順序で権限を捨てないと、権限を捨てる権限がなくて失敗、などが⽣じるので注意
  49. © LY Corporation • プロセスが実⾏するシステムコールを制限する機能 • そのシステムコールが実⾏された時、単にエラーを返すだけでなく 「プロセスを終了させる」「トレーサーを呼び出す」などのアクションが可能 • 今回の実装としては

    libseccomp の関数を呼び出しまくる形 • リストにある各 syscall について seccomp_rule_add(3) を呼び出す • 最後に seccomp_load(3) して適⽤ • これにより、すべてのルールが BPF プログラムとしてコンパイルされ、カーネルにロード されて syscall 監視が開始される 69 seccomp とは?
  50. © LY Corporation • プロセスが実⾏するシステムコールを制限する機能 • そのシステムコールが実⾏された時、単にエラーを返すだけでなく 「プロセスを終了させる」「トレーサーを呼び出す」などのアクションが可能 • 今回の実装としては

    libseccomp の関数を呼び出しまくる形 • リストにある各 syscall について seccomp_rule_add(3) を呼び出す • 最後に seccomp_load(3) して適⽤ • これにより、すべてのルールが BPF プログラムとしてコンパイルされ、カーネルにロード されて syscall 監視が開始される 70 seccomp とは?
  51. © LY Corporation • ある syscall が呼ばれた時に、ユーザ空間にあるトレーサーを動作させたい時に⽤いる • トレーサーは、コンテナ⽴ち上げ時にあらかじめ config.json

    の listenerPath に 指定したソケットを listen しておく • Stage 2 プロセスは、seccomp フィルタをロードした時点で、カーネルから通知を 受け取るための notifyFd を取得できる 71 「トレーサーを呼び出す」……? じゃあ、待機しているトレーサーに この notifyFd を教えてあげれば良さそう! 例: /run/mytracer.socket
  52. © LY Corporation notifyFd をどう転送する? • Stage 2 プロセスから直接トレーサに noitfyFd

    を転送することはできない • 例えばファイルディスクリプタパッシングの仕組みを⽤いて fd を転送しようにも、 stage 2 プロセスは listenerPath にアクセスできるとは限らない • pivot_root 後の stage 2 プロセスは例えばホストの /run/mytracer.socket にアクセスできない • 仕⽅がないので fd の転送を Main Process が仲介する必要がある (図は再掲) Main Process Stage 2 Process 72
  53. © LY Corporation その他 namespace の分離 pid namespace 分離後 もう⼀度

    clone pivot_root, 権限の放棄など Start シグナル待ち Init Ready コンテナの状態を json に書き出す execve Start シグナル Main Process Stage 1 Stage 2 Config のパースなど clone して ⾃分⾃⾝を再実⾏ uid/gid mapping を 書き込む User User namespace の分離 uid/gid mapping request を送信 mapping 完了通知 cgroup の設定など (seccomp notifyFd の受け渡し) 74
  54. © LY Corporation • Kotlin/Native で⾃作した低レイヤコンテナランタイムについて、実装中に直⾯した課題や その解決策について共有した • Namespace 分離処理では

    Kotlin/Native のマルチスレッド特性に、分離以降では 権限を捨てる処理の複雑さ⾃体に苦しめられた • Kotlin/Native でシステムプログラミングをすることについて • 結論: 意外とできる • Kotlin のモダンな⾔語機能を使いながら、既存実装の Rust や Go で書かれている機能が 実装可能であることがわかった • Kotlin はおそらく Rust や C よりは多くの⼈にとって読みやすく・書きやすい⾔語なので、 システムプログラミングへのハードルを下げる意味では良いかもしれない 75 まとめ