]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/sysext/sysext.c
Merge pull request #18741 from poettering/stub-no-cname
[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 <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, (size_t) -1);
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;
512 DissectImageFlags flags = DISSECT_IMAGE_READ_ONLY|DISSECT_IMAGE_REQUIRE_ROOT|DISSECT_IMAGE_MOUNT_ROOT_ONLY;
513
514 r = verity_settings_load(&verity_settings, img->path, NULL, NULL);
515 if (r < 0)
516 return log_error_errno(r, "Failed to read verity artifacts for %s: %m", img->path);
517
518 if (verity_settings.data_path)
519 flags |= DISSECT_IMAGE_NO_PARTITION_TABLE;
520
521 r = loop_device_make_by_path(img->path, O_RDONLY, 0, &d);
522 if (r < 0)
523 return log_error_errno(r, "Failed to set up loopback device: %m");
524
525 r = dissect_image_and_warn(
526 d->fd,
527 img->path,
528 &verity_settings,
529 NULL,
530 flags,
531 &m);
532 if (r < 0)
533 return r;
534
535 r = dissected_image_decrypt_interactively(
536 m, NULL,
537 &verity_settings,
538 flags,
539 &di);
540 if (r < 0)
541 return r;
542
543 r = dissected_image_mount_and_warn(
544 m,
545 p,
546 UID_INVALID,
547 flags);
548 if (r < 0)
549 return r;
550
551 if (di) {
552 r = decrypted_image_relinquish(di);
553 if (r < 0)
554 return log_error_errno(r, "Failed to relinquish DM devices: %m");
555 }
556
557 loop_device_relinquish(d);
558 break;
559 }
560 default:
561 assert_not_reached("Unsupported image type");
562 }
563
564 r = validate_version(
565 p,
566 img,
567 host_os_release_id,
568 host_os_release_version_id,
569 host_os_release_sysext_level);
570 if (r < 0)
571 return r;
572 if (r == 0) {
573 n_ignored++;
574 continue;
575 }
576
577 /* Noice! This one is an extension we want. */
578 r = strv_extend(&extensions, img->name);
579 if (r < 0)
580 return log_oom();
581
582 n_extensions ++;
583 }
584
585 /* Nothing left? Then shortcut things */
586 if (n_extensions == 0) {
587 if (n_ignored > 0)
588 log_info("No suitable extensions found (%u ignored due to incompatible version).", n_ignored);
589 else
590 log_info("No extensions found.");
591 return 0;
592 }
593
594 /* Order by version sort with strverscmp_improved() */
595 typesafe_qsort(extensions, n_extensions, strverscmp_improvedp);
596
597 buf = strv_join(extensions, "', '");
598 if (!buf)
599 return log_oom();
600
601 log_info("Using extensions '%s'.", buf);
602
603 /* Build table of extension paths (in reverse order) */
604 paths = new0(char*, n_extensions + 1);
605 if (!paths)
606 return log_oom();
607
608 for (size_t k = 0; k < n_extensions; k++) {
609 _cleanup_free_ char *p = NULL;
610
611 assert_se(img = hashmap_get(images, extensions[n_extensions - 1 - k]));
612
613 p = path_join(workspace, "extensions", img->name);
614 if (!p)
615 return log_oom();
616
617 paths[k] = TAKE_PTR(p);
618 }
619
620 /* Let's now unmerge the status quo ante, since to build the new overlayfs we need a reference to the
621 * underlying fs. */
622 STRV_FOREACH(h, arg_hierarchies) {
623 _cleanup_free_ char *resolved = NULL;
624
625 r = chase_symlinks(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
626 if (r < 0)
627 return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h);
628
629 r = unmerge_hierarchy(resolved);
630 if (r < 0)
631 return r;
632 }
633
634 /* Create overlayfs mounts for all hierarchies */
635 STRV_FOREACH(h, arg_hierarchies) {
636 _cleanup_free_ char *meta_path = NULL, *overlay_path = NULL;
637
638 meta_path = path_join(workspace, "meta", *h); /* The place where to store metadata about this instance */
639 if (!meta_path)
640 return log_oom();
641
642 overlay_path = path_join(workspace, "overlay", *h); /* The resulting overlayfs instance */
643 if (!overlay_path)
644 return log_oom();
645
646 r = merge_hierarchy(*h, extensions, paths, meta_path, overlay_path);
647 if (r < 0)
648 return r;
649 }
650
651 /* And move them all into place. This is where things appear in the host namespace */
652 STRV_FOREACH(h, arg_hierarchies) {
653 _cleanup_free_ char *p = NULL, *resolved = NULL;
654
655 p = path_join(workspace, "overlay", *h);
656 if (!p)
657 return log_oom();
658
659 if (laccess(p, F_OK) < 0) {
660 if (errno != ENOENT)
661 return log_error_errno(errno, "Failed to check if '%s' exists: %m", p);
662
663 /* Hierarchy apparently was empty in all extensions, and wasn't mounted, ignoring. */
664 continue;
665 }
666
667 r = chase_symlinks(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
668 if (r < 0)
669 return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h);
670
671 r = mkdir_p(resolved, 0755);
672 if (r < 0)
673 return log_error_errno(r, "Failed to create hierarchy mount point '%s': %m", resolved);
674
675 r = mount_nofollow_verbose(LOG_ERR, p, resolved, NULL, MS_BIND, NULL);
676 if (r < 0)
677 return r;
678
679 log_info("Merged extensions into '%s'.", resolved);
680 }
681
682 return 1;
683 }
684
685 static int merge(Hashmap *images) {
686 pid_t pid;
687 int r;
688
689 r = safe_fork("(sd-sysext)", FORK_DEATHSIG|FORK_LOG|FORK_NEW_MOUNTNS, &pid);
690 if (r < 0)
691 return log_error_errno(r, "Failed to fork off child: %m");
692 if (r == 0) {
693 /* Child with its own mount namespace */
694
695 r = merge_subprocess(images, "/run/systemd/sysext");
696 if (r < 0)
697 _exit(EXIT_FAILURE);
698
699 /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we
700 * created below /run. Nice! */
701
702 _exit(r > 0 ? EXIT_SUCCESS : 123); /* 123 means: didn't find any extensions */
703 }
704
705 r = wait_for_terminate_and_check("(sd-sysext)", pid, WAIT_LOG_ABNORMAL);
706 if (r < 0)
707 return r;
708
709 return r != 123; /* exit code 123 means: didn't do anything */
710 }
711
712 static int image_discover_and_read_metadata(Hashmap **ret_images) {
713 _cleanup_(hashmap_freep) Hashmap *images = NULL;
714 Image *img;
715 int r;
716
717 assert(ret_images);
718
719 images = hashmap_new(&image_hash_ops);
720 if (!images)
721 return log_oom();
722
723 r = image_discover(IMAGE_EXTENSION, arg_root, images);
724 if (r < 0)
725 return log_error_errno(r, "Failed to discover extension images: %m");
726
727 HASHMAP_FOREACH(img, images) {
728 r = image_read_metadata(img);
729 if (r < 0)
730 return log_error_errno(r, "Failed to read metadata for image %s: %m", img->name);
731 }
732
733 *ret_images = TAKE_PTR(images);
734
735 return 0;
736 }
737
738 static int verb_merge(int argc, char **argv, void *userdata) {
739 _cleanup_(hashmap_freep) Hashmap *images = NULL;
740 char **p;
741 int r;
742
743 if (!have_effective_cap(CAP_SYS_ADMIN))
744 return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
745
746 r = image_discover_and_read_metadata(&images);
747 if (r < 0)
748 return r;
749
750 /* In merge mode fail if things are already merged. (In --refresh mode below we'll unmerge if we find
751 * things are already merged...) */
752 STRV_FOREACH(p, arg_hierarchies) {
753 _cleanup_free_ char *resolved = NULL;
754
755 r = chase_symlinks(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
756 if (r == -ENOENT) {
757 log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p);
758 continue;
759 }
760 if (r < 0)
761 return log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p);
762
763 r = is_our_mount_point(resolved);
764 if (r < 0)
765 return r;
766 if (r > 0)
767 return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
768 "Hierarchy '%s' is already merged.", *p);
769 }
770
771 return merge(images);
772 }
773
774 static int verb_refresh(int argc, char **argv, void *userdata) {
775 _cleanup_(hashmap_freep) Hashmap *images = NULL;
776 int r;
777
778 if (!have_effective_cap(CAP_SYS_ADMIN))
779 return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
780
781 r = image_discover_and_read_metadata(&images);
782 if (r < 0)
783 return r;
784
785 r = merge(images); /* Returns > 0 if it did something, i.e. a new overlayfs is mounted now. When it
786 * does so it implicitly unmounts any overlayfs placed there before. Returns == 0
787 * if it did nothing, i.e. no extension images found. In this case the old
788 * overlayfs remains in place if there was one. */
789 if (r < 0)
790 return r;
791 if (r == 0) /* No images found? Then unmerge. The goal of --refresh is after all that after having
792 * called there's a guarantee that the merge status matches the installed extensions. */
793 r = unmerge();
794
795 /* Net result here is that:
796 *
797 * 1. If an overlayfs was mounted before and no extensions exist anymore, we'll have unmerged things.
798 *
799 * 2. If an overlayfs was mounted before, and there are still extensions installed' we'll have
800 * unmerged and then merged things again.
801 *
802 * 3. If an overlayfs so far wasn't mounted, and there are extensions installed, we'll have it
803 * mounted now.
804 *
805 * 4. If there was no overlayfs mount so far, and no extensions installed, we implement a NOP.
806 */
807
808 return 0;
809 }
810
811 static int verb_list(int argc, char **argv, void *userdata) {
812 _cleanup_(hashmap_freep) Hashmap *images = NULL;
813 _cleanup_(table_unrefp) Table *t = NULL;
814 Image *img;
815 int r;
816
817 images = hashmap_new(&image_hash_ops);
818 if (!images)
819 return log_oom();
820
821 r = image_discover(IMAGE_EXTENSION, arg_root, images);
822 if (r < 0)
823 return log_error_errno(r, "Failed to discover extension images: %m");
824
825 if ((arg_json_format_flags & JSON_FORMAT_OFF) && hashmap_isempty(images)) {
826 log_info("No OS extensions found.");
827 return 0;
828 }
829
830 t = table_new("name", "type", "path", "time");
831 if (!t)
832 return log_oom();
833
834 HASHMAP_FOREACH(img, images) {
835 r = table_add_many(
836 t,
837 TABLE_STRING, img->name,
838 TABLE_STRING, image_type_to_string(img->type),
839 TABLE_PATH, img->path,
840 TABLE_TIMESTAMP, img->mtime != 0 ? img->mtime : img->crtime);
841 if (r < 0)
842 return table_log_add_error(r);
843 }
844
845 (void) table_set_sort(t, (size_t) 0, (size_t) -1);
846
847 return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
848 }
849
850 static int verb_help(int argc, char **argv, void *userdata) {
851 _cleanup_free_ char *link = NULL;
852 int r;
853
854 r = terminal_urlify_man("systemd-sysext", "1", &link);
855 if (r < 0)
856 return log_oom();
857
858 printf("%1$s [OPTIONS...] [DEVICE]\n"
859 "\n%5$sMerge extension images into /usr/ and /opt/ hierarchies.%6$s\n"
860 "\n%3$sCommands:%4$s\n"
861 " status Show current merge status (default)\n"
862 " merge Merge extensions into /usr/ and /opt/\n"
863 " unmerge Unmerge extensions from /usr/ and /opt/\n"
864 " refresh Unmerge/merge extensions again\n"
865 " list List installed extensions\n"
866 " -h --help Show this help\n"
867 " --version Show package version\n"
868 "\n%3$sOptions:%4$s\n"
869 " --no-pager Do not pipe output into a pager\n"
870 " --no-legend Do not show the headers and footers\n"
871 " --root=PATH Operate relative to root path\n"
872 " --json=pretty|short|off\n"
873 " Generate JSON output\n"
874 " --force Ignore version incompatibilities\n"
875 "\nSee the %2$s for details.\n",
876 program_invocation_short_name,
877 link,
878 ansi_underline(),
879 ansi_normal(),
880 ansi_highlight(),
881 ansi_normal());
882
883 return 0;
884 }
885
886 static int parse_argv(int argc, char *argv[]) {
887
888 enum {
889 ARG_VERSION = 0x100,
890 ARG_NO_PAGER,
891 ARG_NO_LEGEND,
892 ARG_ROOT,
893 ARG_JSON,
894 ARG_FORCE,
895 };
896
897 static const struct option options[] = {
898 { "help", no_argument, NULL, 'h' },
899 { "version", no_argument, NULL, ARG_VERSION },
900 { "no-pager", no_argument, NULL, ARG_NO_PAGER },
901 { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
902 { "root", required_argument, NULL, ARG_ROOT },
903 { "json", required_argument, NULL, ARG_JSON },
904 { "force", no_argument, NULL, ARG_FORCE },
905 {}
906 };
907
908 int c, r;
909
910 assert(argc >= 0);
911 assert(argv);
912
913 while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
914
915 switch (c) {
916
917 case 'h':
918 return verb_help(argc, argv, NULL);
919
920 case ARG_VERSION:
921 return version();
922
923 case ARG_NO_PAGER:
924 arg_pager_flags |= PAGER_DISABLE;
925 break;
926
927 case ARG_NO_LEGEND:
928 arg_legend = false;
929 break;
930
931 case ARG_ROOT:
932 r = parse_path_argument(optarg, false, &arg_root);
933 if (r < 0)
934 return r;
935 break;
936
937 case ARG_JSON:
938 r = parse_json_argument(optarg, &arg_json_format_flags);
939 if (r <= 0)
940 return r;
941
942 break;
943
944 case ARG_FORCE:
945 arg_force = true;
946 break;
947
948 case '?':
949 return -EINVAL;
950
951 default:
952 assert_not_reached("Unhandled option");
953 }
954
955 return 1;
956 }
957
958 static int sysext_main(int argc, char *argv[]) {
959
960 static const Verb verbs[] = {
961 { "status", VERB_ANY, 1, VERB_DEFAULT, verb_status },
962 { "merge", VERB_ANY, 1, 0, verb_merge },
963 { "unmerge", VERB_ANY, 1, 0, verb_unmerge },
964 { "refresh", VERB_ANY, 1, 0, verb_refresh },
965 { "list", VERB_ANY, 1, 0, verb_list },
966 { "help", VERB_ANY, 1, 0, verb_help },
967 {}
968 };
969
970 return dispatch_verb(argc, argv, verbs, NULL);
971 }
972
973 static int run(int argc, char *argv[]) {
974 int r;
975
976 log_setup();
977
978 r = parse_argv(argc, argv);
979 if (r <= 0)
980 return r;
981
982 /* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and
983 * /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline
984 * switch. */
985 r = getenv_path_list("SYSTEMD_SYSEXT_HIERARCHIES", &arg_hierarchies);
986 if (r < 0)
987 return log_error_errno(r, "Failed to parse $SYSTEMD_SYSEXT_HIERARCHIES environment variable: %m");
988
989 if (!arg_hierarchies) {
990 arg_hierarchies = strv_new("/usr", "/opt");
991 if (!arg_hierarchies)
992 return log_oom();
993 }
994
995 return sysext_main(argc, argv);
996 }
997
998 DEFINE_MAIN_FUNCTION(run);