TenForward

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

ケーパビリティバウンディングセット

今更感たっぷりな話題ですが、今頃ケーパビリティについて調べてます。これまで何度も調べてはよくわからなくて挫折を繰り返してるとこです。

ケーパビリティについては

udzura.hatenablog.jp

が詳しいのですが、「ケーパビリティバウンディングセット」の部分だけはちょっと良くわからんのですよね…(偉そうにスミマセン)。

そして、udzura さんのブログも Ambient については記載がまだありませんので、Ambient についてはこちら、

nojima.hatenablog.com

現時点では日本語ではここしかありません(勉強させてもらいました)。

そういうわけで、ケーパビリティバウンディングセットについて調べていてもなんかよくわからんので調べながらメモ。メモなので脈絡なかったり、急になんの前提もなく何かが出てくるかもしれません。

基本はマニュアル

ただでもややこしいのに英語だとなあ、という場合は日本語訳あるけど古いんですよね。ただ、このケーパビリティバウンディングセットの部分は順序が変わっていたりするものの文章の訳としてはそのまま使える部分がほとんどなので、英語苦手な私には参考になります。Ambient ケーパビリティの説明がない頃のやつなんですよね。

結局、マニュアルをすみずみまで読まないとダメということですね。すみません、つまみ食いしてました。man だから入門書のように書くわけにはいかないけど、機能は全部書かないといけない。そのあたりのバランスを取っている結果か、ひとつの機能についてあっちとこっちに書いてある(ときには別の man page にある)なんてことがあるんだなあと思ったりしてます。

まずはマニュアルの"Thread capability sets"の項にある"Bounding"の説明。

Bounding (per-thread since Linux 2.6.25)
       The capability bounding set is a mechanism that can be used to
       limit the capabilities that are gained during execve(2).

            :(略)

       For more details on the capability bounding set, see below.

まずここには

ケーパビリティバウンディングセットは execve(2) の間に得られるケーパビリティを制限するのに使えるメカニズムです。

とあります。ふむふむ、execve(2)の際に使う機能機能なんだな…

で、ケーパビリティあるあるで、execve(2) の際のケーパビリティ決定のアルゴリズム。これがなければ始まらないという感じですね。

P'(ambient)     = (file is privileged) ? 0 : P(ambient)

P'(permitted)   = (P(inheritable) & F(inheritable)) |
                  (F(permitted) & P(bounding)) | P'(ambient)

P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)

P'(inheritable) = P(inheritable)    [i.e., unchanged]

P'(bounding)    = P(bounding)       [i.e., unchanged]

関係するのはP'(permitted)を決定している所です。

詳しくはこっちを見ろと書かれている "Capability bounding set" の項を見ると、

Capability bounding set

    :(略)

    * During an execve(2), the capability bounding set is ANDed with the
      file permitted capability set, and the result of this operation is
      assigned to the thread's permitted capability set.  The capability
      bounding set thus places a limit on the permitted capabilities that
      may be granted by an executable file.

ここは日本語訳と特に変化してないので、そっちを参照:

    * execve(2) 実行時に、ケーパビリティバウンディングセットと ファイルの許
      可ケーパビリティセットの論理和 (AND) を取ったものが、 そのスレッドの許
      可ケーパビリティセットに割り当てられる。つまり、ケーパビリティバウンディ
      ングセットは、実行ファイルが認めている許可ケーパビリティに対して制限を
      課す働きをする。

つまりファイルケーパビリティで設定されているケーパビリティであってもバウンディングセットで許可されていなければファイルケーパビリティの設定が無効になるということです。

ところが、さきほどのアルゴリズムを見ると、この制限したセット (F(permitted) & P(bounding)) は inheritable の条件である (P(inheritable) & F(inheritable)) との OR ですので、バウンディングセットで制限したとしてもプロセスとファイルの inheritable で許可されていれば、制限できないことになります。これは man の後の段落に書かれています。

Note that the bounding set masks the file permitted capabilities, but
not the inheritable capabilities.  If a thread maintains a capability
in its inheritable set that is not in its bounding set, then it can
still gain that capability in its permitted set by executing a file
that has the capability in its inheritable set.

