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