Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

MOUNT Namespace

About Linux Mount Namespace source code.

0x00 Basis

Namespace 核心结构体于 task_struct 当中:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
struct task_struct {
......
/* Namespaces: */
struct nsproxy *nsproxy;
......
}

struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct cgroup_namespace *cgroup_ns;
};

struct mnt_namespace {
atomic_t count;
struct ns_common ns;
struct mount * root; // 根目录挂载点
struct list_head list;
struct user_namespace *user_ns;
struct ucounts *ucounts;
u64 seq; /* Sequence number to prevent loops */
wait_queue_head_t poll;
u64 event;
unsigned int mounts; /* # of mounts in the namespace */
unsigned int pending_mounts;
} __randomize_layout;

struct ns_common {
atomic_long_t stashed;
const struct proc_ns_operations *ops;
unsigned int inum;
};

struct proc_ns_operations {
const char *name;
const char *real_ns_name;
int type;
struct ns_common *(*get)(struct task_struct *task);
void (*put)(struct ns_common *ns); // 回调结构题
int (*install)(struct nsproxy *nsproxy, struct ns_common *ns);
struct user_namespace *(*owner)(struct ns_common *ns);
struct ns_common *(*get_parent)(struct ns_common *ns);
} __randomize_layout;

const struct proc_ns_operations mntns_operations = {
.name = "mnt",
.type = CLONE_NEWNS,
.get = mntns_get,
.put = mntns_put,
.install = mntns_install,
.owner = mntns_owner,
};

每个 mount namespace 都有一份自己的挂载点列表。当我们使用 clone 函数或 unshare 函数并传入 CLONE_NEWNS 标志创建新的 mount namespace 时, 新 mount namespace 中的挂载点其实是从调用者所在的 mount namespace 中拷贝的。但是在新的 mount namespace 创建之后,这两个 mount namespace 及其挂载点就基本上没啥关系了(除了 shared subtree 的情况),两个 mount namespace 是相互隔离的。

0x01 文件系统挂载

Linux中使用树来组织文件系统,整个文件系统构成一棵树。整个全局系统中只有这样一棵文件树(在没有容器的情况下),这棵树描述文件系统的拓扑结构。

根就这颗树是其赖以生存的基础,文件系统以“/”为根。由于linux的树形文件系统是完全抽象的,因此它不和任何介质进行绑定,仅存在于内核当中,内核只要起来,这个虚拟的树就存在了,只是此时只有树根,然而linux此时可以挂载任意类型的文件系统到这个树根,linux可以在initrd中挂载任意文件系统到树根,这是因为内核和文件系统是分离的概念,内核启动并不依赖任何文件系统。

这棵挂载树的建立包括建立根节点“/”和挂载rootfs文件系统到根目录的过程。

挂载普通文件系统,就是将一个文件系统挂载(mount)到VFS目录树上的一个目录的过程,可以简单描述为将某一设备(dev_name)上某一文件系统(file_system_type)安装到VFS目录树上的一个安装点(dir_name),要解决的问题是将对VFS目录中某一目录的操作转化为具体安装到其上的实际文件系统的对应操作。

0x02 进程的文件系统环境

每个进程都有一个它自己当前的工作目录和自己的根目录,这是内核用来表示进程和文件系统相互作用所必须维护的数据。相关信息保存在fs_struct结构体中,在进程的task_struct中保存着当前进程的fs_struct的指针。

1
2
3
4
5
6
7
8
9
10
struct fs_struct {
int users;
spinlock_t lock;
seqcount_t seq;
int umask;
int in_exec;
struct path root, pwd;
// path用于存放着目录的目录项和文件系统对象信息
// 其中root对应着当前进程所在的根目录,pwd则是对应进程的当前目录
} __randomize_layout; // kernel 5.7.10

每一个进程都有自己的namespace,mnt_namespace中是这个命名空间中的根文件系统的挂载实例。对于每一个mount的文件系统,内核中都有一个struct mount数据结构来表示,所有的mount,都存在于一个hash table中,他们通过一个hash数组组织在一起。

对于mount的文件系统,会有在一个文件系统上mount另外一个文件系统的情况,这种情况,可以称原文件系统为父vfsmount, 对于mount上的文件系统,称之为子文件系统。

每一个挂载的文件系统,都有一个mount实例来表示。系统中mount一个文件系统时,首先会创建一个文件系统的的mount实例。在mount实例中的vfsmount存放着这个文件系统相关的挂载点的基本信息。mnt_root指向本文件系统的根路径dentry,mnt_sb指向本文件系统的超级块。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
struct mnt_namespace {
atomic_t count;
struct ns_common ns;
struct mount * root;
struct list_head list;
struct user_namespace *user_ns;
struct ucounts *ucounts;
u64 seq; /* Sequence number to prevent loops */
wait_queue_head_t poll;
u64 event;
unsigned int mounts; /* # of mounts in the namespace */
unsigned int pending_mounts;
} __randomize_layout;