日本語訳:

バウンディングセットがマスクを行うのは、継承可能ケーパビリティではなく、
ファイルの許可ケーパビリティのマスクを行う点に注意すること。あるスレッ
ドの継承可能セットにそのスレッドのバウンディングセットに 存在しないケー
パビリティが含まれている場合、そのスレッドは、継承可能セットに含まれ
るケーパビリティを持つファイルを実行することにより、許可セットに含ま
れるケーパビリティも獲得できるということである。

あくまでバウンディングセットはファイルケーパビリティの permitted を制限するだけで、inheritable で許可されれば、permitted に追加できてしまいます。これじゃあ制限できないのでは? と思いますが、ここで man の次の項目を見てみましょう。

* (Since Linux 2.6.25) The capability bounding set acts as a limiting
  superset for the capabilities that a thread can add to its inheri‐
  table set using capset(2).  This means that if a capability is not
  in the bounding set, then a thread can't add this capability to its
  inheritable set, even if it was in its permitted capabilities, and
  thereby cannot have this capability preserved in its permitted set
  when it execve(2)s a file that has the capability in its inherita‐
  ble set.

これも日本語訳

* (Linux 2.6.25 以降) ケーパビリティバウンディングセットは、スレッドが
  capset(2) により自身の継承可能セットに追加可能なケーパビリティの母集団
  を制限する役割を持つ。 スレッドに許可されたケーパビリティであっても、
  バウンディングセットに含まれていなければ、スレッドはそのケーパビリティ
  は自身の継承可能セットに追加できず、その結果、継承可能セットにそのケー
  パビリティを含むファイルを execve(2) する場合、そのケーパビリティを許
  可セットに持ち続けることができない、ということである。

プロセス(スレッド)は自身のケーパビリティを操作できます。しかし、バウンディングセットに含まれていないケーパビリティは自身の inheritable に追加できないということです。なので、そこから execve(2) してプログラムを実行する場合は、バウンディングセットに含まれていないケーパビリティはそれ以上持ち続けられない(継承できない)ということです。

つまり元々 inheritable にセットされていないけど、自身に capset(2) を使ってケーパビリティを設定しようと思っても、バウンディングセットに含まれていなければ inheritable に追加できません。結果的に execve(2) してもバウンディングセットに含まれていないケーパビリティを持てないということです。

と思って読んでたら、マニュアルの「ケーパビリティバウンディングセット」の最後に書いてあるやん。

Removing a capability from the bounding set does not remove it from
the thread's inheritable set.  However it does prevent the capability
from being added back into the thread's inheritable set in the
future.
バウンディングセットからケーパビリティを削除しても、スレッドの継承可
能セットからはそのケーパビリティは削除されない。しかしながら、バウン
ディングセットからの削除により、この先そのケーパビリティをスレッドの
継承可能セットに追加することはできなくなる。

capset(2)

ここで capset(2) を使ってのケーパビリティの追加を見てみましょう。同じくマニュアルの CAP_SETPCAP の解説、

CAP_SETPCAP
       If file capabilities are supported (i.e., since Linux 2.6.24):
       add any capability from the calling thread's bounding set to
       its inheritable set; drop capabilities from the bounding set
       (via prctl(2) PR_CAPBSET_DROP); make changes to the securebits
       flags.

          :(略)

日本語訳:

ファイルケーパビリティがサポートされている場合(Linux 2.6.24以降): 呼
び出し元スレッドのバウンディングセットの任意のケーパビリティを 自身の
継承可能ケーパビリティセットに追加できる。 (prctl(2) PR_CAPBSET_DROP
を使って) バウンディングセットからケーパビリティを削除できる。
securebits フラグを変更できる。

こはちょっとカーネルを追ってみました(5.3 kernel)。細かい所は(理解してないので :-p)すっとばしますが、まず capset システムコールkernel/capability.c にあります。その後は

sys_capset (kernel/capability.c)
  security_capset (security/security.c)
    cap_capset (security/commoncap.c) <- call_int_hook マクロ経由

という風に呼ばれ、実際の処理は security/commoncap.c 内の cap_capset 関数で行われているようです。

