TenForward

技術ブログ。はてなダイアリーから移転しました

GnuPGで鍵取得しようとするとdirmngrに繋がらないと怒られる

単なるメモ。Plamo-7.0 開発中環境でのお話。

gnupg 2.1.19 までは大丈夫なんだけど、gnupg 2.1.23、2.2.0、2.2.1 にすると、dirmngr がうまく動かない… (2.1.20 〜 22 は作ってないので知らない)。

$ gpg --recv-keys ()
gpg: connecting dirmngr at '/home/karma/.gnupg/S.dirmngr' failed: IPC connect呼び出しに失敗しました
gpg: 鍵サーバからの受信に失敗しました: dirmngrがありません

となる。この時、dirmngr は起動しているけど、

tcp        0      1 10.200.200.232:45602    127.0.0.1:9050          SYN_SENT    27056/dirmngr

のように、どうやら Tor に接続に行っている模様。これコンパイル時に無効化できんの? もしくはデフォルト使わないってできんの?

もちろん dirmngr.conf で使わないように設定すれば使わない。でも Plamo では、システムワイドで dirmngr 起動するわけではなく、ユーザごとに起動することになるので、ユーザの dirmngr.conf にいちいち書かなければならない。

$ cat ~/.gnupg/dirmngr.conf 
no-use-tor

いや、GnuPG 詳しくないから知らんけど

マニュアルには The default is to use Tor if it is available on startup or after reloading dirmngr. と書いてある。でも Tor なんて入ってないと思うんだけど…

GnuPGのdirmngrのコード見ると、dirmngr/server.c

/* This function returns true if a Tor server is running.  The status
 * is cached for the current connection.  */
static int
is_tor_running (ctrl_t ctrl)
{
  /* Check whether we can connect to the proxy.  */

  if (!ctrl || !ctrl->server_local)
    return 0; /* Ooops.  */

  if (!ctrl->server_local->tor_state)
    {
      assuan_fd_t sock;

      sock = assuan_sock_connect_byname (NULL, 0, 0, NULL, ASSUAN_SOCK_TOR);
      if (sock == ASSUAN_INVALID_FD)
        ctrl->server_local->tor_state = -1; /* Not running.  */
      else
        {
          assuan_sock_close (sock);
          ctrl->server_local->tor_state = 1; /* Running.  */
        }
    }
  return (ctrl->server_local->tor_state > 0);
}

こんな関数がある。ここで tor_state が 1 になってる?

libassuan を見ると、src/assuan-socket.c 内に

_assuan_sock_connect_byname (assuan_context_t ctx, const char *host,
                             unsigned short port, int reserved,
                             const char *credentials, unsigned int flags)
{
  assuan_fd_t fd;
  unsigned short socksport;

  if ((flags & ASSUAN_SOCK_TOR))
    socksport = TOR_PORT;
  else if ((flags & ASSUAN_SOCK_SOCKS))
    socksport = SOCKS_PORT;
  else
    {
      gpg_err_set_errno (ENOTSUP);
      return ASSUAN_INVALID_FD;
    }

  if (host && !*host)
    {
      /* Error out early on an empty host name.  See below.  */
      gpg_err_set_errno (EINVAL);
      return ASSUAN_INVALID_FD;
    }

  fd = _assuan_sock_new (ctx, AF_INET, SOCK_STREAM, 0);
  if (fd == ASSUAN_INVALID_FD)
    return fd;

  /* For HOST being NULL we pass an empty string which indicates to
     socks5_connect to stop midway during the proxy negotiation.  Note
     that we can't pass NULL directly as this indicates IP address
     mode to the called function.  */
  if (socks5_connect (ctx, fd, socksport,
                      credentials, host? host:"", port, NULL, 0))
    {
      int save_errno = errno;
      assuan_sock_close (fd);
      gpg_err_set_errno (save_errno);
      return ASSUAN_INVALID_FD;
    }

  return fd;
}

なんて関数がある。よくわからんw

いや、違うな。dirmngr に Tor のオプションを指定すると、オプションが --use-tor--no-use-tor かに関わらず dirmngr はすぐに起動するけど、何も指定しないと起動後 127.0.0.1:9050 へのアクセスをして、だいぶタイムアウトを待った後に、使わない設定で起動するな。dirmngr.conf を準備して、ちゃんと設定しろ、ということかな。なんて不便なソフトウェアだ。

$ strace dirmngr --server --homedir /home/karma/.gnupg -vvv --debug-all
  :(snip)
write(2, "dirmngr[27628]: enabled debug fl"..., 108dirmngr[27628]: enabled debug flags: x509 crypto memory cache memstat hashing ipc dns network lookup extprog) = 108
write(2, "\n", 1
)                       = 1
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(9050), sin_addr=inet_addr("127.0.0.1")}, 16
  :(↑でだんまり)
  :(snip)

