Linux: コンテナ型の仮想化サポートとkuid_t、kgid_t

前回の日記でふと思ったuser_namespace構造体とは何?というのを調べているんだけどその過程で出てきたkuid_tとkgid_tのメモ。

定義自体はinclude/linux/uidgid.hにあって、このような形でとくに面白いものでもない。

 20 typedef struct {
 21         uid_t val;
 22 } kuid_t;
 23 
 24 
 25 typedef struct {
 26         gid_t val;
 27 } kgid_t;

これらがどのような目的で使われているのかというのが知りたいところですよね。もしかしたらLWNに解説記事があるんじゃないかな〜と思って見てみたらありました! このA new approach to user namespacesがビンゴですね。 記事によるとこれらがカーネルに入ったのは2012年の秋頃なので割と最近のことみたいです。目的はコンテナ型仮想化の効率的なサポートというところみたいです。

kernel UIDはプロセスを識別するのだけど、これはホスト側のため(原文だと the base systemだからホスト側かな)に使うのでコンテナ内で使われるUIDとは別物。これは主に権限チェックのために使用する。プロセスからはこの値を知ることはできない。ホントにカーネルが使うためのid。

kuid_t、kgid_tを操作する関数としてはこれらがあるけど(比較系の関数もある)、概要レベルではやりたいことが想像つくからとりあえず読まなくて良いか。

110 extern kuid_t make_kuid(struct user_namespace *from, uid_t uid);
111 extern kgid_t make_kgid(struct user_namespace *from, gid_t gid);
112 
113 extern uid_t from_kuid(struct user_namespace *to, kuid_t uid);
114 extern gid_t from_kgid(struct user_namespace *to, kgid_t gid);
115 extern uid_t from_kuid_munged(struct user_namespace *to, kuid_t uid);
116 extern gid_t from_kgid_munged(struct user_namespace *to, kgid_t gid);
117 

で、このIDがどのように使われているのかを見てみる。lxrで調べるとfs/open.cにあるchown_common()で使っているのでこの辺を。

関数の引数にあるuserとgroupはchmod時に使うuidとgidです。

