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