ユーザー名前空間は、 セキュリティに関連する識別子や属性、 特にユーザー ID やグループ ID (credentials(7) 参照)、 root ディレクトリ、 キー (keyctl(2) 参照)、 ケーパビリティを分離する。 プロセスのユーザー ID とグループ ID はユーザー名前空間の内部と外部で異なる場合がある。 特に、 あるプロセスがユーザー名前空間の外部では通常の非特権ユーザー ID を持つが、 同時にユーザー名前空間の内部ではユーザー ID 0 を持つという場合がある。 言い換えると、 そのプロセスはそのユーザー名前空間の内部での操作に対してすべての特権を持つが、 名前空間の外部での操作では特権を持たない。
カーネルにより (バージョン 3.11 以降では) ユーザー名前空間のネスト数に 32 という上限が課される。 unshare(2) や clone(2) の呼び出しでこの上限を超えてしまう場合はエラー EUSERS で失敗する。
各プロセスは必ず 1 個のユーザー名前空間のメンバーとなる。 CLONE_NEWUSER フラグを指定せずに fork(2) や clone(2) でプロセスを作成した場合、 そのプロセスは親プロセスと同じユーザー名前空間のメンバーとなる。 シングルスレッドのプログラムは、 変更先のユーザー名前空間で CAP_SYS_ADMIN を持っていれば、 setns(2) を使って別のユーザー名前空間に参加することができる。 変更時に、 変更後の名前空間ですべてのケーパビリティを獲得する。
CLONE_NEWUSER を指定して clone(2) や unshare(2) を呼び出すと、 新しいプロセス (clone(2) の場合) や呼び出したプロセス (unshare(2) の場合) がその呼び出しで作成された新しいユーザー名前空間のメンバーとなる。
execve(2) の呼び出しでは、 プロセスのケーパビリティは通常の方法 (capabilities(7) 参照) で再計算され、 通常は、 名前空間内でユーザー ID 0 を持つ場合や実行ファイルが空でない継承可能ケーパビリティマスクを持っている場合を除くと、 すべてのケーパビリティを失うことになる。 下記の、ユーザー ID やグループ ID のマッピングの議論を参照。
CLONE_NEWUSER フラグを使って clone(2), unshare(2), setns(2) を呼び出すと、 子プロセス (clone(2) の場合) や呼び出し元 (unshare(2) や setns(2) の場合) では "securebits" フラグ (capabilities(7) 参照) がデフォルト値に設定される。 呼び出し元は setns(2) の呼び出し後は元のユーザー名前空間ではケーパビリティを持たないので、 setns(2) を 2 回呼び出して一度別のユーザー名前空間に移動して元のユーザー名前空間に戻ることで、 プロセスが元のユーザー名前空間にとどまりつつ自身の "securebits" フラグを再設定することはできない。
ユーザー名前空間内部でケーパビリティを持つというのは、 そのプロセスがその名前空間の支配下にあるリソースに対してのみ (特権を必要とする) 操作を実行できるということである。 プロセスが特定のユーザー名前空間でケーパビリティを持つかどうかを判定するルールは以下の通りである。
ユーザー名前空間以外の名前空間が作成された場合、 その名前空間は呼び出したプロセスが名前空間の作成時にメンバーであったユーザー名前空間により所有される。 ユーザー名前空間以外の名前空間における操作には、 対応するユーザー名前空間でのケーパビリティが必要である。
一つの clone(2) や unshare(2) の呼び出しで CLONE_NEWUSER が他の CLONE_NEW* フラグと一緒に指定された場合、 そのユーザー名前空間が最初に作成されることが保証され、 子プロセス (clone(2) の場合) や呼び出し元 (unshare(2) の場合) はその呼び出しで作成される残りの名前空間で特権を持つ。 したがって、 特権を持たない呼び出し元がフラグを組み合わせて指定することができる。
新しい IPC 名前空間、 マウント名前空間、 ネットワーク名前空間、 PID 名前空間、 UTS 名前空間が clone(2) や unshare(2) で作成される際、 カーネルは新しい名前空間に対して作成したプロセスのユーザー名前空間を記録する (この関連付けは変更できない)。 その新しい名前空間のプロセスがその後名前空間で分離されたグローバルリソースに対して特権操作を行う場合、 カーネルが新しい名前空間に対して関連付けたユーザー名前空間でのプロセスのケーパビリティに基づいてアクセス許可のチェックが行われる。
マウント名前空間に関しては以下の点に注意すること。
以下の段落で uid_map の詳細を説明する。 gid_map に関しても全く同じである。 "user ID" という部分を "group ID" に置き換えればよい。
uid_map ファイルで、 プロセス pid のユーザー名前空間から uid_map をオープンしたプロセスのユーザー名前空間にユーザー ID のマッピングが公開される (公開するポリシーの条件については下記を参照)。 言い換えると、 別のユーザー名前空間のプロセスでは、 特定の uid_map ファイルを読み出した際に潜在的には別の値が見えることがあるということである。 見える値は読み出したプロセスのユーザー名前空間のユーザー ID マッピングに依存する。
uid_map ファイルの各行は 2 つのユーザー名前空間間の連続するユーザー ID の範囲の 1 対 1 マッピングを指定する (ユーザー名前空間が最初に作成された際にはこのファイルは空である)。 各行の指定の形式はホワイトスペース区切りの 3 つの数字である。 最初の 2 つの数字は 2 つの ユーザー名前空間それぞれの開始ユーザー ID を指定する。 3 つ目の数字はマッピングされる範囲の長さを指定する。 詳しくは、各フィールドは以下のように解釈される。
ユーザー ID (グループ ID) を返すシステムコール、例えば getuid(2), getgid(2) や stat(2) が返す構造体の credential フィールド、は呼び出し元のユーザー名前空間にマッピングされたユーザー ID (グループ ID) を返す。
プロセスがファイルにアクセスする場合、 アクセス許可のチェックやファイル作成時の ID 割り当てのために、 そのユーザー ID とグループ ID は初期ユーザー名前空間にマッピングされる。 プロセスが stat(2) を使ってファイルのユーザー ID やグループ ID を取得する際には、 上記の反対方向に ID のマッピングが行われ、 プロセスにおける相対的なユーザー ID とグループ ID の値が生成される。
初期ユーザー名前空間は親の名前空間を持たないが、 一貫性を持たせるため、 カーネルは初期の名前空間に対してダミーのユーザー ID とグループ ID のマッピングを提供する。 初期の名前空間のシェルから uid_map ファイル (gid_map も同じ) を参照するには以下のようにする。
$ cat /proc/$$/uid_map 0 0 4294967295
このマッピングは、 この名前空間のユーザー ID 0 から始まる範囲が (実際には存在しない) 親の名前空間の 0 から始まる範囲にマッピングされ、 範囲の流さは 32 ビットの unsigned integer の最大値である、 と言っている。 (ここで 4294967295 (32 ビットの符号付き -1 の値) は意図的にマッピングされていない。 (uid_t) -1 は (setreuid(2) など) いくつかのインターフェースで "no user ID" (ユーザー ID なし) を示す手段として使用されているので、 意図的にこのようになっている。 (uid_t) -1 をマッピングせず、 利用できないようにすることで、 これらのインターフェースを使った際に混乱が起こらないように保証している。)
新しいユーザー名前空間を作成した後、 新しいユーザー名前空間におけるユーザー ID のマッピングを定義するため、 その名前空間のプロセスの「一つ」の uid_map ファイルに「一度だけ」書き込みを行うことができる。 ユーザー名前空間の uid_map ファイルに二度目以降の書き込みを行おうとすると、 エラー EPERM で失敗する。 gid_map ファイルについては同じルールが適用される。
uid_map (gid_map) に書き込む行は以下のルールに従っていなければならない。
上記のルールを満たさない書き込みはエラー EINVAL で失敗する。
プロセスが /proc/[pid]/uid_map (/proc/[pid]/gid_map) ファイルに書き込むためには、 以下の要件がすべて満たされる必要がある。
上記のルールを満たさない書き込みはエラー EPERM で失敗する。
マッピングされていないユーザー ID (グループ ID) がユーザー空間に公開される場合はいろいろある。 例えば、 新しいユーザー名前空間の最初のプロセスが、 その名前空間に対するユーザー ID マッピングが定義される前に getuid() を呼び出すなどである。 このようなほとんどの場合で、 マッピングされていないユーザー ID はオーバーフローユーザー ID (グループ ID)に変換される。 デフォルトのオーバーフローユーザー ID (グループ ID) は 65534 である。 proc(5) の /proc/sys/kernel/overflowuid と /proc/sys/kernel/overflowgid の説明を参照。
マッピングされていない ID がこのようにマッピングされる場合としては、 ユーザー ID を返すシステムコール (getuid(2), getgid(2) やその同類)、 UNIX ドメインソケットで渡される ID 情報 (credential)、 stat(2) が返す ID 情報、 waitid(2)、 System V IPC "ctl" IPC_STAT 操作、 /proc/PID/status や /proc/sysvipc/* 内のファイルで公開される ID 情報、 シグナル受信時の siginfo_t の si_uid フィールドで返される ID 情報 (sigaction(2) 参照)、 プロセスアカウンティングファイルに書き込まれる ID 情報 (acct(5) 参照)、 POSIX メッセージキュー通知で返される ID 情報 (mq_notify(3) 参照) がある。
マッピングされていないユーザー ID やグループ ID が対応するオーバーフロー ID 値に変換され「ない」重要な場合が一つある。 2 番目のフィールドにマッピングがない uid_map や gid_map ファイルを参照した際、 そのフィールドは 4294967295 (unsigned integer では -1) が表示される。
ユーザー名前空間内のプロセスが set-user-ID (set-group-ID) されたプログラムを実行した場合、 そのプロセスの名前空間内の実効ユーザー ID (実効グループ ID) は、 そのファイルのユーザー ID (グループ ID) にマッピングされる。 しかし、 そのファイルのユーザー ID 「か」グループ ID が名前空間内のマッピングにない場合、 set-user-ID (set-group-ID) ビットは黙って無視される。 新しいプログラムは実行されるが、 そのプロセスの実効ユーザー ID (実効グループ ID) は変更されないままとなる。 (これは MS_NOSUID フラグ付きでマウントされたファイルシステム上にある set-user-ID/set-group-ID プログラムを実行した場合の動作を反映したものである。 mount(2) を参照。)
プロセスのユーザー ID とグループ ID が UNIX ドメインソケットを通して別のユーザー名前空間のプロセスに渡された場合 (unix(7) の SCM_CREDENTIALS の説明を参照)、 ユーザー ID とグループ ID は受信プロセスのユーザー ID とグループ ID のマッピングに基づき対応する値に翻訳される。
Linux 3.8 時点では、 ほとんどの関連するサブシステムはユーザー名前空間に対応しているが、 多くのファイルシステムにユーザー名前空間間でユーザー ID やグループ ID のマッピングを行うのに必要な基盤がなかった。 Linux 3.9 では、 残りの未サポートのファイルシステムの多くで必要な基盤のサポートが追加された (Plan 9 (9P), Andrew File System (AFS), Ceph, CIFS, CODA, NFS, OCFS2)。 Linux 3.11 では、最後の主要な未サポートのファイルシステムであった XFS のサポートが追加された。
まず最初に、実行環境を確認しておく。
$ uname -rs # Linux 3.8 以降が必要 Linux 3.8.0 $ id -u # 非特権ユーザーで実行する 1000 $ id -g 1000
新しいユーザー名前空間 (-U), マウント名前空間 (-m), PID 名前空間 (-p) で新しいシェルを開始する。ユーザー ID (-M) 1000 とグループ ID (-G) 1000 をユーザー名前空間内で 0 にマッピングしている。
$ ./userns_child_exec -p -m -U -M '0 1000 1' -G '0 1000 1' bash
シェルは PID 1 を持つ。このシェルは新しい PID 名前空間の最初のプロセスだからである。
bash$ echo $$ 1
ユーザー名前空間内では、シェルのユーザー ID とグループ ID ともに 0 で、すべての許可ケーパビリティと実効ケーパビリティが有効になっている。
bash$ cat /proc/$$/status | egrep '^[UG]id' Uid: 0 0 0 0 Gid: 0 0 0 0 bash$ cat /proc/$$/status | egrep '^Cap(Prm|Inh|Eff)' CapInh: 0000000000000000 CapPrm: 0000001fffffffff CapEff: 0000001fffffffff
/proc ファイルシステムをマウントし、新しい PID 名前空間で見えるプロセス一覧を表示すると、 シェルからは PID 名前空間外のプロセスが見えないことが分かる。
bash$ mount -t proc proc /proc bash$ ps ax PID TTY STAT TIME COMMAND 1 pts/3 S 0:00 bash 22 pts/3 R+ 0:00 ps ax
/* userns_child_exec.c GNU General Public License v2 以降の元でライセンスされる 新しい名前空間でシェルコマンドを実行する子プロセスを作成する。 ユーザー名前空間を作成する際に UID と GID のマッピングを 指定することができる。 */ #define _GNU_SOURCE #include <sched.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> #include <signal.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #include <limits.h> #include <errno.h> /* 簡単なエラー処理関数: \(aqerrno\(aq の値に基づいて エラーメッセージを出力し、呼び出し元プロセスを終了する。 */ #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \ } while (0) struct child_args { char **argv; /* 子プロセスが実行するコマンドと引き数 */ int pipe_fd[2]; /* 親プロセスと子プロセスを同期するためのパイプ */ }; static int verbose; static void usage(char *pname) { fprintf(stderr, "Usage: %s [options] cmd [arg...]\n\n", pname); fprintf(stderr, "Create a child process that executes a shell " "command in a new user namespace,\n" "and possibly also other new namespace(s).\n\n"); fprintf(stderr, "Options can be:\n\n"); #define fpe(str) fprintf(stderr, " %s", str); fpe("-i New IPC namespace\n"); fpe("-m New mount namespace\n"); fpe("-n New network namespace\n"); fpe("-p New PID namespace\n"); fpe("-u New UTS namespace\n"); fpe("-U New user namespace\n"); fpe("-M uid_map Specify UID map for user namespace\n"); fpe("-G gid_map Specify GID map for user namespace\n"); fpe("-z Map user's UID and GID to 0 in user namespace\n"); fpe(" (equivalent to: -M '0 <uid> 1' -G '0 <gid> 1')\n"); fpe("-v Display verbose messages\n"); fpe("\n"); fpe("If -z, -M, or -G is specified, -U is required.\n"); fpe("It is not permitted to specify both -z and either -M or -G.\n"); fpe("\n"); fpe("Map strings for -M and -G consist of records of the form:\n"); fpe("\n"); fpe(" ID-inside-ns ID-outside-ns len\n"); fpe("\n"); fpe("A map string can contain multiple records, separated" " by commas;\n"); fpe("the commas are replaced by newlines before writing" " to map files.\n"); exit(EXIT_FAILURE); } /* マッピングファイル 'map_file' を 'mapping' で指定 された値で更新する。 'mapping' は UID や GID マッピングを 定義する文字列である。 UID や GID マッピングは以下の形式の改行 で区切られた 1 つ以上のレコードである。 NS 内 ID NS 外 ID 長さ ユーザーに改行を含む文字列を指定するのを求めるのは、 コマンドラインを使う場合にはもちろん不便なことである。 そのため、 この文字列でレコードを区切るのにカンマを 使えるようにして、ファイルにこの文字列を書き込む前に カンマを改行に置換する。 */ static void update_map(char *mapping, char *map_file) { int fd, j; size_t map_len; /* 'mapping' の長さ */ /* マッピング文字列内のカンマを改行で置換する */ map_len = strlen(mapping); for (j = 0; j < map_len; j++) if (mapping[j] == ',') mapping[j] = '\n'; fd = open(map_file, O_RDWR); if (fd == -1) { fprintf(stderr, "ERROR: open %s: %s\n", map_file, strerror(errno)); exit(EXIT_FAILURE); } if (write(fd, mapping, map_len) != map_len) { fprintf(stderr, "ERROR: write %s: %s\n", map_file, strerror(errno)); exit(EXIT_FAILURE); } close(fd); } static int /* クローンされた子プロセスの開始関数 */ childFunc(void *arg) { struct child_args *args = (struct child_args *) arg; char ch; /* 親プロセスが UID と GID マッピングを更新するまで待つ。 main() のコメントを参照。 パイプの end of file を待つ。 親プロセスが一旦マッピングを更新すると、 パイプはクローズされる。 */ close(args->pipe_fd[1]); /* パイプのこちら側の書き込み端のディスク リプターをクローズする。これにより 親プロセスがディスクリプターをクローズ すると EOF が見えるようになる。 */ if (read(args->pipe_fd[0], &ch, 1) != 0) { fprintf(stderr, "Failure in child: read from pipe returned != 0\n"); exit(EXIT_FAILURE); } /* シェルコマンドを実行する */ printf("About to exec %s\n", args->argv[0]); execvp(args->argv[0], args->argv); errExit("execvp"); } #define STACK_SIZE (1024 * 1024) static char child_stack[STACK_SIZE]; /* 子プロセスのスタック空間 */ int main(int argc, char *argv[]) { int flags, opt, map_zero; pid_t child_pid; struct child_args args; char *uid_map, *gid_map; const int MAP_BUF_SIZE = 100; char map_buf[MAP_BUF_SIZE]; char map_path[PATH_MAX]; /* コマンドラインオプションを解析する。 最後の getopt() 引き数の最初の '+' 文字は GNU 風のコマンドラインオプションの並び換えを防止する。 このプログラム自身が実行する「コマンド」にコマンドライン オプションが含まれる場合があるからである。 getopt() にこれらをこのプログラムのオプションとして 扱ってほしくはないのだ。 */ flags = 0; verbose = 0; gid_map = NULL; uid_map = NULL; map_zero = 0; while ((opt = getopt(argc, argv, "+imnpuUM:G:zv")) != -1) { switch (opt) { case 'i': flags |= CLONE_NEWIPC; break; case 'm': flags |= CLONE_NEWNS; break; case 'n': flags |= CLONE_NEWNET; break; case 'p': flags |= CLONE_NEWPID; break; case 'u': flags |= CLONE_NEWUTS; break; case 'v': verbose = 1; break; case 'z': map_zero = 1; break; case 'M': uid_map = optarg; break; case 'G': gid_map = optarg; break; case 'U': flags |= CLONE_NEWUSER; break; default: usage(argv[0]); } } /* -U なしの -M や -G の指定は意味がない */ if (((uid_map != NULL || gid_map != NULL || map_zero) && !(flags & CLONE_NEWUSER)) || (map_zero && (uid_map != NULL || gid_map != NULL))) usage(argv[0]); args.argv = &argv[optind]; /* 親プログラムと子プロセスを同期するためにパイプを使っている。 これは、子プロセスが execve() を呼び出す前に、親プロセスにより UID と GID マップが設定されることを保証するためである。 これにより、新しいユーザー名前空間において子プロセスの実効 ユーザー ID を 0 にマッピングしたいという通常の状況で、 子プロセスが execve() 実行中にそのケーパビリティを維持する ことができる。 この同期を行わないと、 0 以外のユーザー ID で execve() を実行した際に、子プロセスがそのケーパビリティを失う ことになる (execve() 実行中のプロセスのケーパビリティの変化の 詳細については capabilities(7) マニュアルページを参照)。 */ if (pipe(args.pipe_fd) == -1) errExit("pipe"); /* 新しい名前空間で子プロセスを作成する */ child_pid = clone(childFunc, child_stack + STACK_SIZE, flags | SIGCHLD, &args); if (child_pid == -1) errExit("clone"); /* 親プロセスはここを実行する */ if (verbose) printf("%s: PID of child created by clone() is %ld\n", argv[0], (long) child_pid); /* 子プロセスの UID と GID のマッピングを更新する */ if (uid_map != NULL || map_zero) { snprintf(map_path, PATH_MAX, "/proc/%ld/uid_map", (long) child_pid); if (map_zero) { snprintf(map_buf, MAP_BUF_SIZE, "0 %ld 1", (long) getuid()); uid_map = map_buf; } update_map(uid_map, map_path); } if (gid_map != NULL || map_zero) { snprintf(map_path, PATH_MAX, "/proc/%ld/gid_map", (long) child_pid); if (map_zero) { snprintf(map_buf, MAP_BUF_SIZE, "0 %ld 1", (long) getgid()); gid_map = map_buf; } update_map(gid_map, map_path); } /* パイプの書き込み端をクローズし、子プロセスに UID と GID の マッピングが更新されたことを知らせる */ close(args.pipe_fd[1]); if (waitpid(child_pid, NULL, 0) == -1) /* 子プロセスを待つ */ errExit("waitpid"); if (verbose) printf("%s: terminating\n", argv[0]); exit(EXIT_SUCCESS); }
カーネルのソースファイル Documentation/namespaces/resource-control.txt