名前

user_namespaces - Linux ユーザー名前空間の概要

説明

名前空間の概要については namespaces(7) を参照。
 
ユーザー名前空間は、 セキュリティに関連する識別子や属性、 特にユーザー ID やグループ ID ( credentials(7) 参照)、 root ディレクトリ、 キー ( keyctl(2) 参照)、 ケーパビリティを分離する。 プロセスのユーザー ID とグループ ID はユーザー名前空間の内部と外部で異なる場合がある。 特に、 あるプロセスがユーザー名前空間の外部では通常の非特権ユーザー ID を持つが、 同時にユーザー名前空間の内部ではユーザー ID 0 を持つという場合がある。 言い換えると、 そのプロセスはそのユーザー名前空間の内部での操作に対してすべての特権を持つが、 名前空間の外部での操作では特権を持たない。

ネストされた名前空間、名前空間のメンバー

ユーザー名前空間は入れ子にすることができる。 つまり、 最初の ("root") 名前空間以外の各名前空間は親のユーザー名前空間を持ち、 0 個以上のユーザー名前空間を持つということである。 親のユーザー名前空間は、 CLONE_NEWUSER フラグを指定して unshare(2)clone(2) を呼び出してユーザー名前空間を作成したプロセスのユーザー名前空間である。
 
カーネルにより (バージョン 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) の場合) がその呼び出しで作成された新しいユーザー名前空間のメンバーとなる。

ケーパビリティ

CLONE_NEWUSER フラグが指定された clone(2) で作成された子プロセスは、 新しい名前空間ですべてのケーパビリティを持った状態で開始される。 同様に、 unshare(2) を使って新しいユーザー名前空間を作成したり、 setns(2) を使って既存のユーザー名前空間に参加したりしたプロセスは、 その名前空間ですべてのケーパビリティを獲得する。 一方、 そのプロセスは、親のユーザー名前空間 ( clone(2) の場合) や直前のユーザー名前空間 ( unshare(2)setns(2) の場合) では、 root ユーザー (root 名前空間のユーザー ID 0 のプロセス) により新しい名前空間の作成や参加が行われた場合であっても、 ケーパビリティを全く持たない。
 
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" フラグを再設定することはできない。
 
ユーザー名前空間内部でケーパビリティを持つというのは、 そのプロセスがその名前空間の支配下にあるリソースに対してのみ (特権を必要とする) 操作を実行できるということである。 プロセスが特定のユーザー名前空間でケーパビリティを持つかどうかを判定するルールは以下の通りである。
1.
プロセスがその名前空間のメンバーで、実効ケーパビリティセットにそのケーパビリティがあれば、 そのプロセスはユーザー名前空間内でケーパビリティを持つ。 プロセスが実効ケーパビリティセットでケーパビリティを得るにはいくつかの方法がある。 例えば、 set-user-ID プログラムや関連するファイルケーパビリティを持った実行ファイルを実行する。 また、 すでに説明したとおり、 プロセスは clone(2), unshare(2), setns(2) の結果としてケーパビリティを獲得することもできる。
2.
プロセスがユーザー名前空間でケーパビリティを持っている場合、 そのプロセスはすべての子供の名前空間 (および削除された子孫の名前空間) でケーパビリティを持つ。
3.
ユーザー名前空間が作成された際、 カーネルはその名前空間の「所有者」として作成したプロセスの実効ユーザー ID を記録する。 親のユーザー名前空間に属するプロセスで、 そのプロセスの実効ユーザー ID が名前空間の所有者と一致する場合、 そのプロセスはその名前空間ですべてのケーパビリティを持つ。 一つ前のルールも合わせて考えると、 このプロセスはすべての削除された子孫のユーザー名前空間ですべてのケーパビリティを持つことを意味する。

ユーザー名前空間と他の名前空間の関係

