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