Linuxでは複数のユーザやグループを扱うことができます.このユーザやグループを分離するのがUser名前空間です.
元のUser名前空間のユーザやグループと新しいUser名前空間のユーザやグループを対応づけることができます.これを利用すると,元のUser名前空間の一般ユーザが新しいUser名前空間ではrootになったりすることができます.
対応付けられていないユーザやグループはnobody
という名前のユーザやグループに見えます.nobody
ユーザのユーザIDやnobody
グループのグループIDは65534
です.
ユーザIDのマッピングは,/proc
にあるファイルを利用して行います.プロセスIDが1234
のとき,このファイルは/proc/1234/uid_map
です.このファイルはプロセスごとに存在し,元の名前空間にあるプロセスから,ただ1回だけ書き込むことができます.このファイルは改行で区切られた数行からなり,各行はスペース区切りの3つの整数からなります.これらの意味するところは,前から順に新しい名前空間でのユーザID,元の名前空間でのユーザID,マッピングするユーザIDの数となっています.
例として以下のようなマッピングを考えてみましょう.
0 1000 3
このマッピングは,以下のように元の名前空間でのユーザIDと新しい名前空間でのユーザIDを対応付けます.
元の名前空間 | 新しい名前空間 |
---|---|
1000 | 0 |
1001 | 1 |
1002 | 2 |
ファイルのパーミッションもすべて上で述べたマッピングにより対応付けられます.
例として,次のような場合を考えてみます.元の名前空間でのminicamp
というユーザが新しい名前空間でroot
ユーザにマッピングされているとします.元の名前空間で所有者がminicamp
だったファイルは新しい名前空間ではroot
となります.逆に,新しい名前空間でroot
を所有者として新しいファイルを作成すると,元の名前空間ではそのファイルの所有者はminicamp
に見えます.元の名前空間でのsyslog
というユーザが新しい名前空間にマッピングされていないとすると,所有者がsyslog
のファイルは,新しい名前空間では所有者はnobody
に見えます.
User名前空間を利用してユーザやグループが別のものになっても,ファイルなどの権限は変わりません.つまり,見かけ上は別のユーザですが,できることは全く同じです.元のUser名前空間での一般ユーザが新しいUser名前空間でrootになっても,もともと権限が無かった他のユーザのファイルにアクセスしたり,ネットワーク設定などを変更することはできません.nobody
ユーザなどについてもこれは当てはまり,元の名前空間の複数のユーザが新しい名前空間から同じnobody
ユーザとして見えていたとしても,実際にはそれらは別のユーザとして権限制御がなされます.
clone
システムコールにCLONE_NEWUSER
フラグを設定すると,新しいUser名前空間でプロセスが開始されます.
clone(child_func,stack+1024*1024,SIGCHLD|CLONE_NEWUSER,arg);
好きな場所にns_user_example
というディレクトリを作成し,その中に以下のようなディレクトリ構造を作成しましょう.
- [dir] ns_user_example
- [dir] src
- [file] main.c
- [dir] build
- [file] CMakeLists.txt
以下のCコードはmain.c
に記述します.
getpid
システムコールは呼び出したプロセスを実行しているユーザのユーザIDを返します.
uid_t getuid(void);
ユーザIDを標準出力に出力するには,以下のように記述します.
printf("UID=%d\n",getuid());
getuid
を利用して以下のような関数を記述しましょう.
int print_uid(char* s){
printf("[%s]UID=%d\n",s?s:"",getuid());
}
以下のようなmain関数を記述します.
int main(){
print_uid("parent");
}
以下の必要なヘッダファイルをインクルードします.
#include <unistd.h>
#include <stdio.h>
ここまでを行うと以下のようになっていると思います.
#include <unistd.h>
#include <stdio.h>
int print_uid(char* s){
printf("[%s]UID=%d\n",s?s:"",getuid());
}
int main(){
print_uid("parent");
}
以下のようにCMakeLists.txt
に記述します.
cmake_minimum_required(VERSION 3.16)
project(ns_user_example)
set(CMAKE_C_STANDARD 11)
add_executable(example src/main.c)
build
ディレクトリに移動します.
$ cd ./build
cmake
を実行します.CMakeLists.txt
を含むディレクトリ,今回はns_user_example
ディレクトリを..
で指定しています.
$ cmake ..
make
を実行します.
$ make
example
というバイナリが生成されるのでこれを実行します.以前PID名前空間のときはroot権限が必要でしたが,User名前空間はroot権限が不要であることは大きな差異です.
$ ./example
ユーザIDが出力されます.
[parent]UID=1011
子プロセスを新しいUser名前空間で実行するには,clone
システムコールにCLONE_NEWPID
フラグを指定します.clone
の詳細については別ページで述べました.
pid_t c = clone((int(*)(void*))print_uid,stack+1024*1024,CLONE_NEWPID|SIGCHLD,"child");
(int(*)(void*))print_uid
という部分は,prind_uid
関数へのポインタをint func(char*)
の関数のポインタの型にキャストしています.
スタックの準備などを付け加え,以下のようにmain関数を変更します.
int main(){
print_pid("parent");
void* stack = mmap(NULL,1024*1024,PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_GROWSDOWN|MAP_STACK,-1,0);
pid_t c = clone((int(*)(void*))print_uid,stack+1024*1024,CLONE_NEWUSER|SIGCHLD,"child");
if(c == -1){
puts("clone failed!\n");
return -1;
}
waitpid(c,NULL,0);
}
以下のヘッダファイルを追加でインクルードします.
#define _GNU_SOURCE
#include <sched.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
ここまでをすべて行うと次のようになります.
#define _GNU_SOURCE
#include <sched.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int print_uid(char* s){
printf("[%s]UID=%d\n",s?s:"",getuid());
}
int main(){
print_uid("parent");
void* stack = mmap(NULL,1024*1024,PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_GROWSDOWN|MAP_STACK,-1,0);
pid_t c = clone((int(*)(void*))print_uid,stack+1024*1024,CLONE_NEWUSER|SIGCHLD,"child");
if(c == -1){
puts("clone failed!\n");
return -1;
}
waitpid(c,NULL,0);
}
前と同様に,ビルドして実行します.
$ cd ./build
$ make
$ ./example
以下のように結果が出力されると思います.ここでもroot権限が不要であることに注目してください.
[parent]UID=1011
[child]UID=65534
新しく作成されたプロセスを実行しているユーザのユーザIDは65534
であることがわかります.今回はまったくユーザIDのマッピングを行わなかったので,新しい名前空間ではnobody
ユーザになっているということを示します.