まずは CAP_SETPCAP を持っていない場合のチェックです。このチェックは cap_inh_is_capped 関数で行っており、 CAP_SETPCAP ケーパビリティを持っていれば 0 を、持っていなければ 1 を返します。

int cap_capset(struct cred *new,
               const struct cred *old,
               const kernel_cap_t *effective,
               const kernel_cap_t *inheritable,
               const kernel_cap_t *permitted)
{
        if (cap_inh_is_capped() &&
            !cap_issubset(*inheritable,
                          cap_combine(old->cap_inheritable,
                                      old->cap_permitted)))
                /* incapable of using this inheritable set */
                return -EPERM;

新たに設定しようとしている inheritable が、現在の "inheritable | permitted" のサブセットであるかどうかをチェックし、そうでなければエラーとなります。

先のチェックが CAP_SETPCAP がないときのお話でしたので、次は CAP_SETPCAP を持っている場合です。

        if (!cap_issubset(*inheritable,
                          cap_combine(old->cap_inheritable,
                                      old->cap_bset)))
                /* no new pI capabilities outside bounding set */
                return -EPERM;

新たに設定しようとしている inheritable が、現在の "inheritable | bset" (bset=バウンディングセット) のサブセットであるかどうかをチェックし、そうでなければエラーとなります。先に引用したマニュアルの「スレッドに許可されたケーパビリティであっても、バウンディングセットに含まれていなければ、スレッドはそのケーパビリティは自身の継承可能セットに追加できず」というのがこの部分でしょうか。

次は新たな permitted のチェックは、

        /* verify restrictions on target's new Permitted set */
        if (!cap_issubset(*permitted, old->cap_permitted))
                return -EPERM;

シンプルに現在の permitted のサブセットであれば OK です。

新しい effective のチェックは、

        /* verify the _new_Effective_ is a subset of the _new_Permitted_ */
        if (!cap_issubset(*effective, *permitted))
                return -EPERM;

新しい permitted のサブセットであれば OK です。

ここまでくれば新たなケーパビリティセットを設定できるということですので、次の 3 行で新たなケーパビリティとして設定しています。

        new->cap_effective   = *effective;
        new->cap_inheritable = *inheritable;
        new->cap_permitted   = *permitted;

最後に Ambient ケーパビリティです。

        /*
         * Mask off ambient bits that are no longer both permitted and
         * inheritable.
         */
        new->cap_ambient = cap_intersect(new->cap_ambient,
                                         cap_intersect(*permitted,
                                                       *inheritable));
        if (WARN_ON(!cap_ambient_invariant_ok(new)))
                return -EINVAL;
        return 0;
}

新たに決定したケーパビリティセットのうち permitted と inherited の両方で有効になっていないケーパビリティは ambient としては許可されない(マニュアルのAmbientの説明部分参照)ので、permitted & inherited の値と AND を取ります。(new->cap_ambient はどこで設定されてるん? <力尽きたので教えて)

マニュアルの "Capability bounding set" の残りの部分で現在のカーネルに相当する部分は日本語訳で OK です(わざわざ転載する必要もないけど説明として引用しておきます)。

カーネルのバージョンにより、ケーパビリティバウンディングセットはシス
テム共通の属性の場合と、プロセス単位の属性の場合がある。

Linux 2.6.25 以降のケーパビリティバウンディングセット

Linux 2.6.25 以降では、「ケーパビリティバウンディングセット」はスレッ
ド単位の属性である (システム共通のケーパビリティバウンディングセットは
もはや存在しない)。

バウンディングセットは fork(2) 時にはスレッドの親プロセスから継承され、
execve(2) の前後では保持される。

スレッドが CAP_SETPCAP ケーパビリティを持っている場合、そのスレッドは
prctl(2) の PR_CAPBSET_DROP 操作を使って自身のケーパビリティバウンディ
ングセットからケーパビリティを削除することができる。いったんケーパビ
リティをバウンディングセットから削除してしまうと、スレッドはそのケー
パビリティを再度セットすることはできない。 prctl(2) の PR_CAPBSET_READ
操作を使うことで、スレッドがあるケーパビリティが自身のバウンディングセッ
トに含まれているかを知ることができる。