うん、接続を延々待ってるわ。これで失敗したあとにちゃんと dirmngr は起動して、クライアントからのリクエストに応える。不便すぎて涙出るな。

Linux 4.10 で入った Overlayfs の redirect_dir 機能の動きを軽く追ってみた

ちょっと前にリリースされたカーネルですが、4.10 で overlayfs に変更が入っていましたのでちょっと調べてみました。

Overlayfs についてはこちらをどうぞ (ちょっと古い記事なのでカーネルにマージされる前の仕様も説明しています)。

それとコンテナ勉強会のこの発表資料

カーネルの変更と準備

4.10の関連するコミットは以下。

lowerdir側に存在するディレクトリを移動した場合の改良ってところでしょうか。相変わらずパッチも短くて見やすくて変更が追いやすそうですね (あまりちゃんと見てませんが)。

以下は 4.13-rc5 カーネルで試しています。

カーネルの config で Overlayfs: turn on redirect dir feature by default を “Y” にするとデフォルトで有効になるようです (CONFIG_OVERLAY_FS_REDIRECT_DIR)。

従来の動き

まずは従来の動きにするように redirect_dir=off というオプションを与えます。

$ mkdir lower upper work overlay # overlayfs用のディレクトリの作成
$ mkdir lower/lowerdir upper/upperdir # 下層、上層それぞれにディレクトリ作成
$ touch lower/lowerdir/lowfile upper/upperdir/upfile # ディレクトリ内にファイル作成
$ sudo mount -t overlay \
> -o lowerdir=lower,upperdir=upper,workdir=work,redirect_dir=off
> overlayfs overlay/ # マウント

重ね合わされた overlay ディレクトリの中身はこんな風になります。普通の動きですね。

$ find overlay/
overlay/
overlay/lowerdir
overlay/lowerdir/lowfile
overlay/upperdir
overlay/upperdir/upfile

lowerdir を移動しましょう。

$ cd overlay
$ mv lowerdir lowerdir2
$ ls -F
lowerdir2/  upperdir/

ここで下層の lowerdir と上層の upperdir の中を覗いてみます。

$ find lower/
lower/
lower/lowerdir
lower/lowerdir/lowfile

Overlayfs は下層側は変化しませんので、これは当たり前。

$ ls -l upper/
合計 8,192
c--------- 1 root  root   0, 0  81617:32 lowerdir
drwxr-xr-x 2 karma users 4,096  81617:29 lowerdir2/
drwxr-xr-x 2 karma users 4,096  81617:29 upperdir/

Overlayfs では、削除されたファイルやディレクトリは特別なデバイスファイルになるのでした。lowerdir は削除された状態になっており、新たに移動先の lowerdir2 が上層に作成されていますね。lowerdir2 内には、元々下層の lowerdir 内に存在していたファイルがコピーされています。

$ ls -l upper/lowerdir2/
合計 0
-rw-r--r-- 1 karma users 0  81617:29 lowfile

ディレクトリ内に多数のファイルやディレクトリがある場合は時間がかかりそうです。

改良後の動き

4.10 で導入された “redirect on rename-dir” という機能を使ってみましょう。

$ sudo mount -t overlay \
> -o lowerdir=lower,upperdir=upper,workdir=work,redirect_dir=on \
> overlayfs overlay

ここでは明示的に redirect_dir=on としていますが、カーネルの config でこの機能を “Y” にしてあれば、自動的にオンになりますので、これは不要です。

$ find overlay/
overlay/
overlay/lowerdir
overlay/lowerdir/lowfile
overlay/upperdir
overlay/upperdir/upfile

先ほどと同じように下層と上層が重ね合わされた状態になっています。この状態で先ほどと同じようにディレクトリを移動してみましょう。

$ cd overlay/
$ ls
lowerdir/  upperdir/
$ mv lowerdir lowerdir2
$ ls 
lowerdir2/  upperdir/

はい、移動されました。それでは下層と上層のディレクトリ内を覗いてみましょう。

$ ls -l upper/
合計 12,288
c--------- 1 root  root   0, 0  81617:35 lowerdir
drwxr-xr-x 2 karma users 4,096  81617:34 lowerdir2/
drwxr-xr-x 2 karma users 4,096  81617:34 upperdir/

これは先ほどと同じですね。移動前の lowerdir が削除された状態になっており、新たに lowerdir2 が作成されています。この lowerdir2 内を覗いてみましょう。

$ ls -l upper/lowerdir2/
合計 0
$ ls overlay/lowerdir2/
lowfile

おや? 先ほどは内部のファイルもコピーされていましたが、今回はファイルが存在しません。にもかかわらず、重ね合わせたディレクトリにはちゃんとファイルが存在しますね。下層側を見てみると、