Linux 3.8 以降では、 非特権プロセスがユーザー名前空間を作成することができる。 また、 呼び出し元のユーザー名前空間で CAP_SYS_ADMIN ケーパビリティを持っているだけで、 マウント名前空間、 PID 名前空間、 IPC 名前空間、 ネットワーク名前空間、 UTS 名前空間を作成できる。
 
ユーザー名前空間以外の名前空間が作成された場合、 その名前空間は呼び出したプロセスが名前空間の作成時にメンバーであったユーザー名前空間により所有される。 ユーザー名前空間以外の名前空間における操作には、 対応するユーザー名前空間でのケーパビリティが必要である。
 
一つの clone(2)unshare(2) の呼び出しで CLONE_NEWUSER が他の CLONE_NEW* フラグと一緒に指定された場合、 そのユーザー名前空間が最初に作成されることが保証され、 子プロセス ( clone(2) の場合) や呼び出し元 ( unshare(2) の場合) はその呼び出しで作成される残りの名前空間で特権を持つ。 したがって、 特権を持たない呼び出し元がフラグを組み合わせて指定することができる。
 
新しい IPC 名前空間、 マウント名前空間、 ネットワーク名前空間、 PID 名前空間、 UTS 名前空間が clone(2)unshare(2) で作成される際、 カーネルは新しい名前空間に対して作成したプロセスのユーザー名前空間を記録する (この関連付けは変更できない)。 その新しい名前空間のプロセスがその後名前空間で分離されたグローバルリソースに対して特権操作を行う場合、 カーネルが新しい名前空間に対して関連付けたユーザー名前空間でのプロセスのケーパビリティに基づいてアクセス許可のチェックが行われる。

マウント名前空間における制限

マウント名前空間に関しては以下の点に注意すること。
*
マウント名前空間は所有者のユーザー名前空間を持つ。 所有者のユーザー名前空間が親のマウント名前空間の所有者のユーザー名前空間と異なるマウント名前空間は、 特権が少ないマウント名前空間 (less privileged mount namespace) である。
*
特権が少ないマウント名前空間を作成する場合、 共有マウントは slave マウントに縮小される。 これにより、 特権の少ないマウント名前空間で実行されるマッピングが、 より特権を持つマウント名前空間 (more privileged mount namespace) に伝搬しないことが保証される。
*
より特権を持つマウントで一つのまとまりとして行われたマウントは一つにまとまったままとなり、 特権が少ないマウント名前空間で分割することはできない。 ( unshare(2)CLONE_NEWNS 操作では、 元のマウント名前空間のすべてのマウントは一つのまとまりとして扱われ、 マウント名前空間間で伝わる再帰的なマウントでは一つのまとまりとして伝わる。)
*
より特権を持つマウント名前空間から特権の少ないマウント名前空間に伝わる際に、 mount(2)MS_RDONLY, MS_NOSUID, MS_NOEXEC フラグと "atime" フラグ ( MS_NOATIME, MS_NODIRATIME, MS_REALTIME) 設定はロックされ、 特権の少ないマウント名前空間では変更することはできない。
*
ある名前空間でマウントポイントとなっているが別の名前空間でのマウントポイントになっていないファイルやディレクトリは、 マウントポイントになっていないマウント名前空間では (通常のアクセス許可チェックにもとづいて) rename, unlink, remove ( rmdir(2)) を行うことができる。
以前は、 別のマウント名前空間でマウントポイントとなっていたファイルやディレクトリを rename, unlink, remove しようとすると、 エラー EBUSY が返されていた。 この動作は、 (NFS などで) 適用にあたっての技術的な問題があるとともに、 より特権を持つユーザーに対してサービス不能攻撃 (denial-of-service attack) を許してしまっていた (ファイルをバインドマウントで更新することができなくなっていた)。

ユーザー ID とグループ ID のマッピング: uid_map と gid_map