バウンディングセットからのケーパビリティの削除がサポートされるのは、
カーネルのコンパイル時にファイルケーパビリティが有効になっている場合
だけである。Linux 2.6.33 より前のカーネルでは、ファイルケーパビリティ
は 設定オプション CONFIG_SECURITY_FILE_CAPABILITIES で切り替えられる追
加の機能であった。Linux 2.6.33 以降では、この設定オプションは削除され、
ファイルケーパビリティは常にカーネルに組込まれるようになった。ファイ
ルケーパビリティがカーネルにコンパイル時に組み込まれている場合、(全て
のプロセスの先祖である) init プロセスはバウンディングセットで全てのケー
パビリティが セットされた状態で開始する。ファイルケーパビリティが有効
になっていない場合には、 init はバウンディングセットで CAP_SETPCAP 以
外の全てのケーパビリティがセットされた状態で開始する。このようになっ
ているのは、 CAP_SETPCAP ケーパビリティがファイルケーパビリティがサポー
トされていない場合には 違った意味を持つからである。

バウンディングセットからケーパビリティを削除しても、スレッドの継承可
能セットからはそのケーパビリティは削除されない。しかしながら、バウン
ディングセットからの削除により、この先そのケーパビリティをスレッドの
継承可能セットに追加すること はできなくなる。

まとめ

ケーパビリティバウンディングセットは、

  • ファイルケーパビリティに設定されている permitted セットを制限する。しかし、プロセスとファイルの inherited で許可されていれば制限が効かない
  • capset(2) で、スレッドのケーパビリティバウンディングセットで設定されているケーパビリティしか、inherited に追加できない

と理解しました。(間違ってたら優しく教えてください)

ping コマンドの file capability(2)

(理解が不十分なので間違っている所があれば優しく教えてください)

さて、ping コマンドの file capability(1) - TenForward で紹介したように ping コマンドの File capability は Permitted のみが有効になっていて、Effective ビットはオン(+e)になっていません。これで ping が実行できる理由は説明したとおりですが、+e が設定されていない理由は「不要だから」というだけの理由ではありません。

「なんで +e ないんだろう?」とつぶやいていると WhiteAnthrax さんが調べて教えてくれました!ありがとうございます!

これは

www.projectatomic.io

に書かれている通りです。Docker コマンド内の ping コマンドの実行で問題が起こるからということです。

まず Docker コンテナでは、普通に docker run すると cap_net_admin が許可されません。cap_net_adminping -m のように実行する際に必要です(man ping すると -m は "use mark to tag the packets going out." とのことです)。一方で ping を普通に実行する際に必要な cap_net_raw は許可されています。つまり Docker コンテナ内では ping コマンドで -m をつけても目的は達成できません。

ここで(これは明確に man capabilities には書いてない気がするけど…)

  • File capability で +e すると、指定したすべての capability が取得できない場合、コマンドを実行した瞬間に実行は失敗する

ということのようです。

特権が必要なシステムコールの実行が行われる、行われないに関わらず、すべての capability の取得に失敗すると、コマンド自体が実行できないということです。ping --help のような特権が不要な実行であってもです。

ということは、

  • Docker コンテナ内で cap_net_admin,cap_net_raw+ep と設定してしまうと、cap_net_admin が取得できないので、cap_net_raw のみが必要な実行であっても ping コマンドを実行した瞬間にコマンド実行が失敗する

ということです。

今回の ping コマンドはプログラム中で自身で capability をチェックして Permitted がオンであれば capset() を使って Effective を有効にしていますので、+p とだけしておくと、たとえ cap_net_admin が取得できなくてもコマンド実行は成功します。

つまりプログラム中で権限のチェックを行って capability をセットしていますので、

となります。それ以外のシステムコールは問題なく実行されるわけです。

ping -m と実行すると、プログラム中で cap_net_admin のチェックをしているところのみ失敗します。つまり "mark" だけが失敗して、cap_net_raw が必要な処理は行われます。

https://bugzilla.redhat.com/show_bug.cgi?id=1142311#c24 によると

