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