Linuxカーネル4.1の名前空間(ドラフト)

はじめに

前回のLinuxカーネル4.1のSLUBアローケータ(ドラフト) - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモと同じくドラフト版公開です。

カーネルのバージョンは4.1系です。

文書自体も完成版ではないし、markdownから手作業ではてなblogにコピペして修正してるので章立てとか変になってるところとかあるかもしれませんが気にしないでください。 一部は文書修正してます。

名前空間

名前空間とは、Linuxカーネル内のグローバルなリソースを管理する機構です。名前空間によって管理されるリソースはメモリやCPUなどの物理的なリソースではなく、プロセスID、ユーザID、ファイルシステムのマウントポイントなどのデータです。CPUやメモリなど、ハードウェアよりの制限は「XXX章、cgroups」にて解説します。

Linuxにおけるコンテナ型仮想化技術では名前空間を利用し、ホスト・ゲスト間やゲスト間での環境の分離を行っています。これによりコンテナの独立性を確保しています。名前空間の機能はコンテナ型仮想化で使われることが主ですので、以下の説明でもコンテナ型仮想化を行う前提として、親プロセスから名前空間を分離した子プロセスをコンテナと呼びます。

名前空間の利点

名前空間を使用して環境を分離することで、ある名前空間に所属するプロセスの処理内容が名前空間が違う別のプロセスに対して影響を及ぼさないようにすることができます。これによりプロセス間の独立性を高めたり、 セキュリティの向上に役立てることができます。

例えば、「図_PID名前空間の分離例」のようにPID名前空間AとBがあるときに間違ってPID名前空間Bにて kill -kill 4 を実行したとしても、PID名前空間Bには該当するプロセスは存在しないのでコマンドからエラーが変えるだけでPID名前空間A、Bともにプロセスに対してなんの影響も発生しませせん。また、PID名前空間Bにて kill -kill 2 を実行した場合は、PID名前空間BのPID2のプロセスが終了するだけで、PID名前空間AのPID2には影響はありません。名前空間を分けることでこのようにプロセス間の独立性を高めることができます。

f:id:masami256:20190510232224p:plain

図_PID名前空間の分離例

また、User名前空間の機能を利用して、User名前空間Aでのuid 0をホスト上のuid 1000とマッピングすることで、User名前空間Aの中ではrootとして処理を行いつつも、ホスト上では一般ユーザとして処理を行うため、システムに対してクリティカルな変更を名前空間内では行えないようにすることができます。この機能はプロセスを実行する上でroot権限が必要な場合で、ホストの環境には影響を及ぼさないような処理を実行する場合、例えば、rpmなどのパッケージ作成、ホストから分離したMount名前空間内でのパッケージインストールなどに有用です。その他に、万が一、名前空間内のソフトウェアの脆弱性により任意のシェルが実行されたとしても、ホストからは一般ユーザの権限でしかコマンドを実行できないので、システム全体の制御は奪われにくくなります。

名前空間の種類

Linuxでの名前空間は執筆時点で6つのリソースを管理しています(表_名前空間一覧)。名前空間の実装は、Mount名前空間が最初に実装され、その後も継続的に開発が進み、Linux 3.8でLinux 4.0でも使われている機能が揃いました。

名前空間 使用可能になったバージョン
Mount名前空間 Linux 2.4.19
IPC名前空間 Linux 2.6.19
UTS名前空間 Linux 2.6.19
Net名前空間 Linux 2.6.29
PID名前空間 Linux 2.6.24
User名前空間 Linux 3.8

表_名前空間一覧

Mount名前空間

Mount名前空間ファイルシステムのマウントポイントを管理します。Mount名前空間を分離することで同じストレージ上のファイルシステムであってもプロセス間で別のファイルシステム階層として扱うことができます。これにより、プロセスAがファイルシステムに対して行った変更がプロセスBには影響しないという使用方法が可能になります。chroot(2)ではあるディレクトリをルートファイルシステムとして設定しますが、Mount名前空間によるマウントポイントの管理はchroot(2)とは違い、システムのルートファイルシステムそのものが管理対象となります。

IPC名前空間

IPC名前空間はSystem V IPC オブジェクト、POSIX メッセージキューを管理します。これらのIPCリソースは同一の名前空間にあるリソースに対して通信を行うことができますが、別の名前空間にあるリソースとは通信できません。

UTS名前空間

UTS名前空間はホスト名とNIS ドメイン名を管理します。カーネルバージョンやOS名などは変更できません。

Net名前空間

Net名前空間はネットワークに関するリソースを管理します。この名前空間で管理されるリソースはネットワークデバイスIPv4IPv6プロトコルスタックなどです。NICは1つの名前空間にのみ所属させることができます。そのため、一つのNICを複数の名前空間から使用する場合は仮想ネットワークデバイス(veth)にて別の名前空間へのネットワークブリッジを作成し、このブリッジを経由する必要があります。

PID名前空間

PID名前空間はPIDの管理を行います。この機能を使うことでコンテナ内のプロセス番号はホスト側のプロセス番号と独立することができます。PID名前空間を分離してプロセスを作成した場合でも、分離元となったPID名前空間からはそのPID名前空間の番号体系でプロセスを識別できます。「図_PID名前空間例」はPID名前空間Aを大元として、PID名前空間BとPID名前空間Cが存在する状態です。 ここでは、PID名前空間Bのpid 10はPID名前空間Aではpid 1000となっています。さらにPID名前空間Bのpid 10はPID名前空間Dのpid 1でもあります。PID名前空間Cのpid 10はPID名前空間Aではpid 2010です。例のようにPID名前空間が違えば同じpid番号が存在します。ただし、分離元のPID名前空間上ではユニークなPIDが割り当てられるため、同一名前空間内でのPID番号の重複は発生しません。

f:id:masami256:20190510232310p:plain

図_PID名前空間

カーネル起動時に設定され最初のPID名前空間以外でreboot(2)が実行された場合、カーネルの再起動は行わず、PID名前空間内でのinitプロセスに対してシグナルを送信します。送信するシグナルはreboot(2)のcmd引数の値によって「表_rebootのcmdとシグナル」のように変化します。

cmd 送信するシグナル
LINUX_REBOOT_CMD_RESTART SIGHUP
LINUX_REBOOT_CMD_RESTART2 SIGHUP
LINUX_REBOOT_CMD_POWER_OFF SIGINT
LINUX_REBOOT_CMD_HALT SIGINT

表_rebootのcmdとシグナル

User名前空間

User名前空間はセキュリティに関するリソースを管理します。ここで管理するものとしてはUID、GIDなどがあります。User名前空間の機能でホスト側のuid・gidとコンテナゲスト内で使用するuid・gidマッピングを行うことがきます。コンテナ内のuid 0をホストのuid 1000とマッピングすることで、コンテナ内での操作がホスト上ではuid 1000の権限で行うようにすることができます。これにより、コンテナゲスト内ではroot権限で操作を行うことができますが、ホスト側から見た場合にはroot権限ではないためホストの設定を変更するような操作が行えず、セキュリティの向上につなげることができます。

名前空間の管理