[root@29b49db23971 /]# /opt/ping/ping -m 123 -c1 10.3.1.1
PING 10.3.1.1 (10.3.1.1) 56(84) bytes of data.
Warning: Failed to set mark 123
64 bytes from 10.3.1.1: icmp_seq=1 ttl=62 time=0.327 ms

のように mark 処理の部分だけ失敗して、それ以外の処理は行われるようです(試してませんw)。

というわけで +e してしまうと、ping コマンドはヘルプすら表示できず、本来の実行できる処理も実行できなくなるので、+p のみが設定されているということです。

(2019-09-30 追記) 『これは明確に man capabilities には書いてない気がするけど…』と書きましたが、id:udzura さんから man 2 execve

EPERM  A "capability-dumb" applications would not obtain the full set
       of permitted capabilities granted by the executable file.  See
       capabilities(7).

という説明があると教えてもらいました!(これでも曖昧ではあるけど "the full set" とあるのできっと…)

ping コマンドの file capability(1)

(いまさらのお話とは思いますが自分用のメモに)

LinuxCapability、何度も理解しようとして完全に理解できないままなんですが、ちょっとした理由で File capability を調べてます。最近は ambient capability なんてのもあって理解が遠のいてます。:-p

file capability といえば、まず登場するのはこのコマンドではないでしょうか?(違う?) Ubuntu 18.04.3 では次のように setuid されています。file capability は特に設定されていません。

$ ls -l /bin/ping
-rwsr-xr-x 1 root root 64424 Jun 28 20:05 /bin/ping
$ getcap /bin/ping
$ 

これは想定通りだったんですが、先日リリースしたばかりの CentOS 8 で見てみると、

$ ls -l /bin/ping
-rwxr-xr-x. 1 root root 69160 May 11 23:22 /bin/ping
$ getcap /bin/ping 
/bin/ping = cap_net_admin,cap_net_raw+p

setuid されておらず file capability が設定されていて、「おお、イマドキのセキュアな設定っぽい」と思いました(CentOS 7 からそうだったようなので今更感満載です)。でも +p と設定されています。あれ? +ep でじゃないとマズくない? +e がないと実行できなくない? 例のあのややこしい条件式からいっても。

$ man 7 capabilities
  : (snip)
           P'(ambient)     = (file is privileged) ? 0 : P(ambient)

           P'(permitted)   = (P(inheritable) & F(inheritable)) |
                             (F(permitted) & P(bounding)) | P'(ambient)

           P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)

           P'(inheritable) = P(inheritable)    [i.e., unchanged]

           P'(bounding)    = P(bounding)       [i.e., unchanged]

それはそのとおりなのですが、CentOS で採用されている ping が含まれている iputils のコードを確認するとその理由がわかります(たぶん)。

#ifdef HAVE_LIBCAP
int modify_capability(cap_value_t cap, cap_flag_value_t on)
{
    cap_t cap_p = cap_get_proc();
    cap_flag_value_t cap_ok;
    int rc = -1;

    if (!cap_p) {
        error(0, errno, "cap_get_proc");
        goto out;
    }

    cap_ok = CAP_CLEAR;
    cap_get_flag(cap_p, cap, CAP_PERMITTED, &cap_ok);
    if (cap_ok == CAP_CLEAR) {
        rc = on ? -1 : 0;
        goto out;
    }

    cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &cap, on);

    if (cap_set_proc(cap_p) < 0) {
        error(0, errno, "cap_set_proc");
        goto out;
    }

    cap_free(cap_p);
    cap_p = NULL;

    rc = 0;
out:
    if (cap_p)
        cap_free(cap_p);
    return rc;
}

https://github.com/iputils/iputils/blob/4b0a0ab7898b093ef55c4a8cacea9a21c37a451f/ping_common.c#L189

CAP_PERMITTED を確認して、付与されていれば CAP_EFFECTIVE を設定してます(と思う)。

これで +ep でなく +p な理由がわかりました。ping コマンド自体で +p が付いていれば(というか付いている結果として permitted が設定されていれば、かな?)必要な capability を設定してシステムコールを呼ぶプログラムになっているからでした。

とはいえこれでは +e が不要な理由はわかりましたが、付けておいてマズい理由はわかりません。ま、セキュリティ観点から不要な権限は付けておく理由はない、というだけでも理由になりますが。

