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