Skip to content

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

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

  • psコマンド
  • apt updateコマンド

コマンドの挙動を確かめる ​

psコマンド ​

ひとまず、今の状態のコンテナ内でpsコマンドを実行してみましょう。
この節の操作にはroot権限が必要なので、sudo suを実行してrootになってからプログラムを実行してください。

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、すなわち/procにprocfsを再度マウントしてください、と言われています。

apt updateコマンド ​

続いて、apt updateコマンドを実行してみましょう。

console
$ sudo su
# make run
go build -o main *.go
./main run bash
# apt update
Get:1 http://security.ubuntu.com/ubuntu resolute-security InRelease [137 kB]
Get:2 http://archive.ubuntu.com/ubuntu resolute InRelease [136 kB]
Err:1 http://security.ubuntu.com/ubuntu resolute-security InRelease
  Could not execute 'gpgv' to verify signature (is gnupg installed?)
... (省略) ...
Error: The repository 'http://security.ubuntu.com/ubuntu resolute-security InRelease' is not signed.
Notice: Updating from such a repository can't be done securely, and is therefore disabled by default.
Notice: See apt-secure(8) manpage for repository creation and user configuration details.
Warning: OpenPGP signature verification failed: http://archive.ubuntu.com/ubuntu resolute InRelease: Could not execute 'gpgv' to verify signature (is gnupg installed?)
... (省略) ...

gpgvによる署名の検証に失敗しているようです。
詳細は省きますが、gpgvが正しく動くためには、2つの特殊ファイルシステムディレクトリ、/devおよび/tmpが正しく動いていなければいけません。

procfsを再マウントする ​

mount -t proc proc /procと同じ内容、および/devや/tmpのマウント処理をコードで書いて、特殊ファイルシステムを再マウントしてみましょう。
unix.Mount()を使ってマウント処理を行います。

今回マウントする特殊ファイルシステム一覧は以下の通りです。

マウント先のディレクトリファイルシステムの種類
/procproc
/devdevtmpfs
/tmptmpfs
ヒント1

unix.Mount()で特殊ファイルシステムをマウントするには、sourceに空文字列、fstypeにファイルシステムの種類、flagsに0を指定します。

ヒント2

全てのマウントは、先にマウント先のディレクトリを作成しておく必要があります。

ヒント3

今回はマウントするファイルシステムが3つあるので、以下のような構造体を作って配列で情報を持ち、ループでマウント処理を行うと良いでしょう。

go
// マウント情報
type Mount struct {
  Target string `json:"path"`
  FsType string `json:"fs_type"`
}

想定解答 ​

chrootの場合の想定解答
go
// マウント情報
type Mount struct { 
  Target string `json:"path"`
  FsType string `json:"fs_type"`
} 

var mounts = []Mount{ 
  {Target: "proc", FsType: "proc"}, 
  {Target: "dev", FsType: "devtmpfs"}, 
  {Target: "tmp", FsType: "tmpfs"}, 
} 

func SetupRootfs(c RootfsConfig) error {
  // 特殊ディレクトリを作成・マウント
  for _, mount := range mounts { 
    if err := os.MkdirAll(filepath.Join(c.RootDirPath, mount.Target), 0o755); err != nil { 
      return errors.WithStack(err) 
    } 
    if err := unix.Mount("", filepath.Join(c.RootDirPath, mount.Target), mount.FsType, 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
// マウント情報
type Mount struct { 
  Target string `json:"path"`
  FsType string `json:"fs_type"`
} 

var mounts = []Mount{ 
  {Target: "proc", FsType: "proc"}, 
  {Target: "dev", FsType: "devtmpfs"}, 
  {Target: "tmp", FsType: "tmpfs"}, 
} 

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)
  }

  // 特殊ディレクトリを作成・マウント
  for _, mount := range mounts { 
    if err := os.MkdirAll(filepath.Join(c.RootDirPath, mount.Target), 0o755); err != nil { 
      return errors.WithStack(err) 
    } 
    if err := unix.Mount("", filepath.Join(c.RootDirPath, mount.Target), mount.FsType, 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コマンドを実行し、正しく動くことを確かめましょう。

console
$ sudo su
# 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
#

また、apt updateコマンドも正しく動くことを確かめてみましょう。

console
$ sudo su
# make run
go build -o main *.go
./main run bash
# apt update
Get:1 http://security.ubuntu.com/ubuntu resolute-security InRelease [137 kB]
... (省略) ...
Get:17 http://archive.ubuntu.com/ubuntu resolute-backports/universe amd64 Packages [3306 B]
Fetched 26.0 MB in 4s (6117 kB/s)
All packages are up to date.
#

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

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