]>
Commit | Line | Data |
---|---|---|
e0a84778 VB |
1 | import contextlib |
2 | import ctypes | |
3 | import errno | |
4 | import os | |
5 | import pyroute2 | |
6 | import pytest | |
7 | import signal | |
98745499 | 8 | import multiprocessing |
e0a84778 VB |
9 | |
10 | # All allowed namespace types | |
8b549648 VB |
11 | NAMESPACE_FLAGS = dict( |
12 | mnt=0x00020000, | |
13 | uts=0x04000000, | |
14 | ipc=0x08000000, | |
15 | user=0x10000000, | |
16 | pid=0x20000000, | |
17 | net=0x40000000, | |
18 | ) | |
19 | STACKSIZE = 1024 * 1024 | |
e0a84778 | 20 | |
8b549648 | 21 | libc = ctypes.CDLL("libc.so.6", use_errno=True) |
e0a84778 VB |
22 | |
23 | ||
24 | @contextlib.contextmanager | |
25 | def keep_directory(): | |
26 | """Restore the current directory on exit.""" | |
27 | pwd = os.getcwd() | |
28 | try: | |
29 | yield | |
30 | finally: | |
31 | os.chdir(pwd) | |
32 | ||
33 | ||
08e05799 | 34 | def mount_sys(target="/sys"): |
8b549648 VB |
35 | flags = [2 | 4 | 8] # MS_NOSUID | MS_NODEV | MS_NOEXEC |
36 | flags.append(1 << 18) # MS_PRIVATE | |
37 | flags.append(1 << 19) # MS_SLAVE | |
08e05799 | 38 | for fl in flags: |
8b549648 | 39 | ret = libc.mount(b"none", target.encode("ascii"), b"sysfs", fl, None) |
08e05799 VB |
40 | if ret == -1: |
41 | e = ctypes.get_errno() | |
42 | raise OSError(e, os.strerror(e)) | |
43 | ||
44 | ||
98745499 VB |
45 | def mount_tmpfs(target, private=False): |
46 | flags = [0] | |
47 | if private: | |
8b549648 VB |
48 | flags.append(1 << 18) # MS_PRIVATE |
49 | flags.append(1 << 19) # MS_SLAVE | |
98745499 | 50 | for fl in flags: |
8b549648 | 51 | ret = libc.mount(b"none", target.encode("ascii"), b"tmpfs", fl, None) |
98745499 VB |
52 | if ret == -1: |
53 | e = ctypes.get_errno() | |
54 | raise OSError(e, os.strerror(e)) | |
55 | ||
56 | ||
57 | def _mount_proc(target): | |
8b549648 VB |
58 | flags = [2 | 4 | 8] # MS_NOSUID | MS_NODEV | MS_NOEXEC |
59 | flags.append(1 << 18) # MS_PRIVATE | |
60 | flags.append(1 << 19) # MS_SLAVE | |
98745499 | 61 | for fl in flags: |
8b549648 | 62 | ret = libc.mount(b"proc", target.encode("ascii"), b"proc", fl, None) |
98745499 VB |
63 | if ret == -1: |
64 | e = ctypes.get_errno() | |
65 | raise OSError(e, os.strerror(e)) | |
66 | ||
67 | ||
68 | def mount_proc(target="/proc"): | |
69 | # We need to be sure /proc is correct. We do that in another | |
70 | # process as this doesn't play well with setns(). | |
71 | if not os.path.isdir(target): | |
72 | os.mkdir(target) | |
73 | p = multiprocessing.Process(target=_mount_proc, args=(target,)) | |
74 | p.start() | |
75 | p.join() | |
76 | ||
77 | ||
e0a84778 VB |
78 | class Namespace(object): |
79 | """Combine several namespaces into one. | |
80 | ||
81 | This gets a list of namespace types to create and combine into one. The | |
82 | combined namespace can be used as a context manager to enter all the | |
83 | created namespaces and exit them at the end. | |
84 | """ | |
85 | ||
86 | def __init__(self, *namespaces): | |
0ca939b0 | 87 | self.next = [] |
e0a84778 VB |
88 | self.namespaces = namespaces |
89 | for ns in namespaces: | |
90 | assert ns in NAMESPACE_FLAGS | |
91 | ||
92 | # Get a pipe to signal the future child to exit | |
93 | self.pipe = os.pipe() | |
94 | ||
95 | # First, create a child in the given namespaces | |
96 | child = ctypes.CFUNCTYPE(ctypes.c_int)(self.child) | |
97 | child_stack = ctypes.create_string_buffer(STACKSIZE) | |
98 | child_stack_pointer = ctypes.c_void_p( | |
8b549648 VB |
99 | ctypes.cast(child_stack, ctypes.c_void_p).value + STACKSIZE |
100 | ) | |
e0a84778 VB |
101 | flags = signal.SIGCHLD |
102 | for ns in namespaces: | |
103 | flags |= NAMESPACE_FLAGS[ns] | |
104 | pid = libc.clone(child, child_stack_pointer, flags) | |
105 | if pid == -1: | |
106 | e = ctypes.get_errno() | |
107 | raise OSError(e, os.strerror(e)) | |
108 | ||
109 | # If a user namespace, map UID 0 to the current one | |
8b549648 VB |
110 | if "user" in namespaces: |
111 | uid_map = "0 {} 1".format(os.getuid()) | |
112 | gid_map = "0 {} 1".format(os.getgid()) | |
113 | with open("/proc/{}/uid_map".format(pid), "w") as f: | |
e0a84778 | 114 | f.write(uid_map) |
8b549648 VB |
115 | with open("/proc/{}/setgroups".format(pid), "w") as f: |
116 | f.write("deny") | |
117 | with open("/proc/{}/gid_map".format(pid), "w") as f: | |
e0a84778 VB |
118 | f.write(gid_map) |
119 | ||
120 | # Retrieve a file descriptor to this new namespace | |
8b549648 VB |
121 | self.next = [ |
122 | os.open("/proc/{}/ns/{}".format(pid, x), os.O_RDONLY) for x in namespaces | |
123 | ] | |
e0a84778 VB |
124 | |
125 | # Keep a file descriptor to our old namespaces | |
8b549648 VB |
126 | self.previous = [ |
127 | os.open("/proc/self/ns/{}".format(x), os.O_RDONLY) for x in namespaces | |
128 | ] | |
e0a84778 VB |
129 | |
130 | # Tell the child all is done and let it die | |
131 | os.close(self.pipe[0]) | |
8b549648 | 132 | if "pid" not in namespaces: |
e0a84778 | 133 | os.close(self.pipe[1]) |
ad8971ec | 134 | self.pipe = None |
e0a84778 VB |
135 | os.waitpid(pid, 0) |
136 | ||
ad8971ec VB |
137 | def __del__(self): |
138 | for fd in self.next: | |
139 | os.close(fd) | |
140 | for fd in self.previous: | |
141 | os.close(fd) | |
142 | if self.pipe is not None: | |
143 | os.close(self.pipe[1]) | |
144 | ||
e0a84778 VB |
145 | def child(self): |
146 | """Cloned child. | |
147 | ||
148 | Just be here until our parent extract the file descriptor from | |
149 | us. | |
150 | ||
151 | """ | |
152 | os.close(self.pipe[1]) | |
153 | ||
154 | # For a network namespace, enable lo | |
8b549648 | 155 | if "net" in self.namespaces: |
12e81bd1 | 156 | with pyroute2.IPRoute() as ipr: |
8b549648 VB |
157 | lo = ipr.link_lookup(ifname="lo")[0] |
158 | ipr.link("set", index=lo, state="up") | |
e0a84778 | 159 | # For a mount namespace, make it private |
8b549648 VB |
160 | if "mnt" in self.namespaces: |
161 | libc.mount( | |
162 | b"none", | |
163 | b"/", | |
164 | None, | |
165 | # MS_REC | MS_PRIVATE | |
166 | 16384 | (1 << 18), | |
167 | None, | |
168 | ) | |
e0a84778 VB |
169 | |
170 | while True: | |
171 | try: | |
172 | os.read(self.pipe[0], 1) | |
173 | except OSError as e: | |
174 | if e.errno in [errno.EAGAIN, errno.EINTR]: | |
175 | continue | |
176 | break | |
177 | ||
178 | os._exit(0) | |
179 | ||
180 | def fd(self, namespace): | |
181 | """Return the file descriptor associated to a namespace""" | |
182 | assert namespace in self.namespaces | |
183 | return self.next[self.namespaces.index(namespace)] | |
184 | ||
185 | def __enter__(self): | |
186 | with keep_directory(): | |
187 | for n in self.next: | |
188 | if libc.setns(n, 0) == -1: | |
189 | ns = self.namespaces[self.next.index(n)] # NOQA | |
190 | e = ctypes.get_errno() | |
191 | raise OSError(e, os.strerror(e)) | |
192 | ||
193 | def __exit__(self, *exc): | |
194 | with keep_directory(): | |
195 | err = None | |
196 | for p in reversed(self.previous): | |
197 | if libc.setns(p, 0) == -1 and err is None: | |
198 | ns = self.namespaces[self.previous.index(p)] # NOQA | |
199 | e = ctypes.get_errno() | |
200 | err = OSError(e, os.strerror(e)) | |
201 | if err: | |
202 | raise err | |
203 | ||
204 | def __repr__(self): | |
8b549648 | 205 | return "Namespace({})".format(", ".join(self.namespaces)) |
e0a84778 VB |
206 | |
207 | ||
208 | class NamespaceFactory(object): | |
209 | """Dynamically create namespaces as they are created. | |
210 | ||
211 | Those namespaces are namespaces for IPC, net, mount and UTS. PID | |
212 | is a bit special as we have to keep a process for that. We don't | |
213 | do that to ensure that everything is cleaned | |
214 | automatically. Therefore, the child process is killed as soon as | |
215 | we got a file descriptor to the namespace. We don't use a user | |
216 | namespace either because we are unlikely to be able to exit it. | |
217 | ||
218 | """ | |
219 | ||
98745499 | 220 | def __init__(self, tmpdir): |
e0a84778 | 221 | self.namespaces = {} |
98745499 | 222 | self.tmpdir = tmpdir |
e0a84778 VB |
223 | |
224 | def __call__(self, ns): | |
225 | """Return a namespace. Create it if it doesn't exist.""" | |
226 | if ns in self.namespaces: | |
227 | return self.namespaces[ns] | |
98745499 | 228 | |
8b549648 | 229 | self.namespaces[ns] = Namespace("ipc", "net", "mnt", "uts") |
98745499 VB |
230 | with self.namespaces[ns]: |
231 | mount_proc() | |
232 | mount_sys() | |
233 | # Also setup the "namespace-dependant" directory | |
234 | self.tmpdir.join("ns").ensure(dir=True) | |
235 | mount_tmpfs(str(self.tmpdir.join("ns")), private=True) | |
236 | ||
e0a84778 VB |
237 | return self.namespaces[ns] |
238 | ||
239 | ||
240 | @pytest.fixture | |
98745499 VB |
241 | def namespaces(tmpdir): |
242 | return NamespaceFactory(tmpdir) |