$ ls -l lower/lowerdir/
合計 0
-rw-r--r-- 1 karma users 0  81617:34 lowfile

ファイルが存在しますが、これは従来と変わりありません。

ディレクトリを移動した場合は、中身のファイルは元の下層側のファイルを参照しているようですね。参照先はどうやって調べているのでしょう?

先に紹介したコミットにも、Documentation/filesystems/overlayfs.txt にも書かれてあります。

2. If the "redirect_dir" feature is enabled, then the directory will be
   copied up (but not the contents).  Then the "trusted.overlay.redirect"
   extended attribute is set to the path of the original location from the
   root of the overlay.  Finally the directory is moved to the new
   location.

調べてみましょう。

$ sudo getfattr -n trusted.overlay.redirect upper/lowerdir2
# file: upper/lowerdir2
trusted.overlay.redirect="lowerdir"

ここに参照先が保存されていますね。

階層が深くなった時は “he path of the original location from the root of the overlay” ってことだから、マウントポイントからのパスが保存されているのかと思ったら、単なるディレクトリ名でした。まあ追えるからかな。

$ sudo getfattr -n trusted.overlay.redirect upper/lowerdir2/hogehoge2
# file: upper/lowerdir2/hogehoge2
trusted.overlay.redirect="hogehoge"

ちなみに redirect_dir 機能を使って操作された overlayfs (の upperdir) を、redirect_dir 機能をサポートしていないカーネルではマウントできませんので注意しましょう (あまりないか…)。

おまけ

4.10 では、redirect 機能の他に、ディレクトリの移動時の変更がもうひとつ加えられています。xfs の時に効果を発揮するのでしょうか。

shiftfs (s_user_ns version) 試してみた

Open Source Summit Japan 2017 で聞いた話に shiftfs の話があって、興味を持ったので試してみました。この機能の前提機能なんかに関する前提知識に欠けているため、以下には間違いが含まれている可能性が大きいです。是非指摘を頂きたいと思います。試してみただけで内部の処理とかは書いてませんよw

下に関連情報を挙げておきます (こちらを直接見たほうがいいかも)。

直接関係ないけど s_user_ns のパッチも紹介しておきます。

User Namespace に関しては私の連載記事をどうぞ。

一般ユーザ権限のコンテナとイメージ

User Namespace を利用する場合、LXC では shadow に実装されている subuid, subgid を使い、そのユーザに対して使用を許可された uid/gid を使ってコンテナを作成し、起動します。

例えば $HOME/.config/lxc/default.conf

lxc.id_map = u 0 100000 65536
lxc.id_map = g 0 100000 65536

のように設定していれば、lxc-create を実行した時、コンテナ内の uid:0 がホスト上では uid:100000 にマッピングされ、コンテナイメージ上の root 所有のファイルについては、ホスト上で見ると uid/gid が 100000 のユーザ・グループ所有で作成されます。

例えば次のような感じです。

$ ls -l ~/.local/share/lxc/xenial01/rootfs/
合計 77,824
drwxr-xr-x  2 100000 100000 4,096  4222016年 bin/
drwxr-xr-x  2 100000 100000 4,096  4132016年 boot/
drwxr-xr-x  3 100000 100000 4,096  4222016年 dev/
  :(snip)

普段はこれで問題はないのですが、例えば public に流通している root 権限で起動するコンテナで利用する前提のイメージとか、単なる tar.gz のアーカイブとかを一般ユーザで使ったり、逆に一般ユーザ権限用のイメージを root で起動したりする際には、あらかじめ chown などで権限を変更しておく必要があります。一度きりの処理なら良いのですが、起動するたびに入れ代わり立ち代わりユーザを変えて起動したいとかだと不便です。

逆に一般ユーザ権限でコンテナイメージを作成すると、作成環境の ID のマッピング状況に依存してしましますね。

shiftfs

そこでどうやら 4.8 カーネルstruct super_blocks_user_ns という変数が導入され、ファイルシステム上のファイルやディレクトリの処理を行う際には、Namespace 上の ID を使うことができるようになったようです (ここはちゃんと調べてないので間違ってる可能性大)。

ただし、これは単なるディレクトリを bind mount して、コンテナイメージとして使う場合には使えません。なぜなら bind mount は独自の super block は持たないからです (たぶん)。

そこで、s_user_ns を使って User Namespace 内での ID を使うように、super block を使いつつ bind mount をできる shiftfs という機能が James Bottomley 氏が提案しています。現時点では色々な理由であまり受け入れられる感じではなさそうですが…

このパッチは s_user_ns より前のバージョンもあり、その際はマウントオプションで uidmap=0:1000:1 みたいにマッピングを設定していました。(以前のバージョン)

shiftfs のパッチ