ユーザー名前空間が作成された際、 その名前空間は親のユーザー名前空間へのユーザー ID (とグループ ID) のマッピングを行わずに開始される。 ファイル /proc/[pid]/uid_map/proc/[pid]/gid_map (Linux 3.5 以降で利用可能) でプロセス pid のユーザー名前空間内でのユーザー ID とグループ ID のマッピングにアクセスできる。 これらのファイルを読み出してユーザー名前空間内のマッピングを参照したり、 これらのファイルに書き込んでマッピングを (一度だけ) 定義することができる。
 
以下の段落で 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 つ目の数字はマッピングされる範囲の長さを指定する。 詳しくは、各フィールドは以下のように解釈される。
(1)
プロセス pid のユーザー名前空間におけるユーザー ID の範囲の開始値。
(2)
1 番目のフィールドで指定されたユーザー ID がマッピングされる先のユーザー ID の範囲の開始値。 2 番目のフィールドがどのように解釈されるかは、 uid_map をオープンしたプロセスとプロセス pid が同じユーザー名前空間かどうかに依存する。 以下のとおり。
a)
2 つのプロセスが異なるユーザー名前空間に属す場合、 2 番目のフィールドは uid_map をオープンしたプロセスのユーザー名前空間におけるユーザー ID の範囲の開始値である。
b)
2 つのプロセスが同じユーザー名前空間に属す場合、 2 番目のフィールドはプロセス pid の親のユーザー名前空間におけるユーザー ID の範囲の開始値である。 この場合、 uid_map をオープンしたプロセス (よくあるのは /proc/self/uid_map をオープンした場合である) は、 このユーザー名前空間を作成したプロセスのユーザー名前空間に対するユーザー ID マッピングを参照することができる。
(3)
2 つのユーザー名前空間間でマッピングされるユーザー ID の範囲の長さ。
ユーザー 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 とグループ ID のマッピングの定義: uid_map と gid_map への書き込み

新しいユーザー名前空間を作成した後、 新しいユーザー名前空間におけるユーザー ID のマッピングを定義するため、 その名前空間のプロセスの「一つ」の uid_map ファイルに「一度だけ」書き込みを行うことができる。 ユーザー名前空間の uid_map ファイルに二度目以降の書き込みを行おうとすると、 エラー EPERM で失敗する。 gid_map ファイルについては同じルールが適用される。
 
uid_map (gid_map) に書き込む行は以下のルールに従っていなければならない。
*
3 のフィールドは有効な数字でなければならず、最後のフィールドは 0 より大きくなければならない。
*
行は改行文字で終了しなければならない。
*
ファイルの行数には上限がある。 Linux 3.8 時点では、上限は 5 行である。 さらに、 ファイルに書き込むバイト数はシステムページサイズより小さくなければならず、 書き込みはファイルの先頭に対して行わなければならない (つまり、 lseek(2)pwrite(2) を使って 0 以外のファイルオフセットに書き込むことはできない)。
*
各行で指定されるユーザー ID (グループ ID) の範囲は他の行が指定する範囲と重なってはならない。 最初の実装 (Linux 3.8) では、 この要件は、 後続行のフィールド 1 とフィールド 2 の両方の値が昇順になっていなければならないという追加の要件を設け、 これが満たされなかった場合は有効なマッピングは作成されない、 という単純な実装により満たされていた。 Linux 3.9 以降ではこの制限は修正され、 重複がない有効なマッピングであればどんな組み合わせでも指定できるようになった。
*
少なくとも 1 行はファイルに書き込まなければならない。
上記のルールを満たさない書き込みはエラー EINVAL で失敗する。
 
