Merge ~mdeslaur/apport:sec202205 into ~apport-hackers/apport/+git/apport-old:main
- Git
- lp:~mdeslaur/apport
- sec202205
- Merge into main
Proposed by
Marc Deslauriers
Status: | Merged |
---|---|
Merge reported by: | Benjamin Drung |
Merged at revision: | cd7544bff0611ee8443a0051a9aa0041649abbab |
Proposed branch: | ~mdeslaur/apport:sec202205 |
Merge into: | ~apport-hackers/apport/+git/apport-old:main |
Diff against target: |
554 lines (+228/-104) 4 files modified
apport/fileutils.py (+61/-7) data/apport (+140/-91) etc/init.d/apport (+3/-6) tests/test_fileutils.py (+24/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Benjamin Drung | Approve | ||
Review via email: mp+422837@code.launchpad.net |
Commit message
These are the security fixes that went into USN-5427-1.
Description of the change
To post a comment you must log in.
~mdeslaur/apport:sec202205
updated
- cd7544b... by Marc Deslauriers
-
Merge remote-tracking branch 'origin/main' into sec202205
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/apport/fileutils.py b/apport/fileutils.py |
2 | index a8385c8..e050858 100644 |
3 | --- a/apport/fileutils.py |
4 | +++ b/apport/fileutils.py |
5 | @@ -57,6 +57,33 @@ def allowed_to_report(): |
6 | return False |
7 | |
8 | |
9 | +def get_dbus_socket(dbus_addr): |
10 | + '''Extract the socket from a DBus address.''' |
11 | + |
12 | + if not dbus_addr: |
13 | + return None |
14 | + |
15 | + # Only support unix domain sockets, and only the default Ubuntu path |
16 | + if not dbus_addr.startswith("unix:path=/run/user/"): |
17 | + return None |
18 | + |
19 | + # Prevent path traversal |
20 | + if "../" in dbus_addr: |
21 | + return None |
22 | + |
23 | + # Don't support escaped values, multiple addresses, or multiple keys |
24 | + # and values |
25 | + for search in ["%", ",", ";"]: |
26 | + if search in dbus_addr: |
27 | + return None |
28 | + |
29 | + parts = dbus_addr.split("=") |
30 | + if len(parts) != 2: |
31 | + return None |
32 | + |
33 | + return parts[1] |
34 | + |
35 | + |
36 | def find_package_desktopfile(package): |
37 | '''Return a package's .desktop file. |
38 | |
39 | @@ -370,7 +397,7 @@ def get_config(section, setting, default=None, path=None, bool=False): |
40 | fd = None |
41 | f = None |
42 | if not get_config.config: |
43 | - get_config.config = ConfigParser() |
44 | + get_config.config = ConfigParser(interpolation=None) |
45 | |
46 | try: |
47 | fd = os.open(path, os.O_NOFOLLOW | os.O_RDONLY) |
48 | @@ -432,6 +459,22 @@ def get_uid_and_gid(contents): |
49 | return (real_uid, real_gid) |
50 | |
51 | |
52 | +def search_map(mapfd, uid): |
53 | + '''Search for an ID in a map fd''' |
54 | + for line in mapfd: |
55 | + fields = line.split() |
56 | + if len(fields) != 3: |
57 | + continue |
58 | + |
59 | + host_start = int(fields[1]) |
60 | + host_end = host_start + int(fields[2]) |
61 | + |
62 | + if uid >= host_start and uid <= host_end: |
63 | + return True |
64 | + |
65 | + return False |
66 | + |
67 | + |
68 | def get_boot_id(): |
69 | '''Gets the kernel boot id''' |
70 | |
71 | @@ -440,7 +483,18 @@ def get_boot_id(): |
72 | return boot_id |
73 | |
74 | |
75 | -def get_core_path(pid=None, exe=None, uid=None, timestamp=None): |
76 | +def get_process_path(proc_pid_fd=None): |
77 | + '''Gets the process path from a proc directory file descriptor''' |
78 | + |
79 | + if proc_pid_fd is None: |
80 | + return 'unknown' |
81 | + try: |
82 | + return os.readlink('exe', dir_fd=proc_pid_fd) |
83 | + except OSError: |
84 | + return 'unknown' |
85 | + |
86 | + |
87 | +def get_core_path(pid=None, exe=None, uid=None, timestamp=None, proc_pid_fd=None): |
88 | '''Get the path to a core file''' |
89 | |
90 | if pid is None: |
91 | @@ -453,9 +507,8 @@ def get_core_path(pid=None, exe=None, uid=None, timestamp=None): |
92 | timestamp = get_starttime(stat_contents) |
93 | |
94 | if exe is None: |
95 | - exe = 'unknown' |
96 | - else: |
97 | - exe = exe.replace('/', '_').replace('.', '_') |
98 | + exe = get_process_path(proc_pid_fd) |
99 | + exe = exe.replace('/', '_').replace('.', '_') |
100 | |
101 | if uid is None: |
102 | uid = os.getuid() |
103 | @@ -474,6 +527,7 @@ def find_core_files_by_uid(uid): |
104 | '''Searches the core file directory for files that belong to a |
105 | specified uid. Returns a list of lists containing the filename and |
106 | the file modification time.''' |
107 | + uid = str(uid) |
108 | core_files = [] |
109 | uid_files = [] |
110 | |
111 | @@ -482,7 +536,7 @@ def find_core_files_by_uid(uid): |
112 | |
113 | for f in core_files: |
114 | try: |
115 | - if f.split('.')[2] == str(uid): |
116 | + if f.split('.')[2] == uid: |
117 | time = os.path.getmtime(os.path.join(core_dir, f)) |
118 | uid_files.append([f, time]) |
119 | except (IndexError, FileNotFoundError): |
120 | @@ -497,7 +551,7 @@ def clean_core_directory(uid): |
121 | uid_files = find_core_files_by_uid(uid) |
122 | sorted_files = sorted(uid_files, key=itemgetter(1)) |
123 | |
124 | - # Substract a extra one to make room for the new core file |
125 | + # Subtract a extra one to make room for the new core file |
126 | if len(uid_files) > max_corefiles_per_uid - 1: |
127 | for x in range(len(uid_files) - max_corefiles_per_uid + 1): |
128 | os.remove(os.path.join(core_dir, sorted_files[0][0])) |
129 | diff --git a/data/apport b/data/apport |
130 | index 416173e..427f340 100755 |
131 | --- a/data/apport |
132 | +++ b/data/apport |
133 | @@ -200,9 +200,10 @@ def write_user_coredump(pid, timestamp, limit, from_report=None): |
134 | return |
135 | |
136 | (core_name, core_path) = apport.fileutils.get_core_path(pid, |
137 | - options.exe_path, |
138 | + options.executable_path, |
139 | crash_uid, |
140 | - timestamp) |
141 | + timestamp, |
142 | + proc_pid_fd) |
143 | |
144 | try: |
145 | # Limit number of core files to prevent DoS |
146 | @@ -269,12 +270,53 @@ def usable_ram(): |
147 | return (memfree + cached - writeback) * 1024 |
148 | |
149 | |
150 | +def _run_with_output_limit_and_timeout(args, output_limit, timeout, close_fds=True, env=None): |
151 | + '''Run command like subprocess.run() but with output limit and timeout. |
152 | + |
153 | + Return (stdout, stderr).''' |
154 | + |
155 | + stdout = b"" |
156 | + stderr = b"" |
157 | + |
158 | + process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
159 | + close_fds=close_fds, env=env) |
160 | + try: |
161 | + # Don't block so we don't deadlock |
162 | + os.set_blocking(process.stdout.fileno(), False) |
163 | + os.set_blocking(process.stderr.fileno(), False) |
164 | + |
165 | + for _ in range(timeout): |
166 | + alive = process.poll() is None |
167 | + |
168 | + while len(stdout) < output_limit and len(stderr) < output_limit: |
169 | + tempout = process.stdout.read(100) |
170 | + if tempout: |
171 | + stdout += tempout |
172 | + temperr = process.stderr.read(100) |
173 | + if temperr: |
174 | + stderr += temperr |
175 | + if not tempout and not temperr: |
176 | + break |
177 | + |
178 | + if not alive or len(stdout) >= output_limit or len(stderr) >= output_limit: |
179 | + break |
180 | + time.sleep(1) |
181 | + finally: |
182 | + process.kill() |
183 | + |
184 | + return stdout, stderr |
185 | + |
186 | + |
187 | def is_closing_session(): |
188 | '''Check if pid is in a closing user session. |
189 | |
190 | During that, crashes are common as the session D-BUS and X.org are going |
191 | away, etc. These crash reports are mostly noise, so should be ignored. |
192 | ''' |
193 | + # Sanity check, don't do anything for root processes |
194 | + if crash_uid == 0 or crash_gid == 0: |
195 | + return False |
196 | + |
197 | with open('environ', 'rb', opener=proc_pid_opener) as e: |
198 | env = e.read().split(b'\0') |
199 | for e in env: |
200 | @@ -285,6 +327,15 @@ def is_closing_session(): |
201 | error_log('is_closing_session(): no DBUS_SESSION_BUS_ADDRESS in environment') |
202 | return False |
203 | |
204 | + dbus_socket = apport.fileutils.get_dbus_socket(dbus_addr) |
205 | + if not dbus_socket: |
206 | + error_log('is_closing_session(): Could not determine DBUS socket.') |
207 | + return False |
208 | + |
209 | + if not os.path.exists(dbus_socket): |
210 | + error_log("is_closing_session(): DBUS socket doesn't exist.") |
211 | + return False |
212 | + |
213 | # We need to drop both the real and effective uid/gid before calling |
214 | # gdbus because DBUS_SESSION_BUS_ADDRESS is untrusted and may allow |
215 | # reading arbitrary files as a noncefile. We can't just drop effective |
216 | @@ -297,11 +348,11 @@ def is_closing_session(): |
217 | try: |
218 | os.setresgid(crash_gid, crash_gid, real_gid) |
219 | os.setresuid(crash_uid, crash_uid, real_uid) |
220 | - gdbus = subprocess.Popen(['/usr/bin/gdbus', 'call', '-e', '-d', |
221 | - 'org.gnome.SessionManager', '-o', '/org/gnome/SessionManager', '-m', |
222 | - 'org.gnome.SessionManager.IsSessionRunning'], stdout=subprocess.PIPE, |
223 | - stderr=subprocess.PIPE, env={'DBUS_SESSION_BUS_ADDRESS': dbus_addr}) |
224 | - (out, err) = gdbus.communicate() |
225 | + out, err = _run_with_output_limit_and_timeout(['/usr/bin/gdbus', 'call', '-e', '-d', |
226 | + 'org.gnome.SessionManager', '-o', '/org/gnome/SessionManager', '-m', |
227 | + 'org.gnome.SessionManager.IsSessionRunning', '-t', '5'], |
228 | + 1000, 5, env={'DBUS_SESSION_BUS_ADDRESS': dbus_addr}) |
229 | + |
230 | if err: |
231 | error_log('gdbus call error: ' + err.decode('UTF-8')) |
232 | except OSError as e: |
233 | @@ -384,38 +435,46 @@ def parse_arguments(): |
234 | parser.add_argument("-s", "--signal-number", help="signal number (%%s)") |
235 | parser.add_argument("-c", "--core-ulimit", help="core ulimit (%%c)") |
236 | parser.add_argument("-d", "--dump-mode", help="dump mode (%%d)") |
237 | - parser.add_argument("-P", "--global-pid", nargs='?', help="pid in root namespace (%%P)") |
238 | - parser.add_argument("-E", "--executable-path", nargs='?', help="path of executable (%%E)") |
239 | + parser.add_argument("-P", "--global-pid", help="pid in root namespace (%%P)") |
240 | + parser.add_argument("-u", "--uid", type=int, help="real UID (%%u)") |
241 | + parser.add_argument("-g", "--gid", type=int, help="real GID (%%g)") |
242 | + parser.add_argument("executable_path", nargs='*', help="path of executable (%%E)") |
243 | |
244 | options, rest = parser.parse_known_args() |
245 | |
246 | - if options.pid is not None: |
247 | - for arg in rest: |
248 | - error_log("Unknown argument: %s" % arg) |
249 | - |
250 | - elif len(rest) in (4, 5, 6, 7): |
251 | + # Legacy command line needs to remain for the scenario where a more |
252 | + # recent apport is running inside a container with an older apport |
253 | + # running on the host. |
254 | + if options.pid is None and len(sys.argv) == 5: |
255 | # Translate legacy command line |
256 | - options.pid = rest[0] |
257 | - options.signal_number = rest[1] |
258 | - options.core_ulimit = rest[2] |
259 | - options.dump_mode = rest[3] |
260 | - try: |
261 | - options.global_pid = rest[4] |
262 | - except IndexError: |
263 | - options.global_pid = None |
264 | - try: |
265 | - options.exe_path = rest[5].replace('!', '/') |
266 | - except IndexError: |
267 | - options.exe_path = None |
268 | - try: |
269 | - if rest[6] == '(deleted)': |
270 | - options.exe_path += ' %s' % rest[6] |
271 | - except IndexError: |
272 | - pass |
273 | - else: |
274 | + return argparse.Namespace( |
275 | + pid=sys.argv[1], |
276 | + signal_number=sys.argv[2], |
277 | + core_ulimit=sys.argv[3], |
278 | + dump_mode=sys.argv[4], |
279 | + global_pid=None, |
280 | + uid=None, |
281 | + gid=None, |
282 | + executable_path=None, |
283 | + ) |
284 | + |
285 | + if options.pid is None: |
286 | parser.print_usage() |
287 | sys.exit(1) |
288 | |
289 | + for arg in rest: |
290 | + error_log("Unknown argument: %s" % arg) |
291 | + |
292 | + # In kernels before 5.3.0, an executable path with spaces may be split |
293 | + # into separate arguments. If options.executable_path is a list, join |
294 | + # it back into a string. Also restore directory separators. |
295 | + if isinstance(options.executable_path, list): |
296 | + options.executable_path = " ".join(options.executable_path) |
297 | + options.executable_path = options.executable_path.replace('!', '/') |
298 | + # Sanity check to prevent trickery later on |
299 | + if '../' in options.executable_path: |
300 | + options.executable_path = None |
301 | + |
302 | return options |
303 | |
304 | |
305 | @@ -485,32 +544,34 @@ if options.global_pid is not None: |
306 | |
307 | # Instead, attempt to find apport inside the container and |
308 | # forward the process information there. |
309 | - if not os.path.exists('/proc/%d/root/run/apport.socket' % host_pid): |
310 | - error_log('host pid %s crashed in a container without apport support' % |
311 | - options.global_pid) |
312 | - sys.exit(0) |
313 | |
314 | proc_host_pid_fd = os.open('/proc/%d' % host_pid, os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) |
315 | |
316 | def proc_host_pid_opener(path, flags): |
317 | return os.open(path, flags, dir_fd=proc_host_pid_fd) |
318 | |
319 | + # Validate that the target socket is owned by the user namespace of the process |
320 | + try: |
321 | + sock_fd = os.open("root/run/apport.socket", os.O_RDONLY | os.O_PATH, dir_fd=proc_host_pid_fd) |
322 | + socket_uid = os.fstat(sock_fd).st_uid |
323 | + except FileNotFoundError: |
324 | + error_log('host pid %s crashed in a container without apport support' % |
325 | + options.global_pid) |
326 | + sys.exit(0) |
327 | + |
328 | + try: |
329 | + with open("uid_map", "r", opener=proc_host_pid_opener) as fd: |
330 | + if not apport.fileutils.search_map(fd, socket_uid): |
331 | + error_log("user is trying to trick apport into accessing a socket that doesn't belong to the container") |
332 | + sys.exit(0) |
333 | + except FileNotFoundError: |
334 | + pass |
335 | + |
336 | # Validate that the crashed binary is owned by the user namespace of the process |
337 | task_uid = os.stat("exe", dir_fd=proc_host_pid_fd).st_uid |
338 | try: |
339 | with open("uid_map", "r", opener=proc_host_pid_opener) as fd: |
340 | - for line in fd: |
341 | - fields = line.split() |
342 | - if len(fields) != 3: |
343 | - continue |
344 | - |
345 | - host_start = int(fields[1]) |
346 | - host_end = host_start + int(fields[2]) |
347 | - |
348 | - if task_uid >= host_start and task_uid <= host_end: |
349 | - break |
350 | - |
351 | - else: |
352 | + if not apport.fileutils.search_map(fd, task_uid): |
353 | error_log("host pid %s crashed in a container with no access to the binary" |
354 | % options.global_pid) |
355 | sys.exit(0) |
356 | @@ -520,45 +581,28 @@ if options.global_pid is not None: |
357 | task_gid = os.stat("exe", dir_fd=proc_host_pid_fd).st_gid |
358 | try: |
359 | with open("gid_map", "r", opener=proc_host_pid_opener) as fd: |
360 | - for line in fd: |
361 | - fields = line.split() |
362 | - if len(fields) != 3: |
363 | - continue |
364 | - |
365 | - host_start = int(fields[1]) |
366 | - host_end = host_start + int(fields[2]) |
367 | - |
368 | - if task_gid >= host_start and task_gid <= host_end: |
369 | - break |
370 | - |
371 | - else: |
372 | + if not apport.fileutils.search_map(fd, task_gid): |
373 | error_log("host pid %s crashed in a container with no access to the binary" |
374 | % options.global_pid) |
375 | sys.exit(0) |
376 | except FileNotFoundError: |
377 | pass |
378 | |
379 | - # Chdir and chroot to the task |
380 | - # WARNING: After this point, all "import" calls are security issues |
381 | - __builtins__.__dict__['__import__'] = None |
382 | - |
383 | - host_cwd = os.open('cwd', os.O_RDONLY | os.O_PATH | os.O_DIRECTORY, dir_fd=proc_host_pid_fd) |
384 | - |
385 | - os.chdir(host_cwd) |
386 | - # WARNING: we really should be using a file descriptor here, |
387 | - # but os.chroot won't take it |
388 | - os.chroot(os.readlink('root', dir_fd=proc_host_pid_fd)) |
389 | - |
390 | + # Now open the socket |
391 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
392 | try: |
393 | - sock.connect('/run/apport.socket') |
394 | + sock.connect('/proc/self/fd/%s' % sock_fd) |
395 | except Exception: |
396 | error_log('host pid %s crashed in a container with a broken apport' % |
397 | options.global_pid) |
398 | sys.exit(0) |
399 | |
400 | - # Send all arguments except for the first (exec path) and last (global pid) |
401 | - args = ' '.join(sys.argv[1:5]) |
402 | + # Send main arguments only |
403 | + # Older apport in containers doesn't support positional arguments |
404 | + args = "%s %s %s %s" % (options.pid, |
405 | + options.signal_number, |
406 | + options.core_ulimit, |
407 | + options.dump_mode) |
408 | try: |
409 | sock.sendmsg([args.encode()], [ |
410 | # Send a ucred containing the global pid |
411 | @@ -567,7 +611,7 @@ if options.global_pid is not None: |
412 | # Send fd 0 (the coredump) |
413 | (socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array('i', [0]))]) |
414 | sock.shutdown(socket.SHUT_RDWR) |
415 | - except socket.timeout: |
416 | + except Exception: |
417 | error_log('Container apport failed to process crash within 30s') |
418 | |
419 | sys.exit(0) |
420 | @@ -598,16 +642,30 @@ try: |
421 | |
422 | get_pid_info(pid) |
423 | |
424 | - # Check if the process was replaced after the crash happened. |
425 | - # Ideally we'd use the time of dump value provided by the kernel, |
426 | - # but this would be a backwards-incompatible change that would |
427 | - # require adding support to apport outside and inside of containers. |
428 | + # Sanity check to make sure the process wasn't replaced after the crash |
429 | + # happened. The start time isn't fine-grained enough to be an adequate |
430 | + # security check. |
431 | apport_start = get_apport_starttime() |
432 | process_start = get_process_starttime() |
433 | if process_start > apport_start: |
434 | error_log('process was replaced after Apport started, ignoring') |
435 | sys.exit(0) |
436 | |
437 | + # Make sure the process uid/gid match the ones provided by the kernel |
438 | + # if available, if not, it may have been replaced |
439 | + if (options.uid is not None) and (options.gid is not None): |
440 | + if (options.uid != crash_uid) or (options.gid != crash_gid): |
441 | + error_log("process uid/gid doesn't match expected, ignoring") |
442 | + sys.exit(0) |
443 | + |
444 | + # check if the executable was modified after the process started (e. g. |
445 | + # package got upgraded in between). |
446 | + exe_mtime = os.stat('exe', dir_fd=proc_pid_fd).st_mtime |
447 | + process_mtime = os.lstat('cmdline', dir_fd=proc_pid_fd).st_mtime |
448 | + if not os.path.exists(os.readlink('exe', dir_fd=proc_pid_fd)) or exe_mtime > process_mtime: |
449 | + error_log('executable was modified after program start, ignoring') |
450 | + sys.exit(0) |
451 | + |
452 | error_log('called for pid %s, signal %s, core limit %s, dump mode %s' % (pid, signum, core_ulimit, dump_mode)) |
453 | |
454 | try: |
455 | @@ -631,14 +689,6 @@ try: |
456 | write_user_coredump(pid, process_start, core_ulimit) |
457 | sys.exit(0) |
458 | |
459 | - # check if the executable was modified after the process started (e. g. |
460 | - # package got upgraded in between) |
461 | - exe_mtime = os.stat('exe', dir_fd=proc_pid_fd).st_mtime |
462 | - process_mtime = os.lstat('cmdline', dir_fd=proc_pid_fd).st_mtime |
463 | - if not os.path.exists(os.readlink('exe', dir_fd=proc_pid_fd)) or exe_mtime > process_mtime: |
464 | - error_log('executable was modified after program start, ignoring') |
465 | - sys.exit(0) |
466 | - |
467 | info = apport.Report('Crash') |
468 | info['Signal'] = signum |
469 | core_size_limit = usable_ram() * 3 / 4 |
470 | @@ -647,9 +697,8 @@ try: |
471 | |
472 | # We already need this here to figure out the ExecutableName (for scripts, |
473 | # etc). |
474 | - |
475 | - if options.exe_path is not None and os.path.exists(options.exe_path): |
476 | - info['ExecutablePath'] = options.exe_path |
477 | + if options.executable_path is not None and os.path.exists(options.executable_path): |
478 | + info['ExecutablePath'] = options.executable_path |
479 | |
480 | # Drop privileges temporarily to make sure that we don't |
481 | # include information in the crash report that the user should |
482 | @@ -739,7 +788,7 @@ try: |
483 | # os.O_CREAT|os.O_EXCL |
484 | os.unlink(report) |
485 | else: |
486 | - error_log('apport: report %s already exists and unseen, doing nothing to avoid disk usage DoS' % report) |
487 | + error_log('apport: report %s already exists and unseen, skipping to avoid disk usage DoS' % report) |
488 | write_user_coredump(pid, process_start, core_ulimit) |
489 | sys.exit(0) |
490 | # we prefer having a file mode of 0 while writing; |
491 | diff --git a/etc/init.d/apport b/etc/init.d/apport |
492 | index 4997b83..1e10d02 100755 |
493 | --- a/etc/init.d/apport |
494 | +++ b/etc/init.d/apport |
495 | @@ -50,13 +50,9 @@ do_start() |
496 | rm -f /var/lib/pm-utils/resume-hang.log |
497 | fi |
498 | |
499 | - # Old compatibility mode, switch later to second one |
500 | - if true; then |
501 | - echo "|$AGENT %p %s %c %d %P %E" > /proc/sys/kernel/core_pattern |
502 | - else |
503 | - echo "|$AGENT -p%p -s%s -c%c -d%d -P%P -E%E" > /proc/sys/kernel/core_pattern |
504 | - fi |
505 | + echo "|$AGENT -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E" > /proc/sys/kernel/core_pattern |
506 | echo 2 > /proc/sys/fs/suid_dumpable |
507 | + echo 10 > /proc/sys/kernel/core_pipe_limit |
508 | } |
509 | |
510 | # |
511 | @@ -70,6 +66,7 @@ do_stop() |
512 | # 2 if daemon could not be stopped |
513 | # other if a failure occurred |
514 | |
515 | + echo 0 > /proc/sys/kernel/core_pipe_limit |
516 | echo 0 > /proc/sys/fs/suid_dumpable |
517 | |
518 | # Check for a hung resume. If we find one try and grab everything |
519 | diff --git a/tests/test_fileutils.py b/tests/test_fileutils.py |
520 | index 23f9984..0b973a4 100644 |
521 | --- a/tests/test_fileutils.py |
522 | +++ b/tests/test_fileutils.py |
523 | @@ -372,8 +372,32 @@ f6423dfbc4faf022e58b4d3f5ff71a70 %s |
524 | self.assertEqual(apport.fileutils.get_config('spethial', 'nope', 'moo'), 'moo') |
525 | apport.fileutils.get_config.config = None # trash cache |
526 | |
527 | + # interpolation |
528 | + f.write(b'[inter]\none=1\ntwo = TWO\ntest = %(two)s\n') |
529 | + f.flush() |
530 | + self.assertEqual(apport.fileutils.get_config('inter', 'one'), '1') |
531 | + self.assertEqual(apport.fileutils.get_config('inter', 'two'), 'TWO') |
532 | + self.assertEqual(apport.fileutils.get_config('inter', 'test'), '%(two)s') |
533 | + apport.fileutils.get_config.config = None # trash cache |
534 | + |
535 | f.close() |
536 | |
537 | + def test_get_dbus_socket(self): |
538 | + '''get_dbus_socket()''' |
539 | + |
540 | + tests = [("unix:path=/run/user/1000/bus", "/run/user/1000/bus"), |
541 | + ("unix:path=/run/user/1000/bus;unix:path=/run/user/0/bus", None), |
542 | + ("unix:path=%2Frun/user/1000/bus", None), |
543 | + ("unix:path=/run/user/1000/bus,path=/run/user/0/bus", None), |
544 | + ("unix:path=/etc/passwd", None), |
545 | + ("unix:path=/run/user/../../etc/passwd", None), |
546 | + ("unix:path=/run/user/1000/bus=", None), |
547 | + ("", None), |
548 | + ("tcp:host=localhost,port=8100", None)] |
549 | + |
550 | + for (addr, result) in tests: |
551 | + self.assertEqual(apport.fileutils.get_dbus_socket(addr), result) |
552 | + |
553 | def test_shared_libraries(self): |
554 | '''shared_libraries()''' |
555 |
Merged. Thanks.