(続く)

tenforward.hatenablog.com

セサミ mini を導入してみた(3)

前回、わざわざアダプターを作って送ってもらったけれども改善しなかったセサミminiの鍵を使って施錠した場合の問題。

tenforward.hatenablog.com

セサミのサポートのメールで

硬めのクッション(セサミminiの箱の黒いクッションなど)をサムターンに貼って隙間を少し少なくして頂くことも有効かもしれません

とアドバイスをいただいていたのと、ブログへのコメントをいただいた id:UnaStrada さまのアドバイスで、

unastrada.hatenablog.jp

このブログエントリのように、吸水テープで隙間をなくすと良さそうということで試してみました。

とりあえず百均で隙間テープと防水テープというものを買ってきました(Can Doです)。

Can Do購入の防水テープ

隙間テープはスポンジ状の比較的柔らかい材質のものでしたが(捨ててしまって写真ないや)、これだと柔らかすぎてサムターンとセサミに押しつぶされる感じで隙間に入れた意味がありませんでした。少し硬い材質が良いのかもということで、この防水テープを使ってみました。

防水テープを装着したセサミmini

これだとサムターンの幅より狭いのですが、アダプターをこれ以上広くすると貼り付けた意味がなかったので、狭めにして無理矢理サムターンを隙間に押し込む感じで設置しました。

防水テープ付きの設置状態

これで無事鍵で施錠してもサムターンはそれなりの角度になるようになったので、鍵でもアプリでも施錠状態にできるようになりました。

f:id:defiant:20190718004817p:plain
鍵で閉めた場合のサムターン位置

ところで使い続けているとアダプターに結構力がかかるのかアダプターが開いてきませんかね?サムターン受けパーツが開いてくるような。根元に結構力かかってるような…(↑の画像は無理矢理押し込んでるので開くのは仕方ないけど、防水テープ貼る前にしばらく使ってたら結構開いていたような)

P.S. ここに来て WiFi モジュールが不調? WiFi 経由で接続できないことがあったので何度かコンセント抜き差しとリセットしてみて様子見(コンセント抜き差しして復活して数日後またアクセスできず…)

ShiftFS ふたたび 〜 Ubuntu 19.04で導入された ShiftFS を試してみた

ちょうど 2 年ほど前、Open Source Summit で話を聞いて ShiftFS について試したことがありました。

tenforward.hatenablog.com

その後はとりあえずカーネルにパッチを当てたりはしてましたが、あまり使わないままでした。少し前に gihyo.jp の Ubuntu の記事を読んでいると、

discoのカーネルフリーズが4月4日に迫る中,興味深いパッチが投稿されています。パッチの中身は「ShiftFS」と呼ばれる,コンテナ環境でのセキュリティ機能を提供するためのラッパーファイルシステムです
https://gihyo.jp/admin/clip/01/ubuntu-topics/201903/29

なんて書かれています。その後の記事で

ShiftFSは, 前回取り上げた後,4月4日に無事にマージに辿り着いています。
https://gihyo.jp/admin/clip/01/ubuntu-topics/201904/19

と書かれていたので、また変わった所がないか調べて試してみたいなと思っていました。

そこでようやく重い腰を上げて、Ubuntu 19.04 環境を用意して試してみました。

ShiftFS について

詳しくは先に紹介した 2 年前の記事を参照してください。

簡単に言うと、非特権コンテナを起動する場合、どの uid/gid で起動するかは一般的に決まっているわけではないですが、世間で流通しているコンテナイメージ内のファイルの所有権は作成時点で一意に決まりますし、自作のコンテナイメージでも同様でしょう。

そこで非特権コンテナを起動するときには、それを起動するユーザの uid/gid に合わせて chown してあげる必要があります。

この問題を解消するために考えられたのが ShiftFS です。

ShiftFS は OverlayFS のように重ね合わせのファイルシステムとして実装されているようです。元のファイルシステムを「下層」(lower filesystem)とし、その上に「上層」(upper filesystem)のファイルシステムを重ねて所有権を調整しているのでしょう(たぶん)。