プロセスが /proc/[pid]/uid_map (/proc/[pid]/gid_map) ファイルに書き込むためには、 以下の要件がすべて満たされる必要がある。
1.
書き込みプロセスは、 プロセス pid のユーザー名前空間で CAP_SETUID (CAP_SETGID) ケーパビリティを持っていなければならない。
2.
書き込みプロセスは、 プロセス pid のユーザー名前空間もしくはプロセス pid の親のユーザー名前空間に属していなければならない。
3.
マッピングされたユーザー ID (グループ ID) は親のユーザー名前空間にマッピングを持っていなければならない。
4.
以下のいずれか一つが真である。
*
uid_map (gid_map) に書き込まれるデータは、 書き込みを行うプロセスの親のユーザー名前空間でのファイルシステムユーザー ID (グループ ID) をそのユーザー名前空間でのユーザー ID (グループ ID) にマッピングする 1 行で構成されている。
*
オープンしたプロセスが親のユーザー名前空間で CAP_SETUID (CAP_SETGID) ケーパビリティを持っている。 したがって、 特権プロセスは親のユーザー名前空間の任意のユーザー ID (グループ ID) に対するマッピングを作成できる。
上記のルールを満たさない書き込みはエラー EPERM で失敗する。

マッピングされていないユーザー ID とグループ ID

マッピングされていないユーザー 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_tsi_uid フィールドで返される ID 情報 ( sigaction(2) 参照)、 プロセスアカウンティングファイルに書き込まれる ID 情報 ( acct(5) 参照)、 POSIX メッセージキュー通知で返される ID 情報 ( mq_notify(3) 参照) がある。
 
マッピングされていないユーザー ID やグループ ID が対応するオーバーフロー ID 値に変換され「ない」重要な場合が一つある。 2 番目のフィールドにマッピングがない uid_mapgid_map ファイルを参照した際、 そのフィールドは 4294967295 (unsigned integer では -1) が表示される。

set-user-ID や set-group-ID されたプログラム

ユーザー名前空間内のプロセスが 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 独自の機能である。

注意

長年にわたり、Linux カーネルには特権ユーザーに対してだけ利用できる機能が多く追加されて来た。 これは set-user-ID-root アプリケーションを混乱させる潜在的な可能性を考慮してである。 一般的には、 ユーザー名前空間の root ユーザーにだけこれらの機能の使用を許可するのが安全である。 なぜなら、ユーザー名前空間の中にいる間は、 ユーザー名前空間の root ユーザーが持っている以上の特権を得ることはできないからである。

可用性

ユーザー名前空間を使用するには、 CONFIG_USER_NS オプションが有効になったカーネルが必要である。 ユーザー名前空間をカーネルの様々なサブシステムのサポートを必要とする。 サポートされていないサブシステムがカーネルに組み込まれている場合、 ユーザー名前空間のサポートを有効にすることはできない。
 
Linux 3.8 時点では、 ほとんどの関連するサブシステムはユーザー名前空間に対応しているが、 多くのファイルシステムにユーザー名前空間間でユーザー ID やグループ ID のマッピングを行うのに必要な基盤がなかった。 Linux 3.9 では、 残りの未サポートのファイルシステムの多くで必要な基盤のサポートが追加された (Plan 9 (9P), Andrew File System (AFS), Ceph, CIFS, CODA, NFS, OCFS2)。 Linux 3.11 では、最後の主要な未サポートのファイルシステムであった XFS のサポートが追加された。

以下のプログラムは、ユーザー名前空間で実験を行えるように設計されている。 他の種類の名前空間も扱える。 このプログラムはコマンドライン引き数で指定された名前空間を作成し、作成した名前空間内でコマンドを実行する。 コメントとプログラム内の usage() 関数に、プログラムの詳しい説明が書かれている。 以下のシェルセッションに実行例を示す。
 
まず最初に、実行環境を確認しておく。
 

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

関連項目

newgidmap(1), newuidmap(1), clone(2), setns(2), unshare(2), proc(5), subgid(5), subuid(5), credentials(7), capabilities(7), namespaces(7), pid_namespaces(7)
 
カーネルのソースファイル Documentation/namespaces/resource-control.txt

この文書について

この man ページは Linux man-pages プロジェクトのリリース 3.79 の一部 である。プロジェクトの説明とバグ報告に関する情報は http://www.kernel.org/doc/man-pages/ に書かれている。