549 static int chown_common(struct path *path, uid_t user, gid_t group)
550 {
551         struct inode *inode = path->dentry->d_inode;
552         struct inode *delegated_inode = NULL;
553         int error;
554         struct iattr newattrs;
555         kuid_t uid;
556         kgid_t gid;
557 
558         uid = make_kuid(current_user_ns(), user);
559         gid = make_kgid(current_user_ns(), group);
560 
561         newattrs.ia_valid =  ATTR_CTIME;
562         if (user != (uid_t) -1) {
563                 if (!uid_valid(uid))
564                         return -EINVAL;
565                 newattrs.ia_valid |= ATTR_UID;
566                 newattrs.ia_uid = uid;
567         }

chown系の場合はカレントタスクのuser_nsと引数で渡ってきた設定したいuser idのからkuidのマッピングを調べて、この値が-1じゃない場合は562行目からのifブロック内の処理に入る。
この場合、uidがinvalidな可能性もあるのでチェックが入っている。これを見ると-1が返るのはどんな場合なのか気になるのでやっぱりmake_kuid()を見てみよう。

make_kuid()はこれ。KUIDT_INITはkuid_tの変数を作ってメンバ変数のvalに値を設定するマクロなので本体はmap_id_down()。

218 /**
219  *      make_kuid - Map a user-namespace uid pair into a kuid.
220  *      @ns:  User namespace that the uid is in
221  *      @uid: User identifier
222  *
223  *      Maps a user-namespace uid pair into a kernel internal kuid,
224  *      and returns that kuid.
225  *
226  *      When there is no mapping defined for the user-namespace uid
227  *      pair INVALID_UID is returned.  Callers are expected to test
228  *      for and handle INVALID_UID being returned.  INVALID_UID
229  *      may be tested for using uid_valid().
230  */
231 kuid_t make_kuid(struct user_namespace *ns, uid_t uid)
232 {
233         /* Map the uid to a global kernel uid */
234         return KUIDT_INIT(map_id_down(&ns->uid_map, uid));
235 }

map_id_down()はこれ。

172 static u32 map_id_down(struct uid_gid_map *map, u32 id)
173 {
174         unsigned idx, extents;
175         u32 first, last;
176 
177         /* Find the matching extent */
178         extents = map->nr_extents;
179         smp_rmb();
180         for (idx = 0; idx < extents; idx++) {
181                 first = map->extent[idx].first;
182                 last = first + map->extent[idx].count - 1;
183                 if (id >= first && id <= last)
184                         break;
185         }
186         /* Map the id or note failure */
187         if (idx < extents)
188                 id = (id - first) + map->extent[idx].lower_first;
189         else
190                 id = (u32) -1;
191 
192         return id;
193 }

やっていることは見ての通りという気も。使っているデータはuid_gid_map構造体でこれはinclude/linux/user_namespace.hでこのように定義されている。

  9 #define UID_GID_MAP_MAX_EXTENTS 5
 10 
 11 struct uid_gid_map {    /* 64 bytes -- 1 cache line */
 12         u32 nr_extents;
 13         struct uid_gid_extent {
 14                 u32 first;
 15                 u32 lower_first;
 16                 u32 count;
 17         } extent[UID_GID_MAP_MAX_EXTENTS];
 18 };

このmapはどこかで初期化されているんだろけどというのを調べるとkernel/user.cにinit_user_nsの定義が。これが基本的に使われるやつかな。kernel/cred.cにあるinit_credの定義でも設定されているし。

 21 /*
 22  * userns count is 1 for root user, 1 for init_uts_ns,
 23  * and 1 for... ?
 24  */
 25 struct user_namespace init_user_ns = {
 26         .uid_map = {
 27                 .nr_extents = 1,
 28                 .extent[0] = {
 29                         .first = 0,
 30                         .lower_first = 0,
 31                         .count = 4294967295U,
 32                 },
 33         },
 34         .gid_map = {
 35                 .nr_extents = 1,
 36                 .extent[0] = {
 37                         .first = 0,
 38                         .lower_first = 0,
 39                         .count = 4294967295U,
 40                 },
 41         },
 42         .projid_map = {
 43                 .nr_extents = 1,
 44                 .extent[0] = {
 45                         .first = 0,
 46                         .lower_first = 0,
 47                         .count = 4294967295U,
 48                 },
 49         },
 50         .count = ATOMIC_INIT(3),
 51         .owner = GLOBAL_ROOT_UID,
 52         .group = GLOBAL_ROOT_GID,
 53         .proc_inum = PROC_USER_INIT_INO,
 54 #ifdef CONFIG_PERSISTENT_KEYRINGS
 55         .persistent_keyring_register_sem =
 56         __RWSEM_INITIALIZER(init_user_ns.persistent_keyring_register_sem),
 57 #endif
 58 };
 59 EXPORT_SYMBOL_GPL(init_user_ns);

これでmap_id_down()に戻ってループの箇所を見てみると、extentsは1で調べたいIDがmap->extent[0]のfirst~countの間にあった場合はbreakする。

180         for (idx = 0; idx < extents; idx++) {
181                 first = map->extent[idx].first;
182                 last = first + map->extent[idx].count - 1;
183                 if (id >= first && id <= last)
184                         break;
185         }

そして、ループを抜けたところで187行目のif分が真になるのはループをbreakで抜けた場合で-1が返るのはidがcountが0以下もしくはcount以上だった場合ということがわかる。

186         /* Map the id or note failure */
187         if (idx < extents)
188                 id = (id - first) + map->extent[idx].lower_first;
189         else
190                 id = (u32) -1;

ということで、make_kuid(KUIDT_INIT&map_id_down()の組み合わせ)は割とシンプルな仕組みですね。これでユーザープロセスから見えるuidとカーネル用のkuidがマップされるわけですね。

ちなみにこの機能はCONFIG_USER_NS=yの時の話なのですが、うちの.configの元ネタの現時点でのarchの.configではこの機能は使ってませんでした。Fedora 21の.configでは有効になってます。

ここでまたLWNの記事戻る。このマッピングはシステム管理者が/proc/pid/uid_map(2014/08/07時点のtip treeだと/proc/pid/uid_mapでした)に値を書き込むことで行える。これで特定のidの範囲をコンテナ用に取っておくことができたりする。 /proc/pid/uid_mapは以下の3個の数値を設定できる。first-ns-idはuidの中でのvalidな最小値で、0が使用可能。uid0なのでrootということ。マッピングはfirst-target-idから開始される。

  • first-ns-id
  • first-target-id
  • count

独自の名前空間でrootのプロセスとしてうごいているプロセスはその名前空間では大方の期待通りroot権限でうろくけど、もちろんその名前空間の外にはアクセスできない。user namespaceの下にさらに名前空間を作ることもできる。けど、親プロセスの名前空間にあるuid・gidのしか使うことはできない。また、一度設定したら更新はできない。

ざっとだけどこんなものかな。まだuser_namespaceは全然手付かずだけど。。