Linuxでは、名前空間は個別に存在していますが、User名前空間以外の名前空間はNSProxyによって管理されています。これらの名前空間を操作する場合はNSProxyを経由して処理を行います。User名前空間に関してはセキュリティ機構の一つであるcredentials機能が管理します。User名前空間は初期の実装ではNSProxyによって管理されていましたが、Linux 2.6.39よりstruct cred構造体にて管理するようになりました。NSProxyとCredentials機能はプロセスのtask_struct構造体より参照できます。図「図_task_struct構造体と名前空間の関連」にtask_struct構造体と各名前空間の関連を示します。User名前空間以外はNSProxyの管理下に置かれます。

f:id:masami256:20190510232513p:plain

図_task_struct構造体と名前空間の関連

図「図_task_struct構造体と名前空間の関連」に示したように、User名前空間以外の名前空間はUser名前空間を参照します。PID、IPC、MNT、PID、UTS名前空間はそれぞれ独立して存在できますが、User名前空間はセキュリティに関する名前空間のため、他の名前空間からも参照する必要があるためです。各名前空間がUser名前空間を参照するのは、主にsetnsシステムコールによりプロセスの名前空間を別の名前空間に所属させるときです。この場合、プロセスに設定されているケーパビリティと所属対象の名前空間に設定されているケーパビリティにCAP_SYS_ADMINが設定されているかをチェックします。

プロセスと名前空間

図_プロセスと名前空間はUTS名前空間を例にプロセスと名前空間がどのように関連しているかを示しています。例ではホスト名がfooで設定されているpid1001とpid1002、ホスト名がbarで設定されているpid1003があります。 これらはpid1000を親プロセスとしています。この図ではpid1001とpid1001が同じ名前空間に所属し、pid1003が別の名前空間に所属しています。簡単にするためにここではpid1000の名前空間に関しては省略しています。

f:id:masami256:20190510232337p:plain

図_プロセスと名前空間

名前空間のエクスポート

名前空間は擬似ファイル表現され、procファイルシステムにエクスポートされます。これらのファイルは/proc//nsに存在します。ファイルはシンボリックリンクとなっていて、リンク先のinode番号によってどの名前空間に所属しているかが確認できます。図_procファイルシステムの場合、pid 1とpid 2は同一の名前空間に所属しています。そのため、リンク先のinode番号は同一の番号を指しています。

# ls -la /proc/1/ns
total 0
dr-x--x--x 2 root root 0 May 27 22:16 ./
dr-xr-xr-x 8 root root 0 May 27 22:12 ../
lrwxrwxrwx 1 root root 0 May 27 22:17 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 May 27 22:17 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 May 27 22:17 net -> net:[4026531969]
lrwxrwxrwx 1 root root 0 May 27 22:17 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 May 27 22:17 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 May 27 22:17 uts -> uts:[4026531838]

# ls -la /proc/2/ns
total 0
dr-x--x--x 2 root root 0 May 27 22:17 ./
dr-xr-xr-x 8 root root 0 May 27 22:12 ../
lrwxrwxrwx 1 root root 0 May 27 22:17 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 May 27 22:17 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 May 27 22:17 net -> net:[4026531969]
lrwxrwxrwx 1 root root 0 May 27 22:17 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 May 27 22:17 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 May 27 22:17 uts -> uts:[4026531838]

図_procファイルシステム

ユーザ空間にエクスポートされた名前空間のファイルを操作するにはsetns(2)を使用します。これらのファイルに対して読み書きを行うことはありません。カーネル側ではこれらのファイルに対する操作の種類はproc_ns_operations構造体にて定義します(表_proc_ns_operations構造体)。

変数名 内容
name ファイル名
type 名前空間のCLONEフラグ
get 名前空間の参照カウントを増やす関数へのポインタ
put 名前空間の参照カウントを減らす関数へのポインタ
install 新しい名前空間を設定する関数へのポインタ

表_proc_ns_operations構造体

「表_proc_ns_operations構造体」で示したように、定義されている操作(関数)は3個ありますが、このうち、putとinstallが名前空間の操作に関する関数です。put関数はプロセスの終了などにより、その名前空間の参照カウンタを減らす際に使用します。この処理は名前空間毎に行うため、参照数を減らしたあとの処理、例えば、リソースの解放を行う、参照が0になった時点でリソースの解放を行うなどは名前空間によって異なります。get関数は名前空間への参照が増える場合に実行されます。get関数はput関数と違い、どの名前空間でも同じように参照カウンタを増やすだけで名前空間毎固有の処理はありません。

nsfs

nsfsは名前空間のファイルをprocファイルシステムにエクポートするための擬似ファイルシステムです。元来、名前空間のエクスポートは単純にprocファイルシステムを利用してエクポートしていましたが、カーネル3.19よりnsfsというファイルシステムとしてエクスポートするようになりました。この変更はカーネル内部での変更なのでABIに変化はありません。ファイルは3.19以前と同様に/proc以下にエクポートします。 nsfsはファイルシステムですがext4やbtrfsなどの通常のファイルシステムと違い、ユーザ空間からのマウント処理は行われません。ファイルシステムのマウントはカーネルの起動時に初期化のタイミングでstart_kernel関数よりnsfsのマウント処理を実施します。また、カーネルに対してファイルシステムの登録を行わないため、/proc/filesystemsファイルにもnsfsは現れません。 nsfsの主な機能は/proc//ns/uts等の名前空間のファイルにアクセス時に適切なdentryを返すことと、ファイルディスクリプタからfile構造体に取得することの2機能です。 dentryの取得はns_get_path関数が行います。対象のファイルに対してns_get_path関数が始めて呼ばれた時点では、ファイルに対してinodeは割当されていません。このため、初回アクセス時に限りinodeの割当を行い、その後にdentryを返します。ファイルディスクリプタからfile構造体への取得はproc_ns_get_path関数が行います。

名前空間共通データ

すべての名前空間で持つデータ構造にns_common構造体があります。この構造体は表「表_ns_common構造体」に示した変数を持ちます。stashedメンバ変数とinumメンバ変数はprocファイルシステムにエクスポートしたファイルのデータです。

変数名 内容
stashed dentry構造体のアドレス
ops /procにエクスポートしたファイルの操作する関数群
inum inode番号

表_ns_common構造体

「図_uts_namespaceからproc_ns_operationsの関連」にUTS名前空間を例に、ns_common構造体、proc_ns_operations構造体の関連を示します。

f:id:masami256:20190510232551p:plain

図_uts_namespaceからproc_ns_operationsの関連

ns_common構造体はinode構造体のi_private変数に設定されています。setns(2)による名前空間の移動時はsetns(2)の引数で渡されたファイルディスクリプタからinode構造体を取得し、その構造体からi_private変数にアクセスします。

NSProxy構造体

NSproxyは、User名前空間以外の名前空間を管理する機能です。User名前空間はcred構造体が管理しています。PID名前空間などの名前空間の分離を行わない場合は、task_struct構造体のnsproxy変数を親プロセスと共有し、分離を行う場合に新規作成します。個々の名前空間はNSProxy構造体のメンバ変数として表されます。

