Skip to content

4-4. 特殊マウントを処理する

(見かけ上の) ルートディレクトリを変更した後、procfsやsysfsなどの特殊なファイルシステムを再度マウントしないと、いくつかのコマンドがうまく動かなくなります。
ここでは代表的な特殊ファイルシステム、procfsを正しく処理して、psコマンドが正しく動くようにしてみましょう。

psコマンドの挙動を確認する

ひとまず、今の状態のコンテナ内でpsコマンドを実行してみましょう。

console
$ sudo su
# make run
go build -o main *.go
./main run bash
# ps
Error, do this: mount -t proc proc /proc

エラーが出ましたね。
mount -t proc proc /proc、すなわち/procprocfsを再度マウントして下さい、と言われています。

procfsを再マウントする

mount -t proc proc /procと同じ内容をコードで書いて、procfsを再マウントしてみましょう。
unix.Mount()を使ってマウント処理を行います。

ヒント

unix.Mount()でprocfsをマウントするには、sourceに空文字列、fstypeprocflagsに0を指定します。

想定解答

chrootの場合の想定解答
go
func SetupRootfs(c RootfsConfig) error {
  // procディレクトリをマウント
  if err := os.MkdirAll(filepath.Join(c.RootDirPath, "proc"), 0o755); err != nil { 
    return errors.WithStack(err) 
  } 
  if err := unix.Mount("", filepath.Join(c.RootDirPath, "proc"), "proc", 0, ""); err != nil { 
    return errors.WithStack(err) 
  } 

  // 見かけ上のルートディレクトリを変更
  if err := unix.Chroot(c.RootDirPath); err != nil {
    return errors.WithStack(err)
  }

  // カレントディレクトリをルートに
  if err := os.Chdir("/"); err != nil {
    return errors.WithStack(err)
  }

  return nil
}
pivot_rootの場合の想定解答
go
func SetupRootfs(c RootfsConfig) error {
  // ルートディレクトリから再帰的にマウントのプロパゲーションを無効にする
  //  これをやらないと、pivot_root時にホストマシン側の/devや/sysなどの特殊ファイルの
  //  マウントが壊れ、新しいシェルセッションが開けなくなるなどの支障が出る
  if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_SLAVE, ""); err != nil {
    return errors.WithStack(err)
  }

  // 既存のrootfsを移動させるディレクトリを作成
  if err := os.MkdirAll(filepath.Join(c.RootDirPath, "/.old_root"), 0o755); err != nil {
    return errors.WithStack(err)
  }

  // RootDirPathをバインドマウントし、rootfsの管轄外とする
  if err := unix.Mount(c.RootDirPath, c.RootDirPath, "", unix.MS_BIND, ""); err != nil {
    return errors.WithStack(err)
  }

  // procディレクトリをマウント
  if err := os.MkdirAll(filepath.Join(c.RootDirPath, "proc"), 0o755); err != nil { 
    return errors.WithStack(err) 
  } 
  if err := unix.Mount("", filepath.Join(c.RootDirPath, "proc"), "proc", 0, ""); err != nil { 
    return errors.WithStack(err) 
  } 

  // rootfsをRootDirPathにマウントし直す
  if err := unix.PivotRoot(c.RootDirPath, filepath.Join(c.RootDirPath, ".old_root")); err != nil {
    return errors.WithStack(err)
  }

  // 古いrootfsはアンマウント・削除し、不可視にする
  //  注: MNT_DETACHを付けてlazy unmountにしないとアンマウントできない
  if err := unix.Unmount("/.old_root", unix.MNT_DETACH); err != nil {
    return errors.WithStack(err)
  }
  if err := os.Remove("/.old_root"); err != nil {
    return errors.WithStack(err)
  }

  // カレントディレクトリをルートに
  if err := os.Chdir("/"); err != nil {
    return errors.WithStack(err)
  }

  return nil
}

psコマンドが正しく動くことを確かめる

この状態のコンテナ内でpsコマンドを実行し、正しく動くことを確かめましょう。

console
$ make run
go build -o main *.go
./main run bash
# ps
    PID TTY          TIME CMD
   2242 ?        00:00:00 sudo
   2243 ?        00:00:00 su
   2244 ?        00:00:00 bash
  27273 ?        00:00:00 make
  27338 ?        00:00:00 main
  27345 ?        00:00:00 bash
  27388 ?        00:00:00 ps
#

しっかりとpsコマンドが動いていれば成功です!

これと同様に、sysfsdevtmpfsなどの特殊ファイルシステムもマウントすることで、より多くのコマンドが正しく動くようになります。
runcなどOCI Runtime Specに則ったコンテナランタイムでは、全てのマウント情報は外からconfig.jsonという設定ファイルで渡されます。
runc specというコマンドでruncのデフォルトのconfig.jsonが生成できるのですが、この設定ファイルの中では必要な特殊ファイルシステムをほぼ全てマウントするようになっています。
ぜひ一度確認してみてください。

TTYが?になっているのは、ルートディレクトリの移動によってttyを参照できなくなったためです。
これの解決にはかなり複雑な手順を要しますが、余力がある方は是非挑戦してみて下さい。