この ShiftFS は Ubuntu 19.04 の 5.0 カーネルにパッチを適用して使えるようにしており、upstream のカーネルには導入されていない機能です(されそうなのかしら?)。

Ubuntu は 12.04 だったか 14.04 の頃も、まだ upstream な Linux カーネルに導入されていない OverlayFS を先取りして入れていました(カーネルに入ったときには仕様が変わっていて LXC にパッチ送ったりしたのはいい思い出)。また 16.04 なんかでも、かなり後のカーネルで導入される機能がバックポートされていたりして、かなり意欲的に自分たちの求める機能を入れてきます。

ShiftFS によるマウント

マウント方法は2 年前の記事と大きく変わっていません。マウントオプションが増えた程度でしょうか。

マウントは 2 段階で行います。

ShiftFS がないとき

これも2 年前の記事と同じなのですが、簡単に紹介しておきましょう。

ここではファイルシステムとして LXC コンテナのイメージを展開して使用しています(alpineイメージ)。検証のために一部ディレクトリの所有権などをいじっています。root 権限で作成されたコンテナです。

コンテナファイルシステムの root(/)は /var/lib/lxc/ct01/rootfs です。

$ sudo ls -l /var/lib/lxc/ct01/rootfs
total 68
drwxr-xr-x  2 root root 4096 Jul  1 13:00 bin
drwxr-xr-x  3 root root 4096 Jul  2 07:17 dev
drwxr-xr-x 19 root root 4096 Jul  2 07:17 etc
drwxr-xr-x  2 root root 4096 Jun 19 17:14 home
drwxr-xr-x  6 root root 4096 Jul  1 13:00 lib
drwxr-xr-x  5 root root 4096 Jun 19 17:14 media
  : (snip)

上の例のように rootfs 以下は root 所有です。

現在のユーザーをコンテナ内の rootマッピングして User Namespace を作成します。

$ id -u && id -g
1000
1000
$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
# ls -l /var/lib/lxc/ct01/rootfs/
total 68
drwxr-xr-x  2 nobody nogroup 4096 Jul  1 13:00 bin
drwxr-xr-x  3 nobody nogroup 4096 Jul  2 07:17 dev
drwxr-xr-x 19 nobody nogroup 4096 Jul  2 07:17 etc
drwxr-xr-x  2 nobody nogroup 4096 Jun 19 17:14 home
drwxr-xr-x  6 nobody nogroup 4096 Jul  1 13:00 lib
drwxr-xr-x  5 nobody nogroup 4096 Jun 19 17:14 media
  : (snip)

上のように uid/gid=1000/1000 のユーザーをコンテナ内の uid/gid=0/0 にマッピングしただけなので、マッピングされていない uid/gid については nobody/nogroup の 65534/65534 となってしまっています。

ShiftFS があるとき

まずはコンテナの root(/)である /var/lib/lxc/ct01/rootfs を ShiftFS の下層(lower filesystem)としてマークします。この場合、マウントオプションに mark を指定します。

$ sudo mount -t shiftfs -o mark /var/lib/lxc/ct01/rootfs /var/lib/lxc/ct01/rootfs
[sudo] password for ubuntu: 
$ grep shiftfs /proc/self/mountstats 
device /var/lib/lxc/ct01/rootfs mounted on /var/lib/lxc/ct01/rootfs with fstype shiftfs

ShiftFS としてマウントされています(このイメージは root 所有なので、ここでは root ユーザーで mark しています)。

ここで、さきほどと同じように User Namespace を作成します。

$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

そしてさきほど mark した ShiftFS マウントを使って、権限のあるディレクトリに ShiftFS マウントしてみます。

# mount -t shiftfs /var/lib/lxc/ct01/rootfs/ /home/ubuntu/mnt
# grep shiftfs /proc/self/mountstats 
device /var/lib/lxc/ct01/rootfs mounted on /var/lib/lxc/ct01/rootfs with fstype shiftfs
device /var/lib/lxc/ct01/rootfs mounted on /home/ubuntu/mnt with fstype shiftfs

マウントされています。マウント先の /home/ubuntu/mnt を確認してみると、