変数名 内容
count このnsproxyの参照カウンタ
uts_ns UTS名前空間の構造体
ipc_ns IPC名前空間の構造体
mnt_ns Mount名前空間の構造体
pid_ns_for_children PID名前空間の構造体
net_ns Net名前空間の構造体

表_nsproxy構造体のメンバ

NSProxyと名前空間

参照カウンタ

NSProxyと各名前空間はデータ構造として参照カウンタを持っており、この参照カウンタを使用し、参照数が0になったらリソースの解放を行うなどの処理を行います。NSProxyの参照カウンタはプロセスの生成時に名前空間の分離を行わなかった場合に増やされます。各名前空間の参照数を増やすパターンは2つあり、1つはプロセス生成時に名前空間を分離する場合に、分離しない名前空間に関して参照数を1増やします。2つめのパターンはsetnsシステムコールを実行した時で、所属する名前空間の参照数を増やします。

UTS、IPC、MNT、NET、PID名前空間は自身が所属するUser名前空間を参照します。これらの名前空間は、新規に名前空間を作成するときに参照しているUser名前空間の参照カウンタを1つ増やします。名前空間は参照カウンタにより名前空間が使用されているかチェックしているため、参照数が0になったら使用していたリソースを解放します。

NSProxyと名前空間の関連

プロセス生成を行う関数(fork、vfork、clone)を実行した場合、copy_process関数よりcopy_namespaces関数が呼ばれます。copy_namespaces関数はflasgを確認し、User名前空間を除く名前空間の分離が指定されていない場合(CLONE_NEWUTSなど、名前空間のフラグが設定されていない)、子プロセスは親プロセスのnsproxy構造体を参照します。この場合、count変数をインクリメントし参照数を増やします。 名前空間を分離する場合、まずNSProxy構造体の新規作成を行いcount変数を1に設定します。その後、各名前空間の分離、もしくは名前空間の参照数を増やします。名前空間とNSProxyの関係は、ある名前空間から見た場合、1:nの関係になります。NSProxyと各名前空間の関連の例を「図_NSProxyと名前空間」に示します。

f:id:masami256:20190510232831p:plain

図_NSProxyと名前空間

「図_NSProxyと名前空間」ではPID 1000とPID 2000はともにPID 1から派生したが、UTS名前空間の分離を行った状態を表しています。PID 1とPID 1000はNSProxyを共有しているため、参照カウントは2となります。このNSproxyが管理しているUTS名前空間はPID 1とPID 1000が参照しているため、countは2になります。PID名前空間はPID 1とPID 1000の他、PID 2000からも参照されているためcountは3となります。この図のように、プロセスがある名前空間を分離した時、分離を行わなかった名前空間に関しては既存の名前空間を参照し続けます。

名前空間へのアクセス

名前空間へのアクセスするには2種類のパターンがあります(図_名前空間へのアクセス)。1つはカーネル空間からアクセスする場合で、この場合はtask_struct構造体からアクセスできます。もう1つはユーザ空間からアクセスする場合で、これはsetns(2)を使用した場合になります。

f:id:masami256:20190510232848p:plain

図_名前空間へのアクセス

task_structから参照する場合、User名前空間へはcred構造体のreal_credメンバ変数からアクセスが可能です。その他の名前空間へはnsproxyメンバ変数よりアクセスが行えます。

    struct task_struct *p = current;
    p->real_cred-_>user_ns = new_user_ns;

図_cred構造体からアクセス

    struct task_struct *p = current;
    p->nsproxy->uts_ns = new_uts_ns;

図_nsproxyからの名前空間へアクセス

/procにエクスポートされたファイルからアクセスする場合、task_struct構造体から参照が行えません。このため、操作対象名前空間のファイルのinodeを取得し、ここからproc_ns_operations構造体を経由して対象の名前空間にアクセスを行います。UTS名前空間へアクセスする場合のフローを「図_UTS名前空間へのアクセス」に示します。

sys_open() ------------------------------------------+
  -> do_sys_open()                                   |
    -> do_filp_open()                                |- vfs層
      -> path_openat()                               |
        -> follow_link() ----------------------------+
          -> proc_ns_follow_link() ------------------- proc filesystem層
            -> ns_get_path() ------------------------- nsfs層
        -> utsns_get() ------------------------- uts namespace層

図_UTS名前空間へのアクセス

/procにエクスポートされたファイルから名前空間へアクセスする場合、「図_UTS名前空間へのアクセス」のようにvfs層からprocfs層、nsfs層を経由し、対象の名前空間へとアクセスします。proc_ns_follow_link関数までの処理で、ファイルのdentry構造体を取得し、proc_ns_follow_link関数でdentry構造体からinode構造体を取得。そして、inode構造体からPROC_I関数を使用してproc_ns_operations構造体を取得します。

デフォルトの名前空間

カーネルが作成するプロセス「init_task」が使用する名前空間がデフォルトの名前空間です。nsproxy構造体はinit_nsproxyという名前の変数で、カーネルコンパイル時に「表_init_nsproxy」に示した内容が設定されます。

変数名 内容
count 1
uts_ns init_uts_ns
ipc_ns init_ipc_ns
mnt_ns NULL
pid_ns_for_children init_pid_ns
net_ns init_net

表_init_nsproxy

Mount名前空間は実際にカーネルが起動するまではデータの設定を行えないのでNULLが設定されますが、その他の名前空間はデフォルトの設定が使用され、名前空間の分離をするまでは子プロセスへと引き継がれていきます。 init_nsproxyはデフォルトの名前空間となっています。特にPID名前空間の場合、全てのPID名前空間のルートがこのinit_nsproxyのPID名前空間となります。 User名前空間はinit_user_nsという名前の変数で定義されています。この変数はデフォルトのcred構造体のuser_ns変数に設定されます。このcred構造体もinit_taskで使用します。 Mount名前空間以外の各名前空間のデフォルト値はcファイルにて定義されています。

名前空間 ファイル
UTS init/version.c
IPC ipc/msgutil.c
PID kernel/pid.c
Net net/core/net_namespace.c
User kernel/user.c

名前空間の共有・分離・移動

通常プロセスは、作成時に親プロセスの名前空間を引き継ぎます。プロセスが親プロセスから名前空間を分離させるには、「表_名前空間を操作するシステムコール」に示したシステムコールを使用します。clone関数システムコール発行時にCLONE_NEWPIDなどの名前空間用フラグを用いて、プロセス生成時に分離させる、もしくはプロセス作成完了後にunshare関数システムコールを用いて親プロセスの名前空間から分離させる、またはsetns関数システムコールにより既存の名前空間に所属させることができます。cloneシステムコールとunshareシステムコールでは「表_名前空間のCLONEフラグ」に示したフラグを用いて分離させたい名前空間を指定します。setnsシステムコールでは、CLONEフラグの他、所属させる名前空間のファイルディスクリプタを引数として受け取ります。ここで渡すファイルディスクリプタは/proc/ns配下に存在する名前空間のファイルのファイルディスクリプタです。setnsシステムコール名前空間を 変更した場合、その効果はPID名前空間を除き、すぐにプロセスに反映されます。PID名前空間変更した場合は、名前空間変更後、そのプロセスがforkやcloneなどのシステムコールで作成した子プロセスから反映されます。

