]>
Commit | Line | Data |
---|---|---|
3a6ce677 BR |
1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
2 | ||
3 | #include <fcntl.h> | |
4 | #include <getopt.h> | |
5 | #include <sys/mount.h> | |
6 | #include <unistd.h> | |
7 | ||
8 | #include "capability-util.h" | |
9 | #include "discover-image.h" | |
10 | #include "dissect-image.h" | |
11 | #include "env-util.h" | |
12 | #include "escape.h" | |
13 | #include "extension-release.h" | |
14 | #include "fd-util.h" | |
15 | #include "fileio.h" | |
16 | #include "format-table.h" | |
17 | #include "fs-util.h" | |
18 | #include "hashmap.h" | |
19 | #include "log.h" | |
20 | #include "main-func.h" | |
21 | #include "missing_magic.h" | |
22 | #include "mkdir.h" | |
23 | #include "mount-util.h" | |
24 | #include "mountpoint-util.h" | |
25 | #include "os-util.h" | |
26 | #include "pager.h" | |
27 | #include "parse-argument.h" | |
28 | #include "parse-util.h" | |
29 | #include "pretty-print.h" | |
30 | #include "process-util.h" | |
31 | #include "sort-util.h" | |
32 | #include "stat-util.h" | |
33 | #include "terminal-util.h" | |
34 | #include "user-util.h" | |
35 | #include "verbs.h" | |
36 | ||
37 | static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default */ | |
38 | static char *arg_root = NULL; | |
39 | static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; | |
40 | static PagerFlags arg_pager_flags = 0; | |
41 | static bool arg_legend = true; | |
42 | static bool arg_force = false; | |
43 | ||
44 | STATIC_DESTRUCTOR_REGISTER(arg_hierarchies, strv_freep); | |
45 | STATIC_DESTRUCTOR_REGISTER(arg_root, freep); | |
46 | ||
47 | static int is_our_mount_point(const char *p) { | |
48 | _cleanup_free_ char *buf = NULL, *f = NULL; | |
49 | struct stat st; | |
50 | dev_t dev; | |
51 | int r; | |
52 | ||
53 | r = path_is_mount_point(p, NULL, 0); | |
54 | if (r == -ENOENT) { | |
55 | log_debug_errno(r, "Hierarchy '%s' doesn't exist.", p); | |
56 | return false; | |
57 | } | |
58 | if (r < 0) | |
59 | return log_error_errno(r, "Failed to determine whether '%s' is a mount point: %m", p); | |
60 | if (r == 0) { | |
61 | log_debug("Hierarchy '%s' is not a mount point, skipping.", p); | |
62 | return false; | |
63 | } | |
64 | ||
65 | /* So we know now that it's a mount point. Now let's check if it's one of ours, so that we don't | |
66 | * accidentally unmount the user's own /usr/ but just the mounts we established ourselves. We do this | |
67 | * check by looking into the metadata directory we place in merged mounts: if the file | |
68 | * .systemd-sysext/dev contains the major/minor device pair of the mount we have a good reason to | |
69 | * believe this is one of our mounts. This thorough check has the benefit that we aren't easily | |
70 | * confused if people tar up one of our merged trees and untar them elsewhere where we might mistake | |
71 | * them for a live sysext tree. */ | |
72 | ||
73 | f = path_join(p, ".systemd-sysext/dev"); | |
74 | if (!f) | |
75 | return log_oom(); | |
76 | ||
77 | r = read_one_line_file(f, &buf); | |
78 | if (r == -ENOENT) { | |
79 | log_debug("Hierarchy '%s' does not carry a .systemd-sysext/dev file, not a sysext merged tree.", p); | |
80 | return false; | |
81 | } | |
82 | if (r < 0) | |
83 | return log_error_errno(r, "Failed to determine whether hierarchy '%s' contains '.systemd-sysext/dev': %m", p); | |
84 | ||
85 | r = parse_dev(buf, &dev); | |
86 | if (r < 0) | |
87 | return log_error_errno(r, "Failed to parse device major/minor stored in '.systemd-sysext/dev' file on '%s': %m", p); | |
88 | ||
89 | if (lstat(p, &st) < 0) | |
90 | return log_error_errno(r, "Failed to stat %s: %m", p); | |
91 | ||
92 | if (st.st_dev != dev) { | |
93 | log_debug("Hierarchy '%s' reports a different device major/minor than what we are seeing, assuming offline copy.", p); | |
94 | return false; | |
95 | } | |
96 | ||
97 | return true; | |
98 | } | |
99 | ||
100 | static int unmerge_hierarchy(const char *p) { | |
101 | int r; | |
102 | ||
103 | for (;;) { | |
104 | /* We only unmount /usr/ if it is a mount point and really one of ours, in order not to break | |
105 | * systems where /usr/ is a mount point of its own already. */ | |
106 | ||
107 | r = is_our_mount_point(p); | |
108 | if (r < 0) | |
109 | return r; | |
110 | if (r == 0) | |
111 | break; | |
112 | ||
113 | r = umount_verbose(LOG_ERR, p, MNT_DETACH|UMOUNT_NOFOLLOW); | |
114 | if (r < 0) | |
115 | return log_error_errno(r, "Failed to unmount file system '%s': %m", p); | |
116 | ||
117 | log_info("Unmerged '%s'.", p); | |
118 | } | |
119 | ||
120 | return 0; | |
121 | } | |
122 | ||
123 | static int unmerge(void) { | |
124 | int r, ret = 0; | |
125 | char **p; | |
126 | ||
127 | STRV_FOREACH(p, arg_hierarchies) { | |
128 | _cleanup_free_ char *resolved = NULL; | |
129 | ||
130 | r = chase_symlinks(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL); | |
131 | if (r == -ENOENT) { | |
132 | log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p); | |
133 | continue; | |
134 | } | |
135 | if (r < 0) { | |
136 | log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p); | |
137 | if (ret == 0) | |
138 | ret = r; | |
139 | ||
140 | continue; | |
141 | } | |
142 | ||
143 | r = unmerge_hierarchy(resolved); | |
144 | if (r < 0 && ret == 0) | |
145 | ret = r; | |
146 | } | |
147 | ||
148 | return ret; | |
149 | } | |
150 | ||
151 | static int verb_unmerge(int argc, char **argv, void *userdata) { | |
152 | ||
153 | if (!have_effective_cap(CAP_SYS_ADMIN)) | |
154 | return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged."); | |
155 | ||
156 | return unmerge(); | |
157 | } | |
158 | ||
159 | static int verb_status(int argc, char **argv, void *userdata) { | |
160 | _cleanup_(table_unrefp) Table *t = NULL; | |
161 | int r, ret = 0; | |
162 | char **p; | |
163 | ||
164 | t = table_new("hierarchy", "extensions", "since"); | |
165 | if (!t) | |
166 | return log_oom(); | |
167 | ||
168 | (void) table_set_empty_string(t, "-"); | |
169 | ||
170 | STRV_FOREACH(p, arg_hierarchies) { | |
171 | _cleanup_free_ char *resolved = NULL, *f = NULL, *buf = NULL; | |
172 | _cleanup_strv_free_ char **l = NULL; | |
173 | struct stat st; | |
174 | ||
175 | r = chase_symlinks(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL); | |
176 | if (r == -ENOENT) { | |
177 | log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p); | |
178 | continue; | |
179 | } | |
180 | if (r < 0) { | |
181 | log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p); | |
182 | goto inner_fail; | |
183 | } | |
184 | ||
185 | r = is_our_mount_point(resolved); | |
186 | if (r < 0) | |
187 | goto inner_fail; | |
188 | if (r == 0) { | |
189 | r = table_add_many( | |
190 | t, | |
191 | TABLE_PATH, *p, | |
192 | TABLE_STRING, "none", | |
193 | TABLE_SET_COLOR, ansi_grey(), | |
194 | TABLE_EMPTY); | |
195 | if (r < 0) | |
196 | return table_log_add_error(r); | |
197 | ||
198 | continue; | |
199 | } | |
200 | ||
201 | f = path_join(*p, ".systemd-sysext/extensions"); | |
202 | if (!f) | |
203 | return log_oom(); | |
204 | ||
205 | r = read_full_file(f, &buf, NULL); | |
206 | if (r < 0) | |
207 | return log_error_errno(r, "Failed to open '%s': %m", f); | |
208 | ||
209 | l = strv_split_newlines(buf); | |
210 | if (!l) | |
211 | return log_oom(); | |
212 | ||
213 | if (stat(*p, &st) < 0) | |
214 | return log_error_errno(r, "Failed to stat() '%s': %m", *p); | |
215 | ||
216 | r = table_add_many( | |
217 | t, | |
218 | TABLE_PATH, *p, | |
219 | TABLE_STRV, l, | |
220 | TABLE_TIMESTAMP, timespec_load(&st.st_mtim)); | |
221 | if (r < 0) | |
222 | return table_log_add_error(r); | |
223 | ||
224 | continue; | |
225 | ||
226 | inner_fail: | |
227 | if (ret == 0) | |
228 | ret = r; | |
229 | } | |
230 | ||
231 | (void) table_set_sort(t, (size_t) 0); | |
232 | ||
233 | r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); | |
234 | if (r < 0) | |
235 | return r; | |
236 | ||
237 | return ret; | |
238 | } | |
239 | ||
240 | static int mount_overlayfs( | |
241 | const char *where, | |
242 | char **layers) { | |
243 | ||
244 | _cleanup_free_ char *options = NULL; | |
245 | bool separator = false; | |
246 | char **l; | |
247 | int r; | |
248 | ||
249 | assert(where); | |
250 | ||
251 | options = strdup("lowerdir="); | |
252 | if (!options) | |
253 | return log_oom(); | |
254 | ||
255 | STRV_FOREACH(l, layers) { | |
256 | _cleanup_free_ char *escaped = NULL; | |
257 | ||
258 | escaped = shell_escape(*l, ",:"); | |
259 | if (!escaped) | |
260 | return log_oom(); | |
261 | ||
262 | if (!strextend(&options, separator ? ":" : "", escaped)) | |
263 | return log_oom(); | |
264 | ||
265 | separator = true; | |
266 | } | |
267 | ||
268 | /* Now mount the actual overlayfs */ | |
269 | r = mount_nofollow_verbose(LOG_ERR, "sysext", where, "overlay", MS_RDONLY, options); | |
270 | if (r < 0) | |
271 | return r; | |
272 | ||
273 | return 0; | |
274 | } | |
275 | ||
276 | static int merge_hierarchy( | |
277 | const char *hierarchy, | |
278 | char **extensions, | |
279 | char **paths, | |
280 | const char *meta_path, | |
281 | const char *overlay_path) { | |
282 | ||
283 | _cleanup_free_ char *resolved_hierarchy = NULL, *f = NULL, *buf = NULL; | |
284 | _cleanup_strv_free_ char **layers = NULL; | |
285 | struct stat st; | |
286 | char **p; | |
287 | int r; | |
288 | ||
289 | assert(hierarchy); | |
290 | assert(meta_path); | |
291 | assert(overlay_path); | |
292 | ||
293 | /* Resolve the path of the host's version of the hierarchy, i.e. what we want to use as lowest layer | |
294 | * in the overlayfs stack. */ | |
295 | r = chase_symlinks(hierarchy, arg_root, CHASE_PREFIX_ROOT, &resolved_hierarchy, NULL); | |
296 | if (r == -ENOENT) | |
297 | log_debug_errno(r, "Hierarchy '%s' on host doesn't exist, not merging.", hierarchy); | |
298 | else if (r < 0) | |
299 | return log_error_errno(r, "Failed to resolve host hierarchy '%s': %m", hierarchy); | |
300 | else { | |
301 | r = dir_is_empty(resolved_hierarchy); | |
302 | if (r < 0) | |
303 | return log_error_errno(r, "Failed to check if host hierarchy '%s' is empty: %m", resolved_hierarchy); | |
304 | if (r > 0) { | |
305 | log_debug("Host hierarchy '%s' is empty, not merging.", resolved_hierarchy); | |
306 | resolved_hierarchy = mfree(resolved_hierarchy); | |
307 | } | |
308 | } | |
309 | ||
310 | /* Let's generate a metadata file that lists all extensions we took into account for this | |
311 | * hierarchy. We include this in the final fs, to make things nicely discoverable and | |
312 | * recognizable. */ | |
313 | f = path_join(meta_path, ".systemd-sysext/extensions"); | |
314 | if (!f) | |
315 | return log_oom(); | |
316 | ||
317 | buf = strv_join(extensions, "\n"); | |
318 | if (!buf) | |
319 | return log_oom(); | |
320 | ||
321 | r = write_string_file(f, buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755); | |
322 | if (r < 0) | |
323 | return log_error_errno(r, "Failed to write extension meta file '%s': %m", f); | |
324 | ||
325 | /* Put the meta path (i.e. our synthesized stuff) at the top of the layer stack */ | |
326 | layers = strv_new(meta_path); | |
327 | if (!layers) | |
328 | return log_oom(); | |
329 | ||
330 | /* Put the extensions in the middle */ | |
331 | STRV_FOREACH(p, paths) { | |
332 | _cleanup_free_ char *resolved = NULL; | |
333 | ||
334 | r = chase_symlinks(hierarchy, *p, CHASE_PREFIX_ROOT, &resolved, NULL); | |
335 | if (r == -ENOENT) { | |
336 | log_debug_errno(r, "Hierarchy '%s' in extension '%s' doesn't exist, not merging.", hierarchy, *p); | |
337 | continue; | |
338 | } | |
339 | if (r < 0) | |
340 | return log_error_errno(r, "Failed to resolve hierarchy '%s' in extension '%s': %m", hierarchy, *p); | |
341 | ||
342 | r = dir_is_empty(resolved); | |
343 | if (r < 0) | |
344 | return log_error_errno(r, "Failed to check if hierarchy '%s' in extension '%s' is empty: %m", resolved, *p); | |
345 | if (r > 0) { | |
346 | log_debug("Hierarchy '%s' in extension '%s' is empty, not merging.", hierarchy, *p); | |
347 | continue; | |
348 | } | |
349 | ||
350 | r = strv_consume(&layers, TAKE_PTR(resolved)); | |
351 | if (r < 0) | |
352 | return log_oom(); | |
353 | } | |
354 | ||
355 | if (!layers[1]) /* No extension with files in this hierarchy? Then don't do anything. */ | |
356 | return 0; | |
357 | ||
358 | if (resolved_hierarchy) { | |
359 | /* Add the host hierarchy as last (lowest) layer in the stack */ | |
360 | r = strv_consume(&layers, TAKE_PTR(resolved_hierarchy)); | |
361 | if (r < 0) | |
362 | return log_oom(); | |
363 | } | |
364 | ||
365 | r = mkdir_p(overlay_path, 0700); | |
366 | if (r < 0) | |
367 | return log_error_errno(r, "Failed to make directory '%s': %m", overlay_path); | |
368 | ||
369 | r = mount_overlayfs(overlay_path, layers); | |
370 | if (r < 0) | |
371 | return r; | |
372 | ||
373 | /* The overlayfs superblock is read-only. Let's also mark the bind mount read-only. Extra turbo safety 😎 */ | |
374 | r = bind_remount_recursive(overlay_path, MS_RDONLY, MS_RDONLY, NULL); | |
375 | if (r < 0) | |
376 | return log_error_errno(r, "Failed to make bind mount '%s' read-only: %m", overlay_path); | |
377 | ||
378 | /* Now we have mounted the new file system. Let's now figure out its .st_dev field, and make that | |
379 | * available in the metadata directory. This is useful to detect whether the metadata dir actually | |
380 | * belongs to the fs it is found on: if .st_dev of the top-level mount matches it, it's pretty likely | |
381 | * we are looking at a live sysext tree, and not an unpacked tar or so of one. */ | |
382 | if (stat(overlay_path, &st) < 0) | |
383 | return log_error_errno(r, "Failed to stat mount '%s': %m", overlay_path); | |
384 | ||
385 | free(f); | |
386 | f = path_join(meta_path, ".systemd-sysext/dev"); | |
387 | if (!f) | |
388 | return log_oom(); | |
389 | ||
390 | r = write_string_filef(f, WRITE_STRING_FILE_CREATE, "%u:%u", major(st.st_dev), minor(st.st_dev)); | |
391 | if (r < 0) | |
392 | return log_error_errno(r, "Failed to write '%s': %m", f); | |
393 | ||
394 | /* Make sure the top-level dir has an mtime marking the point we established the merge */ | |
395 | if (utimensat(AT_FDCWD, meta_path, NULL, AT_SYMLINK_NOFOLLOW) < 0) | |
396 | return log_error_errno(r, "Failed fix mtime of '%s': %m", meta_path); | |
397 | ||
398 | return 1; | |
399 | } | |
400 | ||
401 | static int strverscmp_improvedp(char *const* a, char *const* b) { | |
402 | /* usable in qsort() for sorting a string array with strverscmp_improved() */ | |
403 | return strverscmp_improved(*a, *b); | |
404 | } | |
405 | ||
406 | static int validate_version( | |
407 | const char *root, | |
408 | const Image *img, | |
409 | const char *host_os_release_id, | |
410 | const char *host_os_release_version_id, | |
411 | const char *host_os_release_sysext_level) { | |
412 | ||
413 | int r; | |
414 | ||
415 | assert(root); | |
416 | assert(img); | |
417 | ||
418 | if (arg_force) { | |
419 | log_debug("Force mode enabled, skipping version validation."); | |
420 | return 1; | |
421 | } | |
422 | ||
423 | /* Insist that extension images do not overwrite the underlying OS release file (it's fine if | |
424 | * they place one in /etc/os-release, i.e. where things don't matter, as they aren't | |
425 | * merged.) */ | |
426 | r = chase_symlinks("/usr/lib/os-release", root, CHASE_PREFIX_ROOT, NULL, NULL); | |
427 | if (r < 0) { | |
428 | if (r != -ENOENT) | |
429 | return log_error_errno(r, "Failed to determine whether /usr/lib/os-release exists in the extension image: %m"); | |
430 | } else | |
431 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), | |
432 | "Extension image contains /usr/lib/os-release file, which is not allowed (it may carry /etc/os-release), refusing."); | |
433 | ||
434 | return extension_release_validate( | |
435 | img->name, | |
436 | host_os_release_id, | |
437 | host_os_release_version_id, | |
438 | host_os_release_sysext_level, | |
439 | img->extension_release); | |
440 | } | |
441 | ||
442 | static int merge_subprocess(Hashmap *images, const char *workspace) { | |
443 | _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL, *host_os_release_sysext_level = NULL, | |
444 | *buf = NULL; | |
445 | _cleanup_strv_free_ char **extensions = NULL, **paths = NULL; | |
446 | size_t n_extensions = 0; | |
447 | unsigned n_ignored = 0; | |
448 | Image *img; | |
449 | char **h; | |
450 | int r; | |
451 | ||
452 | /* Mark the whole of /run as MS_SLAVE, so that we can mount stuff below it that doesn't show up on | |
453 | * the host otherwise. */ | |
454 | r = mount_nofollow_verbose(LOG_ERR, NULL, "/run", NULL, MS_SLAVE|MS_REC, NULL); | |
455 | if (r < 0) | |
456 | return log_error_errno(r, "Failed to remount /run/ MS_SLAVE: %m"); | |
457 | ||
458 | /* Let's create the workspace if it's missing */ | |
459 | r = mkdir_p(workspace, 0700); | |
460 | if (r < 0) | |
461 | return log_error_errno(r, "Failed to create /run/systemd/sysext: %m"); | |
462 | ||
463 | /* Let's mount a tmpfs to our workspace. This way we don't need to clean up the inodes we mount over, | |
464 | * but let the kernel do that entirely automatically, once our namespace dies. Note that this file | |
465 | * system won't be visible to anyone but us, since we opened our own namespace and then made the | |
466 | * /run/ hierarchy (which our workspace is contained in) MS_SLAVE, see above. */ | |
467 | r = mount_nofollow_verbose(LOG_ERR, "sysexit", workspace, "tmpfs", 0, "mode=0700"); | |
468 | if (r < 0) | |
469 | return r; | |
470 | ||
471 | /* Acquire host OS release info, so that we can compare it with the extension's data */ | |
472 | r = parse_os_release( | |
473 | arg_root, | |
474 | "ID", &host_os_release_id, | |
475 | "VERSION_ID", &host_os_release_version_id, | |
476 | "SYSEXT_LEVEL", &host_os_release_sysext_level); | |
477 | if (r < 0) | |
478 | return log_error_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(arg_root)); | |
479 | ||
480 | /* Let's now mount all images */ | |
481 | HASHMAP_FOREACH(img, images) { | |
482 | _cleanup_free_ char *p = NULL; | |
483 | ||
484 | p = path_join(workspace, "extensions", img->name); | |
485 | if (!p) | |
486 | return log_oom(); | |
487 | ||
488 | r = mkdir_p(p, 0700); | |
489 | if (r < 0) | |
490 | return log_error_errno(r, "Failed to create %s: %m", p); | |
491 | ||
492 | switch (img->type) { | |
493 | case IMAGE_DIRECTORY: | |
494 | case IMAGE_SUBVOLUME: | |
495 | r = mount_nofollow_verbose(LOG_ERR, img->path, p, NULL, MS_BIND, NULL); | |
496 | if (r < 0) | |
497 | return r; | |
498 | ||
499 | /* Make this a read-only bind mount */ | |
500 | r = bind_remount_recursive(p, MS_RDONLY, MS_RDONLY, NULL); | |
501 | if (r < 0) | |
502 | return log_error_errno(r, "Failed to make bind mount '%s' read-only: %m", p); | |
503 | ||
504 | break; | |
505 | ||
506 | case IMAGE_RAW: | |
507 | case IMAGE_BLOCK: { | |
508 | _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; | |
509 | _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; | |
510 | _cleanup_(decrypted_image_unrefp) DecryptedImage *di = NULL; | |
511 | _cleanup_(verity_settings_done) VeritySettings verity_settings = VERITY_SETTINGS_DEFAULT; | |
8b3d4ff0 MB |
512 | DissectImageFlags flags = |
513 | DISSECT_IMAGE_READ_ONLY | | |
514 | DISSECT_IMAGE_GENERIC_ROOT | | |
515 | DISSECT_IMAGE_REQUIRE_ROOT | | |
516 | DISSECT_IMAGE_MOUNT_ROOT_ONLY | | |
517 | DISSECT_IMAGE_USR_NO_ROOT; | |
3a6ce677 BR |
518 | |
519 | r = verity_settings_load(&verity_settings, img->path, NULL, NULL); | |
520 | if (r < 0) | |
521 | return log_error_errno(r, "Failed to read verity artifacts for %s: %m", img->path); | |
522 | ||
523 | if (verity_settings.data_path) | |
524 | flags |= DISSECT_IMAGE_NO_PARTITION_TABLE; | |
525 | ||
526 | r = loop_device_make_by_path(img->path, O_RDONLY, 0, &d); | |
527 | if (r < 0) | |
8b3d4ff0 | 528 | return log_error_errno(r, "Failed to set up loopback device for %s: %m", img->path); |
3a6ce677 BR |
529 | |
530 | r = dissect_image_and_warn( | |
531 | d->fd, | |
532 | img->path, | |
533 | &verity_settings, | |
534 | NULL, | |
8b3d4ff0 MB |
535 | d->uevent_seqnum_not_before, |
536 | d->timestamp_not_before, | |
3a6ce677 BR |
537 | flags, |
538 | &m); | |
539 | if (r < 0) | |
540 | return r; | |
541 | ||
542 | r = dissected_image_decrypt_interactively( | |
543 | m, NULL, | |
544 | &verity_settings, | |
545 | flags, | |
546 | &di); | |
547 | if (r < 0) | |
548 | return r; | |
549 | ||
550 | r = dissected_image_mount_and_warn( | |
551 | m, | |
552 | p, | |
553 | UID_INVALID, | |
8b3d4ff0 | 554 | UID_INVALID, |
3a6ce677 BR |
555 | flags); |
556 | if (r < 0) | |
557 | return r; | |
558 | ||
559 | if (di) { | |
560 | r = decrypted_image_relinquish(di); | |
561 | if (r < 0) | |
562 | return log_error_errno(r, "Failed to relinquish DM devices: %m"); | |
563 | } | |
564 | ||
565 | loop_device_relinquish(d); | |
566 | break; | |
567 | } | |
568 | default: | |
569 | assert_not_reached("Unsupported image type"); | |
570 | } | |
571 | ||
572 | r = validate_version( | |
573 | p, | |
574 | img, | |
575 | host_os_release_id, | |
576 | host_os_release_version_id, | |
577 | host_os_release_sysext_level); | |
578 | if (r < 0) | |
579 | return r; | |
580 | if (r == 0) { | |
581 | n_ignored++; | |
582 | continue; | |
583 | } | |
584 | ||
585 | /* Noice! This one is an extension we want. */ | |
586 | r = strv_extend(&extensions, img->name); | |
587 | if (r < 0) | |
588 | return log_oom(); | |
589 | ||
590 | n_extensions ++; | |
591 | } | |
592 | ||
593 | /* Nothing left? Then shortcut things */ | |
594 | if (n_extensions == 0) { | |
595 | if (n_ignored > 0) | |
596 | log_info("No suitable extensions found (%u ignored due to incompatible version).", n_ignored); | |
597 | else | |
598 | log_info("No extensions found."); | |
599 | return 0; | |
600 | } | |
601 | ||
602 | /* Order by version sort with strverscmp_improved() */ | |
603 | typesafe_qsort(extensions, n_extensions, strverscmp_improvedp); | |
604 | ||
605 | buf = strv_join(extensions, "', '"); | |
606 | if (!buf) | |
607 | return log_oom(); | |
608 | ||
609 | log_info("Using extensions '%s'.", buf); | |
610 | ||
611 | /* Build table of extension paths (in reverse order) */ | |
612 | paths = new0(char*, n_extensions + 1); | |
613 | if (!paths) | |
614 | return log_oom(); | |
615 | ||
616 | for (size_t k = 0; k < n_extensions; k++) { | |
617 | _cleanup_free_ char *p = NULL; | |
618 | ||
619 | assert_se(img = hashmap_get(images, extensions[n_extensions - 1 - k])); | |
620 | ||
621 | p = path_join(workspace, "extensions", img->name); | |
622 | if (!p) | |
623 | return log_oom(); | |
624 | ||
625 | paths[k] = TAKE_PTR(p); | |
626 | } | |
627 | ||
628 | /* Let's now unmerge the status quo ante, since to build the new overlayfs we need a reference to the | |
629 | * underlying fs. */ | |
630 | STRV_FOREACH(h, arg_hierarchies) { | |
631 | _cleanup_free_ char *resolved = NULL; | |
632 | ||
633 | r = chase_symlinks(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL); | |
634 | if (r < 0) | |
635 | return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h); | |
636 | ||
637 | r = unmerge_hierarchy(resolved); | |
638 | if (r < 0) | |
639 | return r; | |
640 | } | |
641 | ||
642 | /* Create overlayfs mounts for all hierarchies */ | |
643 | STRV_FOREACH(h, arg_hierarchies) { | |
644 | _cleanup_free_ char *meta_path = NULL, *overlay_path = NULL; | |
645 | ||
646 | meta_path = path_join(workspace, "meta", *h); /* The place where to store metadata about this instance */ | |
647 | if (!meta_path) | |
648 | return log_oom(); | |
649 | ||
650 | overlay_path = path_join(workspace, "overlay", *h); /* The resulting overlayfs instance */ | |
651 | if (!overlay_path) | |
652 | return log_oom(); | |
653 | ||
654 | r = merge_hierarchy(*h, extensions, paths, meta_path, overlay_path); | |
655 | if (r < 0) | |
656 | return r; | |
657 | } | |
658 | ||
659 | /* And move them all into place. This is where things appear in the host namespace */ | |
660 | STRV_FOREACH(h, arg_hierarchies) { | |
661 | _cleanup_free_ char *p = NULL, *resolved = NULL; | |
662 | ||
663 | p = path_join(workspace, "overlay", *h); | |
664 | if (!p) | |
665 | return log_oom(); | |
666 | ||
667 | if (laccess(p, F_OK) < 0) { | |
668 | if (errno != ENOENT) | |
669 | return log_error_errno(errno, "Failed to check if '%s' exists: %m", p); | |
670 | ||
671 | /* Hierarchy apparently was empty in all extensions, and wasn't mounted, ignoring. */ | |
672 | continue; | |
673 | } | |
674 | ||
675 | r = chase_symlinks(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL); | |
676 | if (r < 0) | |
677 | return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h); | |
678 | ||
679 | r = mkdir_p(resolved, 0755); | |
680 | if (r < 0) | |
681 | return log_error_errno(r, "Failed to create hierarchy mount point '%s': %m", resolved); | |
682 | ||
683 | r = mount_nofollow_verbose(LOG_ERR, p, resolved, NULL, MS_BIND, NULL); | |
684 | if (r < 0) | |
685 | return r; | |
686 | ||
687 | log_info("Merged extensions into '%s'.", resolved); | |
688 | } | |
689 | ||
690 | return 1; | |
691 | } | |
692 | ||
693 | static int merge(Hashmap *images) { | |
694 | pid_t pid; | |
695 | int r; | |
696 | ||
697 | r = safe_fork("(sd-sysext)", FORK_DEATHSIG|FORK_LOG|FORK_NEW_MOUNTNS, &pid); | |
698 | if (r < 0) | |
699 | return log_error_errno(r, "Failed to fork off child: %m"); | |
700 | if (r == 0) { | |
701 | /* Child with its own mount namespace */ | |
702 | ||
703 | r = merge_subprocess(images, "/run/systemd/sysext"); | |
704 | if (r < 0) | |
705 | _exit(EXIT_FAILURE); | |
706 | ||
707 | /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we | |
708 | * created below /run. Nice! */ | |
709 | ||
710 | _exit(r > 0 ? EXIT_SUCCESS : 123); /* 123 means: didn't find any extensions */ | |
711 | } | |
712 | ||
713 | r = wait_for_terminate_and_check("(sd-sysext)", pid, WAIT_LOG_ABNORMAL); | |
714 | if (r < 0) | |
715 | return r; | |
716 | ||
717 | return r != 123; /* exit code 123 means: didn't do anything */ | |
718 | } | |
719 | ||
720 | static int image_discover_and_read_metadata(Hashmap **ret_images) { | |
721 | _cleanup_(hashmap_freep) Hashmap *images = NULL; | |
722 | Image *img; | |
723 | int r; | |
724 | ||
725 | assert(ret_images); | |
726 | ||
727 | images = hashmap_new(&image_hash_ops); | |
728 | if (!images) | |
729 | return log_oom(); | |
730 | ||
731 | r = image_discover(IMAGE_EXTENSION, arg_root, images); | |
732 | if (r < 0) | |
733 | return log_error_errno(r, "Failed to discover extension images: %m"); | |
734 | ||
735 | HASHMAP_FOREACH(img, images) { | |
736 | r = image_read_metadata(img); | |
737 | if (r < 0) | |
738 | return log_error_errno(r, "Failed to read metadata for image %s: %m", img->name); | |
739 | } | |
740 | ||
741 | *ret_images = TAKE_PTR(images); | |
742 | ||
743 | return 0; | |
744 | } | |
745 | ||
746 | static int verb_merge(int argc, char **argv, void *userdata) { | |
747 | _cleanup_(hashmap_freep) Hashmap *images = NULL; | |
748 | char **p; | |
749 | int r; | |
750 | ||
751 | if (!have_effective_cap(CAP_SYS_ADMIN)) | |
752 | return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged."); | |
753 | ||
754 | r = image_discover_and_read_metadata(&images); | |
755 | if (r < 0) | |
756 | return r; | |
757 | ||
758 | /* In merge mode fail if things are already merged. (In --refresh mode below we'll unmerge if we find | |
759 | * things are already merged...) */ | |
760 | STRV_FOREACH(p, arg_hierarchies) { | |
761 | _cleanup_free_ char *resolved = NULL; | |
762 | ||
763 | r = chase_symlinks(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL); | |
764 | if (r == -ENOENT) { | |
765 | log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p); | |
766 | continue; | |
767 | } | |
768 | if (r < 0) | |
769 | return log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p); | |
770 | ||
771 | r = is_our_mount_point(resolved); | |
772 | if (r < 0) | |
773 | return r; | |
774 | if (r > 0) | |
775 | return log_error_errno(SYNTHETIC_ERRNO(EBUSY), | |
776 | "Hierarchy '%s' is already merged.", *p); | |
777 | } | |
778 | ||
779 | return merge(images); | |
780 | } | |
781 | ||
782 | static int verb_refresh(int argc, char **argv, void *userdata) { | |
783 | _cleanup_(hashmap_freep) Hashmap *images = NULL; | |
784 | int r; | |
785 | ||
786 | if (!have_effective_cap(CAP_SYS_ADMIN)) | |
787 | return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged."); | |
788 | ||
789 | r = image_discover_and_read_metadata(&images); | |
790 | if (r < 0) | |
791 | return r; | |
792 | ||
793 | r = merge(images); /* Returns > 0 if it did something, i.e. a new overlayfs is mounted now. When it | |
794 | * does so it implicitly unmounts any overlayfs placed there before. Returns == 0 | |
795 | * if it did nothing, i.e. no extension images found. In this case the old | |
796 | * overlayfs remains in place if there was one. */ | |
797 | if (r < 0) | |
798 | return r; | |
799 | if (r == 0) /* No images found? Then unmerge. The goal of --refresh is after all that after having | |
800 | * called there's a guarantee that the merge status matches the installed extensions. */ | |
801 | r = unmerge(); | |
802 | ||
803 | /* Net result here is that: | |
804 | * | |
805 | * 1. If an overlayfs was mounted before and no extensions exist anymore, we'll have unmerged things. | |
806 | * | |
807 | * 2. If an overlayfs was mounted before, and there are still extensions installed' we'll have | |
808 | * unmerged and then merged things again. | |
809 | * | |
810 | * 3. If an overlayfs so far wasn't mounted, and there are extensions installed, we'll have it | |
811 | * mounted now. | |
812 | * | |
813 | * 4. If there was no overlayfs mount so far, and no extensions installed, we implement a NOP. | |
814 | */ | |
815 | ||
816 | return 0; | |
817 | } | |
818 | ||
819 | static int verb_list(int argc, char **argv, void *userdata) { | |
820 | _cleanup_(hashmap_freep) Hashmap *images = NULL; | |
821 | _cleanup_(table_unrefp) Table *t = NULL; | |
822 | Image *img; | |
823 | int r; | |
824 | ||
825 | images = hashmap_new(&image_hash_ops); | |
826 | if (!images) | |
827 | return log_oom(); | |
828 | ||
829 | r = image_discover(IMAGE_EXTENSION, arg_root, images); | |
830 | if (r < 0) | |
831 | return log_error_errno(r, "Failed to discover extension images: %m"); | |
832 | ||
833 | if ((arg_json_format_flags & JSON_FORMAT_OFF) && hashmap_isempty(images)) { | |
834 | log_info("No OS extensions found."); | |
835 | return 0; | |
836 | } | |
837 | ||
838 | t = table_new("name", "type", "path", "time"); | |
839 | if (!t) | |
840 | return log_oom(); | |
841 | ||
842 | HASHMAP_FOREACH(img, images) { | |
843 | r = table_add_many( | |
844 | t, | |
845 | TABLE_STRING, img->name, | |
846 | TABLE_STRING, image_type_to_string(img->type), | |
847 | TABLE_PATH, img->path, | |
848 | TABLE_TIMESTAMP, img->mtime != 0 ? img->mtime : img->crtime); | |
849 | if (r < 0) | |
850 | return table_log_add_error(r); | |
851 | } | |
852 | ||
853 | (void) table_set_sort(t, (size_t) 0); | |
854 | ||
855 | return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); | |
856 | } | |
857 | ||
858 | static int verb_help(int argc, char **argv, void *userdata) { | |
859 | _cleanup_free_ char *link = NULL; | |
860 | int r; | |
861 | ||
862 | r = terminal_urlify_man("systemd-sysext", "1", &link); | |
863 | if (r < 0) | |
864 | return log_oom(); | |
865 | ||
866 | printf("%1$s [OPTIONS...] [DEVICE]\n" | |
867 | "\n%5$sMerge extension images into /usr/ and /opt/ hierarchies.%6$s\n" | |
868 | "\n%3$sCommands:%4$s\n" | |
869 | " status Show current merge status (default)\n" | |
870 | " merge Merge extensions into /usr/ and /opt/\n" | |
871 | " unmerge Unmerge extensions from /usr/ and /opt/\n" | |
872 | " refresh Unmerge/merge extensions again\n" | |
873 | " list List installed extensions\n" | |
874 | " -h --help Show this help\n" | |
875 | " --version Show package version\n" | |
876 | "\n%3$sOptions:%4$s\n" | |
877 | " --no-pager Do not pipe output into a pager\n" | |
878 | " --no-legend Do not show the headers and footers\n" | |
879 | " --root=PATH Operate relative to root path\n" | |
880 | " --json=pretty|short|off\n" | |
881 | " Generate JSON output\n" | |
882 | " --force Ignore version incompatibilities\n" | |
883 | "\nSee the %2$s for details.\n", | |
884 | program_invocation_short_name, | |
885 | link, | |
886 | ansi_underline(), | |
887 | ansi_normal(), | |
888 | ansi_highlight(), | |
889 | ansi_normal()); | |
890 | ||
891 | return 0; | |
892 | } | |
893 | ||
894 | static int parse_argv(int argc, char *argv[]) { | |
895 | ||
896 | enum { | |
897 | ARG_VERSION = 0x100, | |
898 | ARG_NO_PAGER, | |
899 | ARG_NO_LEGEND, | |
900 | ARG_ROOT, | |
901 | ARG_JSON, | |
902 | ARG_FORCE, | |
903 | }; | |
904 | ||
905 | static const struct option options[] = { | |
906 | { "help", no_argument, NULL, 'h' }, | |
907 | { "version", no_argument, NULL, ARG_VERSION }, | |
908 | { "no-pager", no_argument, NULL, ARG_NO_PAGER }, | |
909 | { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, | |
910 | { "root", required_argument, NULL, ARG_ROOT }, | |
911 | { "json", required_argument, NULL, ARG_JSON }, | |
912 | { "force", no_argument, NULL, ARG_FORCE }, | |
913 | {} | |
914 | }; | |
915 | ||
916 | int c, r; | |
917 | ||
918 | assert(argc >= 0); | |
919 | assert(argv); | |
920 | ||
921 | while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) | |
922 | ||
923 | switch (c) { | |
924 | ||
925 | case 'h': | |
926 | return verb_help(argc, argv, NULL); | |
927 | ||
928 | case ARG_VERSION: | |
929 | return version(); | |
930 | ||
931 | case ARG_NO_PAGER: | |
932 | arg_pager_flags |= PAGER_DISABLE; | |
933 | break; | |
934 | ||
935 | case ARG_NO_LEGEND: | |
936 | arg_legend = false; | |
937 | break; | |
938 | ||
939 | case ARG_ROOT: | |
940 | r = parse_path_argument(optarg, false, &arg_root); | |
941 | if (r < 0) | |
942 | return r; | |
943 | break; | |
944 | ||
945 | case ARG_JSON: | |
946 | r = parse_json_argument(optarg, &arg_json_format_flags); | |
947 | if (r <= 0) | |
948 | return r; | |
949 | ||
950 | break; | |
951 | ||
952 | case ARG_FORCE: | |
953 | arg_force = true; | |
954 | break; | |
955 | ||
956 | case '?': | |
957 | return -EINVAL; | |
958 | ||
959 | default: | |
960 | assert_not_reached("Unhandled option"); | |
961 | } | |
962 | ||
963 | return 1; | |
964 | } | |
965 | ||
966 | static int sysext_main(int argc, char *argv[]) { | |
967 | ||
968 | static const Verb verbs[] = { | |
969 | { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status }, | |
970 | { "merge", VERB_ANY, 1, 0, verb_merge }, | |
971 | { "unmerge", VERB_ANY, 1, 0, verb_unmerge }, | |
972 | { "refresh", VERB_ANY, 1, 0, verb_refresh }, | |
973 | { "list", VERB_ANY, 1, 0, verb_list }, | |
974 | { "help", VERB_ANY, 1, 0, verb_help }, | |
975 | {} | |
976 | }; | |
977 | ||
978 | return dispatch_verb(argc, argv, verbs, NULL); | |
979 | } | |
980 | ||
981 | static int run(int argc, char *argv[]) { | |
982 | int r; | |
983 | ||
984 | log_setup(); | |
985 | ||
986 | r = parse_argv(argc, argv); | |
987 | if (r <= 0) | |
988 | return r; | |
989 | ||
990 | /* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and | |
991 | * /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline | |
992 | * switch. */ | |
993 | r = parse_env_extension_hierarchies(&arg_hierarchies); | |
994 | if (r < 0) | |
995 | return log_error_errno(r, "Failed to parse $SYSTEMD_SYSEXT_HIERARCHIES environment variable: %m"); | |
996 | ||
997 | return sysext_main(argc, argv); | |
998 | } | |
999 | ||
1000 | DEFINE_MAIN_FUNCTION(run); |