どういう経緯かわからないですが、linuxkit に 4.11 用のパッチがありました。これを 4.12 に当ててみました。私の手元は aufs のパッチも当ててるので Makefile だけ適用エラーになりましたが、多分 4.12 にはそのまま当たります。

shiftfs 使わない場合

root 権限で lxc-create で作った Alpine Linux のイメージで試してみました (あらかじめコンテナディレクトリやrootfsディレクトリは一般ユーザでアクセスできるようにしてあります)。

ホスト上で確認すると、root で作成しているので、当たり前ですが root 所有のディレクトリが並びます。

# ls -ld /var/lib/lxc/alpine01/rootfs
drwxr-xr-x 1 root root 108  7520:32 /var/lib/lxc/alpine01/rootfs/
# ls -l /var/lib/lxc/alpine01/rootfs
合計 0
drwxr-xr-x 1 root root   868  5403:12 bin/
drwxr-xr-x 1 root root   130  5403:12 dev/
drwxr-xr-x 1 root root   584  62222:32 etc/
drwxr-xr-x 1 root root     0  5403:12 home/
drwxr-xr-x 1 root root   354  5403:12 lib/
drwxr-xr-x 1 root root    28  5403:12 media/
drwxr-xr-x 1 root root     0  5403:12 mnt/
drwxr-xr-x 1 root root     0  5403:12 proc/
drwx------ 1 root root    24  62220:54 root/
drwxr-xr-x 1 root root     0  5403:12 run/
drwxr-xr-x 1 root root 1,610  5403:12 sbin/
drwxr-xr-x 1 root root     0  5403:12 srv/
drwxr-xr-x 1 root root     0  5403:12 sys/
drwxrwxrwt 1 root root    36  62222:02 tmp/
drwxr-xr-x 1 root root    40  5403:12 usr/
drwxr-xr-x 1 root root    78  62220:17 var/

このコンテナイメージを、普通に User Namespace を作成して、Namespace 内から見てみましょう。

$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# cat /proc/self/{u,g}id_map
         0       1000          1
         0        100          1
# ls -ld /var/lib/lxc/alpine01/rootfs
drwxrwxrwx 1 nobody nogroup 108  62222:02 /var/lib/lxc/alpine01/rootfs/
# ls -l /var/lib/lxc/alpine01/rootfs
合計 0
drwxr-xr-x 1 nobody nogroup   868  5403:12 bin/
drwxr-xr-x 1 nobody nogroup   130  5403:12 dev/
drwxr-xr-x 1 nobody nogroup   584  62222:32 etc/
drwxr-xr-x 1 nobody nogroup     0  5403:12 home/
drwxr-xr-x 1 nobody nogroup   354  5403:12 lib/
drwxr-xr-x 1 nobody nogroup    28  5403:12 media/
drwxr-xr-x 1 nobody nogroup     0  5403:12 mnt/
drwxr-xr-x 1 nobody nogroup     0  5403:12 proc/
drwx------ 1 nobody nogroup    24  62220:54 root/
drwxr-xr-x 1 nobody nogroup     0  5403:12 run/
drwxr-xr-x 1 nobody nogroup 1,610  5403:12 sbin/
drwxr-xr-x 1 nobody nogroup     0  5403:12 srv/
drwxr-xr-x 1 nobody nogroup     0  5403:12 sys/
drwxrwxrwt 1 nobody nogroup    36  62222:02 tmp/
drwxr-xr-x 1 nobody nogroup    40  5403:12 usr/
drwxr-xr-x 1 nobody nogroup    78  62220:17 var/
root@enterprise:~# 

マッピングされていないので nobody:nogroup に。このままではこのイメージ以下に書き込みできません。(なぜnobody:nogroupになるのかは私の連載に書いてます)

shiftfs の利用

s_user_ns バージョンの shiftfs は、まずマウントしたいディレクトリに印を付ける必要があるようです。まずは root で以下を実行。-o mark と mark オプションでマウントします。

# mount -t shiftfs -o mark /var/lib/lxc/alpine01/rootfs/ /var/lib/lxc/alpine01/rootfs/
# grep alpine01 /proc/self/mounts 
/var/lib/lxc/alpine01/rootfs /var/lib/lxc/alpine01/rootfs shiftfs rw,relatime,mark 0 0

この後、また一般ユーザで userns 作成して、別ディレクトリに shiftfs マウントしてみます。この時は特にマウントオプションは不要です。

$ id -u ; id -g
1000
100
$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# cat /proc/self/{u,g}id_map
         0       1000          1
         0        100          1
# mount -t shiftfs /var/lib/lxc/alpine01/rootfs/ /home/karma/mnt
# grep shiftfs /proc/self/mounts
/var/lib/lxc/alpine01/rootfs /var/lib/lxc/alpine01/rootfs shiftfs rw,relatime,mark 0 0
/var/lib/lxc/alpine01/rootfs /home/karma/mnt shiftfs rw,relatime 0 0