システムコール 機能
clone プロセスの生成時に名前空間を分離する
unshare プロセスの名前空間を分離する
setns プロセスの名前空間を他のプロセスの名前空間に所属させる

表_名前空間を操作するシステムコール

フラグ名 内容
CLONE_NEWIPC IPC名前空間の分離
CLONE_NEWNET NET名前空間の分離
CLONE_NEWNS MOUNT名前空間の分離
CLONE_NEWPID PID名前空間の分離
CLONE_NEWUSER USER名前空間の分離
CLONE_NEWUTS UTS名前空間の分離

表_名前空間のCLONEフラグ

名前空間の共有と複製
fork(2)

fork(2)による名前空間の共有の例を説明します。ここでは2つのプロセスPID1234とPID 1192があり、これらの親プロセスはPID784と同じですが、名前空間は別となっています。「図_fork後の名前空間」では、pid1326は親プロセスと名前空間を共有しているため、同じ名前空間に対して線が繋がっています。

f:id:masami256:20190510233013p:plain

図_fork後の名前空間

clone(2)

clone(2)システムコールはプロセスを作成するための機能です。プロセスの生成はfork(2)と同様の方法で行いますが、より細かい制御が行えます。プロセス生成時にflags引数に 表_名前空間のCLONEフラグ で示した CLONE で始まるフラグを設定することで子プロセスの設定が可能です。CLONEフラグのうち、名前空間に関するうフラグは表CLONEフラグで示すものがあり、フラグで指定された名前空間を親プロセスから分離します。

clone(2)ではflagsで指定した名前空間のみ新規に作成し、それ以外の名前空間は親プロセスと共有します。「図_clone後の名前空間」ではcone(2)にてNet名前空間のみを分離した時の状態です。Net名前空間は親プロセスと別の名前空間を使用しますが、それ以外は共有します。

f:id:masami256:20190510233037p:plain

図_clone後の名前空間

fork(2)・clone(2)時の処理

fork(2)やclone(2)はカーネル内部ではdo_fork関数にて共通化されています。そのため、fork(2)・clone(2)における名前空間 の処理は同じパスを通ります。プロセス生成時のコールフローを図_プロセス生成時のコールフローに示します。 プロセス生成時の名前空間に関する処理はdo_fork関数から呼ばれるcopy_process関数にてユーザ名前空間に対するの処理と、NSProxyに対する処理を行います。

do_fork()
  -> copy_process()                     (1)
    -> copy_creds()                     (2)
      -> prepare_creds()                (3)
      -> create_user_ns()               (4)
        -> set_cred_user_ns()           (5)
    -> copy_namespaces()                (6)
      -> create_new_namespaces()        (7)
        -> create_nsproxy()             (8)
        -> copy_mnt_ns()                (9)
        -> copy_uts_ns()                (10)
        -> copy_ipcs()                  (11)
        -> copy_pid_ns()                (12)
        -> copy_net_ns()                (13)

図_プロセス生成時のコールフロー

(1)のcopy_process関数の最初の処理で親プロセスのtask_struct構造体がこれから作成するプロセスのtask_struct構造体にコピーします。よって、この時点では親プロセス・子プロセスで名前空間を共有した状態になっています。(2)から(5)でUser名前空間に対する処理を行います。CLONE_NEWUSERフラグが設定されていた場合はUser名前空間を新規に作成します。(6)以降はNSProxyとその管理下にある名前空間の参照数の増加または新規作成処理となります。まず(6)のcopy_namespaces関数でフラグを確認します。、名前空間に関するCLONEフラグが設定されていいない場合はNSProxyの参照カウントを増加して関数を終了します。fork(2)の場合は名前空間は親プロセスと共有するためここで名前空間に関する処理は終了となります。clone(2)で名前空間に関するフラグが1つでも設定されていた場合は(7)以降の処理に進みます。まず、名前空間(NSProxy)を親プロセスと共有しないため、(8)のcreate_nsproxy関数でNSProxy構造体のインスタンスを初期化します。 その後、各名前空間のコピーを行う関数*1を順次呼び出していきます。これらの関数の詳細説明は本説では行いません。各名前空間で共通するのはCLONEフラグが設定されていなければ、自身の参照カウントをインクリメントし、CLONEフラグが設定されている場合は新規に名前空間を設定するという処理となります。名前空間の最後の処理は作成中のプロセスのnsproxy構造体の変数を新たに作成したnsproxyに置き換えを行います。これにて作成しているプロセス/スレッドの名前空間が新しい名前空間に切り替わります。

unshare(2)

unshare(2)は名前空間を現在の名前空間から分離し、新規に名前空間を作成します。分離対象の名前空間の指定はclone(2)と同じくCLONEフラグを使用します。「図unshare後の名前空間」ではunshare(2)により、PID1326のNet名前空間を親プロセスから分離して、新規に名前空間を作成した状態です。図_unshare後の名前空間

名前空間の分離処理

名前空間の分離はunshare(2)で行いますが、このシステムコール名前空間以外も操作します。本説では名前空間に関する部分のみ説明します。名前空間分離時のコールフローを 図_名前空間分離のコルーフローに示します。名前空間を分離する場合の主な処理はclone(2)と同じです。大きく違うのは呼び出し元と、名前空間(task_struct構造体のnsproxy変数)を切り替える処理が必要になる点です。

sys_unshare()                            (1)
  -> check_unshare_flags()               (2)
  -> unshare_userns()                    (3)
    -> prepare_creds()                   (4)
    -> create_user_ns()                  (5)
  -> unshare_nsproxy_namespaces()        (6)
    -> create_new_namespaces()           (7)
  -> exit_shm()                          (8)
  -> shm_init_task()                     (9)
  -> switch_task_namespaces()            (10)
    -> free_nsproxy()                    (11)

図_名前空間分離のコルーフロー

(1)のsys_unshare関数は関数を準備呼び出していきます。(2)のcheck_unshare_flags関数はflags引数が妥当かをチェックします。(3)のunshare_userns関数ではCLONE_NEWUSERフラグが設定されているかを確認した後は、prepare_creds関数とcreate_user_ns関数の呼び出しを行います。この処理の流れはfork(2)/clone(2)時のcopy_creds関数と同様です。create_user_ns関数が成功した場合は、関数の引数で渡されたnew_cred変数に作成したcred構造体を設定します。 (6)のunshare_nsproxy_namespaces関数からNSProxy管理下の名前空間の処理になります。最初にflags引数のチェックで分離が必要か確認します。分離を行う場合は分離操作を行う権限があるかを確認し、権限がない場合はエラーを返します。権限に問題がなければ(7)のcreate_new_namespaces関数を呼び出し、作成したNSProxy構造体の変数を引数で渡されたnew_nsp変数に設定します。この時点ではNSProxyの切り替えは行われません。 unshare_nsproxy_namespaces関数が終了したらsys_unshare関数に戻ります。IPC名前空間を分離していた場合、既存のセマフォオブジェクトを(8)のexit_shm関数で解放し、(9)のshm_init_task関数にて再度初期化します。 NSProxy管理下の名前空間を分離した場合は(10)のswitch_task_namespaces関数でプロセスに設定されているNSProxyを作成したNSProxyに差し替えます。 switch_task_namespaces関数はtask_structのロックを取得し、nsproxyを差し替えます。次に元のnsproxyの参照数を減らし、もし他に使用者がいなければnsproyとそれに紐づく名前空間のリソースを(11)のfree_nsproy関数にて解放します。