# ls -l /home/ubuntu/mnt
total 68
drwxr-xr-x  2 root root 4096 Jul  1 13:00 bin
drwxr-xr-x  3 root root 4096 Jul  2 07:17 dev
drwxr-xr-x 19 root root 4096 Jul  2 07:17 etc
drwxr-xr-x  2 root root 4096 Jun 19 17:14 home
drwxr-xr-x  6 root root 4096 Jul  1 13:00 lib
drwxr-xr-x  5 root root 4096 Jun 19 17:14 media
  : (snip)

ちゃんと root 所有になっており、ShiftFS が効いていることがわかります。

ちなみにこの「下層」(mark)のマウントはホストのNamespaceで行っているので、ホストから見えますが、ホストでは「上層」は見えません。

$ grep shiftfs /proc/self/mountstats (ホスト上で実行)
device /var/lib/lxc/ct01/rootfs mounted on /var/lib/lxc/ct01/rootfs with fstype shiftfs
$

コンテナ起動後は、余計な情報を見せないためにホスト上ではこの mark 用 ShiftFS は umount できます。

動きは 2 年前の変わっていないようです。

passthrough オプション

Ubuntuのパッチでは ShiftFS のオプションとして passthrough オプションが追加されているようです。2 年前の記事の時点ではおそらくなかったオプションだと思います。

さきほどの例では何も指定していませんので、passthrough=0 が指定されているのと同じ意味になるようです。

さきほどの例でマウントした ShiftFS で 1 が指定されていない効果を確認してみます。

# stat -f /home/ubuntu/mnt/root
  File: "/home/ubuntu/mnt/root"
    ID: ad6dc6d23bfb15c1 Namelen: 255     Type: UNKNOWN (0x6a656a62)
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 4111327    Free: 3014986    Available: 2801214
Inodes: Total: 1048576    Free: 972147

ファイルシステムタイプとして Type: UNKNOWN と返っています。

(2 の指定は気軽に試すには私にはハードルが高そうなので、)とりあえず 3 を指定してみます。

$ sudo mount -t shiftfs -o mark,passthrough=3 /var/lib/lxc/ct01/rootfs /var/lib/lxc/ct01/rootfs
$ grep shiftfs /proc/self/mountinfo 
540 28 0:53 / /var/lib/lxc/ct01/rootfs rw,relatime shared:294 - shiftfs /var/lib/lxc/ct01/rootfs rw,mark,passthrough=3

passthrough=3 付きでマウントされています。

では User namespace を作成しましょう。

$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# mount -t shiftfs -o passthrough=3 /var/lib/lxc/ct01/rootfs /home/ubuntu/mnt
# stat -f /home/ubuntu/mnt/root
  File: "/home/ubuntu/mnt/root"
    ID: ad6dc6d23bfb15c1 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 4111327    Free: 3019139    Available: 2805367
Inodes: Total: 1048576    Free: 972149

stat コマンドで確認すると Type: ext2/ext3 と返ってきています。

2 の ioctl() 周りですが、ShiftFS のパッチ(の一部) を見る限りでは、

static long shiftfs_ioctl(struct file *file, unsigned int cmd,
              unsigned long arg)
{
    switch (cmd) {
    case FS_IOC_GETVERSION:
        /* fall through */
    case FS_IOC_GETFLAGS:
        /* fall through */
    case FS_IOC_SETFLAGS:
        break;
    default:
        return -ENOTTY;
    }

    return shiftfs_real_ioctl(file, cmd, arg);
}

static long shiftfs_compat_ioctl(struct file *file, unsigned int cmd,
                 unsigned long arg)
{
    switch (cmd) {
    case FS_IOC32_GETVERSION:
        /* fall through */
    case FS_IOC32_GETFLAGS:
        /* fall through */
    case FS_IOC32_SETFLAGS:
        break;
    default:
        return -ENOIOCTLCMD;
    }

    return shiftfs_real_ioctl(file, cmd, arg);
}

のような実装になってるので、ここに挙がってるリクエストは許可されているのでしょう(知らんけど)。

LXD

ちなみに LXD ではすでに ShiftFS が使えるようになっています(3.12以降)。

github.com

使い方は次で説明されています。

discuss.linuxcontainers.org