ここでマウントしたディレクトリ以下を見てみると、

# ls -l /home/karma/mnt
合計 0
drwxr-xr-x 1 root root   868  5403:12 bin/
drwxr-xr-x 1 root root   130  5403:12 dev/
drwxr-xr-x 1 root root   584  62222:32 etc/
drwxr-xr-x 1 root root     0  5403:12 home/
drwxr-xr-x 1 root root   354  5403:12 lib/
drwxr-xr-x 1 root root    28  5403:12 media/
drwxr-xr-x 1 root root     0  5403:12 mnt/
drwxr-xr-x 1 root root     0  5403:12 proc/
drwx------ 1 root root    24  62220:54 root/
drwxr-xr-x 1 root root     0  5403:12 run/
drwxr-xr-x 1 root root 1,610  5403:12 sbin/
drwxr-xr-x 1 root root     0  5403:12 srv/
drwxr-xr-x 1 root root     0  5403:12 sys/
drwxrwxrwt 1 root root    36  62222:02 tmp/
drwxr-xr-x 1 root root    40  5403:12 usr/
drwxr-xr-x 1 root root    78  62220:17 var/

ちゃんと User Namespace 内から見ても root 所有に表示されています。

4.12 で nsfs の見た目がちょっと変わった

4.12 で nsfs に変更が加わってますね。

nsfs ってのは /proc/$PID/ns 以下の、そのプロセスがどの Namespace に所属しているのかを表している特殊なリンクがあるディレクトリです。

4.11 まではこんな感じ。

# ls -l /proc/self/ns
total 0
lrwxrwxrwx 1 root root 0 Jul  4 17:14 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 uts -> uts:[4026531838]

サポートしている Namespace それぞれの Namespace を表しています。

4.12 は

# ls -l /proc/self/ns
total 0
lrwxrwxrwx 1 root root 0 Jul  4 17:00 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 mnt -> mnt:[4026532397]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 net -> net:[4026531961]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 pid -> pid:[4026532399]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 pid_for_children -> pid:[4026532399]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 uts -> uts:[4026531838]

こんな感じ。pid_for_children というファイルが加わっています。CRIU 方面で必要だからということのようです (よく知りません)。kernel のコミットでいうと、

これは、PID Namespace だけちょっと特殊な部分があるからでしょうね。この辺りは Masami Ichikawa さんの、

の 28 ページ辺りをご覧になるのがよろしいかと。(nsfs についても 35 ページ辺りに言及があります)

つまり新しいプロセスを生成しない限りは、新しい PID Namespace に所属できません。unshare(2) とか setns(2) でも CLONE_NEWPID は指定できますが、指定した新しい PID Namespace に所属するのはその子プロセスからです。

お気軽コンテナコマンド unshare コマンドも --pid だけ指定してもエラーになります。--fork を同時に指定する必要があります。

ということは、プロセスの状態として、子供用に PID Namespace を持っていながら、自身は元の Namespace に所属しているという状態があるということですね。そういうプロセスの「子供用 PID Namespace を知る」ための機能ということでしょうか。

カーネルの Namespace を実現するための構造体、nsproxy

    30 struct nsproxy {
    31         atomic_t count;
    32         struct uts_namespace *uts_ns;
    33         struct ipc_namespace *ipc_ns;
    34         struct mnt_namespace *mnt_ns;
    35         struct pid_namespace *pid_ns_for_children;
    36         struct net »         *net_ns;
    37         struct cgroup_namespace *cgroup_ns;
    38 };

という風に pid_namespace *pid_ns_for_children となってますね。これを表示しているのかな (たぶん)。

試してみました。

# unshare --mount --pid --mount-proc --fork
# ls -l /proc/self/ns | grep pid
lrwxrwxrwx 1 root root 0 Jul  4 17:27 pid -> pid:[4026532399]
lrwxrwxrwx 1 root root 0 Jul  4 17:27 pid_for_children -> pid:[4026532399]

↑は新しい PID Namespace 内で調べてるのでまあこんな感じ。元の PID Namespace から見てみましょう。

# pgrep unshare
5354
# pstree -p 5354
unshare(5354)───bash(5355)

unshare コマンド (pid: 5354) の子プロセスとして bash が pid: 5355 で起動していますね。

# ls -l /proc/5354/ns | grep pid
lrwxrwxrwx 1 root root 0  7月  4日  17:00 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0  7月  4日  17:00 pid_for_children -> pid:[4026532399]
# ls -l /proc/5355/ns | grep pid
lrwxrwxrwx 1 root root 0  7月  4日  17:00 pid -> pid:[4026532399]
lrwxrwxrwx 1 root root 0  7月  4日  17:04 pid_for_children -> pid:[4026532399]