名前空間の移動
setns(2)

setns(2)は名前空間を既存の名前空間から他の名前空間に移動します。「図_setns後の名前空間」ではsetns(2)により、PID1326のNet名前空間をPID1234の名前空間に変更した状態です。図_setns後の名前空間

名前空間の移動処理
sys_setns()                            (1)
  -> proc_fs_get()                     (2)
  -> get_proc_fs()                     (3)
  -> create_new_namespaces()           (4)
  -> install()                         (5)
  -> switch_task_namespaces()            (6)

図_名前空間移動のコールフロー

名前空間の移動は(1)のsetns(2)により移動先の名前空間のファイルディスクリプタ、移動する名前空間の種類を指定して実行します。まず(2)〜(3)にてsetns(2)の引数で渡されたファイルディスクリプタより、移動先名前空間のns_common構造体を取得します。(4)のcreate_new_namespaces関数で新しくnsproxy構造体を設定します。この時に、setns関数ではcreate_new_namespace関数のflag引数に0を渡して呼び出します。これにより、nsproxy構造体を作成し、ser名前空間を除く全ての名前空間の参照数をインクリメントします。User名前空間はnsproxy構造体の管理対象では無いため、移動する名前空間がUser名前空間の場合はまだ何も設定されていない状態です。次の(5)のns_common構造体のops変数に設定されたinstall関数を実行し、名前空間の移動処理を行います。名前空間移動時に行う処理は各名前空間で違うため、ここでは共通して行われる部分のみ説明します。名前空間の移動では、移動元・移動先の名前空間において、名前空間の移動を行うケーパビリティ(CAP_SYS_ADMIN)があるかのチェックを行い、権限がなければエラーを返します。名前空間を移動するときは、現在の名前空間から抜けるため、移動元の名前空間の参照数を減らす必要があります。これとは逆に、移動先名前空間の参照数を増やす必要があります。この権限チェックと参照カウンタの設定は全ての名前空間において、名前空間の移動時に行われる処理となります。install関数により名前空間の移動処理が完了したら、(6)のswitch_task_namespaces関数でプロセスのnsproxy構造体を入れ替え、名前空間の移動処理を完了します。

Mount名前空間

Mount名前空間ではシステムにマウントするファイルシステムをコンテナ間で分離することができます。コンテナの作成時にMount名前空間を分離した場合、ホスト側でUSBメモリスティックを/mnt配下にマウントしてもコンテナ側の/mntには影響はありません。

Mount名前空間の実装

Mount名前空間はmnt_namespace構造体にて管理されています。

変数名 説明
count この名前空間の参照カウンタ
ns ns_common構造体
root この名前空間におけるルートファイルシステム
list mount構造体のmnt_list変数に繋がるMount名前空間のリスト
user_ns この名前空間が所属するユーザ名前空間
seq マウントがループしないようにするためのシーケンス番号
event マウント/アンマウントのイベント発生回数を記録

表_mnt_namespace構造体

Mount名前空間の初期化

Mount名前空間コンパイル時点ではマウントするファイルシステムの情報が無いため、カーネルのブート時に初期化を行います。初期化の流れは図_Mount名前空間の初期化フローの流れで実行します。名前空間の初期化に関係するのはinit_mount_tree関数とcreate_mnt_ns関数の2関数です。

-> start_kernel()               (1)
  -> vfs_caches_init()          (2)
    -> mnt_init()               (3)
      -> init_mount_tree()      (4)
        -> create_mnt_ns()      (5)

図_Mount名前空間の初期化フロー

create_mnt_ns関数はmnt_namespace構造体のメモリ確保、Mount名前空間のルートファイルシステムを設定を行います。 init_mount_tree関数はcreate_mnt_ns関数を呼び、Mount名前空間インスタンスを生成を行い、init_taskのMount名前空間に作成したmnt_namespace構造体を設定します。そして、参照数を1つ増やしてMount名前空間の初期化処理が完了します。

Mount名前空間の分離

Mount名前空間の分離はcopy_mnt_ns関数により行われます。

