setnsの実装を読む

カーネル4.3でのsetnsの実装を読みます。

setns()はkernel/nsproxy.cにあり、30行程度の短い関数です。 流れとしてはこのようになります。

  1. 移動先の名前空間のfdからns_common構造体を取得
  2. nsproxyの作成
  3. 移動先名前空間のinstall()を呼んで名前空間のほうに移動の処理をしてもらう
  4. 既存のnsproxyと新しく作ったnsproxyを切り替え

setns(2)のmanには名前空間ごとに制限がありますが、それらの制限はinstall()のほうで行っています。

setnsの実装はこのようになってます。

221 SYSCALL_DEFINE2(setns, int, fd, int, nstype)
222 {
223         struct task_struct *tsk = current;
224         struct nsproxy *new_nsproxy;
225         struct file *file;
226         struct ns_common *ns;
227         int err;
228 
229         file = proc_ns_fget(fd);
230         if (IS_ERR(file))
231                 return PTR_ERR(file);
232 
233         err = -EINVAL;
234         ns = get_proc_ns(file_inode(file));
235         if (nstype && (ns->ops->type != nstype))
236                 goto out;
237 
238         new_nsproxy = create_new_namespaces(0, tsk, current_user_ns(), tsk->fs);
239         if (IS_ERR(new_nsproxy)) {
240                 err = PTR_ERR(new_nsproxy);
241                 goto out;
242         }
243 
244         err = ns->ops->install(new_nsproxy, ns);
245         if (err) {
246                 free_nsproxy(new_nsproxy);
247                 goto out;
248         }
249         switch_task_namespaces(tsk, new_nsproxy);
250 out:
251         fput(file);
252         return err;
253 }

ここはファイルディスクリプタからfile構造体を取得しています。

229         file = proc_ns_fget(fd);

ここで、file構造体からinode構造体にアクセスし、inode構造体のi_private変数に設定されているns_common構造体を取得します。

234         ns = get_proc_ns(file_inode(file));

get_proc_nsはこのようなマクロです。

69 #define get_proc_ns(inode) ((struct ns_common *)(inode)->i_private)

つぎにcreate_new_namespaces()でtask_structに設定するNSProxy構造体を作成します。1番目の引数に0を渡しているので名前空間の分離はなく、各名前空間の参照数が1増えます。

238         new_nsproxy = create_new_namespaces(0, tsk, current_user_ns(), tsk->fs);

名前空間の移動に関する処理はproc_ns_operations構造体に設定されているinstall()で行います。

244         err = ns->ops->install(new_nsproxy, ns);

最後にcurrent->nsproxyを先ほど作ったnew_nsproxyに置き換えて完了です。

249         switch_task_namespaces(tsk, new_nsproxy);

では、install()の処理を簡単に見てみます。まずは簡単なところでuts名前空間を。実装はkernel/utsname.cにあります。

実装はこうなっています。わかりやすいですね。

119 static int utsns_install(struct nsproxy *nsproxy, struct ns_common *new)
120 {
121         struct uts_namespace *ns = to_uts_ns(new);
122 
123         if (!ns_capable(ns->user_ns, CAP_SYS_ADMIN) ||
124             !ns_capable(current_user_ns(), CAP_SYS_ADMIN))
125                 return -EPERM;
126 
127         get_uts_ns(ns);
128         put_uts_ns(nsproxy->uts_ns);
129         nsproxy->uts_ns = ns;
130         return 0;
131 }
132 

utsns_install()は最初に現在の名前空間と移動先の名前空間でケーパビリティのCAP_SYS_ADMINがあるかチェックして、無ければエラーにします。

次は移動先の名前空間の参照数を1つ増やします。

127         get_uts_ns(ns);

その次は、このプロセスはに現在の名前空間から離れるので参照数を1つ減らします。

128         put_uts_ns(nsproxy->uts_ns);

最後にsetnsで作成したnsproxyのuts名前空間を移動先の名前空間に設定します。

129         nsproxy->uts_ns = ns;

以上がuts名前空間のinstall()処理です。NSProxyが管理している名前空間(net, mount, ipc, uts, pid)は基本的にこのような感じで名前空間の移動処理をします。 User名前空間はNSProxyが管理していないので多少処理が違います。User名前空間install()kernel/user_namespace.cにあります。

実装はこうなっていて、ほとんどが移動可能かのチェックになります。setnsでUser名前空間への参加ができない場合のルールはmanに書かれていて、このチェックをしています。

969 static int userns_install(struct nsproxy *nsproxy, struct ns_common *ns)
970 {
971         struct user_namespace *user_ns = to_user_ns(ns);
972         struct cred *cred;
973 
974         /* Don't allow gaining capabilities by reentering
975          * the same user namespace.
976          */
977         if (user_ns == current_user_ns())
978                 return -EINVAL;
979 
980         /* Tasks that share a thread group must share a user namespace */
981         if (!thread_group_empty(current))
982                 return -EINVAL;
983 
984         if (current->fs->users != 1)
985                 return -EINVAL;
986 
987         if (!ns_capable(user_ns, CAP_SYS_ADMIN))
988                 return -EPERM;
989 
990         cred = prepare_creds();
991         if (!cred)
992                 return -ENOMEM;
993 
994         put_user_ns(cred->user_ns);
995         set_cred_user_ns(cred, get_user_ns(user_ns));
996 
997         return commit_creds(cred);
998 }

最初のto_user_ns()はns_common構造体からuser_namespace構造体へアクセスします。処理はこうなっています。container_ofマクロはLinuxカーネルではよく使うマクロですね。このマクロについては過去に書いたLinux: inodeからtask_struct構造体を取得 - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモという記事で説明しました。

948 static inline struct user_namespace *to_user_ns(struct ns_common *ns)
949 {
950         return container_of(ns, struct user_namespace, ns);
951 }

User名前空間はcred構造体の管理下にあり、最初にこのcred構造体を作成します。

990         cred = prepare_creds();

ここは今のUser名前空間の参照数を1つ減らします。

994         put_user_ns(cred->user_ns);

次はget_user_ns()で移動先の名前空間の参照数を増やして、set_cred_user_ns()でcred構造体のuser名前空間の変数を移動先のものに設定します。

995         set_cred_user_ns(cred, get_user_ns(user_ns));

名前空間を設定しているのはset_cred_user_ns()の最後です。

 33 static void set_cred_user_ns(struct cred *cred, struct user_namespace *user_ns)
 34 {
~~~
 48         /* tgcred will be cleared in our caller bc CLONE_THREAD won't be set */
 49         cred->user_ns = user_ns;
 50 }

最後にcommit_creds()でcurrentのcred構造体を差し替えます。この処理は名前空間の機能ではないのでこれくらいにしておきます。

ということで、setnsの処理とuts・user名前空間のinstall()を読んでみました。