というわけで、unshare コマンド自身の pid_for_children は、子プロセスの bashpid (PID Namespace) と一致していますね。でも unshare コマンド自身の pid (PID Namespace) はそれとは異なります。

というわけで、4.12 でちょっと nsfs に変わったのを調べてみました。(間違いの指摘歓迎!!)

nsproxy まわりについても同じく Ichikawa さんのブログが参考になります

pivot_root できる条件

ふとしたきっかけで man 2 pivot_root の制限に疑問を持ったので、雑にカーネルのコードを読んでみたエントリです。かなり雑にみただけなので間違いの指摘を歓迎します。というか指摘を受けるために書いたようなもの😅

pivot_root の使われ方

コンテナを起動して、コンテナイメージの root をコンテナの root に設定する際、chroot を抜けられないように権限を制御したりしながら chroot を使ったりすることがあります。

一方で、LXC や Docker では pivot_root が使われます。chroot は比較的簡単に使えるのに対して、pivot_root はいくつか制限があります。

man 2 pivot_root すると、その制限について説明があります。

new_root および put_old には以下の制限がある:

(「差す」は「指す」の Typo ?)

しかし、この説明は少し微妙です。例えば、LXC ではコンテナイメージの root を、例えば /usr/lib/lxc/rootfs にバインドマウントして、そこに pivot_root します。

バインドマウントですので、言ってみれば new_root は新たにマウントされたファイルシステムとも言えますが、同じファイルシステム上のディレクトリとも言えます。

他に、

 998        /* change into new root fs */
 999        if (fchdir(newroot)) {
1000                SYSERROR("can't chdir to new rootfs '%s'", rootfs);
1001                goto fail;
1002        }
1003
1004        /* pivot_root into our new root fs */
1005        if (pivot_root(".", ".")) {
1006                SYSERROR("pivot_root syscall failed");
1007                goto fail;
1008        }
1009
1010        /*
1011         * at this point the old-root is mounted on top of our new-root
1012         * To unmounted it we must not be chdir'd into it, so escape back
1013         * to old-root
1014         */
1015        if (fchdir(oldroot) < 0) {
1016                SYSERROR("Error entering oldroot");
1017                goto fail;
1018        }
1019        if (umount2(".", MNT_DETACH) < 0) {
1020                SYSERROR("Error detaching old root");
1021                goto fail;
1022        }
1023
1024        if (fchdir(newroot) < 0) {
1025                SYSERROR("Error re-entering newroot");
1026                goto fail;
1027        }

(lxc-2.0.8時点のsrc/lxc/conf.c)

このあたりですね。新しく root としたいコンテナの root (newroot) に移動したあと、pivot_root(".", ".") として、以前の root である oldrootnewroot と同じディレクトリにマウントしてしまいます。その後 oldroot をアンマウントしています。これは「すなわち put_old を指す文字列に 1 個以上の ../ を付けることによって new_root と同じディレクトリが得られなければならない」ではないようにも思えてしまいます。

なので、実際どうなのかカーネルのコードをみてみました。

カーネルのコメント

pivot_root システムコールfs/namespace.c 内にあります。手元にはなぜか 4.1.15 のソースコードがあるので、それでみると 2941 行目付近からが実装です。

実はここのコメントにも詳細な説明があります。これを読めば一件落着! かも。

/*
 * pivot_root Semantics:
 * Moves the root file system of the current process to the directory put_old,
 * makes new_root as the new root file system of the current process, and sets
 * root/cwd of all processes which had them on the current root to new_root.
 *
 * Restrictions:
 * The new_root and put_old must be directories, and  must not be on the
 * same file  system as the current process root. The put_old  must  be
 * underneath new_root,  i.e. adding a non-zero number of /.. to the string
 * pointed to by put_old must yield the same directory as new_root. No other
 * file system may be mounted on put_old. After all, new_root is a mountpoint.
 *
 * Also, the current root cannot be on the 'rootfs' (initial ramfs) filesystem.
 * See Documentation/filesystems/ramfs-rootfs-initramfs.txt for alternatives
 * in this situation.
 *
 * Notes:
 *  - we don't move root/cwd if they are not at the root (reason: if something
 *    cared enough to change them, it's probably wrong to force them elsewhere)
 *  - it's okay to pick a root that isn't the root of a file system, e.g.
 *    /nfs/my_root where /nfs is the mount point. It must be a mountpoint,
 *    though, so you may need to say mount --bind /nfs/my_root /nfs/my_root
 *    first.
 */

私の超(=ヒドい)訳を。

