Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

PID Namespace

About Linux PID Namespace source code.

0x00 PID 框架设计

内核的大量数据结构被哈希表和链表链接起来,最主要的目的就是在于查找。可想而知一个好的框架,应该要考虑到检索速度,还有考虑功能的划分。在 PID 框架中,主要考虑以下几个因素:

  • 如何通过 task_struct 快速找到对应的 pid
  • 如何通过 pid 快速找到对应的 task_struct
  • 如何快速的分配一个唯一的 pid

0x01 原始 PID 框架

1
2
3
4
5
6
struct task_struct
{
.....
pid_t pid;
.....
} // Kernel 2.4

一个进程对应一个 pid ,可以通过 task_struct 轻松找到 pid 。但通过 pid 查找 task_struct 难以实现。

0x02 引入 hlist 和 pid 位图

1
2
3
4
5
struct task_struct *pidhash[PIDHASH_SZ];
struct pidmap {
atomic_t nr_free; //表示当前可用的pid个数
void *page; //用来存放位图
};

使用了 pid 位图,标记 pid 是否可用, pid 位图的方式更加节约内存。通过将每一位设置为 0 或者是 1 ,可以用来表示是否可用,第 1 位的 0 和 1 用来表示 pid 为 1 是否可用,以此类推。

0x03 引入 PID 类型

由于一个进程除了进程 pid ,还有进程组 pid 和会话 pid ,因此引入了 pid 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct task_struct
{
....
pid_t pid;
pid_t session;
struct task_struct *group_leader;
....
}

struct signal
{
....
pid_t __pgrp;
....
} // Kernel 2.6.24

对于进程组 pid 来说,信号需要知道,通过这个 pid ,可以实现对一组进程进行控制,所以这个 pid 出现在了 signal 这个结构体中。无论是 session pid 还是 group pid 其实都不占用 pid 的资源,因为 session pid 是和领导进程组的组 pid 相同,而 group pid 则是和这个进程组中的领导进程的 pid 相同。

0x04 引入 PID 命名空间后的 PID 框架

随着内核不断的添加新的内核特性,尤其是 PID Namespace 机制的引入,使得 PID 存在命名空间的概念,并且命名空间还有层级的概念存在,高级别的可以被低级别的看到,这就导致高级别的进程有多个 PID 。

比如说在默认命名空间下,创建了一个新的命名空间,叫做 Level 1,默认命名空间这里称之为 Level 0,在 Level 1 中运行了一个进程,其在 Level 1 中的 pid 为 1。因为高级别的 pid namespace 需要被低级别的 pid namespace 所看见,所以这个进程在 Level 0 中会有另外一个 pid,假设为 xxx。套用上面说到的 pid 位图的概念,对于每一个 pid namespace 来说都应该有一个 pidmap,上文中提到的 Level 1 进程有两个 pid 一个是 1,另一个是 xxx,其中 pid 为 1 是在 Level 1 中的 pidmap 进行分配的,pid 为 xxx 则是在 Level 0 的 pidmap 中分配的。下图是整个 pid namespace 的一个框架:

引入了 PID 命名空间后,一个 pid 就不仅仅是一个数值那么简单了,还要包含这个 pid 所在的命名空间、父命名空间和命名空间多对应的 pidmap、命名空间的 pid 等等。因此内核对 pid 做了一个封装,封装成 struct pid:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
enum pid_type
{
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};

struct pid
{
unsigned int level; // pid 所在的层级
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX]; // 一个 hash 表,分三个表头:分别是 pid 表头、进程组 pid 表头和会话 pid 表头
struct upid numbers[1];
// numbers[1] 域是一个可扩展 upid 结构体。
// 一个 PID 可以属于不同的 namespace
// numbers[0] 表示 global namespace
// numbers[i] 表示第 i 层 namespace,i 越大所在层级越低。
};

struct upid { // 包装命名空间所抽象出来的一个结构体
int nr; // pid 在该命名空间中的 pid 数值
struct pid_namespace *ns; // 对应的命名空间
struct hlist_node pid_chain; // 通过 pidhash 将一个 pid 对应的所有的命名空间连接起来.
};

struct pid_namespace {
struct kref kref;
struct pidmap pidmap[PIDMAP_ENTRIES]; // 一个 pid 命名空间其独立的 pidmap
int last_pid; // 上次分配的 pid
unsigned int nr_hashed;
struct task_struct *child_reaper; // 这个 pid 命名空间对应的 init 进程,因为如果父进程挂了需要找养父
struct kmem_cache *pid_cachep;
unsigned int level; // 所在的命名空间层次
struct pid_namespace *parent; // 父命名空间,构建命名空间的层次关系
struct user_namespace *user_ns;
struct work_struct proc_work;
kgid_t pid_gid;
int hide_pid;
int reboot; /* group exit code if this pidns was rebooted */
unsigned int proc_inum;
};

引入了 pid namespace 后,结构变得很复杂多了。进程如何和 struct pid 关联起来呢?内核为了统一管理 pid、进程组 pid 和会话 pid,将这三类 pid,进行了整合。现在 task_struct 要和三个 struct pid 关联,还要区分 struct pid 的类型。所以内核又引入了中间结构将 task_struct 和 pid 进行了 1 对 3 的关联。其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct pid_link
{
struct hlist_node node;
struct pid *pid;
};

struct task_struct
{
.............
pid_t pid;
struct pid_link pids[PIDTYPE_MAX];
.............
}

enum pid_type
{
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX
};

其中进程 A,B,C 是一个进程组的,A 是组长进程,所以 B 和 C 的 task_struct 结构体中的 pid_link 成员的 node 字段就被邻接到进程 A 对应的 struct pid 中的 tasks[1]。 struct upid 通过 pid_hash 和 pid 数值关联了起来,这样就可以通过 pid 数值快速的找到所有命名空间的 upid 结构, numbers 是一个 struct pid 的最后一个成员,利用可变数组来表示这个pid结构当前有多少个命名空间。

0x05 调试实验

Todo

参考资料:

https://blog.csdn.net/zhangyifei216/article/details/49926459

https://carecraft.github.io/basictheory/2017/03/linux-pid-manage/