struct mount {
struct hlist_node mnt_hash;
struct mount *mnt_parent;
struct dentry *mnt_mountpoint;
struct vfsmount mnt;
union {
struct rcu_head mnt_rcu;
struct llist_node mnt_llist;
};
#ifdef CONFIG_SMP
struct mnt_pcp __percpu *mnt_pcp;
#else
int mnt_count;
int mnt_writers;
#endif
struct list_head mnt_mounts; /* list of children, anchored here */
struct list_head mnt_child; /* and going through their mnt_child */
struct list_head mnt_instance; /* mount instance on sb->s_mounts */
const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
struct list_head mnt_list;
struct list_head mnt_expire; /* link in fs-specific expiry list */
struct list_head mnt_share; /* circular list of shared mounts */
struct list_head mnt_slave_list;/* list of slave mounts */
struct list_head mnt_slave; /* slave list entry */
struct mount *mnt_master; /* slave is on master->mnt_slave_list */
struct mnt_namespace *mnt_ns; /* containing namespace */
struct mountpoint *mnt_mp; /* where is it mounted */
union {
struct hlist_node mnt_mp_list; /* list mounts with the same mountpoint */
struct hlist_node mnt_umount;
};
struct list_head mnt_umounting; /* list entry for umount propagation */
#ifdef CONFIG_FSNOTIFY
struct fsnotify_mark_connector __rcu *mnt_fsnotify_marks;
__u32 mnt_fsnotify_mask;
#endif
int mnt_id; /* mount identifier */
int mnt_group_id; /* peer group identifier */
int mnt_expiry_mark; /* true if marked for expiry */
struct hlist_head mnt_pins;
struct hlist_head mnt_stuck_children;
} __randomize_layout;

0x03 命名空间

Mount namespaces (Linux2.4.19) 隔离一组进程的文件系统层次。在使用了新 Mount Namespaces 后,mount() 和 umount() 就会在新的文件系统层次上操作,而不是在全局(默认)文件系统层次上。

每个 mnt_namespace 有自己独立的 mount *root ,即根挂载点是互相独立的,同时由 mount->mnt_child 串接起来的子 mnt 链表,以及继续往下都是彼此独立的,产生的外在效果就是某个 mnt_namespace 中的 mount、 umount 不会对其他 namespace 产生影响,因为整个mount树是每个 namespace 各有一份,彼此间无干扰, path lookup 也在各自的 mount 树中进行。这里和 chroot 之类的操作不一样, chroot 改变的只是 task_struct 相关的 fs_struct 中的root,影响的是 path lookup 的起始点,对整个 mount 树并无关系。

chroot 的环境只隔离文件系统,使用 ps 仍然会将系统上的所有进程列出。而 namespace 提供的是一个全新的挂在树。

不同的 mnt_namespace 可以引用不同的根文件系统,组织不同的文件系统挂载树,形成不同的目录结构。一般而言,新创建的进程总是与其父进程共用 mnt_namespace 。而所有进程都是1号进程 (init) 的子孙进程,则一般情况下所有进程都使用相同的 mnt_namespace ,都存在于相同的目录结构中。

但是在通过 clone 系统调用创建新进程时,可以指定 CLONE_NEWNS 标志,为子进程创建新的名字空间(其中就包含了 mnt_namespace ,此外名字空间还有其他内容)。

0x04 pivot_root和root

chroot 即 change root directory (更改 root 目录)。在 Linux 系统中,系统默认的目录结构都是 / ,即以根 (root) 开始的。而在使用 chroot 之后,系统的目录结构将以指定的位置作为 / 位置。

pivot_root 和 chroot 的主要区别是,pivot_root 主要是把整个系统切换到一个新的 root 目录,而移除对之前 root 文件系统的依赖,这样你就能够 umount 原先的 root 文件系统。而 chroot 是针对某个进程,而系统的其它部分依旧运行于老的 root 目录。

但 pivot_root 只能针对具体设备上的目录进行操作,因此 LXC 中的 pivot_root 不能支持使用 ramfs 作为容器的 rootfs 来启动容器。

参考资料:

https://www.jianshu.com/p/f288d6fe7528

https://www.cnblogs.com/sparkdev/p/9424649.html

https://blog.csdn.net/tanzhe2017/article/details/81001981