/*
 * pivot_root のセマンティクス:
 * カレントプロセスのルートファイルシステムを put_old ディレクトリへ移
 *  動させ、new_root をカレントプロセスの新しいルートファイルシステムに
 *  します。そして、現在のルートを使用しているすべてのプロセスのルート
 *  とカレントワーキングディレクトリを新しいルートに設定します
 *
 * 制限:
 * new_root と put_old はディレクトリでなくてはなりません。そして、
 * 現在のプロセスのルートと同じファイルシステム上にあってはなりません。
 * put_old は new_root の下になくてはなりません。すなわち、put_old
 * が指す文字列に 0 個以外の /.. を追加すると new_root と同じディレク
 * トリにならなくてはいけません。他のファイルシステムが put_old にマ
 * ウントされていてはいけません。結局、new_root はマウントポイントで
 * なくてはなりません。
 *
 * さらに、カレントの root が 'rootfs' (initramfs) となることはできま
 * せん。この場合の代替策は
 * Documentation/filesystems/ramfs-rootfs-initramfs.txt をご覧ください。
 *
 * 注意:
 *  - root にいない場合は、root/cwd に移動しません (理由: 十分に注意し
 *    て、それらを変更するのであれば、別の場所を強制するのはたぶん間違
 *    いでしょう)
 *  - ファイルシステムの root でない root を選択することができます。例
 *    えば、/nfs がマウントポイントである /nfs/my_root を選択できます。
 *    マウントポイントでなくてはなりませんので、mount --bind
 *    /nfs/my_root /nfs/my_root を最初に実行しておく必要があるかもしれ
 *    ません
 */

これでほぼ解決でしょうか :-)

カーネルコード

ですが、カーネルのコードを追って、どのような条件が設定されていうのかを確認しておきましょう。

