Linuxのスレッド内でchrootした場合の影響範囲を調べた


今年の目標は「はじめから無理と思わない」「基礎を学ぶ」なので、C言語からも逃げずに立ち向かう1年にしたい。
本日は、Linuxのスレッド内でchrootした場合の影響範囲を調べたのでまとめる。

はじめに

本エントリではLinuxでスレッドを作成して、システムコールの “chroot()” を実行した場合にスレッド内のみchrootされるのか?それとも、プロセス全体に影響するのかを調査していく。

実際に動かして確認する

スレッドを作成して、chrootを実行するプログラムをC言語で作成する。

#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>

void* thread_1(void* param);
void* thread_2(void* param);
void* thread_3(void* param);
void* set_chroot(char* path);
void* show_files(char* path);

int main(int argc, char *argv[]){
  pthread_t thread_id_1, thread_id_2, thread_id_3;

 show_files("/");

  # スレッドを作って順番に実行していく
  pthread_create(&thread_id_1, NULL, thread_1, NULL);
  pthread_join(thread_id_1,NULL);

  pthread_create(&thread_id_2, NULL, thread_2, NULL);
  pthread_join(thread_id_2,NULL);

  pthread_create(&thread_id_3, NULL, thread_3, NULL);
  pthread_join(thread_id_3,NULL);

 show_files("/");  
  return 0;
}

void* thread_1(void* param){
  show_files("/");
}

void* thread_2(void* param){
  set_chroot("/tmp");
  show_files("/");
}

void* thread_3(void* param){
  show_files("/");
}

void* set_chroot(char* path){
  printf("=== chroot ===\n");
  if(chroot(path) < 0){
    printf("chroot(%d) %s\n", errno, strerror(errno));
  }
}

void* show_files(char* path){
  DIR *dir;
  struct dirent *dp;

  dir=opendir(path);
  for(dp=readdir(dir); dp!=NULL; dp=readdir(dir)){
    printf("%s\n", dp->d_name);
  }
  closedir(dir);
}

上記の様にスレッドを作って、順番に処理させる。
“show_files()” は指定したディレクトリの中身を表示するメソッドで、”set_chroot()” は指定したディレクトリをchrootに設定するメソッドです。
以下のタイミングで “show_files( / )” でディレクトリ一覧を取得し、”set_chroot( /tmp )” を実行したタイミングでディレクトリ一覧が “/tmp” になるパターンを調べる。

  • main関数実行直後
  • スレッド1実行直後
  • スレッド2実行直後
  • スレッド3実行直後
  • main関数終了直前

“touch /tmp/tmp_here” を実行し、tmpディレクトリと分かるようにファイルを配置しておく。

結果
[root@pmilter thread]# gcc -lpthread kume.c && ./a.out

## main関数実行直後
## => `/` が参照されている。

.
..
boot
dev
proc
run
 :

## スレッド1実行直後
## => `/` が参照されている。

.
..
boot
dev
proc
run
 :

## スレッド2実行直後

# chroot("/tmp")を実行
=== chroot ===

# => ここで、`/tmp` が参照されている。
.
..
tmp_here

## スレッド3実行直後
## => 以降も `/tmp` が参照されている。

.
..
tmp_here

## main関数実行直後
## => ここでも `/tmp` が参照されている。

.
..
tmp_here

結果としては、スレッド内で “chroot()” が実行されて以降は親プロセスを含めて全てにchrootが適応されていることが分かった。
もちろん、main関数内でchrootした場合も同様だった。

なぜ、スレッド内でchrootした場合に、そのプロセス全体がchroot状態になるのか?

  • chroot()のソースを見てみる。
SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
	struct path path;
	int error;
	unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
retry:
	error = user_path_at(AT_FDCWD, filename, lookup_flags, &path);
	if (error)
		goto out;

	error = inode_permission(path.dentry->d_inode, MAY_EXEC | MAY_CHDIR);
	if (error)
		goto dput_and_out;

	error = -EPERM;
	if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
		goto dput_and_out;
	error = security_path_chroot(&path);
	if (error)
		goto dput_and_out;

	set_fs_root(current->fs, &path);
	error = 0;
dput_and_out:
	path_put(&path);
	if (retry_estale(error, lookup_flags)) {
		lookup_flags |= LOOKUP_REVAL;
		goto retry;
	}
out:
	return error;
}

“set_fs_root(current->fs, &path)” このようにchrootを設定していることが分かる。

void set_fs_root(struct fs_struct *fs, const struct path *path)
{
	struct path old_root;

	path_get(path);
	spin_lock(&fs->lock);
	write_seqcount_begin(&fs->seq);
	old_root = fs->root;
	fs->root = *path;
	write_seqcount_end(&fs->seq);
	spin_unlock(&fs->lock);
	if (old_root.dentry)
		path_put(&old_root);
}

“fs->root = *path;” このようにchrootされたディレクトリを格納していることが分かる。

sched.h – struct task_struct

上記にあった “fs_struct” 構造体は “task_struct” 構造体の中にリンクされている。

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
	/*
	 * For reasons of header soup (see current_thread_info()), this
	 * must be the first element of task_struct.
	 */
	struct thread_info thread_info;
#endif
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	void *stack;
  : (snip)
  /* filesystem information */
  struct fs_struct *fs;
  : (snip)
}

“task_struct” 構造体の中にはスレッドの情報が格納される、 “thread_info” が存在する。
“thread_info” 構造体の中には、chrootされているpathを記録する “fs_struct” 構造体などは存在しない。

結論

以下の事実を元にすると

  • “chroot()” 実行後は親プロセスを含む全てのスレッドはchroot状態となる。
  • “chroot()” 実行時は、 “fs_stract” 構造体の “root” にchrootのpathが記録される。
  • “fs_stract” 構造体はプロセスの情報が格納される、”task_struct” 構造体にリンクされている。
  • “thread_info” 構造体の中には、chrootされているpathを記録する “fs_struct” 構造体は存在しない。

threadで”chroot()”を実行した場合もプロセスとして持っている “fs_stract” 構造体にchrootの情報を格納するため、
“chroot()” 実行後は親プロセスを含む全てのスレッドはchroot状態となると思われる。