create_new_namespaces()        (1)
  -> copy_mnt_ns()             (2)
    -> alloc_mnt_ns()          (3)
    -> copy_tree()             (4
      -> clone_mnt()           (5)

図_Mount名前空間の分離フロー

copy_mnt_ns関数はまずalloc_mnt_ns関数にて新しいmnt_namespace 構造体のインスタンスを生成します。alloc_mnt_ns関数は/proc//ns/mntファイルに使用するinodeの取得、ファイル操作時のオペレーション構造体の設定、参照数の設定などを行います。インスタンスの初期化が終わったらファイルシステムをコピーするためのフラグを設定します。この時、プロセスに設定されているUser名前空間とプロセスが使用しているMount名前空間のUser名前空間が違う場合はCL_SHARED_TO_SLAVEとCL_UNPRIVILEGEDが追加されます。copy_flagsに設定しているフラグの内容を「表_CLフラグ」にて説明します。

フラグ 説明
CL_COPY_UNBINDABLE コピーするマウントポイントにMNT_UNBINDLEが設定されていた場合に、コピーを行わない
CL_EXPIRE マウントポイントの期限切れを管理するリストに登録する
CL_SHARED_TO_SLAVE マウントポイントを複製する時に制限をかける(共有サブツリー機能)
CL_UNPRIVILEGED MS_NOSUIDやMS_RDONLYなどのフラグの設定変更を許可しない

表_CLフラグ

マウントしポイントのコピーは2段階で行われます。最初のステップではclone_mnt関数とcopy_tree関数にて行います。copy_tree関数のインターフェースを表_copy_treeの引数に示します。

引数名 説明
old 現在のMount名前空間に設定されているルートファイルシステム
root oldのディレクトリエントリ
flags 先の手順で設定したcopy_flags

表_copy_treeの引数

copy_tree関数は最初にclone_mnt関数を呼び、mount構造体のインスタンスを作成と引数のflagsの値に応じてmount構造体を設定します。ここではルートファイルシステムを設定します。その後、マウントポイントを走査して、コピーすべきマウントポイントがあればclone_mnt関数を呼びmount構造体を作成し、最初に作成したmount構造体のインスタンスのリスト(mnt_list変数)につなぎます。これを全てのマウントポイントに対して行っていきます。マウントポイントをコピーしない条件はcooy_mnt_ns関数で設定したflagsにCL_COPY_UNBINDABLEが設定されいている場合と、マウントポイントにMNT_UNBINDABLEが設定されている場合です。copy_tree関数にてファイルシステムツリーのコピーが完了したらcopy_mnt_ns関数に戻り、2段階目の処理に入ります。この処理ではcopy_tree関数で作成したmount構造体に対して所属する名前空間の設定を行います。また、マウントポイントの参照数のインクリメントも行います。

IPC名前空間

IPC名前空間はInter Process Communication(プロセス間通信)のうち、System V IPC オブジェクトと、POSIX メッセージキューを分離します。これらの仕組みを使ってプロセス間通信を行う場合、プロセスは同一のIPC名前空間に所属している必要があります。IPC名前区間が管理する機能はセマフォ、System V メッセージキュー、共有メモリ、POSIXメッセージキューです。

IPC名前空間の実装

IPC名前空間はipc_namespace構造体にて管理されています。

変数名 説明
count この名前空間の参照カウンタ
ids セマフォを管理する配列
sem_ctls SEMMSL,SEMMNS,EMOPM,SEMMNIを表す配列
used_sems 作成済みセマフォ
msg_ctlmax SystemVメッセージの最大サイズ
msg_ctlmnb SystemVメッセージキューが保持できるメッセージの最大値
msg_bytes SystemVメッセージキューのサイズ
msg_hdrs SystemVメッセージキュー数
shm_tot 確保された共有メモリ数
shm_ctlmni 共有メモリセグメントの最小サイズ
shm_rmid_forced 1を設定した場合、使用者がなくなれば全てのSystemV共有メモリセグメントに破棄済みマークを設定する
ipcns_nb notifier chain
mq_mnt mqueuefsのマウントデータ
mq_queues_count POSIXメッセージキュー数
mq_queues_max POSIXメッセージキューの最大値
mq_msg_max 1つのキューに入れることができるメッセージの最大数
mq_msgsize_max メッセージの最大サイズ
mq_msg_default mq_open関数呼び出し時にattrをNULLに指定した場合のmq_maxmsgのデフォルト値
mq_msgsize_default mq_open関数呼び出し時にattrをNULLに指定した場合のmq_msgsizeのデフォルト値
user_ns IPC名前空間が所属するユーザ名前空間
ns ns_common構造体

表_ipc_namespace構造体

ipc_ids構造体(表_ipc_ids構造体)はセマフォを管理するに使用します。

変数名 説明
in_use 割り当てたIPC識別子数
seq IPCオブジェクト生成のシーケンス番号
rwsem IPC名前空間を操作するときに使用するセマフォ
ipcs_idr IPCオブジェクトのIDを管理
next_id IPDオブジェクト作成時に設定するID

表_ipc_ids構造体

IPC名前空間の初期化

PID1に設定されるIPC名前空間はinit_ipc_nsで、コンパイル時に図_init_ipc_nsのように設定が行われます。

struct ipc_namespace init_ipc_ns = {
        .count          = ATOMIC_INIT(1),
        .user_ns = &init_user_ns,
         .ns.inum = PROC_IPC_INIT_INO,
#ifdef CONFIG_IPC_NS
        .ns.ops = &ipcns_operations,
#endif
};

図_init_ipc_ns

ただし、この段階ではIPC名前空間に関する部分の初期化だけで、メッセージキューなどの初期化は行われません。これらの初期化はLinuxカーネルの起動時により詳細な設定が行われます。IPCのリソースの初期化はipc_init関数にて行います。

ipc_init()              (1)
 -> sem_init()          (2)
 -> msg_init()          (3)
 -> shm_init()          (4)

セマフォsem_init関数にて初期化を行います。まずipc_namespace構造体のsc_semctls変数、セマフォの使用数、セマフォが使用するipc_ids構造体の初期化を行います。その次に/proc/sysvipc/semファイルを作成します。メッセージキューはmsg_init関数にて初期化します。この関数もセマフォと同様に変数の初期化と/procファイルシステムにファイル(/proc/sysvipc/msg)を作成を行います。共有メモリの初期化はshm_init関数にて行いますが、メッセージキューやセマフォと違い、shm_init関数では/proc/sysvipc/shmファイルの作成のみを行います。変数の初期化は別途ipc_ns_init関数にて行います。

UTS名前空間

UTS名前空間ではunameシステムコールが返すデータのうち、nodenameを名前空間毎に設定することができます。カーネルのバージョンやCPUアーキテクチャなど、カーネルが必要とする項目は扱えません。名前空間の分離を行うときは既存の名前空間のデータを引き継ぎます。

UTS名前空間の実装

UTS名前空間のデータ構造は比較的シンプルで名前空間に必要な最低限の構造となっています。

変数名 説明
kref 参照カウンタ
name ホスト名、バージョンなどを管理
user_ns 所属するユーザ名前空間
ns ns_common構造体

表_uts_namespace構造体

UTS名前空間の作成

clone(2)やunshre(2)などではUTS名前空間を新規作成しますが、UTS名前空間名前空間固有の処理はありません。そのため、copy_utsname関数では/proc//ns/utsファイルのinode取得や参照カウンタの初期化といった全ての名前空間で共通な処理を行い、最後にカレントスレッドに設定されているuts_namespace構造体の参照数を1つ減らします。カレントスレッドのuts_namespace構造体と新しいuts_namespace構造体の入れ替えはcreate_new_namespaces関数が行います。

UTS名前空間の移動

UTS名前空間のインストール関数は現在の名前空間と移動先の名前空間でCAP_SYS_ADMIN権限を持っているかチェックします。次に移動先の名前空間の参照カウンタをインクリメントし、移動前の名前空間の参照カウンタをデクリメントします。そして、nsproxyのUTS名前空間を差し替えて終了します。NSProxyが管理する名前空間は概ね同様の処理を行います。

Net名前空間

Net名前空間ではネットワーク関する設定、例えば、IPv4/IPv6プロトコルスタック、ルーティングの他にsocketが使用するポート番号、仮想ネットワークデバイス機能(veth)などがあります。名前空間を分離するときは既存の設定を引き継がず、新規にデータが作成されます。そのため、名前空間の分離が完了した時点ではloを含めてネットワークデバイスは一切存在しませんので、必要なデバイスをipコマンドなどで設定する必要があります。

Net名前空間の実装

net構造体がNet名前空間を表現します。この構造体に名前空間を管理するためのデータやプロトコルなどのネットワーク機能のデータが含まれます。

変数名 説明
count このNet名前空間の参照数
list 作成したNet名前空間を保持するリスト
user_ns Net名前空間に設定するユーザ名前空間
netns_ids Net名前空間のID
ns ns_common構造体
proc_net /proc/netディレクトリのデータ
proc_net_stat /proc/net/statディレクトリのデータ
gen 汎用のデータを保存するための構造体

表_net構造体

また、機能ごとに任意のデータを設定するための汎用データ構造体としてnet_generic構造体があります。net_genric構造体の内容を表_net_generic構造体に示します。この構造体を配列として扱いますが、実際に配列となるのは変数ptrです。ptrはサイズが1の配列として宣言されていますが、メモリを確保するときにnet_generic構造体のサイズ+(sizeof(void *) * データ数)のようにメモリを確保し、6番目のデータにアクセスする際はptr[5]のようにアクセスします。net_generic構造体はその名前が示す通り汎用の構造体です。net構造体を使用するモジュールがプライベートなデータを設定して使用することができます。名前空間の作成時に要素13の配列としてメモリ確保しますが、足りなくなった場合は新たにメモリを確保し、既存のデータをコピー。そしてnet->genのポインタを新規にメモリ確保した変数に置き換えます。

変数名 説明
len 配列の長さ
rcu rcuによるロックの取得に使用
ptr 実際のデータを格納する配列

net_generic構造体

Net名前空間の管理

Net名前空間を作成した場合、作成した名前空間のデータはnet_namespace_listというリストに登録します。このリストは次節で説明するコンストラクタやデストラクタを実行する際など、全てのNet名前空間に対して処理を行う場合に使用します。net_namespace_listへの登録にはnet構造体のメンバ変数listを使用します。

ネットワーク機能のコンストラクタ・デストラク

ネットワークデバイスプロトコル、ネットワーク機能のモジュールはNet名前空間の初期化・終了時に呼ばれるコンストラクタ・デストラクタを設定することができます。設定にはpernet_operations構造体を使用します。pernet_operations構造体の内容を表_pernet_operationsに示します。

変数名 説明
list linked list
init コンストラク
exit デストラク
net_exit_list デストラク
id id
size size

表_pernet_operations

この構造体うち、idやsizeなどは設定しなくても問題ありません。例えば、IPv4TCPではinitとexitのみが使用されます。この構造体は各モジュールよりregister_pernet_device関数もしくはregister_pernet_subsys関数により登録を行います。いずれの場合もpernet_listというリンクリストに登録します。登録時にはリンクリストへの登録と、init関数が設定されている場合は、作成されている全てのNet名前空間に対してinit関数を実行します。

グローバルなNet名前空間の設定

グローバルな名前空間(init_nsproxy)に設定するNet名前空間のデータ(init_net)は、コンパイル時には双方向リンクリストの初期化しか行われません。そのため、各種の初期化はカーネルの起動時にnet_init_init関数にて行われます。この初期化処理ではNet名前空間の終了時に使用するデータクリーンアップ用のリストの初期化、net構造体のメモリ確保時に使用するSLABキャッシュの作成、net_genric構造体の初期化などを行います。また、Net名前空間の初期化と終了時に呼ばれるコンストラクタ・デストラクタを登録します。コンストラクタではNet名前空間を操作するためのproc_ns_operations構造体の設定と、/proc/<pid>/ns/netに割り当てるinodeの設定を行います。デストラクタではコンストラクタで設定したinodeを解放します。

Net名前空間の作成と分離

Net名前空間の作成はcopy_net_ns関数にて行います。作成処理では最初にnet構造体とnet->gen変数のメモリ確保を行います。net->genはnet_genric構造体です。参照カウンタの設定やユーザ名前空間の設定を行ったら、現在作成中のNet名前空間に対してpernet_listに設定されているモジュールのinit関数を実行します。最後に作成したNet名前空間をnet_namespace_listに登録します。 Net名前空間を分離する場合は、新たにNet名前空間を作成するため、処理としては作成時と同様になります。

Net名前空間の移動

Net名前空間の移動ではNet名前空間独特の処理はありません。現在のスレッドのnsproxyに設定されているNet名前空間の参照を減らし、移動先のNet名前空間のnet構造体をnsproxy構造体のnet_nsに設定します。

PID名前空間

PIDは名前空間はPIDを管理します。プロセスIDの管理を分離し、独立させることでコンテナを別のサーバに移行しても、コンテナ内で動作していたプロセスのPIDには影響が置きません。もし、PID名前空間を使用しない場合、コンテナ内のPID 23414が別のサーバに移行した時に同じPIDが使用されていたら別のPIDを振り直す必要があります。しかし、LinuxにはPIDを変更する機能はありませんので、プロセスを終了し、再度起動させなくてはなりません。PID名前空間を分離させることでこのような問題を解決することができます。しかし、プロセスの情報は/procにエクスポートされるため、PID名前空間を分離する場合、/procも適切に分離を行わないと正しく動作できません。PID名前空間の分離は、分離前のPID名前空間を親として階層構造を作成します。この階層構造はinit_taskのPID名前空間を起点として32段階までと制限されています。

PID名前空間の実装

PID名前空間はpid_namespace構造体にて管理されています。

変数名 説明
kref リファレンスカウンタ
pidmap PIDを管理するビットマップ
rcu pid構造体のロック
last_pid 最後に使用したPID
nr_hashed PID構造体を管理するハッシュテーブルのデータ数
child_reaper ゾンビプロセスを回収するためのプロセス。init_taskを設定する
pid_cachep pid構造体のスラブキャッシュ
level PID名前空間の階層
parent 親のPID名前空間
proc_mnt procファイルシステム
proc_self procファイルシステムのselfファイル
bacct BSDプロセスアカウンティング情報
user_ns この名前空間が所属するUser名前空間
pid_gid procファイルシステムにアクセスする際に利用されるgid
hide_pid procファイルシステムのマウントオプションのhidepid
reboot PID名前空間をリブートするときのexitコード
ns ns_common構造体

表_pid_namespace構造体

PID1に設定されるPID名前空間は図_init_pid_nsのようになります。init_pid_nsはPID1に設定されるので、以降のプロセスはこのPID名前空間を共有、もしくはこのPID名前空間を親とした階層構造上にあるPID名前空間に所属します。

struct pid_namespace init_pid_ns = {
        .kref = {
                .refcount       = ATOMIC_INIT(2),
        },
        .pidmap = {
                [ 0 ... PIDMAP_ENTRIES-1] = { ATOMIC_INIT(BITS_PER_PAGE), NULL }
        },
        .last_pid = 0,
        .nr_hashed = PIDNS_HASH_ADDING,
        .level = 0,
        .child_reaper = &init_task,
        .user_ns = &init_user_ns,
        .ns.inum = PROC_PID_INIT_INO,
#ifdef CONFIG_PID_NS
        .ns.ops = &pidns_operations,
#endif
};

図_init_pid_ns

PID名前空間の作成

cloneシステムコールやunshareシステムコールでCLONE_NEWPIDが指定された場合は新たにPID名前空間を作成します。PID名前空間はcreate_pid_namespace関数にて作成します。 PID名前空間の作成では最初に階層数のチェックを行います。階層数がMAX_PID_NS_LEVELを超える場合はエラーとなります。Linuxカーネルv4.1ではMAX_PID_NS_LEVELは32となります。名前空間の初期化処理ではPIDビットマップ領域のメモリ確保、PID構造体のメモリを確保するためのSLABキャッシュ作成などメモリ管理に関する初期化や、/proc//ns/pidファイルに使用するinodeの確保とファイル操作のオペレーション設定などファイルに関する初期化があります。その他の設定として、作成するPID名前空間に対する親となるPID名前空間の設定を行います。処理の最後に作成中のPID名前空間でのPID 1に対する設定を行います。設定はpidmap構造体配列の0番目の要素に対してPID1に該当するビットのセット、空きPID数のデクリメントを行います。そして、pidmap構造体の1番目以降の各pidmap構造体に対して空きビットマップ数を設定します。PID名前空間は分離元の名前空間の下の階層に置かれ、元の名前空間においてもPIDが発行されるので分離元の名前空間の参照数のインクリメントも行います。

PID名前空間の移動

setnsシステムコール実行時には既存のPID名前空間から別の名前空間に所属を変更することができます。ただし、制限として移動可能なPID名前空間は現在と同じ名前空間、もしくは現在のPID名前空間の配下にある名前空間になります。自PID名前空間の上位にある名前空間には移動できません。これにより、名前空間の移動により現在所属している名前空間から抜けてしまうことを防いでいます。このチェック方法としては、pid_namespace構造体のlevel変数を見て、自身の名前空間よりも上位に移動でできないこと、図PID名前空間の移動において、Namespace Dに所属するプロセスはNamespace Bに移動できません。また、移動先の名前空間のparent変数を辿って、現在の名前空間にたどり着けることと確認します。図PID名前空間の移動ではNamespace Dに所属するプロセスはNamespace Cへの移動はできません。後者のチェックは自身の名前空間と無関係な名前空間に移動できないようにするために行います。自名前空間配下にある名前空間は、現在の名前空間の管理下にあるため移動が可能となっています。現在所属している名前空間の参照数、移動先名前空間の参照数の更新は他の名前空間と同様に行います。

                  Namespace A
      |-----------|-----------|
          |                       |
      Namespace B             Namespace C
          |
      Namespace D

図_PID名前空間の移動

PID名前空間の削除とプロセスの終了

あるPID名前空間にてPID 1のプロセスが終了する場合は、プロセスの終了処理の中で対象の名前空間に所属するプロセスを全て終了させます。この終了処理ではまず、これ以上のプロセスを生成させないようにPIDの新規発行をストップします。そして、SIGCHALDによるシグナルを無視するように設定し、PID名前空間が管理しているPIDのビットマップを順に調べながら有効なPIDに対してSIGKILLを送信していきます。このようにして全てのプロセスを終了させますが、SIGCHLDを無視している間にゾンビ状態(EXIT_ZOMBIE)になったプロセスがいるかもしれません。そのため、これらのプロセスを回収するためにwait4(2)をカーネルから呼び出し、EXIT_ZOMBIE状態のプロセスを回収します。ただし、EXIT_DEAD状態のプロセスについてはwait4(2)にて回収できません。しかし、グローバルなPID名前空間(init_pid_nsの名前空間)のinitプロセスで回収可能なため特別な処理は行いません。次にカレントプロセスの状態をシグナル割り込み禁止(TASK_UNINTERRUPTIBLE)にします。そしてnr_hashedの数が1もしくは2になるまでschedule関数を呼び出します。これにより、PID名前空間内のプロセスに対する親プロセスの変更を行います。nr_hashedの値として1または2どちらを使うかはカレントプロセスがスレッドグループリーダーかどうかによります。プロセスがスレッドグループリーダーの場合は1、違う場合は2となります。PIDの解放処理(free_pid関数)によりnr_hashedの数が終了条件に達したらカレントプロセスの状態をTASK_RUNNINGに変更します。最後にBSDプロセスアカウンティング情報を消去します。

fork関数/clone関数実行時の各PID名前空間へのpid番号発行

プロセスの生成時にはpidを発行しますが、fork関数を実行したプロセスが所属するPID名前空間全てでpid番号を発行する必要があります。そのため、pid番号の発行を行うalloc_pid関数では、現在のPID名前空間から階層を上がって行き、各PID名前空間にてpid番号の発行を行います。また、task_struct構造体にはpid変数があり、プロセスに割り当てられたpid番号を設定しますが、ここで設定するpid番号はプロセスが所属するPID名前空間のpid番号となります。例として、デフォルトのPID名前空間から分離したプロセス内でfork関数を行い、pid番号に144が振られた場合、このプロセスのtask_struct構造体のpid変数に設定されるのは144となります。

User名前空間

User名前空間はUID、GIDをホストとコンテナで分離します。より正確にはホストのUIDとコンテナのUIDをマッピングさせます。このマッピングにより、例えば、コンテナでのUID 0をホストのUID 1000にマッピングすることで、コンテナ内ではroot権限を使用することができますが、ホストから見るとコンテナ内のrootユーザはUID 1000ですので、ホストに対して大きな影響を及ぼす操作を制限することができます。この例の場合、コンテナ内のrootユーザが作成したファイルはホストではUID 1000番のユーザが作成したと認識されます。 User名前空間の分離時に明示的にUIDとGIDマッピングを行わなかった場合はUID、GID共に65534番にマッピングされます。IDマッピングは分離元のUID・GIDと分離後のUID・GIDマッピングを行います。よって、User名前空間もPID名前空間と同様に分離元と分離後の名前空間で親子関係になります。

User名前空間の実装
変数名 説明
uid_map uidのマッピング
uid_map gidマッピング
projid_map プロジェクト識別子のマッピング
count 名前空間の参照数
parent 親の名前空間
level ユーザー名前空間の階層数
owner プロセスのeuid
group プロセスのegid
ns ns_common構造体
flags setgroups(2)の実行可否を設定

表_user_namespace

User名前空間の作成

User名前空間の作成時は最初にcred構造体を初期化します。新規にuser_namespace構造体を作成します。構造体の初期化ではprocfsにエクスポートするファイルのinodeなどの設定など、新しい名前空間の設定を行います。User名前空間は最大で32段階の階層を作ることができるため、名前空間の作成により32段階以上ある場合はエラーとします。また、chroot環境で実行された場合もエラーとなります。次に、現在のUser名前空間と作成中の名前空間において、uidとgidマッピングが行われているかチェックを行います。ユーザ名前空間のメモリを確保し、/proc//ns/userファイルに設定するinodeを確保します。inodeが確保できたらユーザ名前空間を操作する構造体を設定します。作成した名前空間の初期設定を行います。他のプロセスと名前空間を共有していないので参照数は1になります。分離元(parent)の設定、uid・gidマッピングなどを設定します。cred構造体に作成したユーザ名前空間を設定します。User名前空間の初期化が完了したら、set_cred_user_ns関数でcred構造体と名前空間を関連付けします。

User名前空間の移動

User名前空間はNSProxyが管理していないため処理が多少変わります。移動先と現在の名前空間が同じな場合はエラーとなります。マルチスレッドのプログラムにおいて、あるスレッドが名前空間の移動を行おうとした場合もエラーとなります。これによってあるプロセスがスレッドに毎に違う名前空間に所属するのを防いでいます。プロセスがファイルシステム情報を子プロセスと共有している場合もエラーとなります。移動先の名前空間でCAP_SYS_ADMINケーパビリティを持っている必要がります。これらのチェックで問題がなければ名前空間の移動処理を行うことができます。prepare_creds関数にて新しいcred構造体を作成、その後、現在の名前空間の参照数を減らします。次に移動先の名前空間の参照数を増やします。そして、set_cred_user_ns関数でcred構造体のuser_ns変数を移動先の名前空間に設定します。最後に現在のtask_structに設定されているcred構造体を新しく作成したcred構造体に変更し、処理が完了します。

ルーター自作でわかるパケットの流れ

ルーター自作でわかるパケットの流れ

*1:9)から(13