2966SYSCALL_DEFINE2(pivot_root, const char __user *, new_root,
2967                const char __user *, put_old)
2968{
2969        struct path new, old, parent_path, root_parent, root;
2970        struct mount *new_mnt, *root_mnt, *old_mnt;
2971        struct mountpoint *old_mp, *root_mp;
2972        int error;

文字列で与えられたパスから struct path を取得します。ここで new_rootput_oldディレクトリであるかどうかのチェックをしているようです。また、カレントプロセスの root のパス (= root) を取得します。

2977        error = user_path_dir(new_root, &new);
  :(snip)
2981        error = user_path_dir(put_old, &old);
  :(snip)
2989        get_fs_root(current->fs, &root);

それぞれのパスから、struct mount を取得します。

2996        new_mnt = real_mount(new.mnt);
2997        root_mnt = real_mount(root.mnt);
2998        old_mnt = real_mount(old.mnt);

shared mount 以外

まず最初の条件、

2999        if (IS_MNT_SHARED(old_mnt) ||
3000                IS_MNT_SHARED(new_mnt->mnt_parent) ||
3001                IS_MNT_SHARED(root_mnt->mnt_parent))

new_mntroot_mntstruct mount で、そのメンバ mnt_parent はマウントの親子関係がある場合、要は他のマウント配下にマウントポイントがあり、そこにマウントされているような場合に、その親マウントを示すメンバです (たぶん)。(struct mount)

以前の root の移動先のマウント (put_old)、新しい root (new_root)およびカレントプロセスの root がマウントされている親のマウント (ファイルシステム) が shared マウントであってはいけません。

新しいrootのマウントが現プロセスと同じマウント名前空間

ふたつめの条件、

 if (!check_mnt(root_mnt) || !check_mnt(new_mnt))
        goto out4;

check_mnt は、次のように引数 mnt で指定したマウントの名前空間とカレントプロセスのマウント名前空間が等しいかどうかをチェックしています。

 771static inline int check_mnt(struct mount *mnt)
 772{
 773        return mnt->mnt_ns == current->nsproxy->mnt_ns;
 774}

つまり、新しい root のマウントは、カレントプロセスのマウント名前空間に属していなければなりません。

カレントプロセスのrootと、new, oldが異なるマウント

同じファイルシステムでグルグル循環してはいけませんので、

3011        if (new_mnt == root_mnt || old_mnt == root_mnt)
3012                goto out4; /* loop, on the same file system  */
  • 新しい root の mount 構造体と、カレントプロセスの root の mount 構造体が同じ、つまり同じマウント (ファイルシステム) である
  • 古い root の mount 構造体と、カレントプロセスの root の mount 構造体が同じ、つまり同じマウント (ファイルシステム) である

このような場合はエラーとなります。old_mntroot_mnt は同じじゃないの? と一瞬思ってしまうかもしれませんが、元の root を、新しい root 以下の put_old にマウントする際のマウントを表しています。例えば、マウントポイントが違いますので構造体のインスタンスは別ですね。

そもそもここが同じだと、pivot_root でルートを移動することになりません。

カレントプロセスのrootはマウントポイント

root.mnt->mnt_rootで、カレントプロセスの root マウント (ファイルシステム) の root の dentry を求めています。これがカレントプロセスの root の dentry と異なっている場合はエラーになります。

3014        if (root.mnt->mnt_root != root.dentry)
3015                goto out4; /* not a mountpoint */

ややこしいですが、カレントプロセスの root ディレクトリがマウントポイントと異なっていてはいけません。これは、カレントプロセスが chroot でマウントポイント以外を root としている場合でしょう (たぶん)。

カレントプロセスの root は attach されている

ここはちょっとよくわからないのですが、カレントプロセスの root がマウントの親子関係のツリー内にいるかどうかをチェックしています。

3016        if (!mnt_has_parent(root_mnt))
3017                goto out4; /* not attached */

mnt_has_parentfs/mount.h 内にあります。

static inline int mnt_has_parent(struct mount *mnt)
{
    return mnt != mnt->mnt_parent;
}

指定した mnt と、自身のメンバである親マウントを表す mnt->mnt_parent が異なっている場合、つまり親マウントがあるかどうかをチェックしています。(構造体の初期化時点で mnt->mnt_parent = mnt という処理があるので、ちゃんと親子関係がなければ mnt == mnt->mnt_parent となるはずです、たぶん)

システム起動時の root を含め、きちんとマウントされていれば、親マウントは存在するんだと思います。というのは、カーネルパラメータで指定した root を起動時にマウントする際、do_move_mountを通りますが、この中でも同じようなチェックをしていて、エラーになったら root がマウントできないはずです (たぶん)。

このあたりで、元の root のマウントを mnt_parent に入れているような処理があります (たぶん、do_mount_moveattach_mntmnt_set_mountpointあたりの流れ)。

新しい root はマウントポイントで attach されている

3019        if (new.mnt->mnt_root != new.dentry)
3020                goto out4; /* not a mountpoint */
3021        if (!mnt_has_parent(new_mnt))
3022                goto out4; /* not attached */

先の説明のカレントプロセスのチェックと同じですね。新しい root マウントの root ディレクトリはマウントポイントで、きちんとマウントの親子関係の中に入っている必要があります。

old は new (新たな root) 配下

3023        /* make sure we can reach put_old from new_root */
3024        if (!is_path_reachable(old_mnt, old.dentry, &new))
3025                goto out4;

is_path_reachable は関数名から想像はつきますが、

2916/*
2917 * Return true if path is reachable from root
2918 *
2919 * namespace_sem or mount_lock is held
2920 */
2921bool is_path_reachable(struct mount *mnt, struct dentry *dentry,
2922                         const struct path *root)
2923{
2924        while (&mnt->mnt != root->mnt && mnt_has_parent(mnt)) {
2925                dentry = mnt->mnt_mountpoint;
2926                mnt = mnt->mnt_parent;
2927        }
2928        return &mnt->mnt == root->mnt && is_subdir(dentry, root->dentry);
2929}

という関数です。つまり、

  • old と new が異なるマウントで old に親マウントがある場合には、old の親マウントを参照

という処理を終えた後に条件判定をしていますので、

  • old の親 (マウント) が new
  • old (のマウントポイント) が new (のマウントポイント) のサブディレクト

という条件になります。ちなみに is_subdir はこんな。

3302/**
3303 * is_subdir - is new dentry a subdirectory of old_dentry
3304 * @new_dentry: new dentry
3305 * @old_dentry: old dentry
3306 *
3307 * Returns 1 if new_dentry is a subdirectory of the parent (at any depth).
3308 * Returns 0 otherwise.
3309 * Caller must ensure that "new_dentry" is pinned before calling is_subdir()3310 */
3311
3312int is_subdir(struct dentry *new_dentry, struct dentry *old_dentry)
3313{
3314        int result;
3315        unsigned seq;
3316
3317        if (new_dentry == old_dentry)
3318                return 1;

(fs/dcache.cのis_subdir付近付近)

3317行目で、指定されているふたつのディレクトリが同じでも 1 を返してるので、「"/..“ をつけて」とかは説明につけなくても良いのでは?

new はカレントプロセス root 配下

3026        /* make certain new is below the root */
3027        if (!is_path_reachable(new_mnt, new.dentry, &root))
3028                goto out4;

これは上 (old は new 配下) と同じですね。

まとめ

ここまできて、man やコード中のコメントをみても、わかったようなわかってないような気分になるのは、用語の曖昧さと気づきました。

ファイルシステム
マウントポイントにマウントされるマウントの情報と含まれるツリー (=`mount`構造体)

とするとすっきりします。つまり

new_rootput_old は、man 2 pivot_root にあったように:

加えて、

こんな感じでしょうか。

ファイルシステムの実体 (配下のディレクトリやファイル) は判定には関係なく、マウントそのものとマウントポイントがキーとなります。新しい root はマウントポイントであれば良いので、bind マウントでも良いということになりますね。

pivot_root の条件のところだけ追って力尽きたので、実際のマウントやら移動の部分の解説はありません😅